Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
fff756cbe0 | |||
e38778dbf9 | |||
cc9081b011 | |||
14e8f7b775 | |||
a70e6c7118 | |||
48ca885a2c | |||
09cb340a9d | |||
b6ebd6bef6 | |||
2ec25fd1a2 | |||
bc99865ba8 | |||
f834351ce2 | |||
0f1a02f65b | |||
6ad0a34645 | |||
fdc71475fc | |||
047defebd1 | |||
6148e889aa | |||
1d7affcd84 |
@ -1,17 +1,17 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
@ -24,14 +24,14 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true">
|
||||
<receiver android:exported="false"
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
|
||||
<receiver android:exported="false"
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
@ -58,6 +58,13 @@
|
||||
<data android:host="sn.solsynth.dev" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="https" />
|
||||
</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="solink" />
|
||||
</intent-filter>
|
||||
|
||||
@ -66,21 +73,21 @@
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2"/>
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
@ -89,8 +96,8 @@
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
android/app/src/main/res/drawable-night-v21/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
BIN
android/app/src/main/res/drawable-night/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
After Width: | Height: | Size: 355 KiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
After Width: | Height: | Size: 69 B |
@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
19
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
19
android/app/src/main/res/values-v31/styles.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
@ -157,6 +157,8 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_webrtc (0.11.3):
|
||||
@ -288,6 +290,7 @@ DEPENDENCIES:
|
||||
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
@ -361,6 +364,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_webrtc:
|
||||
@ -433,6 +438,7 @@ SPEC CHECKSUMS:
|
||||
FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
|
22
ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "background.png",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "darkbackground.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
vendored
Normal file
After Width: | Height: | Size: 69 B |
BIN
ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
vendored
Normal file
After Width: | Height: | Size: 69 B |
@ -1,23 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 233 KiB |
@ -16,13 +16,19 @@
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
@ -32,6 +38,7 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchImage" width="1026" height="1024"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@ -81,5 +81,7 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -112,6 +112,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
label: 'bsPreparingData',
|
||||
action: () async {
|
||||
final AuthProvider auth = Get.find();
|
||||
try {
|
||||
await Future.wait([
|
||||
Get.find<StickerProvider>().refreshAvailableStickers(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
@ -121,6 +122,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
]);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
|
10
lib/exceptions/request.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RequestException implements Exception {
|
||||
final Response data;
|
||||
|
||||
const RequestException(this.data);
|
||||
|
||||
@override
|
||||
String toString() => 'Request failed ${data.statusCode}: ${data.bodyString}';
|
||||
}
|
6
lib/exceptions/unauthorized.dart
Normal file
@ -0,0 +1,6 @@
|
||||
class UnauthorizedException implements Exception {
|
||||
const UnauthorizedException();
|
||||
|
||||
@override
|
||||
String toString() => 'Unauthorized';
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
|
||||
extension SolianExtenions on BuildContext {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
@ -48,15 +50,48 @@ extension SolianExtenions on BuildContext {
|
||||
}
|
||||
|
||||
Future<void> showErrorDialog(dynamic exception) {
|
||||
var stack = StackTrace.current;
|
||||
var stackTrace = '$stack';
|
||||
Widget content = Text(exception.toString().capitalize!);
|
||||
if (exception is UnauthorizedException) {
|
||||
content = Text('errorHappenedUnauthorized'.tr);
|
||||
}
|
||||
if (exception is RequestException) {
|
||||
String overall;
|
||||
switch (exception.data.statusCode) {
|
||||
case 400:
|
||||
overall = 'errorHappenedRequestBad'.tr;
|
||||
break;
|
||||
case 401:
|
||||
overall = 'errorHappenedUnauthorized'.tr;
|
||||
break;
|
||||
case 403:
|
||||
overall = 'errorHappenedRequestForbidden'.tr;
|
||||
break;
|
||||
case 404:
|
||||
overall = 'errorHappenedRequestNotFound'.tr;
|
||||
break;
|
||||
case null:
|
||||
overall = 'errorHappenedRequestConnection'.tr;
|
||||
break;
|
||||
default:
|
||||
overall = 'errorHappenedRequestUnknown'.tr;
|
||||
break;
|
||||
}
|
||||
|
||||
if (exception.data.statusCode != null) {
|
||||
content = Text(
|
||||
'$overall\n\n(${exception.data.statusCode}) ${exception.data.bodyString}',
|
||||
);
|
||||
} else {
|
||||
content = Text(overall);
|
||||
}
|
||||
}
|
||||
|
||||
return showDialog<void>(
|
||||
useRootNavigator: true,
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('errorHappened'.tr),
|
||||
content: Text('${exception.toString().capitalize!}\n\nStack Trace: $stackTrace'),
|
||||
content: content,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
|
@ -1,22 +1,26 @@
|
||||
class NetworkPackage {
|
||||
String method;
|
||||
String? endpoint;
|
||||
String? message;
|
||||
Map<String, dynamic>? payload;
|
||||
|
||||
NetworkPackage({
|
||||
required this.method,
|
||||
this.endpoint,
|
||||
this.message,
|
||||
this.payload,
|
||||
});
|
||||
|
||||
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
|
||||
method: json['w'],
|
||||
endpoint: json['e'],
|
||||
message: json['m'],
|
||||
payload: json['p'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'w': method,
|
||||
'e': endpoint,
|
||||
'm': message,
|
||||
'p': payload,
|
||||
};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/account_status.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
@ -33,15 +35,14 @@ class StatusProvider extends GetConnect {
|
||||
|
||||
Future<Response> getCurrentStatus() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
|
||||
return await client.get('/users/me/status');
|
||||
}
|
||||
|
||||
Future<Response> getSomeoneStatus(String name) =>
|
||||
get('/users/$name/status');
|
||||
Future<Response> getSomeoneStatus(String name) => get('/users/$name/status');
|
||||
|
||||
Future<Response> setStatus(
|
||||
String type,
|
||||
@ -53,7 +54,7 @@ class StatusProvider extends GetConnect {
|
||||
DateTime? clearAt,
|
||||
}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
|
||||
@ -74,7 +75,7 @@ class StatusProvider extends GetConnect {
|
||||
}
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -82,13 +83,13 @@ class StatusProvider extends GetConnect {
|
||||
|
||||
Future<Response> clearStatus() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
|
||||
final resp = await client.delete('/users/me/status');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
@ -7,6 +7,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/request/request.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
@ -81,7 +83,7 @@ class AuthProvider extends GetConnect {
|
||||
'grant_type': 'refresh_token',
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
credentials = TokenSet(
|
||||
accessToken: resp.body['access_token'],
|
||||
@ -128,7 +130,7 @@ class AuthProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<void> ensureCredentials() async {
|
||||
if (isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
if (credentials == null) await loadCredentials();
|
||||
|
||||
if (credentials!.isExpired) {
|
||||
@ -158,7 +160,7 @@ class AuthProvider extends GetConnect {
|
||||
'password': password,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
} else if (resp.body['is_finished'] == false) {
|
||||
throw RiskyAuthenticateException(resp.body['ticket']['id']);
|
||||
}
|
||||
@ -218,7 +220,7 @@ class AuthProvider extends GetConnect {
|
||||
final client = configureClient('auth');
|
||||
final resp = await client.get('/users/me');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
userProfile.value = resp.body;
|
||||
|
@ -2,6 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:solian/models/call.dart';
|
||||
@ -88,7 +90,7 @@ class ChatCallProvider extends GetxController {
|
||||
|
||||
Future<(String, String)> getRoomToken() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
@ -101,7 +103,7 @@ class ChatCallProvider extends GetxController {
|
||||
endpoint = 'wss://${resp.body['endpoint']}';
|
||||
return (token!, endpoint!);
|
||||
} else {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
@ -89,7 +91,7 @@ class AttachmentProvider extends GetConnect {
|
||||
Map<String, dynamic>? metadata,
|
||||
) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient(
|
||||
'uc',
|
||||
@ -118,7 +120,7 @@ class AttachmentProvider extends GetConnect {
|
||||
});
|
||||
final resp = await client.post('/attachments', payload);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Attachment.fromJson(resp.body);
|
||||
@ -131,7 +133,7 @@ class AttachmentProvider extends GetConnect {
|
||||
Map<String, dynamic>? metadata,
|
||||
) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('uc');
|
||||
|
||||
@ -156,7 +158,7 @@ class AttachmentProvider extends GetConnect {
|
||||
'metadata': metadata,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return AttachmentPlaceholder.fromJson(resp.body);
|
||||
@ -169,7 +171,7 @@ class AttachmentProvider extends GetConnect {
|
||||
String cid,
|
||||
) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient(
|
||||
'uc',
|
||||
@ -181,7 +183,7 @@ class AttachmentProvider extends GetConnect {
|
||||
});
|
||||
final resp = await client.post('/attachments/multipart/$rid/$cid', payload);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Attachment.fromJson(resp.body);
|
||||
@ -193,7 +195,7 @@ class AttachmentProvider extends GetConnect {
|
||||
bool isMature = false,
|
||||
}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('files');
|
||||
|
||||
@ -203,7 +205,7 @@ class AttachmentProvider extends GetConnect {
|
||||
});
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -211,13 +213,13 @@ class AttachmentProvider extends GetConnect {
|
||||
|
||||
Future<Response> deleteAttachment(int id) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('files');
|
||||
|
||||
var resp = await client.delete('/attachments/$id');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/account/relative_select.dart';
|
||||
@ -16,7 +18,7 @@ class ChannelProvider extends GetxController {
|
||||
|
||||
Future<void> refreshAvailableChannel() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
isLoading.value = true;
|
||||
final resp = await listAvailableChannel();
|
||||
@ -29,13 +31,13 @@ class ChannelProvider extends GetxController {
|
||||
|
||||
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -44,13 +46,13 @@ class ChannelProvider extends GetxController {
|
||||
Future<Response> getMyChannelProfile(String alias,
|
||||
{String realm = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/$alias/me');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -59,7 +61,7 @@ class ChannelProvider extends GetxController {
|
||||
Future<Response?> getChannelOngoingCall(String alias,
|
||||
{String realm = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
@ -67,7 +69,7 @@ class ChannelProvider extends GetxController {
|
||||
if (resp.statusCode == 404) {
|
||||
return null;
|
||||
} else if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -75,13 +77,13 @@ class ChannelProvider extends GetxController {
|
||||
|
||||
Future<Response> listChannel({String scope = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$scope');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -89,13 +91,13 @@ class ChannelProvider extends GetxController {
|
||||
|
||||
Future<Response> listAvailableChannel({String realm = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/me/available');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -103,13 +105,13 @@ class ChannelProvider extends GetxController {
|
||||
|
||||
Future<Response> createChannel(String scope, dynamic payload) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.post('/channels/$scope', payload);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -118,7 +120,7 @@ class ChannelProvider extends GetxController {
|
||||
Future<Response?> createDirectChannel(
|
||||
BuildContext context, String scope) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final related = await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
@ -141,7 +143,7 @@ class ChannelProvider extends GetxController {
|
||||
'is_encrypted': false,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -149,13 +151,13 @@ class ChannelProvider extends GetxController {
|
||||
|
||||
Future<Response> updateChannel(String scope, int id, dynamic payload) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.put('/channels/$scope/$id', payload);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
@ -28,7 +30,7 @@ class PostProvider extends GetConnect {
|
||||
: '/recommendations/$channel?${queries.join('&')}',
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -36,7 +38,7 @@ class PostProvider extends GetConnect {
|
||||
|
||||
Future<Response> listDraft(int page) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
@ -45,7 +47,7 @@ class PostProvider extends GetConnect {
|
||||
final client = auth.configureClient('interactive');
|
||||
final resp = await client.get('/posts/drafts?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -63,7 +65,7 @@ class PostProvider extends GetConnect {
|
||||
];
|
||||
final resp = await get('/posts?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -72,7 +74,7 @@ class PostProvider extends GetConnect {
|
||||
Future<Response> listPostReplies(String alias, int page) async {
|
||||
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -81,7 +83,7 @@ class PostProvider extends GetConnect {
|
||||
Future<Response> getPost(String alias) async {
|
||||
final resp = await get('/posts/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -90,7 +92,7 @@ class PostProvider extends GetConnect {
|
||||
Future<Response> getArticle(String alias) async {
|
||||
final resp = await get('/articles/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.body);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
@ -8,7 +10,7 @@ class RealmProvider extends GetxController {
|
||||
|
||||
Future<void> refreshAvailableRealms() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
isLoading.value = true;
|
||||
final resp = await listAvailableRealm();
|
||||
@ -21,13 +23,13 @@ class RealmProvider extends GetxController {
|
||||
|
||||
Future<Response> getRealm(String alias) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/realms/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -35,13 +37,13 @@ class RealmProvider extends GetxController {
|
||||
|
||||
Future<Response> listAvailableRealm() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/realms/me/available');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
@ -9,6 +9,7 @@ class LinkExpandController extends GetxController {
|
||||
final Map<String, LinkMeta?> _cachedResponse = {};
|
||||
|
||||
Future<LinkMeta?> expandLink(String url) async {
|
||||
log('[LinkExpander] Expanding link... $url');
|
||||
final target = utf8.fuse(base64).encode(url);
|
||||
if (_cachedResponse.containsKey(target)) return _cachedResponse[target];
|
||||
final client = ServiceFinder.configureClient('dealer');
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
@ -29,7 +30,7 @@ Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
|
||||
if (resp.statusCode == 404) {
|
||||
return null;
|
||||
} else if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Event.fromJson(resp.body);
|
||||
@ -42,7 +43,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
|
||||
bool Function(List<Event> items)? onBrake,
|
||||
take = 10,
|
||||
offset = 0,
|
||||
}) async {
|
||||
}) async {
|
||||
if (remainDepth <= 0) {
|
||||
return null;
|
||||
}
|
||||
@ -57,7 +58,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
final PaginationResult response = PaginationResult.fromJson(resp.body);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
@ -42,7 +43,7 @@ class RelationshipProvider extends GetxController {
|
||||
final client = auth.configureClient('auth');
|
||||
final resp = await client.post('/users/me/relations?related=$username', {});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -57,7 +58,7 @@ class RelationshipProvider extends GetxController {
|
||||
{},
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
@ -71,7 +72,7 @@ class RelationshipProvider extends GetxController {
|
||||
{'status': status},
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
|
@ -6,6 +6,7 @@ import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
@ -50,9 +51,9 @@ class WebSocketProvider extends GetxController {
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
await auth.ensureCredentials();
|
||||
|
||||
if (auth.credentials == null) await auth.loadCredentials();
|
||||
try {
|
||||
await auth.ensureCredentials();
|
||||
|
||||
final uri = Uri.parse(ServiceFinder.buildUrl(
|
||||
'dealer',
|
||||
@ -61,21 +62,21 @@ class WebSocketProvider extends GetxController {
|
||||
|
||||
isConnecting.value = true;
|
||||
|
||||
try {
|
||||
websocket = WebSocketChannel.connect(uri);
|
||||
await websocket?.ready;
|
||||
} catch (e) {
|
||||
listen();
|
||||
|
||||
isConnected.value = true;
|
||||
} catch (err) {
|
||||
log('Unable connect dealer via websocket... $err');
|
||||
if (!noRetry) {
|
||||
await auth.refreshCredentials();
|
||||
return connect(noRetry: true);
|
||||
}
|
||||
}
|
||||
|
||||
listen();
|
||||
|
||||
isConnected.value = true;
|
||||
} finally {
|
||||
isConnecting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
websocket?.sink.close(WebSocketStatus.normalClosure);
|
||||
@ -87,6 +88,7 @@ class WebSocketProvider extends GetxController {
|
||||
websocket?.stream.listen(
|
||||
(event) {
|
||||
final packet = NetworkPackage.fromJson(jsonDecode(event));
|
||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||
stream.sink.add(packet);
|
||||
},
|
||||
onDone: () {
|
||||
@ -147,8 +149,8 @@ class WebSocketProvider extends GetxController {
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
final _descriptionController = TextEditingController();
|
||||
final _birthdayController = TextEditingController();
|
||||
|
||||
int? _avatar;
|
||||
int? _banner;
|
||||
String? _avatar;
|
||||
String? _banner;
|
||||
DateTime? _birthday;
|
||||
|
||||
bool _isBusy = false;
|
||||
@ -109,11 +109,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AttachmentProvider provider = Get.find();
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
Attachment? attachResult;
|
||||
try {
|
||||
attachResult = await provider.createAttachmentDirectly(
|
||||
attachResult = await attach.createAttachmentDirectly(
|
||||
await file.readAsBytes(),
|
||||
file.path,
|
||||
'avatar',
|
||||
@ -129,7 +129,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
final resp = await client.put(
|
||||
'/users/me/$position',
|
||||
{'attachment': attachResult.id},
|
||||
{'attachment': attachResult.rid},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
|
@ -13,8 +13,13 @@ import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||
|
||||
class CallScreen extends StatefulWidget {
|
||||
final bool hideAppBar;
|
||||
final bool isExpandable;
|
||||
|
||||
const CallScreen({super.key, this.hideAppBar = false});
|
||||
const CallScreen({
|
||||
super.key,
|
||||
this.hideAppBar = false,
|
||||
this.isExpandable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CallScreen> createState() => _CallScreenState();
|
||||
@ -308,6 +313,15 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
),
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.isExpandable)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
ctrl.gotoScreen(context);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
@ -317,6 +331,8 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 20, right: 16),
|
||||
),
|
||||
),
|
||||
|
@ -24,6 +24,7 @@ import 'package:solian/widgets/channel/channel_call_indicator.dart';
|
||||
import 'package:solian/widgets/chat/call/chat_call_action.dart';
|
||||
import 'package:solian/widgets/chat/chat_event_list.dart';
|
||||
import 'package:solian/widgets/chat/chat_message_input.dart';
|
||||
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
@ -103,12 +104,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
final List<ChannelMember> _typingUsers = List.empty(growable: true);
|
||||
final Map<int, Timer> _typingInactiveTimer = {};
|
||||
|
||||
void _listenMessages() {
|
||||
final WebSocketProvider provider = Get.find();
|
||||
_subscription = provider.stream.stream.listen((event) {
|
||||
final WebSocketProvider ws = Get.find();
|
||||
_subscription = ws.stream.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
case 'events.new':
|
||||
final payload = Event.fromJson(event.payload!);
|
||||
final typingIdx =
|
||||
_typingUsers.indexWhere((x) => x.id == payload.senderId);
|
||||
if (typingIdx != -1) _typingUsers.removeAt(typingIdx);
|
||||
_chatController.receiveEvent(payload);
|
||||
break;
|
||||
case 'calls.new':
|
||||
@ -123,6 +130,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
setState(() => _ongoingCall = null);
|
||||
}
|
||||
break;
|
||||
case 'status.typing':
|
||||
if (event.payload?['channel_id'] != _channel!.id) break;
|
||||
final member = ChannelMember.fromJson(event.payload!['member']);
|
||||
if (member.id == _channelProfile!.id) break;
|
||||
if (!_typingUsers.any((x) => x.id == member.id)) {
|
||||
setState(() {
|
||||
_typingUsers.add(member);
|
||||
});
|
||||
}
|
||||
_typingInactiveTimer[member.id]?.cancel();
|
||||
_typingInactiveTimer[member.id] = Timer(
|
||||
const Duration(seconds: 3),
|
||||
() {
|
||||
setState(() {
|
||||
_typingUsers.removeWhere((x) => x.id == member.id);
|
||||
_typingInactiveTimer.remove(member.id);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -141,7 +167,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
if (_isOutOfSyncSince == null) break;
|
||||
if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 60) break;
|
||||
if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 30) break;
|
||||
_keepUpdateWithServer();
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
@ -269,26 +295,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isOutOfSyncSince != null)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 8),
|
||||
tileColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
leading: const Icon(Icons.history_toggle_off),
|
||||
title: Text('messageOutOfSync'.tr),
|
||||
subtitle: Text('messageOutOfSyncCaption'.tr),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
setState(() => _isOutOfSyncSince = null);
|
||||
},
|
||||
),
|
||||
onTap: _isBusy
|
||||
? null
|
||||
: () {
|
||||
_keepUpdateWithServer();
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
if (_chatController.isLoading.isTrue) {
|
||||
return const LinearProgressIndicator().animate().slideY();
|
||||
@ -300,7 +306,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: SafeArea(
|
||||
child: ChatMessageInput(
|
||||
child: Column(
|
||||
children: [
|
||||
ChatTypingIndicator(users: _typingUsers),
|
||||
ChatMessageInput(
|
||||
edit: _messageToEditing,
|
||||
reply: _messageToReplying,
|
||||
realm: widget.realm,
|
||||
@ -318,6 +327,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -331,7 +342,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
child: Row(children: [
|
||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||
Expanded(
|
||||
child: CallScreen(hideAppBar: true),
|
||||
child: CallScreen(
|
||||
hideAppBar: true,
|
||||
isExpandable: true,
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
@ -346,6 +360,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var timer in _typingInactiveTimer.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_subscription?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
|
@ -80,13 +80,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onTap: () {
|
||||
final ChannelProvider provider = Get.find();
|
||||
provider
|
||||
final ChannelProvider channels = Get.find();
|
||||
channels
|
||||
.createDirectChannel(context, 'global')
|
||||
.then((resp) {
|
||||
if (resp != null) {
|
||||
_channels.refreshAvailableChannel();
|
||||
}
|
||||
}).catchError((e) {
|
||||
context.showErrorDialog(e);
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -125,6 +127,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
noCategory: true,
|
||||
channels: _channels.directChannels,
|
||||
selfId: selfId,
|
||||
useReplace: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -41,6 +41,8 @@ const i18nEnglish = {
|
||||
'openInBrowser': 'Open in browser',
|
||||
'notification': 'Notification',
|
||||
'errorHappened': 'An error occurred',
|
||||
'errorHappenedUnauthorized':
|
||||
'Unauthorized request, please sign in or try resign in.',
|
||||
'forgotPassword': 'Forgot password',
|
||||
'email': 'Email',
|
||||
'username': 'Username',
|
||||
@ -381,4 +383,7 @@ const i18nEnglish = {
|
||||
'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.',
|
||||
'messageHistoryWipe': 'Wipe local message history',
|
||||
'unknown': 'Unknown',
|
||||
'collapse': 'Collapse',
|
||||
'expand': 'Expand',
|
||||
'typingMessage': '@user are typing...',
|
||||
};
|
||||
|
@ -41,6 +41,12 @@ const i18nSimplifiedChinese = {
|
||||
'openInBrowser': '在浏览器中打开',
|
||||
'notification': '通知',
|
||||
'errorHappened': '发生错误了',
|
||||
'errorHappenedUnauthorized': '未经授权的请求,请登录或尝试重新登录。',
|
||||
'errorHappenedRequestBad': '请求错误,服务器拒绝处理该请求,请检查您的请求数据。',
|
||||
'errorHappenedRequestForbidden': '请求错误,权限不足。',
|
||||
'errorHappenedRequestNotFound': '请求错误,请求的数据不存在。',
|
||||
'errorHappenedRequestConnection': '网络请求失败,请检查连接状态与服务状态后再试。',
|
||||
'errorHappenedRequestUnknown': '请求错误,类型未知,请将本提示完整截图提交反馈。',
|
||||
'forgotPassword': '忘记密码',
|
||||
'email': '邮件地址',
|
||||
'username': '用户名',
|
||||
@ -347,4 +353,7 @@ const i18nSimplifiedChinese = {
|
||||
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
|
||||
'messageHistoryWipe': '清除消息记录',
|
||||
'unknown': '未知',
|
||||
'collapse': '折叠',
|
||||
'expand': '展开',
|
||||
'typingMessage': '@user 正在输入中…',
|
||||
};
|
||||
|
@ -28,11 +28,11 @@ class _AttachmentAttrEditorDialogState
|
||||
bool _isMature = false;
|
||||
|
||||
Future<Attachment?> _updateAttachment() async {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final resp = await provider.updateAttachment(
|
||||
final resp = await attach.updateAttachment(
|
||||
widget.item.id,
|
||||
_altController.value.text,
|
||||
isMature: _isMature,
|
||||
|
@ -147,17 +147,21 @@ class _AttachmentItemImage extends StatelessWidget {
|
||||
errorWidget: (context, url, error) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Center(
|
||||
child: const Icon(Icons.close, size: 32)
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.close, size: 32)
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
Text(error.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
Image.network(
|
||||
ServiceFinder.buildUrl('files', '/attachments/${item.id}'),
|
||||
ServiceFinder.buildUrl('files', '/attachments/${item.rid}'),
|
||||
fit: fit,
|
||||
loadingBuilder: (BuildContext context, Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
@ -174,10 +178,14 @@ class _AttachmentItemImage extends StatelessWidget {
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Center(
|
||||
child: const Icon(Icons.close, size: 32)
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.close, size: 32)
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
Text(error.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -132,6 +132,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
_getMetadataList();
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.attachmentsId.isEmpty) {
|
||||
@ -139,12 +142,24 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
}
|
||||
|
||||
if (_isLoading) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.file_copy,
|
||||
size: 12,
|
||||
color: _unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': widget.attachmentsId.length.toString()},
|
||||
),
|
||||
child: const LinearProgressIndicator(),
|
||||
);
|
||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
)
|
||||
.paddingSymmetric(horizontal: 8)
|
||||
.animate(onPlay: (c) => c.repeat(reverse: true))
|
||||
.fadeIn(duration: 1250.ms);
|
||||
}
|
||||
|
||||
if (widget.isColumn) {
|
||||
@ -157,8 +172,11 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
final element = _attachmentsMeta[idx];
|
||||
idx++;
|
||||
if (element == null) return const SizedBox();
|
||||
double ratio = element.metadata!['ratio']?.toDouble() ?? 16 / 9;
|
||||
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.columnMaxWidth,
|
||||
maxHeight: 640,
|
||||
@ -204,6 +222,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
final element = _attachmentsMeta[idx];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
|
@ -197,8 +197,11 @@ class ChatEvent extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
).paddingOnly(right: 12),
|
||||
if (!isContentPreviewing)
|
||||
_buildLinkExpansion().paddingOnly(left: 52, right: 8),
|
||||
_buildAttachment(context, isMinimal: isContentPreviewing).paddingOnly(
|
||||
left: isContentPreviewing ? 12 : 56,
|
||||
right: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
@ -7,10 +10,12 @@ import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||
import 'package:solian/widgets/chat/chat_event.dart';
|
||||
@ -196,6 +201,36 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
}
|
||||
}
|
||||
|
||||
Timer? _typingNotifyTimer;
|
||||
bool _typingStatus = false;
|
||||
|
||||
Future<void> _sendTypingStatus() async {
|
||||
final WebSocketProvider ws = Get.find();
|
||||
ws.websocket?.sink.add(jsonEncode(
|
||||
NetworkPackage(
|
||||
method: 'status.typing',
|
||||
endpoint: 'messaging',
|
||||
payload: {
|
||||
'channel_id': widget.channel.id,
|
||||
},
|
||||
).toJson(),
|
||||
));
|
||||
}
|
||||
|
||||
void _pingEnterMessageStatus() {
|
||||
if (!_typingStatus) {
|
||||
_sendTypingStatus();
|
||||
_typingStatus = true;
|
||||
}
|
||||
|
||||
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
|
||||
_typingNotifyTimer?.cancel();
|
||||
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
|
||||
_typingStatus = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _resetInput() {
|
||||
if (widget.onReset != null) widget.onReset!();
|
||||
_editTo = null;
|
||||
@ -239,7 +274,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
var insertText = '';
|
||||
|
||||
if (suggestion.type == 'emotes') {
|
||||
insertText = suggestion.content;
|
||||
insertText = '${suggestion.content} ';
|
||||
startText = replaceText.replaceFirstMapped(
|
||||
RegExp(r':(?:([-\w]+)~)?([-\w]+)$'),
|
||||
(Match m) => insertText,
|
||||
@ -247,7 +282,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
}
|
||||
|
||||
if (suggestion.type == 'users') {
|
||||
insertText = suggestion.content;
|
||||
insertText = '${suggestion.content} ';
|
||||
startText = replaceText.replaceFirstMapped(
|
||||
RegExp(r'(?:\s|^)@([-\w]+)$'),
|
||||
(Match m) => insertText,
|
||||
@ -269,6 +304,20 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController.addListener(_pingEnterMessageStatus);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.removeListener(_pingEnterMessageStatus);
|
||||
_textController.dispose();
|
||||
_typingNotifyTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifyBannerActions = [
|
||||
|
58
lib/widgets/chat/chat_typing_indicator.dart
Normal file
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
|
||||
class ChatTypingIndicator extends StatefulWidget {
|
||||
final List<ChannelMember> users;
|
||||
|
||||
const ChatTypingIndicator({super.key, required this.users});
|
||||
|
||||
@override
|
||||
State<ChatTypingIndicator> createState() => _ChatTypingIndicatorState();
|
||||
}
|
||||
|
||||
class _ChatTypingIndicatorState extends State<ChatTypingIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
vsync: this,
|
||||
);
|
||||
late final Animation<double> _animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ChatTypingIndicator oldWidget) {
|
||||
if (widget.users.isNotEmpty) {
|
||||
_controller.animateTo(1);
|
||||
} else {
|
||||
_controller.animateTo(0);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizeTransition(
|
||||
sizeFactor: _animation,
|
||||
axis: Axis.vertical,
|
||||
axisAlignment: -1,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.more_horiz),
|
||||
const SizedBox(width: 6),
|
||||
Text('typingMessage'.trParams({
|
||||
'user': widget.users.map((x) => x.account.nick).join(', '),
|
||||
})),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -17,8 +18,36 @@ class LinkExpansion extends StatelessWidget {
|
||||
return SvgPicture.network(url, width: width, height: height);
|
||||
}
|
||||
return PlatformInfo.canCacheImage
|
||||
? CachedNetworkImage(imageUrl: url, width: width, height: height)
|
||||
: Image.network(url, width: width, height: height);
|
||||
? CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
width: width,
|
||||
height: height,
|
||||
errorWidget: (context, url, error) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Center(
|
||||
child: const Icon(Icons.close, size: 32)
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.network(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Center(
|
||||
child: const Icon(Icons.close, size: 32)
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -13,7 +13,7 @@ import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/account_status_action.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation_region.dart';
|
||||
|
||||
class AppNavigationDrawer extends StatefulWidget {
|
||||
final String? routeName;
|
||||
@ -24,7 +24,23 @@ class AppNavigationDrawer extends StatefulWidget {
|
||||
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
|
||||
}
|
||||
|
||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
||||
with TickerProviderStateMixin {
|
||||
bool _isCollapsed = false;
|
||||
|
||||
late final AnimationController _drawerAnimationController =
|
||||
AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
late final Animation<double> _drawerAnimation = Tween<double>(
|
||||
begin: 80.0,
|
||||
end: 304.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _drawerAnimationController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
AccountStatus? _accountStatus;
|
||||
|
||||
Future<void> _getStatus() async {
|
||||
@ -40,34 +56,19 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
}
|
||||
}
|
||||
|
||||
void _closeDrawer() {
|
||||
rootScaffoldKey.currentState!.closeDrawer();
|
||||
}
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getStatus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget _buildUserInfo() {
|
||||
return Obx(() {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
return Drawer(
|
||||
backgroundColor:
|
||||
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Obx(() {
|
||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
||||
leading: const Icon(Icons.account_circle),
|
||||
title: Text('guest'.tr),
|
||||
subtitle: Text('unsignedIn'.tr),
|
||||
if (_isCollapsed) {
|
||||
return InkWell(
|
||||
child: const Icon(Icons.account_circle).paddingSymmetric(
|
||||
horizontal: 28,
|
||||
vertical: 20,
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
_closeDrawer();
|
||||
@ -76,37 +77,24 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||
title: Text(
|
||||
auth.userProfile.value!['nick'],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
subtitle: Builder(
|
||||
builder: (context) {
|
||||
if (_accountStatus == null) {
|
||||
return Text('loading'.tr);
|
||||
}
|
||||
final info = StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
);
|
||||
return Text(
|
||||
info.$3,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
);
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
||||
leading: const Icon(Icons.account_circle),
|
||||
title: !_isCollapsed ? Text('guest'.tr) : null,
|
||||
subtitle: !_isCollapsed ? Text('unsignedIn'.tr) : null,
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
leading: Obx(() {
|
||||
);
|
||||
}
|
||||
|
||||
final leading = Obx(() {
|
||||
final statusBadgeColor = _accountStatus != null
|
||||
? StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
).$2
|
||||
? StatusProvider.determineStatus(_accountStatus!).$2
|
||||
: Colors.grey;
|
||||
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final accountNotifications =
|
||||
relations.friendRequestCount.value;
|
||||
final accountNotifications = relations.friendRequestCount.value;
|
||||
|
||||
return badges.Badge(
|
||||
badgeContent: Text(
|
||||
@ -120,8 +108,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
),
|
||||
child: badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle:
|
||||
badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
@ -131,7 +118,47 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
return InkWell(
|
||||
child: !_isCollapsed
|
||||
? Row(
|
||||
children: [
|
||||
leading,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
auth.userProfile.value!['nick'],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).paddingOnly(left: 16),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (_accountStatus == null) {
|
||||
return Text('loading'.tr).paddingOnly(left: 16);
|
||||
}
|
||||
final info = StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
);
|
||||
return Text(
|
||||
info.$3,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
color: _unFocusColor,
|
||||
),
|
||||
).paddingOnly(left: 16);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 20, vertical: 16)
|
||||
: leading.paddingSymmetric(horizontal: 20, vertical: 16),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed('account');
|
||||
_closeDrawer();
|
||||
@ -148,17 +175,93 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
});
|
||||
},
|
||||
);
|
||||
}).paddingSymmetric(vertical: 8),
|
||||
});
|
||||
}
|
||||
|
||||
void _expandDrawer() {
|
||||
_drawerAnimationController.animateTo(1);
|
||||
}
|
||||
|
||||
void _collapseDrawer() {
|
||||
_drawerAnimationController.animateTo(0);
|
||||
}
|
||||
|
||||
void _closeDrawer() {
|
||||
_autoResize();
|
||||
rootScaffoldKey.currentState!.closeDrawer();
|
||||
}
|
||||
|
||||
void _autoResize() {
|
||||
if (SolianTheme.isExtraLargeScreen(context)) {
|
||||
_expandDrawer();
|
||||
} else if (SolianTheme.isLargeScreen(context)) {
|
||||
_collapseDrawer();
|
||||
} else {
|
||||
_drawerAnimationController.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getStatus();
|
||||
Future.delayed(Duration.zero, () => _autoResize());
|
||||
_drawerAnimationController.addListener(() {
|
||||
if (_drawerAnimation.value > 180 && _isCollapsed) {
|
||||
setState(() => _isCollapsed = false);
|
||||
} else if (_drawerAnimation.value < 180 && !_isCollapsed) {
|
||||
setState(() => _isCollapsed = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_drawerAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _drawerAnimation,
|
||||
builder: (context, child) {
|
||||
return Drawer(
|
||||
width: _drawerAnimation.value,
|
||||
backgroundColor:
|
||||
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildUserInfo().paddingSymmetric(vertical: 8),
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Column(
|
||||
children: AppNavigation.destinations
|
||||
.map(
|
||||
(e) => ListTile(
|
||||
(e) => _isCollapsed
|
||||
? Tooltip(
|
||||
message: e.label,
|
||||
child: InkWell(
|
||||
child: Icon(e.icon, size: 20).paddingSymmetric(
|
||||
horizontal: 28,
|
||||
vertical: 16,
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed(e.page);
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
)
|
||||
: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
leading: Icon(e.icon, size: 20).paddingAll(2),
|
||||
title: Text(e.label),
|
||||
title: !_isCollapsed ? Text(e.label) : null,
|
||||
enabled: true,
|
||||
onTap: () {
|
||||
AppRouter.instance.goNamed(e.page);
|
||||
@ -167,10 +270,11 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Expanded(
|
||||
child: AppNavigationRegions(
|
||||
child: AppNavigationRegion(
|
||||
isCollapsed: _isCollapsed,
|
||||
onSelected: (item) {
|
||||
_closeDrawer();
|
||||
},
|
||||
@ -179,6 +283,24 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Column(
|
||||
children: [
|
||||
if (_isCollapsed)
|
||||
Tooltip(
|
||||
message: 'settings'.tr,
|
||||
child: InkWell(
|
||||
child: const Icon(
|
||||
Icons.settings,
|
||||
size: 20,
|
||||
).paddingSymmetric(
|
||||
horizontal: 28,
|
||||
vertical: 10,
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
minTileHeight: 0,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@ -191,6 +313,33 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
if (_isCollapsed)
|
||||
Tooltip(
|
||||
message: 'expand'.tr,
|
||||
child: InkWell(
|
||||
child: const Icon(Icons.chevron_right, size: 20)
|
||||
.paddingSymmetric(
|
||||
horizontal: 28,
|
||||
vertical: 10,
|
||||
),
|
||||
onTap: () {
|
||||
_expandDrawer();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
ListTile(
|
||||
minTileHeight: 0,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
leading:
|
||||
const Icon(Icons.chevron_left, size: 20).paddingAll(2),
|
||||
title: Text('collapse'.tr),
|
||||
onTap: () {
|
||||
_collapseDrawer();
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: 8,
|
||||
|
@ -5,10 +5,15 @@ import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class AppNavigationRegions extends StatelessWidget {
|
||||
class AppNavigationRegion extends StatelessWidget {
|
||||
final bool isCollapsed;
|
||||
final Function(Channel item) onSelected;
|
||||
|
||||
const AppNavigationRegions({super.key, required this.onSelected});
|
||||
const AppNavigationRegion({
|
||||
super.key,
|
||||
required this.onSelected,
|
||||
this.isCollapsed = false,
|
||||
});
|
||||
|
||||
void _gotoChannel(Channel item) {
|
||||
AppRouter.instance.pushReplacementNamed(
|
||||
@ -25,6 +30,16 @@ class AppNavigationRegions extends StatelessWidget {
|
||||
Widget _buildEntry(BuildContext context, Channel item) {
|
||||
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||
|
||||
if (isCollapsed) {
|
||||
return InkWell(
|
||||
child: const Icon(Icons.tag_outlined, size: 20).paddingSymmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
onTap: () => _gotoChannel(item),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
minTileHeight: 0,
|
||||
leading: const Icon(Icons.tag_outlined),
|
||||
@ -51,6 +66,27 @@ class AppNavigationRegions extends StatelessWidget {
|
||||
.where((x) => x.type == 0 && x.realmId != null)
|
||||
.toList();
|
||||
|
||||
if (isCollapsed) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||
SliverList.builder(
|
||||
itemCount:
|
||||
noRealmGroupChannels.length + hasRealmGroupChannels.length,
|
||||
itemBuilder: (context, index) {
|
||||
final element = index >= noRealmGroupChannels.length
|
||||
? hasRealmGroupChannels[index - noRealmGroupChannels.length]
|
||||
: noRealmGroupChannels[index];
|
||||
return Tooltip(
|
||||
message: element.name,
|
||||
child: _buildEntry(context, element),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
90
pubspec.lock
@ -38,6 +38,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -278,6 +286,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -715,14 +731,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.17+1"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
|
||||
sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.21"
|
||||
version: "2.0.22"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -865,10 +889,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
|
||||
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.3"
|
||||
version: "14.2.7"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -885,6 +909,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.4"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -953,10 +985,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56"
|
||||
sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+12"
|
||||
version: "0.8.12+13"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1153,10 +1185,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit
|
||||
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a"
|
||||
sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.10+1"
|
||||
version: "1.1.11"
|
||||
media_kit_libs_android_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1193,34 +1225,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_libs_video
|
||||
sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067"
|
||||
sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
media_kit_libs_windows_video:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_libs_windows_video
|
||||
sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122"
|
||||
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
version: "1.0.10"
|
||||
media_kit_native_event_loop:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: media_kit_native_event_loop
|
||||
sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e
|
||||
sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.0.9"
|
||||
media_kit_video:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: media_kit_video
|
||||
sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882
|
||||
sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.4"
|
||||
version: "1.2.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1385,10 +1417,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: d220eb8476b466d58b161e10b3001d93999010a26228a3fb89c4280db1249546
|
||||
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+1"
|
||||
version: "0.1.3+2"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1481,10 +1513,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process_run
|
||||
sha256: c917dfb5f7afad4c7485bc00a4df038621248fce046105020cea276d1a87c820
|
||||
sha256: "112a77da35be50617ed9e2230df68d0817972f225e7f97ce8336f76b4e601606"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1665,10 +1697,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
|
||||
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1906,6 +1938,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
universal_io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1934,10 +1974,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
|
||||
sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.9"
|
||||
version: "6.3.10"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2140,4 +2180,4 @@ packages:
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
10
pubspec.yaml
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.2.1+20
|
||||
version: 1.2.1+24
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -85,6 +85,7 @@ dev_dependencies:
|
||||
build_runner: ^2.1.2
|
||||
sqflite_common_ffi: ^2.3.3
|
||||
sqflite_common_ffi_web: ^0.4.3+1
|
||||
flutter_native_splash: ^2.4.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
@ -128,7 +129,7 @@ flutter_launcher_icons:
|
||||
generate: true
|
||||
image_path: "assets/icon-w-shadow.png"
|
||||
background_color: "#ffffff"
|
||||
theme_color: "#4b5094"
|
||||
theme_color: "#92645d"
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "assets/icon-w-shadow.png"
|
||||
@ -137,5 +138,10 @@ flutter_launcher_icons:
|
||||
generate: true
|
||||
image_path: "assets/icon-w-shadow.png"
|
||||
|
||||
flutter_native_splash:
|
||||
color: "#fef8f7"
|
||||
color_dark: "#191110"
|
||||
image: assets/icon-w-shadow.png
|
||||
|
||||
msix_config:
|
||||
protocol_activation: solink
|
||||
|
115
web/index.html
@ -1,6 +1,4 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!DOCTYPE html><html><head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
@ -14,24 +12,24 @@
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF" />
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
|
||||
<meta name="description" content="A new Flutter project." />
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="solian" />
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="solian">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Cropper.js -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.css" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
|
||||
<!-- Loading styles -->
|
||||
<style>
|
||||
@ -68,14 +66,95 @@
|
||||
</style>
|
||||
|
||||
<title>Solian</title>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
</head>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
<style id="splash-screen-style">
|
||||
html {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background-color: #fef8f7;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.contain {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stretch {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
}
|
||||
|
||||
.cover {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, 0);
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.bottomLeft {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.bottomRight {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #191110;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script id="splash-screen-script">
|
||||
function removeSplashFromWeb() {
|
||||
document.getElementById("splash")?.remove();
|
||||
document.getElementById("splash-branding")?.remove();
|
||||
document.body.style.background = "transparent";
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<picture id="splash">
|
||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
||||
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||
</picture>
|
||||
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
|
||||
|
||||
</body></html>
|
BIN
web/splash/img/dark-1x.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
web/splash/img/dark-2x.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
web/splash/img/dark-3x.png
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
web/splash/img/dark-4x.png
Normal file
After Width: | Height: | Size: 355 KiB |
BIN
web/splash/img/light-1x.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
web/splash/img/light-2x.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
web/splash/img/light-3x.png
Normal file
After Width: | Height: | Size: 233 KiB |
BIN
web/splash/img/light-4x.png
Normal file
After Width: | Height: | Size: 355 KiB |