Compare commits
20 Commits
4238ea6fdc
...
1.2.1+2
| Author | SHA1 | Date | |
|---|---|---|---|
| 19eabfaba1 | |||
| ec2eadad6d | |||
| 54e176e75d | |||
| 0a7ccaeefa | |||
| a5f093e185 | |||
| a4f68dd175 | |||
| 8067c35c70 | |||
| ebe381053e | |||
| 03f2470dae | |||
| ea434815cf | |||
| bbea4b4359 | |||
| e0b485cc81 | |||
| 87bb37ac01 | |||
| 989b5babd9 | |||
| 9ea364640d | |||
| a9f55a489d | |||
| 4616f3a3e2 | |||
| 425bae9d13 | |||
| 07771e8979 | |||
| 0ad4854443 |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 406 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 360 KiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 360 KiB |
@@ -113,7 +113,7 @@ PODS:
|
||||
- TOCropViewController (~> 2.7.4)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- livekit_client (2.2.2):
|
||||
- livekit_client (2.2.3):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
@@ -295,7 +295,7 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
livekit_client: c767049a635d5b6d43de3273dca3c439b8a6e970
|
||||
livekit_client: bad83a7776a41abc42e1f26d903eeac9164c8a9f
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 815 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
@@ -24,6 +26,7 @@ class BootstrapperShell extends StatefulWidget {
|
||||
class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
bool _isBusy = true;
|
||||
bool _isErrored = false;
|
||||
bool _isDismissable = true;
|
||||
String? _subtitle;
|
||||
|
||||
Color get _unFocusColor =>
|
||||
@@ -38,6 +41,32 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
await context.read<ThemeSwitcher>().restoreTheme();
|
||||
},
|
||||
),
|
||||
(
|
||||
label: 'bsCheckForUpdate',
|
||||
action: () async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||
final resp = await GetConnect().get(
|
||||
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?limit=1',
|
||||
);
|
||||
if (resp.body[0]['name'] != localVersionString) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
|
||||
? 'bsCheckForUpdateDescApple'.tr
|
||||
: 'bsCheckForUpdateDescCommon'.tr;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckForUpdateFailed'.tr;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
label: 'bsCheckingServer',
|
||||
action: () async {
|
||||
@@ -47,12 +76,14 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckingServerDown'.tr;
|
||||
_isDismissable = false;
|
||||
});
|
||||
throw Exception('unable connect to server');
|
||||
} else if (resp.statusCode == null) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckingServerFail'.tr;
|
||||
_isDismissable = false;
|
||||
});
|
||||
throw Exception('unable connect to server');
|
||||
}
|
||||
@@ -81,13 +112,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
label: 'bsPreparingData',
|
||||
action: () async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isTrue) {
|
||||
await Future.wait([
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
await Future.wait([
|
||||
Get.find<StickerProvider>().refreshAvailableStickers(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
||||
Get.find<RelationshipProvider>().refreshFriendList(),
|
||||
]);
|
||||
}
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
]);
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -111,7 +144,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
try {
|
||||
for (var idx = 0; idx < _periods.length; idx++) {
|
||||
await _periods[idx].action();
|
||||
if (_isErrored) break;
|
||||
if (_isErrored && !_isDismissable) break;
|
||||
if (_periodCursor < _periods.length - 1) {
|
||||
setState(() => _periodCursor++);
|
||||
}
|
||||
@@ -140,17 +173,20 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
height: 280,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Image.asset('assets/logo.png', width: 80, height: 80)
|
||||
.animate(onPlay: (c) => c.repeat())
|
||||
.rotate(duration: 850.ms, curve: Curves.easeInOut),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isErrored)
|
||||
const Icon(Icons.cancel, size: 24)
|
||||
else
|
||||
if (_isErrored && !_isDismissable && !_isBusy)
|
||||
const Icon(Icons.cancel, size: 24),
|
||||
if (_isErrored && _isDismissable && !_isBusy)
|
||||
const Icon(Icons.warning, size: 24),
|
||||
if ((_isErrored && _isDismissable && _isBusy) || _isBusy)
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
@@ -161,15 +197,24 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
maxWidth: 280,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
_subtitle ??
|
||||
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
if (_subtitle == null)
|
||||
Text(
|
||||
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_subtitle != null)
|
||||
Text(
|
||||
_subtitle!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
).paddingOnly(bottom: 4),
|
||||
Text(
|
||||
'2024 © Solsynth LLC',
|
||||
textAlign: TextAlign.center,
|
||||
@@ -185,12 +230,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
),
|
||||
onTap: () {
|
||||
if (_isBusy) return;
|
||||
setState(() {
|
||||
_isBusy = true;
|
||||
_isErrored = false;
|
||||
_periodCursor = 0;
|
||||
});
|
||||
_runPeriods();
|
||||
if (_isDismissable) {
|
||||
setState(() {
|
||||
_isBusy = false;
|
||||
_isErrored = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isBusy = true;
|
||||
_isErrored = false;
|
||||
_periodCursor = 0;
|
||||
});
|
||||
_runPeriods();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
|
||||
@@ -30,6 +30,23 @@ extension SolianExtenions on BuildContext {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showInfoDialog(String title, body) {
|
||||
return showDialog<void>(
|
||||
useRootNavigator: true,
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(body),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: Text('okay'.tr),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showErrorDialog(dynamic exception) {
|
||||
var stack = StackTrace.current;
|
||||
var stackTrace = '$stack';
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/firebase_options.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
@@ -120,6 +121,7 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => AuthProvider());
|
||||
Get.lazyPut(() => RelationshipProvider());
|
||||
Get.lazyPut(() => PostProvider());
|
||||
Get.lazyPut(() => StickerProvider());
|
||||
Get.lazyPut(() => AttachmentProvider());
|
||||
Get.lazyPut(() => WebSocketProvider());
|
||||
Get.lazyPut(() => StatusProvider());
|
||||
|
||||
122
lib/models/stickers.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
|
||||
class Sticker {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String alias;
|
||||
String name;
|
||||
int attachmentId;
|
||||
Attachment attachment;
|
||||
int packId;
|
||||
StickerPack? pack;
|
||||
int accountId;
|
||||
Account account;
|
||||
|
||||
Sticker({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.alias,
|
||||
required this.name,
|
||||
required this.attachmentId,
|
||||
required this.attachment,
|
||||
required this.packId,
|
||||
required this.pack,
|
||||
required this.accountId,
|
||||
required this.account,
|
||||
});
|
||||
|
||||
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
|
||||
id: json['id'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'] != null
|
||||
? DateTime.parse(json['deleted_at'])
|
||||
: json['deleted_at'],
|
||||
alias: json['alias'],
|
||||
name: json['name'],
|
||||
attachmentId: json['attachment_id'],
|
||||
attachment: Attachment.fromJson(json['attachment']),
|
||||
packId: json['pack_id'],
|
||||
pack: json['pack'] != null ? StickerPack.fromJson(json['pack']) : null,
|
||||
accountId: json['account_id'],
|
||||
account: Account.fromJson(json['account']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt?.toIso8601String(),
|
||||
'alias': alias,
|
||||
'name': name,
|
||||
'attachment_id': attachmentId,
|
||||
'attachment': attachment.toJson(),
|
||||
'pack_id': packId,
|
||||
'account_id': accountId,
|
||||
'account': account.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class StickerPack {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String prefix;
|
||||
String name;
|
||||
String description;
|
||||
List<Sticker>? stickers;
|
||||
int accountId;
|
||||
Account account;
|
||||
|
||||
StickerPack({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.prefix,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.stickers,
|
||||
required this.accountId,
|
||||
required this.account,
|
||||
});
|
||||
|
||||
factory StickerPack.fromJson(Map<String, dynamic> json) => StickerPack(
|
||||
id: json['id'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'] != null
|
||||
? DateTime.parse(json['deleted_at'])
|
||||
: json['deleted_at'],
|
||||
prefix: json['prefix'],
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
stickers: json['stickers'] == null
|
||||
? []
|
||||
: List<Sticker>.from(
|
||||
json['stickers']!.map((x) => Sticker.fromJson(x))),
|
||||
accountId: json['account_id'],
|
||||
account: Account.fromJson(json['account']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt?.toIso8601String(),
|
||||
'prefix': prefix,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'stickers': stickers == null
|
||||
? []
|
||||
: List<dynamic>.from(stickers!.map((x) => x.toJson())),
|
||||
'account_id': accountId,
|
||||
'account': account.toJson(),
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class AttachmentUploadTask {
|
||||
double progress = 0;
|
||||
bool isUploading = false;
|
||||
bool isCompleted = false;
|
||||
dynamic error;
|
||||
|
||||
AttachmentUploadTask({
|
||||
required this.file,
|
||||
@@ -66,7 +67,7 @@ class AttachmentUploaderController extends GetxController {
|
||||
queueOfUpload.remove(task);
|
||||
}
|
||||
|
||||
Future<Attachment> performSingleTask(int queueIndex) async {
|
||||
Future<Attachment?> performSingleTask(int queueIndex) async {
|
||||
isUploading.value = true;
|
||||
progressOfUpload.value = 0;
|
||||
|
||||
@@ -83,9 +84,15 @@ class AttachmentUploaderController extends GetxController {
|
||||
queueOfUpload[queueIndex].progress = value;
|
||||
_progressOfUpload = value;
|
||||
},
|
||||
onError: (err) {
|
||||
queueOfUpload[queueIndex].error = err;
|
||||
queueOfUpload[queueIndex].isUploading = false;
|
||||
},
|
||||
);
|
||||
|
||||
queueOfUpload.removeAt(queueIndex);
|
||||
if (queueOfUpload[queueIndex].error == null) {
|
||||
queueOfUpload.removeAt(queueIndex);
|
||||
}
|
||||
_stopProgressSyncTimer();
|
||||
_syncProgress();
|
||||
|
||||
@@ -103,6 +110,10 @@ class AttachmentUploaderController extends GetxController {
|
||||
_startProgressSyncTimer();
|
||||
|
||||
for (var idx = 0; idx < queueOfUpload.length; idx++) {
|
||||
if (queueOfUpload[idx].isUploading || queueOfUpload[idx].error != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
queueOfUpload[idx].isUploading = true;
|
||||
|
||||
final task = queueOfUpload[idx];
|
||||
@@ -115,15 +126,20 @@ class AttachmentUploaderController extends GetxController {
|
||||
queueOfUpload[idx].progress = value;
|
||||
_progressOfUpload = (idx + value) / queueOfUpload.length;
|
||||
},
|
||||
onError: (err) {
|
||||
queueOfUpload[idx].error = err;
|
||||
queueOfUpload[idx].isUploading = false;
|
||||
},
|
||||
);
|
||||
_progressOfUpload = (idx + 1) / queueOfUpload.length;
|
||||
onData(result);
|
||||
if (result != null) onData(result);
|
||||
|
||||
queueOfUpload[idx].isUploading = false;
|
||||
queueOfUpload[idx].isCompleted = false;
|
||||
queueOfUpload[idx].isCompleted = true;
|
||||
}
|
||||
|
||||
queueOfUpload.clear();
|
||||
queueOfUpload.value =
|
||||
queueOfUpload.where((x) => x.error == null).toList(growable: true);
|
||||
_stopProgressSyncTimer();
|
||||
_syncProgress();
|
||||
|
||||
@@ -135,7 +151,7 @@ class AttachmentUploaderController extends GetxController {
|
||||
String path,
|
||||
String usage,
|
||||
Map<String, dynamic>? metadata,
|
||||
Function(Attachment) callback,
|
||||
Function(Attachment?) callback,
|
||||
) async {
|
||||
if (isUploading.value) throw Exception('uploading blocked');
|
||||
|
||||
@@ -153,7 +169,7 @@ class AttachmentUploaderController extends GetxController {
|
||||
callback(result);
|
||||
}
|
||||
|
||||
Future<Attachment> uploadAttachment(
|
||||
Future<Attachment?> uploadAttachment(
|
||||
Uint8List data,
|
||||
String path,
|
||||
String usage,
|
||||
@@ -175,9 +191,9 @@ class AttachmentUploaderController extends GetxController {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Attachment> _rawUploadAttachment(
|
||||
Future<Attachment?> _rawUploadAttachment(
|
||||
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
|
||||
{Function(double)? onProgress}) async {
|
||||
{Function(double)? onProgress, Function(dynamic err)? onError}) async {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
try {
|
||||
final result = await provider.createAttachment(
|
||||
@@ -189,7 +205,10 @@ class AttachmentUploaderController extends GetxController {
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
if (onError != null) {
|
||||
onError(err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/get_rx.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:solian/models/call.dart';
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:dio/dio.dart' as dio;
|
||||
@@ -21,6 +22,50 @@ class AttachmentProvider extends GetConnect {
|
||||
|
||||
final Map<int, Attachment> _cachedResponses = {};
|
||||
|
||||
Future<List<Attachment?>> listMetadata(
|
||||
List<int> id, {
|
||||
noCache = false,
|
||||
}) async {
|
||||
if (id.isEmpty) return List.empty();
|
||||
|
||||
List<Attachment?> result = List.filled(id.length, null);
|
||||
List<int> pendingQuery = List.empty(growable: true);
|
||||
if (!noCache) {
|
||||
for (var idx = 0; idx < id.length; idx++) {
|
||||
if (_cachedResponses.containsKey(id[idx])) {
|
||||
result[idx] = _cachedResponses[id[idx]];
|
||||
} else {
|
||||
pendingQuery.add(id[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final resp = await get(
|
||||
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
||||
);
|
||||
if (resp.statusCode != 200) return result;
|
||||
|
||||
final rawOut = PaginationResult.fromJson(resp.body);
|
||||
if (rawOut.data == null) return result;
|
||||
|
||||
final List<Attachment> out =
|
||||
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
|
||||
for (final item in out) {
|
||||
if (item.destination != 0 && item.isAnalyzed) {
|
||||
_cachedResponses[item.id] = item;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
for (var j = 0; j < id.length; j++) {
|
||||
if (out[i].id == id[j]) {
|
||||
result[j] = out[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Attachment?> getMetadata(int id, {noCache = false}) async {
|
||||
if (!noCache && _cachedResponses.containsKey(id)) {
|
||||
return _cachedResponses[id]!;
|
||||
|
||||
@@ -4,15 +4,19 @@ import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class RelationshipProvider extends GetxController {
|
||||
final RxInt friendRequestCount = 0.obs;
|
||||
|
||||
final RxList<Relationship> _friends = RxList.empty(growable: true);
|
||||
|
||||
Future<void> refreshFriendList() async {
|
||||
final resp = await listRelationWithStatus(1);
|
||||
_friends.value = resp.body
|
||||
Future<void> refreshRelativeList() async {
|
||||
final resp = await listRelation();
|
||||
final List<Relationship> result = resp.body
|
||||
.map((e) => Relationship.fromJson(e))
|
||||
.toList()
|
||||
.cast<Relationship>();
|
||||
_friends.value = result.where((x) => x.status == 1).toList();
|
||||
_friends.refresh();
|
||||
friendRequestCount.value = result.where((x) => x.status == 0).length;
|
||||
}
|
||||
|
||||
bool hasFriend(Account account) {
|
||||
|
||||
38
lib/providers/stickers.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/stickers.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class StickerProvider extends GetxController {
|
||||
final RxMap<String, String> aliasImageMapping = RxMap();
|
||||
final RxMap<String, List<Sticker>> availableStickers = RxMap();
|
||||
|
||||
Future<void> refreshAvailableStickers() async {
|
||||
final client = ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/stickers/manifest?take=100',
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
|
||||
if (out == null) return;
|
||||
|
||||
for (final pack in out) {
|
||||
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
|
||||
sticker.pack = pack;
|
||||
final imageUrl = ServiceFinder.buildUrl(
|
||||
'files',
|
||||
'/attachments/${sticker.attachmentId}',
|
||||
);
|
||||
aliasImageMapping['${pack.prefix}${sticker.alias}'.camelCase!] =
|
||||
imageUrl;
|
||||
if (availableStickers[pack.prefix] == null) {
|
||||
availableStickers[pack.prefix] = List.empty(growable: true);
|
||||
}
|
||||
availableStickers[pack.prefix]!.add(sticker);
|
||||
}
|
||||
}
|
||||
}
|
||||
availableStickers.refresh();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/account/friend.dart';
|
||||
import 'package:solian/screens/account/personalize.dart';
|
||||
import 'package:solian/screens/account/profile_page.dart';
|
||||
import 'package:solian/screens/account/stickers.dart';
|
||||
import 'package:solian/screens/channel/channel_chat.dart';
|
||||
import 'package:solian/screens/channel/channel_detail.dart';
|
||||
import 'package:solian/screens/channel/channel_organize.dart';
|
||||
@@ -226,6 +227,14 @@ abstract class AppRouter {
|
||||
name: 'accountFriend',
|
||||
builder: (context, state) => const FriendScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/stickers',
|
||||
name: 'accountStickers',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const StickerScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/personalize',
|
||||
name: 'accountPersonalize',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@@ -18,9 +17,11 @@ class AboutScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/logo.png', width: 64, height: 64)
|
||||
.animate(onPlay: (c) => c.repeat())
|
||||
.rotate(duration: 1000.ms),
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
@@ -56,10 +57,9 @@ class AboutScreen extends StatelessWidget {
|
||||
applicationVersion: '${info.version} (${info.buildNumber})',
|
||||
applicationLegalese:
|
||||
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||
applicationIcon: Image.asset(
|
||||
'assets/logo.png',
|
||||
width: 56,
|
||||
height: 56,
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 60, height: 60),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -3,11 +3,13 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/widgets/account/account_heading.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
|
||||
class AccountScreen extends StatefulWidget {
|
||||
const AccountScreen({super.key});
|
||||
@@ -23,9 +25,32 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
(
|
||||
const Icon(Icons.color_lens),
|
||||
'accountPersonalize'.tr,
|
||||
'accountPersonalize'
|
||||
'accountPersonalize',
|
||||
),
|
||||
(
|
||||
Obx(() {
|
||||
final RelationshipProvider relations = Get.find();
|
||||
return badges.Badge(
|
||||
badgeContent: Text(
|
||||
relations.friendRequestCount.value.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
showBadge: relations.friendRequestCount.value > 0,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -12,
|
||||
end: -8,
|
||||
),
|
||||
child: const Icon(Icons.diversity_1),
|
||||
);
|
||||
}),
|
||||
'accountFriend'.tr,
|
||||
'accountFriend',
|
||||
),
|
||||
(
|
||||
const Icon(Icons.emoji_symbols),
|
||||
'accountStickers'.tr,
|
||||
'accountStickers',
|
||||
),
|
||||
(const Icon(Icons.diversity_1), 'accountFriend'.tr, 'accountFriend'),
|
||||
];
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/relative_list.dart';
|
||||
|
||||
class FriendScreen extends StatefulWidget {
|
||||
@@ -21,15 +21,15 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
|
||||
List<Relationship> _relations = List.empty();
|
||||
|
||||
List<Relationship> filterByStatus(int status) {
|
||||
List<Relationship> _filterByStatus(int status) {
|
||||
return _relations.where((x) => x.status == status).toList();
|
||||
}
|
||||
|
||||
Future<void> loadRelations() async {
|
||||
Future<void> _loadRelations() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final RelationshipProvider provider = Get.find();
|
||||
final resp = await provider.listRelation();
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final resp = await relations.listRelation();
|
||||
|
||||
setState(() {
|
||||
_relations = resp.body
|
||||
@@ -38,9 +38,12 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
.cast<Relationship>();
|
||||
_isBusy = false;
|
||||
});
|
||||
|
||||
relations.friendRequestCount.value =
|
||||
_relations.where((x) => x.status == 0).length;
|
||||
}
|
||||
|
||||
void promptAddFriend() async {
|
||||
void _promptAddFriend() async {
|
||||
final RelationshipProvider provider = Get.find();
|
||||
|
||||
final controller = TextEditingController();
|
||||
@@ -104,8 +107,8 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
loadRelations().then((_) {
|
||||
if (filterByStatus(0).isEmpty) {
|
||||
_loadRelations().then((_) {
|
||||
if (_filterByStatus(0).isEmpty) {
|
||||
_tabController.animateTo(1);
|
||||
}
|
||||
});
|
||||
@@ -119,6 +122,19 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
title: Text('accountFriend'.tr),
|
||||
actions: [
|
||||
if (_isBusy)
|
||||
SizedBox(
|
||||
height: 48,
|
||||
width: 48,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
).paddingAll(14),
|
||||
),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
@@ -130,52 +146,40 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () => promptAddFriend(),
|
||||
onPressed: () => _promptAddFriend(),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () => loadRelations(),
|
||||
onRefresh: () => _loadRelations(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
SilverRelativeList(
|
||||
items: filterByStatus(0),
|
||||
onUpdate: () => loadRelations(),
|
||||
items: _filterByStatus(0),
|
||||
onUpdate: () => _loadRelations(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: () => loadRelations(),
|
||||
onRefresh: () => _loadRelations(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
SilverRelativeList(
|
||||
items: filterByStatus(1),
|
||||
onUpdate: () => loadRelations(),
|
||||
items: _filterByStatus(1),
|
||||
onUpdate: () => _loadRelations(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: () => loadRelations(),
|
||||
onRefresh: () => _loadRelations(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
SilverRelativeList(
|
||||
items: filterByStatus(3),
|
||||
onUpdate: () => loadRelations(),
|
||||
items: _filterByStatus(3),
|
||||
onUpdate: () => _loadRelations(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -86,11 +86,17 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
toolbarTitle: 'cropImage'.tr,
|
||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
||||
aspectRatioPresets: [CropAspectRatioPreset.square],
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'cropImage'.tr,
|
||||
aspectRatioPresets: [CropAspectRatioPreset.square],
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
WebUiSettings(
|
||||
context: context,
|
||||
@@ -346,3 +352,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
|
||||
@override
|
||||
(int, int)? get data => (16, 7);
|
||||
|
||||
@override
|
||||
String get name => '16x7';
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
List<Post> _pinnedPosts = List.empty();
|
||||
int _totalUpvote = 0, _totalDownvote = 0;
|
||||
|
||||
Future<void> getUserinfo() async {
|
||||
Future<void> _getUserinfo() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
var client = ServiceFinder.configureClient('auth');
|
||||
@@ -114,7 +114,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
}
|
||||
});
|
||||
|
||||
getUserinfo();
|
||||
_getUserinfo();
|
||||
getPinnedPosts();
|
||||
}
|
||||
|
||||
@@ -189,8 +189,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
: () async {
|
||||
setState(() => _isMakingFriend = true);
|
||||
try {
|
||||
await _relationshipProvider.makeFriend(widget.name);
|
||||
context.showSnackbar('accountFriendRequestSent'.tr);
|
||||
await _relationshipProvider
|
||||
.makeFriend(widget.name);
|
||||
context.showSnackbar(
|
||||
'accountFriendRequestSent'.tr,
|
||||
);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
} finally {
|
||||
@@ -274,6 +277,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: PostListEntryWidget(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
item: element,
|
||||
isClickable: true,
|
||||
isNestedClickable: true,
|
||||
|
||||
181
lib/screens/account/stickers.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/stickers.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/stickers/sticker_uploader.dart';
|
||||
|
||||
class StickerScreen extends StatefulWidget {
|
||||
const StickerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<StickerScreen> createState() => _StickerScreenState();
|
||||
}
|
||||
|
||||
class _StickerScreenState extends State<StickerScreen> {
|
||||
final PagingController<int, StickerPack> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
Future<bool> _promptDelete(Sticker item, String prefix) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return false;
|
||||
|
||||
final confirm = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('stickerDeletionConfirm'.tr),
|
||||
content: Text(
|
||||
'stickerDeletionConfirmCaption'.trParams({
|
||||
'name': ':${'$prefix${item.alias}'.camelCase}:',
|
||||
}),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm != true) return false;
|
||||
|
||||
final client = auth.configureClient('files');
|
||||
final resp = await client.delete('/stickers/${item.id}');
|
||||
|
||||
return resp.statusCode == 200;
|
||||
}
|
||||
|
||||
Future<bool?> _promptUploadSticker({Sticker? edit}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => StickerUploadDialog(
|
||||
edit: edit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmoteEntry(Sticker item, String prefix) {
|
||||
final imageUrl = ServiceFinder.buildUrl(
|
||||
'files',
|
||||
'/attachments/${item.attachmentId}',
|
||||
);
|
||||
return ListTile(
|
||||
title: Text(item.name),
|
||||
subtitle: Text(':${'$prefix${item.alias}'.camelCase}:'),
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 14),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_square),
|
||||
onPressed: () {
|
||||
_promptUploadSticker(edit: item).then((value) {
|
||||
if (value == true) _pagingController.refresh();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
_promptDelete(item, prefix).then((value) {
|
||||
if (value == true) _pagingController.refresh();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: PlatformInfo.canCacheImage
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: 28,
|
||||
height: 28,
|
||||
)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final AuthProvider auth = Get.find();
|
||||
final name = auth.userProfile.value!['name'];
|
||||
_pagingController.addPageRequestListener((pageKey) async {
|
||||
final client = ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/stickers/manifest?take=10&offset=$pageKey&author=$name',
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
|
||||
if (out != null && result.data!.length >= 10) {
|
||||
_pagingController.appendPage(out, pageKey + out.length);
|
||||
} else if (out != null) {
|
||||
_pagingController.appendLastPage(out);
|
||||
}
|
||||
} else {
|
||||
_pagingController.error = resp.bodyString;
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final StickerProvider sticker = Get.find();
|
||||
sticker.refreshAvailableStickers();
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
_promptUploadSticker().then((value) {
|
||||
if (value == true) _pagingController.refresh();
|
||||
});
|
||||
},
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PagedSliverList<int, StickerPack>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (BuildContext context, item, int index) {
|
||||
return ExpansionTile(
|
||||
title: Text(item.name),
|
||||
subtitle: Text(
|
||||
item.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
children: item.stickers
|
||||
?.map((x) => _buildEmoteEntry(x, item.prefix))
|
||||
.toList() ??
|
||||
List.empty(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -87,75 +87,77 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
|
||||
Widget _buildListLayout() {
|
||||
final ChatCallProvider call = Get.find();
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: call.focusTrack.value != null
|
||||
? InteractiveParticipantWidget(
|
||||
isFixed: false,
|
||||
participant: call.focusTrack.value!,
|
||||
onTap: () {},
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: SizedBox(
|
||||
height: 128,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, call.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final track = call.participantTracks[index];
|
||||
if (track.participant.sid ==
|
||||
call.focusTrack.value?.participant.sid) {
|
||||
return Container();
|
||||
}
|
||||
return Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: call.focusTrack.value != null
|
||||
? InteractiveParticipantWidget(
|
||||
isFixedAvatar: false,
|
||||
participant: call.focusTrack.value!,
|
||||
onTap: () {},
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: SizedBox(
|
||||
height: 128,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, call.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final track = call.participantTracks[index];
|
||||
if (track.participant.sid ==
|
||||
call.focusTrack.value?.participant.sid) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
isFixed: true,
|
||||
width: 120,
|
||||
height: 120,
|
||||
color: Theme.of(context).cardColor,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack.value?.participant.sid) {
|
||||
call.changeFocusTrack(track);
|
||||
}
|
||||
},
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
isFixedAvatar: true,
|
||||
width: 120,
|
||||
height: 120,
|
||||
color: Theme.of(context).cardColor,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack.value?.participant.sid) {
|
||||
call.changeFocusTrack(track);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridLayout() {
|
||||
final ChatCallProvider call = Get.find();
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double screenWidth = constraints.maxWidth;
|
||||
double screenHeight = constraints.maxHeight;
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
double screenWidth = constraints.maxWidth;
|
||||
double screenHeight = constraints.maxHeight;
|
||||
|
||||
int columns = (math.sqrt(call.participantTracks.length)).ceil();
|
||||
int rows = (call.participantTracks.length / columns).ceil();
|
||||
int columns = (math.sqrt(call.participantTracks.length)).ceil();
|
||||
int rows = (call.participantTracks.length / columns).ceil();
|
||||
|
||||
double tileWidth = screenWidth / columns;
|
||||
double tileHeight = screenHeight / rows;
|
||||
double tileWidth = screenWidth / columns;
|
||||
double tileHeight = screenHeight / rows;
|
||||
|
||||
return GridView.builder(
|
||||
return Obx(
|
||||
() => GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
childAspectRatio: tileWidth / tileHeight,
|
||||
@@ -165,25 +167,26 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
final track = call.participantTracks[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
isFixed: true,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack.value?.participant.sid) {
|
||||
call.changeFocusTrack(track);
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack.value?.participant.sid) {
|
||||
call.changeFocusTrack(track);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -234,31 +237,33 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
body: SafeArea(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Obx(
|
||||
() => Column(
|
||||
children: [
|
||||
SizeTransition(
|
||||
sizeFactor: _controlsAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 64,
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(child: SizedBox()),
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizeTransition(
|
||||
sizeFactor: _controlsAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 64,
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(child: SizedBox()),
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 10),
|
||||
),
|
||||
Expanded(
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
elevation: 2,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_layoutMode) {
|
||||
@@ -270,20 +275,20 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (provider.room.localParticipant != null)
|
||||
SizeTransition(
|
||||
sizeFactor: _controlsAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ControlsWidget(
|
||||
provider.room,
|
||||
provider.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
if (provider.room.localParticipant != null)
|
||||
SizeTransition(
|
||||
sizeFactor: _controlsAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ControlsWidget(
|
||||
provider.room,
|
||||
provider.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_toggleControls();
|
||||
|
||||
@@ -128,7 +128,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.edit == null) _editorController.localRead();
|
||||
if (widget.edit == null && widget.reply == null && widget.repost == null) {
|
||||
_editorController.localRead();
|
||||
}
|
||||
if (widget.reply != null) {
|
||||
_editorController.replyTo.value = widget.reply;
|
||||
}
|
||||
if (widget.repost != null) {
|
||||
_editorController.repostTo.value = widget.repost;
|
||||
}
|
||||
_editorController.contentController.addListener(() => setState(() {}));
|
||||
_syncWidget();
|
||||
}
|
||||
@@ -219,10 +227,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
children: [
|
||||
PostItem(
|
||||
item: _replyTo!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 280),
|
||||
child: SingleChildScrollView(
|
||||
child: PostItem(
|
||||
item: _replyTo!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_repostTo != null)
|
||||
@@ -237,10 +250,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
collapsedBackgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
children: [
|
||||
PostItem(
|
||||
item: _repostTo!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 280),
|
||||
child: SingleChildScrollView(
|
||||
child: PostItem(
|
||||
item: _repostTo!,
|
||||
isReactable: false,
|
||||
).paddingOnly(bottom: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -52,6 +52,7 @@ const i18nEnglish = {
|
||||
'account': 'Account',
|
||||
'accountPersonalize': 'Personalize',
|
||||
'accountPersonalizeApplied': 'Account personalize settings has been saved.',
|
||||
'accountStickers': 'Stickers',
|
||||
'accountFriend': 'Friend',
|
||||
'accountFriendNew': 'New friend',
|
||||
'accountFriendNewHint':
|
||||
@@ -162,9 +163,11 @@ const i18nEnglish = {
|
||||
'attachmentAutoUpload': 'Auto Upload',
|
||||
'attachmentUploadQueue': 'Upload Queue',
|
||||
'attachmentUploadQueueStart': 'Start All',
|
||||
'attachmentUploadInProgress': 'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...',
|
||||
'attachmentUploadInProgress':
|
||||
'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...',
|
||||
'attachmentAttached': 'Exists Files',
|
||||
'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...',
|
||||
'attachmentUploadBlocked':
|
||||
'Upload blocked, there is currently a task in progress...',
|
||||
'attachmentAdd': 'Attach attachments',
|
||||
'attachmentAddGalleryPhoto': 'Gallery photo',
|
||||
'attachmentAddGalleryVideo': 'Gallery video',
|
||||
@@ -173,7 +176,8 @@ const i18nEnglish = {
|
||||
'attachmentAddClipboard': 'Paste file',
|
||||
'attachmentAddFile': 'Attach file',
|
||||
'attachmentAddLink': 'Link attachments',
|
||||
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment',
|
||||
'attachmentAddLinkHint':
|
||||
'Enter attachment serial number to link that attachment',
|
||||
'attachmentAddLinkInput': 'Serial number',
|
||||
'attachmentSetting': 'Adjust attachment',
|
||||
'attachmentAlt': 'Alternative text',
|
||||
@@ -314,6 +318,13 @@ const i18nEnglish = {
|
||||
'accountStatusNeutral': 'Neutral',
|
||||
'accountStatusPositive': 'Positive',
|
||||
'bsLoadingTheme': 'Loading Theme',
|
||||
'bsCheckForUpdate': 'Checking For Updates',
|
||||
'bsCheckForUpdateFailed': 'Unable to Check Updates',
|
||||
'bsCheckForUpdateNew': 'Found New Version',
|
||||
'bsCheckForUpdateDescApple':
|
||||
'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.',
|
||||
'bsCheckForUpdateDescCommon':
|
||||
'Please head to our website download and install latest version of application to prevent error happens and get latest functions.',
|
||||
'bsCheckingServer': 'Checking Server Status',
|
||||
'bsCheckingServerFail':
|
||||
'Unable connect to server, check your network connection',
|
||||
@@ -331,7 +342,22 @@ const i18nEnglish = {
|
||||
'themeColorMiku': 'Miku Blue',
|
||||
'themeColorKagamine': 'Kagamine Yellow',
|
||||
'themeColorLuka': 'Luka Pink',
|
||||
'stickerDeletionConfirm': 'Confirm sticker delete',
|
||||
'stickerDeletionConfirmCaption':
|
||||
'Are you sure to delete sticker @name? This action cannot be undo.',
|
||||
'themeColorApplied': 'Global theme color has been applied.',
|
||||
'attachmentSaved': 'Attachment saved to your system album.',
|
||||
'cropImage': 'Crop Image',
|
||||
'stickerUploader': 'Upload sticker',
|
||||
'stickerUploaderAttachmentNew': 'Upload new attachment',
|
||||
'stickerUploaderAttachment': 'Attachment serial number',
|
||||
'stickerUploaderPack': 'Sticker pack serial number',
|
||||
'stickerUploaderPackHint':
|
||||
'Don\'t have pack id? Head to creator platform and create one!',
|
||||
'stickerUploaderAlias': 'Alias',
|
||||
'stickerUploaderAliasHint':
|
||||
'Will be used as a placeholder with the sticker pack prefix when entered.',
|
||||
'stickerUploaderName': 'Name',
|
||||
'stickerUploaderNameHint':
|
||||
'A human-friendly name given to the user in the sticker selection interface.',
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ const i18nSimplifiedChinese = {
|
||||
'account': '账号',
|
||||
'accountPersonalize': '个性化',
|
||||
'accountPersonalizeApplied': '账户的个性化设置已保存。',
|
||||
'accountStickers': '贴图',
|
||||
'accountFriend': '好友',
|
||||
'accountFriendNew': '添加好友',
|
||||
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
|
||||
@@ -293,6 +294,11 @@ const i18nSimplifiedChinese = {
|
||||
'accountStatusNeutral': '中性',
|
||||
'accountStatusPositive': '积极',
|
||||
'bsLoadingTheme': '正在装载主题',
|
||||
'bsCheckForUpdate': '正在检查更新',
|
||||
'bsCheckForUpdateFailed': '无法检查更新',
|
||||
'bsCheckForUpdateNew': '发现新版本',
|
||||
'bsCheckForUpdateDescApple': '请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。',
|
||||
'bsCheckForUpdateDescCommon': '请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。',
|
||||
'bsCheckingServer': '检查服务器状态中',
|
||||
'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态',
|
||||
'bsCheckingServerDown': '当前服务器不可用,请稍后重试',
|
||||
@@ -309,6 +315,17 @@ const i18nSimplifiedChinese = {
|
||||
'themeColorKagamine': '镜音黄',
|
||||
'themeColorLuka': '流音粉',
|
||||
'themeColorApplied': '全局主题颜色已应用',
|
||||
'stickerDeletionConfirm': '确认删除贴图',
|
||||
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
|
||||
'attachmentSaved': '附件已保存到系统相册',
|
||||
'cropImage': '裁剪图片',
|
||||
'stickerUploader': '上传贴图',
|
||||
'stickerUploaderAttachmentNew': '上传附件',
|
||||
'stickerUploaderAttachment': '附件序列号',
|
||||
'stickerUploaderPack': '贴图包序号',
|
||||
'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。',
|
||||
'stickerUploaderAlias': '贴图别名',
|
||||
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
|
||||
'stickerUploaderName': '贴图名称',
|
||||
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
|
||||
};
|
||||
|
||||
@@ -8,9 +8,9 @@ import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_heading.dart';
|
||||
|
||||
class AccountProfilePopup extends StatefulWidget {
|
||||
final Account account;
|
||||
final String name;
|
||||
|
||||
const AccountProfilePopup({super.key, required this.account});
|
||||
const AccountProfilePopup({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<AccountProfilePopup> createState() => _AccountProfilePopupState();
|
||||
@@ -21,11 +21,11 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
||||
|
||||
Account? _userinfo;
|
||||
|
||||
void getUserinfo() async {
|
||||
void _getUserinfo() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
final resp = await client.get('/users/${widget.account.name}');
|
||||
final resp = await client.get('/users/${widget.name}');
|
||||
if (resp.statusCode == 200) {
|
||||
_userinfo = Account.fromJson(resp.body);
|
||||
setState(() => _isBusy = false);
|
||||
@@ -38,7 +38,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getUserinfo();
|
||||
_getUserinfo();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -226,7 +226,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _clearAtController,
|
||||
readOnly: true,
|
||||
@@ -238,7 +238,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
|
||||
),
|
||||
onTap: () => selectClearAt(),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
@@ -281,7 +281,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const SizedBox(height: 8),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
|
||||
@@ -35,7 +35,7 @@ class SilverRelativeList extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
AccountProfilePopup(
|
||||
account: element.related,
|
||||
name: element.related.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -23,16 +23,26 @@ import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||
|
||||
class AttachmentEditorPopup extends StatefulWidget {
|
||||
final String usage;
|
||||
final List<int> initialAttachments;
|
||||
final bool singleMode;
|
||||
final bool imageOnly;
|
||||
final bool autoUpload;
|
||||
final double? imageMaxWidth;
|
||||
final double? imageMaxHeight;
|
||||
final List<int>? initialAttachments;
|
||||
final void Function(int) onAdd;
|
||||
final void Function(int) onRemove;
|
||||
|
||||
const AttachmentEditorPopup({
|
||||
super.key,
|
||||
required this.usage,
|
||||
required this.initialAttachments,
|
||||
required this.onAdd,
|
||||
required this.onRemove,
|
||||
this.singleMode = false,
|
||||
this.imageOnly = false,
|
||||
this.autoUpload = false,
|
||||
this.imageMaxWidth,
|
||||
this.imageMaxHeight,
|
||||
this.initialAttachments,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -43,7 +53,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
final _imagePicker = ImagePicker();
|
||||
final AttachmentUploaderController _uploadController = Get.find();
|
||||
|
||||
bool _isAutoUpload = false;
|
||||
late bool _isAutoUpload = widget.autoUpload;
|
||||
|
||||
bool _isBusy = false;
|
||||
bool _isFirstTimeBusy = true;
|
||||
@@ -54,13 +64,28 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final medias = await _imagePicker.pickMultiImage();
|
||||
if (medias.isEmpty) return;
|
||||
if (widget.singleMode) {
|
||||
final medias = await _imagePicker.pickMultiImage(
|
||||
maxWidth: widget.imageMaxWidth,
|
||||
maxHeight: widget.imageMaxHeight,
|
||||
);
|
||||
if (medias.isEmpty) return;
|
||||
|
||||
_enqueueTaskBatch(medias.map((x) {
|
||||
final file = File(x.path);
|
||||
return AttachmentUploadTask(file: file, usage: widget.usage);
|
||||
}));
|
||||
_enqueueTaskBatch(medias.map((x) {
|
||||
final file = File(x.path);
|
||||
return AttachmentUploadTask(file: file, usage: widget.usage);
|
||||
}));
|
||||
} else {
|
||||
final media = await _imagePicker.pickMedia(
|
||||
maxWidth: widget.imageMaxWidth,
|
||||
maxHeight: widget.imageMaxHeight,
|
||||
);
|
||||
if (media == null) return;
|
||||
|
||||
_enqueueTask(
|
||||
AttachmentUploadTask(file: File(media.path), usage: widget.usage),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickVideoToUpload() async {
|
||||
@@ -164,6 +189,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
if (result != null) {
|
||||
widget.onAdd(result.id);
|
||||
setState(() => _attachments.add(result));
|
||||
if (widget.singleMode) Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,9 +205,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
widget.usage,
|
||||
null,
|
||||
(item) {
|
||||
if (item == null) return;
|
||||
widget.onAdd(item.id);
|
||||
if (mounted) {
|
||||
setState(() => _attachments.add(item));
|
||||
if (widget.singleMode) Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -209,12 +237,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
void _revertMetadataList() {
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (widget.initialAttachments.isEmpty) {
|
||||
if (widget.initialAttachments?.isEmpty ?? true) {
|
||||
_isFirstTimeBusy = false;
|
||||
return;
|
||||
} else {
|
||||
_attachments = List.filled(
|
||||
widget.initialAttachments.length,
|
||||
widget.initialAttachments!.length,
|
||||
null,
|
||||
growable: true,
|
||||
);
|
||||
@@ -222,19 +250,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
int progress = 0;
|
||||
for (var idx = 0; idx < widget.initialAttachments.length; idx++) {
|
||||
attach.getMetadata(widget.initialAttachments[idx]).then((resp) {
|
||||
progress++;
|
||||
_attachments[idx] = resp;
|
||||
if (progress == widget.initialAttachments.length) {
|
||||
setState(() {
|
||||
_isBusy = false;
|
||||
_isFirstTimeBusy = false;
|
||||
});
|
||||
}
|
||||
attach
|
||||
.listMetadata(widget.initialAttachments ?? List.empty())
|
||||
.then((result) {
|
||||
setState(() {
|
||||
_attachments = result;
|
||||
_isBusy = false;
|
||||
_isFirstTimeBusy = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _showAttachmentPreview(Attachment element) {
|
||||
@@ -355,7 +379,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
child: Icon(Icons.check),
|
||||
),
|
||||
),
|
||||
if (!element.isCompleted && canBeCrop)
|
||||
if (element.error != null)
|
||||
IconButton(
|
||||
tooltip: element.error!.toString(),
|
||||
icon: const Icon(Icons.warning),
|
||||
onPressed: () {},
|
||||
),
|
||||
if (!element.isCompleted &&
|
||||
element.error == null &&
|
||||
canBeCrop)
|
||||
Obx(
|
||||
() => IconButton(
|
||||
color: Colors.teal,
|
||||
@@ -368,7 +400,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!element.isCompleted && !element.isUploading)
|
||||
if (!element.isCompleted &&
|
||||
!element.isUploading &&
|
||||
element.error == null)
|
||||
Obx(
|
||||
() => IconButton(
|
||||
color: Colors.green,
|
||||
@@ -380,9 +414,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
_uploadController
|
||||
.performSingleTask(index)
|
||||
.then((r) {
|
||||
if (r == null) return;
|
||||
widget.onAdd(r.id);
|
||||
if (mounted) {
|
||||
setState(() => _attachments.add(r));
|
||||
if (widget.singleMode) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -525,6 +563,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
widget.onAdd(r.id);
|
||||
if (mounted) {
|
||||
setState(() => _attachments.add(r));
|
||||
if (widget.singleMode) Navigator.pop(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -557,9 +596,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'attachmentAdd'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
Expanded(
|
||||
child: Text(
|
||||
'attachmentAdd'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Obx(() {
|
||||
@@ -676,6 +719,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
ignoring: _uploadController.isUploading.value,
|
||||
child: Container(
|
||||
height: 64,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
@@ -692,9 +736,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
if (PlatformInfo.isDesktop ||
|
||||
PlatformInfo.isIOS ||
|
||||
PlatformInfo.isWeb)
|
||||
if ((PlatformInfo.isDesktop ||
|
||||
PlatformInfo.isIOS ||
|
||||
PlatformInfo.isWeb) &&
|
||||
!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.paste),
|
||||
label: Text('attachmentAddClipboard'.tr),
|
||||
@@ -707,36 +752,40 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _pickPhotoToUpload(),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add_road),
|
||||
label: Text('attachmentAddGalleryVideo'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _pickVideoToUpload(),
|
||||
),
|
||||
if (!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.add_road),
|
||||
label: Text('attachmentAddGalleryVideo'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _pickVideoToUpload(),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.photo_camera_back),
|
||||
label: Text('attachmentAddCameraPhoto'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _takeMediaToUpload(false),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.video_camera_back_outlined),
|
||||
label: Text('attachmentAddCameraVideo'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _takeMediaToUpload(true),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.file_present_rounded),
|
||||
label: Text('attachmentAddFile'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _pickFileToUpload(),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.link),
|
||||
label: Text('attachmentAddFile'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _linkAttachments(),
|
||||
),
|
||||
if (!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.video_camera_back_outlined),
|
||||
label: Text('attachmentAddCameraVideo'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _takeMediaToUpload(true),
|
||||
),
|
||||
if (!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.file_present_rounded),
|
||||
label: Text('attachmentAddFile'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _pickFileToUpload(),
|
||||
),
|
||||
if (!widget.imageOnly)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.link),
|
||||
label: Text('attachmentAddFile'.tr),
|
||||
style: const ButtonStyle(visualDensity: density),
|
||||
onPressed: () => _linkAttachments(),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
),
|
||||
|
||||
@@ -257,6 +257,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
'#${widget.item.id}',
|
||||
style: metaTextStyle,
|
||||
),
|
||||
if (widget.item.metadata?['width'] != null &&
|
||||
widget.item.metadata?['height'] != null)
|
||||
Text(
|
||||
|
||||
@@ -45,7 +45,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
List<Attachment?> _attachmentsMeta = List.empty();
|
||||
|
||||
void _getMetadataList() {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (widget.attachmentsId.isEmpty) {
|
||||
return;
|
||||
@@ -53,25 +53,16 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
_attachmentsMeta = List.filled(widget.attachmentsId.length, null);
|
||||
}
|
||||
|
||||
int progress = 0;
|
||||
for (var idx = 0; idx < widget.attachmentsId.length; idx++) {
|
||||
provider.getMetadata(widget.attachmentsId[idx]).then((resp) {
|
||||
progress++;
|
||||
if (resp != null) {
|
||||
_attachmentsMeta[idx] = resp;
|
||||
}
|
||||
if (progress == widget.attachmentsId.length) {
|
||||
calculateAspectRatio();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
attach.listMetadata(widget.attachmentsId).then((result) {
|
||||
setState(() {
|
||||
_attachmentsMeta = result;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
_calculateAspectRatio();
|
||||
});
|
||||
}
|
||||
|
||||
void calculateAspectRatio() {
|
||||
void _calculateAspectRatio() {
|
||||
bool isConsistent = true;
|
||||
double? consistentValue;
|
||||
int portrait = 0, square = 0, landscape = 0;
|
||||
|
||||
@@ -57,7 +57,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void promptAddMember() async {
|
||||
void _promptAddMember() async {
|
||||
final input = await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@@ -141,7 +141,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
'channelMembersAddHint'
|
||||
.trParams({'channel': '#${widget.channel.alias}'}),
|
||||
),
|
||||
onTap: () => promptAddMember(),
|
||||
onTap: () => _promptAddMember(),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
@@ -160,7 +160,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
account: element.account,
|
||||
name: element.account.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -201,7 +201,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Color? color;
|
||||
final bool isFixed;
|
||||
final bool isFixedAvatar;
|
||||
final ParticipantTrack participant;
|
||||
final Function() onTap;
|
||||
|
||||
@@ -210,7 +210,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.isFixed = false,
|
||||
this.isFixedAvatar = false,
|
||||
required this.participant,
|
||||
required this.onTap,
|
||||
});
|
||||
@@ -222,7 +222,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
child: ParticipantWidget.widgetFor(participant, isFixed: isFixed),
|
||||
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
|
||||
),
|
||||
onTap: () => onTap(),
|
||||
onLongPress: () {
|
||||
|
||||
@@ -243,7 +243,7 @@ class ChatEvent extends StatelessWidget {
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
account: item.sender.account,
|
||||
name: item.sender.account.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:markdown/markdown.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'account/account_profile_popup.dart';
|
||||
|
||||
class MarkdownTextContent extends StatelessWidget {
|
||||
final String content;
|
||||
final bool isSelectable;
|
||||
@@ -34,6 +39,8 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
<markdown.InlineSyntax>[
|
||||
_UserNameCardInlineSyntax(),
|
||||
_CustomEmoteInlineSyntax(),
|
||||
markdown.EmojiSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||
@@ -41,6 +48,23 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
if (href.startsWith('solink://')) {
|
||||
final segments = href.replaceFirst('solink://', '').split('/');
|
||||
switch (segments[0]) {
|
||||
case 'users':
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: segments[1],
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await launchUrlString(
|
||||
href,
|
||||
mode: LaunchMode.externalApplication,
|
||||
@@ -57,3 +81,39 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
return _buildContent(context);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserNameCardInlineSyntax extends InlineSyntax {
|
||||
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
|
||||
|
||||
@override
|
||||
bool onMatch(markdown.InlineParser parser, Match match) {
|
||||
final alias = match[0]!;
|
||||
final anchor = markdown.Element.text('a', alias)
|
||||
..attributes['href'] = Uri.encodeFull(
|
||||
'solink://users/${alias.substring(1)}',
|
||||
);
|
||||
parser.addNode(anchor);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomEmoteInlineSyntax extends InlineSyntax {
|
||||
_CustomEmoteInlineSyntax() : super(r':([a-z0-9_+-]+):');
|
||||
|
||||
@override
|
||||
bool onMatch(markdown.InlineParser parser, Match match) {
|
||||
final StickerProvider sticker = Get.find();
|
||||
final alias = match[1]!;
|
||||
if (sticker.aliasImageMapping[alias] == null) {
|
||||
parser.advanceBy(1);
|
||||
return false;
|
||||
}
|
||||
|
||||
final element = markdown.Element.empty('img');
|
||||
element.attributes['src'] = sticker.aliasImageMapping[alias]!;
|
||||
parser.addNode(element);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:solian/models/account_status.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/shells/root_shell.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@@ -60,8 +61,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
setState(() => _selectedIndex = null);
|
||||
_closeDrawer();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -130,22 +130,36 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||
);
|
||||
},
|
||||
),
|
||||
leading: Builder(builder: (context) {
|
||||
final badgeColor = _accountStatus != null
|
||||
leading: Obx(() {
|
||||
final statusBadgeColor = _accountStatus != null
|
||||
? StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
).$2
|
||||
: Colors.grey;
|
||||
|
||||
final RelationshipProvider relations = Get.find();
|
||||
final accountNotifications = relations.friendRequestCount.value;
|
||||
|
||||
return badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle: badges.BadgeStyle(badgeColor: badgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
badgeContent: Text(
|
||||
accountNotifications.toString(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
child: AccountAvatar(
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
showBadge: accountNotifications > 0,
|
||||
position: badges.BadgePosition.topEnd(
|
||||
top: -10,
|
||||
end: -6,
|
||||
),
|
||||
child: badges.Badge(
|
||||
showBadge: _accountStatus != null,
|
||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||
position: badges.BadgePosition.bottomEnd(
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
),
|
||||
child: AccountAvatar(
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -310,7 +310,7 @@ class _PostItemState extends State<PostItem> {
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
account: item.author,
|
||||
name: item.author.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -157,7 +157,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
account: element.account,
|
||||
name: element.account.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
211
lib/widgets/stickers/sticker_uploader.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/stickers.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||
|
||||
class StickerUploadDialog extends StatefulWidget {
|
||||
final Sticker? edit;
|
||||
|
||||
const StickerUploadDialog({super.key, this.edit});
|
||||
|
||||
@override
|
||||
State<StickerUploadDialog> createState() => _StickerUploadDialogState();
|
||||
}
|
||||
|
||||
class _StickerUploadDialogState extends State<StickerUploadDialog> {
|
||||
final TextEditingController _attachmentController = TextEditingController();
|
||||
final TextEditingController _packController = TextEditingController();
|
||||
final TextEditingController _aliasController = TextEditingController();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
void _promptUploadNewAttachment() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => AttachmentEditorPopup(
|
||||
usage: 'sticker',
|
||||
singleMode: true,
|
||||
imageOnly: true,
|
||||
autoUpload: true,
|
||||
imageMaxHeight: 28,
|
||||
imageMaxWidth: 28,
|
||||
onAdd: (value) {
|
||||
setState(() {
|
||||
_attachmentController.text = value.toString();
|
||||
});
|
||||
},
|
||||
initialAttachments: const [],
|
||||
onRemove: (_) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _applySticker() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
if ([
|
||||
_nameController.text.isEmpty,
|
||||
_aliasController.text.isEmpty,
|
||||
_packController.text.isEmpty,
|
||||
_attachmentController.text.isEmpty,
|
||||
].any((x) => x)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
Response resp;
|
||||
final client = auth.configureClient('files');
|
||||
if (widget.edit == null) {
|
||||
resp = await client.post('/stickers', {
|
||||
'name': _nameController.text,
|
||||
'alias': _aliasController.text,
|
||||
'pack_id': int.tryParse(_packController.text),
|
||||
'attachment_id': int.tryParse(_attachmentController.text),
|
||||
});
|
||||
} else {
|
||||
resp = await client.put('/stickers/${widget.edit!.id}', {
|
||||
'name': _nameController.text,
|
||||
'alias': _aliasController.text,
|
||||
'pack_id': int.tryParse(_packController.text),
|
||||
'attachment_id': int.tryParse(_attachmentController.text),
|
||||
});
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
} else {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.edit != null) {
|
||||
_attachmentController.text = widget.edit!.attachmentId.toString();
|
||||
_packController.text = widget.edit!.packId.toString();
|
||||
_aliasController.text = widget.edit!.alias;
|
||||
_nameController.text = widget.edit!.name;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_attachmentController.dispose();
|
||||
_packController.dispose();
|
||||
_aliasController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('stickerUploader'.tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('stickerUploaderAttachmentNew'.tr),
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 13),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
onTap: () {
|
||||
_promptUploadNewAttachment();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _attachmentController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixText: '#',
|
||||
labelText: 'stickerUploaderAttachment'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _packController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixText: '#',
|
||||
labelText: 'stickerUploaderPack'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
|
||||
child: Text(
|
||||
'stickerUploaderPackHint'.tr,
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _aliasController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'stickerUploaderAlias'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
|
||||
child: Text(
|
||||
'stickerUploaderAliasHint'.tr,
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'stickerUploaderName'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 4),
|
||||
child: Text(
|
||||
'stickerUploaderNameHint'.tr,
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||
),
|
||||
onPressed: _isBusy ? null : () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _applySticker(),
|
||||
child: Text('apply'.tr),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- livekit_client (2.2.2):
|
||||
- livekit_client (2.2.3):
|
||||
- FlutterMacOS
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- macos_window_utils (1.0.0):
|
||||
@@ -108,7 +108,7 @@ PODS:
|
||||
- screen_brightness_macos (0.1.0):
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (8.32.0)
|
||||
- sentry_flutter (8.5.0):
|
||||
- sentry_flutter (8.6.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.32.0)
|
||||
@@ -240,7 +240,7 @@ SPEC CHECKSUMS:
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
livekit_client: c24af2b8474a39325596e714118e05551ec5eacc
|
||||
livekit_client: a59d8778582019242d96fe9da69d4ec48833b5ca
|
||||
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
|
||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
@@ -253,7 +253,7 @@ SPEC CHECKSUMS:
|
||||
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
|
||||
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5
|
||||
sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
|
||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
|
||||
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 5.0 KiB |
50
pubspec.lock
@@ -261,10 +261,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32"
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+1"
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -317,10 +317,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dev_build
|
||||
sha256: "76dd5b2587a891ab9c1e7f7eea6ca4a3504667321e0186ecb1e385183889d89b"
|
||||
sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+10"
|
||||
version: "1.0.0+11"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -369,14 +369,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.9"
|
||||
easy_debounce:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: easy_debounce
|
||||
sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -684,10 +676,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_webrtc
|
||||
sha256: d305793e6737c59a81c45b18484e1f985710827704eeb9092573387efcbae272
|
||||
sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.5"
|
||||
version: "0.11.6"
|
||||
font_awesome_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -732,10 +724,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8"
|
||||
sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
version: "14.2.2"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -769,7 +761,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
@@ -820,10 +812,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3"
|
||||
sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
version: "3.0.5"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -948,10 +940,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: livekit_client
|
||||
sha256: e6b1e8a3cdcae95f7e62c0371590648444bac245fce3a1bcfb4ec05889ad82f3
|
||||
sha256: be2a3375851a6147d5de94a870edd6e831ab8d3d793e3563ba1ff1b05490b3de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1524,10 +1516,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "3a293170d4d9403c3254ee05b84e62e8a9b3c5808ebd17de6a33fe9ea6457936"
|
||||
sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1701,14 +1693,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
textfield_tags:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: textfield_tags
|
||||
sha256: d1f2204114157a1296bb97c20d7f8c8c7fd036212812afb2e19de7bb34acc55b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
timeago:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1809,10 +1793,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.2"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.2.0+7
|
||||
version: 1.2.1+2
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@@ -27,7 +27,6 @@ dependencies:
|
||||
crypto: ^3.0.3
|
||||
path: ^1.9.0
|
||||
intl: ^0.19.0
|
||||
image: ^4.1.7
|
||||
font_awesome_flutter: ^10.7.0
|
||||
web_socket_channel: ^3.0.0
|
||||
permission_handler: ^11.3.1
|
||||
@@ -50,7 +49,6 @@ dependencies:
|
||||
media_kit: ^1.1.10+1
|
||||
media_kit_video: ^1.2.4
|
||||
media_kit_libs_video: ^1.0.4
|
||||
textfield_tags: ^3.0.1
|
||||
pasteboard: ^0.2.0
|
||||
desktop_drop: ^0.4.4
|
||||
badges: ^3.1.2
|
||||
@@ -60,7 +58,6 @@ dependencies:
|
||||
flutter_cache_manager: ^3.3.3
|
||||
flutter_markdown_selectionarea: ^0.6.17+1
|
||||
shared_preferences: ^2.2.3
|
||||
easy_debounce: ^2.0.3
|
||||
provider: ^6.1.2
|
||||
gal: ^2.3.0
|
||||
dio: ^5.5.0+1
|
||||
@@ -116,7 +113,7 @@ flutter:
|
||||
flutter_launcher_icons:
|
||||
android:
|
||||
generate: "launcher_icon"
|
||||
image_path: "assets/icon-fit.png"
|
||||
image_path: "assets/icon.png"
|
||||
ios: true
|
||||
image_path: "assets/icon.png"
|
||||
min_sdk_android: 21
|
||||
|
||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 39 KiB |