Compare commits

...

13 Commits

Author SHA1 Message Date
19eabfaba1 🚀 Launch 1.2.1+2 2024-08-04 13:27:14 +08:00
ec2eadad6d 🐛 Fix bootstrapper icon issue 2024-08-04 12:59:13 +08:00
54e176e75d 🐛 Fix post editor cannot reply either repost 2024-08-04 12:55:05 +08:00
0a7ccaeefa 🐛 Fix attachment editor title overflow 2024-08-04 12:23:39 +08:00
a5f093e185 🐛 Fix unauthorized wont load stickers 2024-08-04 11:10:25 +08:00
a4f68dd175 🚀 Launch 1.2.1+1 2024-08-04 01:54:35 +08:00
8067c35c70 Follow the manifest to load emotes 2024-08-04 01:53:52 +08:00
ebe381053e Load emojis 2024-08-04 01:37:54 +08:00
03f2470dae Basic sticker management 2024-08-04 01:03:09 +08:00
ea434815cf Create sticker
 Single file mode attachment editor and more options
2024-08-03 21:29:48 +08:00
bbea4b4359 🍱 Update app icons 2024-08-03 17:44:36 +08:00
e0b485cc81 🐛 Fix mis-style 2024-08-03 14:00:52 +08:00
87bb37ac01 ⚗️ Markdown embed content 2024-08-03 12:29:13 +08:00
72 changed files with 899 additions and 118 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,5 +1,4 @@
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';
@ -9,6 +8,7 @@ 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';
@ -112,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(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(),
]);
}
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]);
},
),
(
@ -142,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++);
}
@ -171,19 +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 && !_isDismissable)
if (_isErrored && !_isDismissable && !_isBusy)
const Icon(Icons.cancel, size: 24),
if (_isErrored && _isDismissable)
if (_isErrored && _isDismissable && !_isBusy)
const Icon(Icons.warning, size: 24),
if (!_isErrored && _isBusy)
if ((_isErrored && _isDismissable && _isBusy) || _isBusy)
const SizedBox(
width: 24,
height: 24,

View File

@ -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
View 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(),
};
}

View File

@ -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;
}
}
}

View File

@ -26,6 +26,8 @@ class AttachmentProvider extends GetConnect {
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) {

View 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();
}
}

View File

@ -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',

View File

@ -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),
),
);
},

View File

@ -46,6 +46,11 @@ class _AccountScreenState extends State<AccountScreen> {
'accountFriend'.tr,
'accountFriend',
),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
];
final AuthProvider auth = Get.find();

View File

@ -43,7 +43,7 @@ class _FriendScreenState extends State<FriendScreen>
_relations.where((x) => x.status == 0).length;
}
void promptAddFriend() async {
void _promptAddFriend() async {
final RelationshipProvider provider = Get.find();
final controller = TextEditingController();
@ -146,7 +146,7 @@ class _FriendScreenState extends State<FriendScreen>
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => promptAddFriend(),
onPressed: () => _promptAddFriend(),
),
body: TabBarView(
controller: _tabController,

View File

@ -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';
}

View File

@ -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,

View 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(),
);
},
),
),
],
),
),
);
}
}

View File

@ -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(

View File

@ -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',
@ -317,8 +321,10 @@ const i18nEnglish = {
'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.',
'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',
@ -336,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.',
};

View File

@ -52,6 +52,7 @@ const i18nSimplifiedChinese = {
'account': '账号',
'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountStickers': '贴图',
'accountFriend': '好友',
'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
@ -314,6 +315,17 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用',
'stickerDeletionConfirm': '确认删除贴图',
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
'attachmentSaved': '附件已保存到系统相册',
'cropImage': '裁剪图片',
'stickerUploader': '上传贴图',
'stickerUploaderAttachmentNew': '上传附件',
'stickerUploaderAttachment': '附件序列号',
'stickerUploaderPack': '贴图包序号',
'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。',
'stickerUploaderAlias': '贴图别名',
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
'stickerUploaderName': '贴图名称',
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
};

View File

@ -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

View File

@ -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(

View File

@ -35,7 +35,7 @@ class SilverRelativeList extends StatelessWidget {
context: context,
builder: (context) =>
AccountProfilePopup(
account: element.related,
name: element.related.name,
),
);
},

View File

@ -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,7 +250,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = true);
attach.listMetadata(widget.initialAttachments).then((result) {
attach
.listMetadata(widget.initialAttachments ?? List.empty())
.then((result) {
setState(() {
_attachments = result;
_isBusy = false;
@ -349,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,
@ -362,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,
@ -374,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);
}
}
});
},
@ -519,6 +563,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
widget.onAdd(r.id);
if (mounted) {
setState(() => _attachments.add(r));
if (widget.singleMode) Navigator.pop(context);
}
});
}
@ -551,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(() {
@ -670,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(
@ -686,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),
@ -701,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),
),

View File

@ -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(

View File

@ -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,
),
);
},

View File

@ -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,
),
);
},

View File

@ -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;
}
}

View File

@ -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,
),
);
},

View File

@ -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,
),
);
},

View 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),
),
],
);
}
}

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.2.0+9
version: 1.2.1+2
environment:
sdk: ">=3.3.4 <4.0.0"
@ -113,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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 39 KiB