Compare commits

..

20 Commits

Author SHA1 Message Date
4f6c5aa053 🐛 Bug fixes 2024-08-04 21:12:35 +08:00
d8e79fb4f9 🚀 Launch 1.2.1+5 2024-08-04 20:49:11 +08:00
06e0fa465b Article has special badge 2024-08-04 20:48:51 +08:00
895a257f50 Better overflow effect 2024-08-04 20:43:25 +08:00
d9804ba00b 🚸 Enhanced share feature 2024-08-04 18:32:16 +08:00
62ff1c2f1c 🚀 Launch 1.2.1+4 2024-08-04 18:14:28 +08:00
a157596a2e Optimize and fixes 2024-08-04 18:13:59 +08:00
12102bf527 Limit content and read more in posts 2024-08-04 17:39:22 +08:00
c00a018380 🐛 Fix draft box 2024-08-04 17:15:56 +08:00
53b3cac4ca Show hint when dismissible error 2024-08-04 16:26:05 +08:00
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
34 changed files with 1235 additions and 246 deletions

View File

@@ -8,6 +8,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@@ -111,13 +112,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
label: 'bsPreparingData', label: 'bsPreparingData',
action: () async { action: () async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) {
await Future.wait([ await Future.wait([
Get.find<RealmProvider>().refreshAvailableRealms(), Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(), Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]); ]);
}
}, },
), ),
( (
@@ -141,7 +144,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
try { try {
for (var idx = 0; idx < _periods.length; idx++) { for (var idx = 0; idx < _periods.length; idx++) {
await _periods[idx].action(); await _periods[idx].action();
if (_isErrored) break; if (_isErrored && !_isDismissable) break;
if (_periodCursor < _periods.length - 1) { if (_periodCursor < _periods.length - 1) {
setState(() => _periodCursor++); setState(() => _periodCursor++);
} }
@@ -160,7 +163,8 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _isErrored) { if (_isBusy || _isErrored) {
return Material( return GestureDetector(
child: Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@@ -172,18 +176,18 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 80, height: 80), child:
Image.asset('assets/logo.png', width: 80, height: 80),
), ),
), ),
), ),
GestureDetector( Column(
child: Column(
children: [ children: [
if (_isErrored && !_isDismissable) if (_isErrored && !_isDismissable && !_isBusy)
const Icon(Icons.cancel, size: 24), const Icon(Icons.cancel, size: 24),
if (_isErrored && _isDismissable) if (_isErrored && _isDismissable && !_isBusy)
const Icon(Icons.warning, size: 24), const Icon(Icons.warning, size: 24),
if (!_isErrored && _isBusy) if ((_isErrored && _isDismissable && _isBusy) || _isBusy)
const SizedBox( const SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@@ -212,6 +216,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
color: _unFocusColor, color: _unFocusColor,
), ),
).paddingOnly(bottom: 4), ).paddingOnly(bottom: 4),
if (!_isBusy && _isErrored && _isDismissable)
Text(
'bsDismissibleErrorHint'.tr,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 5),
Text( Text(
'2024 © Solsynth LLC', '2024 © Solsynth LLC',
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -225,6 +238,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
), ),
], ],
), ),
],
),
),
onTap: () { onTap: () {
if (_isBusy) return; if (_isBusy) return;
if (_isDismissable) { if (_isDismissable) {
@@ -241,9 +257,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
_runPeriods(); _runPeriods();
} }
}, },
)
],
),
); );
} }

View File

@@ -11,6 +11,7 @@ import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart'; import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.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/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@@ -120,6 +121,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => RelationshipProvider()); Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider()); Get.lazyPut(() => PostProvider());
Get.lazyPut(() => StickerProvider());
Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => WebSocketProvider()); Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => StatusProvider()); 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; double progress = 0;
bool isUploading = false; bool isUploading = false;
bool isCompleted = false; bool isCompleted = false;
dynamic error;
AttachmentUploadTask({ AttachmentUploadTask({
required this.file, required this.file,
@@ -66,7 +67,7 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload.remove(task); queueOfUpload.remove(task);
} }
Future<Attachment> performSingleTask(int queueIndex) async { Future<Attachment?> performSingleTask(int queueIndex) async {
isUploading.value = true; isUploading.value = true;
progressOfUpload.value = 0; progressOfUpload.value = 0;
@@ -83,9 +84,15 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload[queueIndex].progress = value; queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value; _progressOfUpload = value;
}, },
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
},
); );
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex); queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@@ -103,6 +110,10 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer(); _startProgressSyncTimer();
for (var idx = 0; idx < queueOfUpload.length; idx++) { for (var idx = 0; idx < queueOfUpload.length; idx++) {
if (queueOfUpload[idx].isUploading || queueOfUpload[idx].error != null) {
continue;
}
queueOfUpload[idx].isUploading = true; queueOfUpload[idx].isUploading = true;
final task = queueOfUpload[idx]; final task = queueOfUpload[idx];
@@ -115,15 +126,21 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload[idx].progress = value; queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length; _progressOfUpload = (idx + value) / queueOfUpload.length;
}, },
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
},
); );
_progressOfUpload = (idx + 1) / queueOfUpload.length; _progressOfUpload = (idx + 1) / queueOfUpload.length;
onData(result); if (result != null) onData(result);
queueOfUpload[idx].isUploading = false; queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = false; queueOfUpload[idx].isCompleted = true;
} }
queueOfUpload.clear(); queueOfUpload.value = queueOfUpload
.where((x) => x.error == null && x.isCompleted)
.toList(growable: true);
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@@ -135,7 +152,7 @@ class AttachmentUploaderController extends GetxController {
String path, String path,
String usage, String usage,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
Function(Attachment) callback, Function(Attachment?) callback,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
@@ -153,7 +170,7 @@ class AttachmentUploaderController extends GetxController {
callback(result); callback(result);
} }
Future<Attachment> uploadAttachment( Future<Attachment?> uploadAttachment(
Uint8List data, Uint8List data,
String path, String path,
String usage, String usage,
@@ -175,9 +192,9 @@ class AttachmentUploaderController extends GetxController {
return result; return result;
} }
Future<Attachment> _rawUploadAttachment( Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata, 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(); final AttachmentProvider provider = Get.find();
try { try {
final result = await provider.createAttachment( final result = await provider.createAttachment(
@@ -189,7 +206,10 @@ class AttachmentUploaderController extends GetxController {
); );
return result; return result;
} catch (err) { } catch (err) {
rethrow; if (onError != null) {
onError(err);
}
return null;
} }
} }
} }

View File

@@ -26,6 +26,8 @@ class AttachmentProvider extends GetConnect {
List<int> id, { List<int> id, {
noCache = false, noCache = false,
}) async { }) async {
if (id.isEmpty) return List.empty();
List<Attachment?> result = List.filled(id.length, null); List<Attachment?> result = List.filled(id.length, null);
List<int> pendingQuery = List.empty(growable: true); List<int> pendingQuery = List.empty(growable: true);
if (!noCache) { if (!noCache) {
@@ -87,6 +89,8 @@ class AttachmentProvider extends GetConnect {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
await auth.ensureCredentials();
final filePayload = final filePayload =
dio.MultipartFile.fromBytes(data, filename: basename(path)); dio.MultipartFile.fromBytes(data, filename: basename(path));
final fileAlt = basename(path).contains('.') final fileAlt = basename(path).contains('.')

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/friend.dart';
import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/account/profile_page.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_chat.dart';
import 'package:solian/screens/channel/channel_detail.dart'; import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
@@ -103,7 +104,6 @@ abstract class AppRouter {
reply: arguments?.reply, reply: arguments?.reply,
repost: arguments?.repost, repost: arguments?.repost,
realm: arguments?.realm, realm: arguments?.realm,
postListController: arguments?.postListController,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0, mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
), ),
transitionsBuilder: transitionsBuilder:
@@ -226,6 +226,14 @@ abstract class AppRouter {
name: 'accountFriend', name: 'accountFriend',
builder: (context, state) => const FriendScreen(), builder: (context, state) => const FriendScreen(),
), ),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountPersonalize', name: 'accountPersonalize',

View File

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

View File

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

View File

@@ -86,11 +86,17 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
toolbarTitle: 'cropImage'.tr, toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary, toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary, toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [CropAspectRatioPreset.square], aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
), ),
IOSUiSettings( IOSUiSettings(
title: 'cropImage'.tr, title: 'cropImage'.tr,
aspectRatioPresets: [CropAspectRatioPreset.square], aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
), ),
WebUiSettings( WebUiSettings(
context: context, context: context,
@@ -346,3 +352,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
super.dispose(); super.dispose();
} }
} }
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@@ -300,6 +300,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
PostWarpedListWidget( PostWarpedListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
), ),
]), ]),
), ),

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

@@ -21,7 +21,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
final PagingController<int, Post> _pagingController = final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: 0);
getPosts(int pageKey) async { _getPosts(int pageKey) async {
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
Response resp; Response resp;
@@ -49,7 +49,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(getPosts); _pagingController.addPageRequestListener(_getPosts);
} }
@override @override
@@ -76,6 +76,9 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return PostOwnedListEntry( return PostOwnedListEntry(
item: item, item: item,
isFullContent: true,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
onTap: () async { onTap: () async {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
@@ -85,7 +88,13 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
noReact: true, noReact: true,
), ),
).then((value) { ).then((value) {
if (value != null) _pagingController.refresh(); if (value is Future) {
value.then((_) {
_pagingController.refresh();
});
} else if (value != null) {
_pagingController.refresh();
}
}); });
}, },
).paddingOnly(left: 12, right: 12, bottom: 4); ).paddingOnly(left: 12, right: 12, bottom: 4);

View File

@@ -77,7 +77,10 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostWarpedListWidget(controller: _pagingController), PostWarpedListWidget(
controller: _pagingController,
onUpdate: () => _pagingController.refresh(),
),
], ],
), ),
), ),

View File

@@ -4,7 +4,6 @@ import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
@@ -47,15 +46,20 @@ class _HomeScreenState extends State<HomeScreen>
child: Scaffold( child: Scaffold(
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), child: const Icon(Icons.add),
onPressed: () { onPressed: () async {
showModalBottomSheet( final value = await showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => PostCreatePopup( builder: (context) => const PostCreatePopup(),
controller: _postController,
),
); );
if (value is Future) {
value.then((_) {
_postController.reloadAllOver();
});
} else if (value != null) {
_postController.reloadAllOver();
}
}, },
), ),
body: NestedScrollView( body: NestedScrollView(
@@ -100,6 +104,7 @@ class _HomeScreenState extends State<HomeScreen>
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
PostWarpedListWidget( PostWarpedListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
), ),
]), ]),
), ),
@@ -121,12 +126,10 @@ class _HomeScreenState extends State<HomeScreen>
class PostCreatePopup extends StatelessWidget { class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox; final bool hideDraftBox;
final PostListController controller;
const PostCreatePopup({ const PostCreatePopup({
super.key, super.key,
this.hideDraftBox = false, this.hideDraftBox = false,
required this.controller,
}); });
@override @override
@@ -142,13 +145,14 @@ class PostCreatePopup extends StatelessWidget {
icon: const Icon(Icons.post_add), icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr, label: 'postEditorModeStory'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(
context,
AppRouter.instance.pushNamed( AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(postListController: controller),
queryParameters: { queryParameters: {
'mode': 0.toString(), 'mode': 0.toString(),
}, },
),
); );
}, },
), ),
@@ -156,13 +160,14 @@ class PostCreatePopup extends StatelessWidget {
icon: const Icon(Icons.description), icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr, label: 'postEditorModeArticle'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(
context,
AppRouter.instance.pushNamed( AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(postListController: controller),
queryParameters: { queryParameters: {
'mode': 1.toString(), 'mode': 1.toString(),
}, },
),
); );
}, },
), ),
@@ -170,8 +175,10 @@ class PostCreatePopup extends StatelessWidget {
icon: const Icon(Icons.drafts), icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr, label: 'draftBoxOpen'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(
AppRouter.instance.pushNamed('draftBox'); context,
AppRouter.instance.pushNamed('draftBox'),
);
}, },
), ),
]; ];

View File

@@ -59,8 +59,10 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: PostItem( child: PostItem(
item: item!, item: item!,
isClickable: true, isClickable: false,
isOverrideEmbedClickable: true,
isFullDate: true, isFullDate: true,
isFullContent: true,
isShowReply: false, isShowReply: false,
isContentSelectable: true, isContentSelectable: true,
), ),

View File

@@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:markdown_toolbar/markdown_toolbar.dart'; import 'package:markdown_toolbar/markdown_toolbar.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@@ -23,14 +22,12 @@ class PostPublishArguments {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
PostPublishArguments({ PostPublishArguments({
this.edit, this.edit,
this.reply, this.reply,
this.repost, this.repost,
this.realm, this.realm,
this.postListController,
}); });
} }
@@ -39,7 +36,6 @@ class PostPublishScreen extends StatefulWidget {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
final int mode; final int mode;
const PostPublishScreen({ const PostPublishScreen({
@@ -48,7 +44,6 @@ class PostPublishScreen extends StatefulWidget {
this.reply, this.reply,
this.repost, this.repost,
this.realm, this.realm,
this.postListController,
required this.mode, required this.mode,
}); });
@@ -95,9 +90,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else { } else {
_editorController.localClear(); _editorController.localClear();
if (widget.postListController != null) {
widget.postListController!.reloadAllOver();
}
AppRouter.instance.pop(resp.body); AppRouter.instance.pop(resp.body);
} }
@@ -128,7 +120,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
@override @override
void initState() { void initState() {
super.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(() {})); _editorController.contentController.addListener(() => setState(() {}));
_syncWidget(); _syncWidget();
} }
@@ -219,10 +219,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
constraints: const BoxConstraints(maxHeight: 280),
child: SingleChildScrollView(
child: PostItem(
item: _replyTo!, item: _replyTo!,
isReactable: false, isReactable: false,
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
),
),
], ],
), ),
if (_repostTo != null) if (_repostTo != null)
@@ -237,10 +242,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
constraints: const BoxConstraints(maxHeight: 280),
child: SingleChildScrollView(
child: PostItem(
item: _repostTo!, item: _repostTo!,
isReactable: false, isReactable: false,
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
),
),
], ],
), ),
Expanded( Expanded(

View File

@@ -12,6 +12,7 @@ const i18nEnglish = {
'draftBox': 'Draft Box', 'draftBox': 'Draft Box',
'more': 'More', 'more': 'More',
'share': 'Share', 'share': 'Share',
'shareNoUri': 'Share text content',
'feed': 'Feed', 'feed': 'Feed',
'unlink': 'Unlink', 'unlink': 'Unlink',
'feedSearch': 'Search Feed', 'feedSearch': 'Search Feed',
@@ -52,6 +53,7 @@ const i18nEnglish = {
'account': 'Account', 'account': 'Account',
'accountPersonalize': 'Personalize', 'accountPersonalize': 'Personalize',
'accountPersonalizeApplied': 'Account personalize settings has been saved.', 'accountPersonalizeApplied': 'Account personalize settings has been saved.',
'accountStickers': 'Stickers',
'accountFriend': 'Friend', 'accountFriend': 'Friend',
'accountFriendNew': 'New friend', 'accountFriendNew': 'New friend',
'accountFriendNewHint': 'accountFriendNewHint':
@@ -162,9 +164,11 @@ const i18nEnglish = {
'attachmentAutoUpload': 'Auto Upload', 'attachmentAutoUpload': 'Auto Upload',
'attachmentUploadQueue': 'Upload Queue', 'attachmentUploadQueue': 'Upload Queue',
'attachmentUploadQueueStart': 'Start All', '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', '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', 'attachmentAdd': 'Attach attachments',
'attachmentAddGalleryPhoto': 'Gallery photo', 'attachmentAddGalleryPhoto': 'Gallery photo',
'attachmentAddGalleryVideo': 'Gallery video', 'attachmentAddGalleryVideo': 'Gallery video',
@@ -173,7 +177,8 @@ const i18nEnglish = {
'attachmentAddClipboard': 'Paste file', 'attachmentAddClipboard': 'Paste file',
'attachmentAddFile': 'Attach file', 'attachmentAddFile': 'Attach file',
'attachmentAddLink': 'Link attachments', 'attachmentAddLink': 'Link attachments',
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment', 'attachmentAddLinkHint':
'Enter attachment serial number to link that attachment',
'attachmentAddLinkInput': 'Serial number', 'attachmentAddLinkInput': 'Serial number',
'attachmentSetting': 'Adjust attachment', 'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text', 'attachmentAlt': 'Alternative text',
@@ -317,8 +322,10 @@ const i18nEnglish = {
'bsCheckForUpdate': 'Checking For Updates', 'bsCheckForUpdate': 'Checking For Updates',
'bsCheckForUpdateFailed': 'Unable to Check Updates', 'bsCheckForUpdateFailed': 'Unable to Check Updates',
'bsCheckForUpdateNew': 'Found New Version', 'bsCheckForUpdateNew': 'Found New Version',
'bsCheckForUpdateDescApple': 'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.', 'bsCheckForUpdateDescApple':
'bsCheckForUpdateDescCommon': 'Please head to our website download and install latest version of application to prevent error happens and get latest functions.', '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', 'bsCheckingServer': 'Checking Server Status',
'bsCheckingServerFail': 'bsCheckingServerFail':
'Unable connect to server, check your network connection', 'Unable connect to server, check your network connection',
@@ -327,6 +334,7 @@ const i18nEnglish = {
'bsEstablishingConn': 'Establishing Connection', 'bsEstablishingConn': 'Establishing Connection',
'bsPreparingData': 'Preparing User Data', 'bsPreparingData': 'Preparing User Data',
'bsRegisteringPushNotify': 'Enabling Push Notifications', 'bsRegisteringPushNotify': 'Enabling Push Notifications',
'bsDismissibleErrorHint': 'Click anywhere to ignore this error',
'postShareContent': 'postShareContent':
'@content\n\n@username on the Solar Network\nCheck it out: @link', '@content\n\n@username on the Solar Network\nCheck it out: @link',
'postShareSubject': '@username posted a post on the Solar Network', 'postShareSubject': '@username posted a post on the Solar Network',
@@ -336,7 +344,23 @@ const i18nEnglish = {
'themeColorMiku': 'Miku Blue', 'themeColorMiku': 'Miku Blue',
'themeColorKagamine': 'Kagamine Yellow', 'themeColorKagamine': 'Kagamine Yellow',
'themeColorLuka': 'Luka Pink', '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.', 'themeColorApplied': 'Global theme color has been applied.',
'attachmentSaved': 'Attachment saved to your system album.', 'attachmentSaved': 'Attachment saved to your system album.',
'cropImage': 'Crop Image', '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.',
'readMore': 'Read more',
}; };

View File

@@ -20,6 +20,7 @@ const i18nSimplifiedChinese = {
'draftBox': '草稿箱', 'draftBox': '草稿箱',
'more': '更多', 'more': '更多',
'share': '分享', 'share': '分享',
'shareNoUri': '分享文字内容',
'feed': '资讯', 'feed': '资讯',
'unlink': '移除链接', 'unlink': '移除链接',
'feedSearch': '搜索资讯', 'feedSearch': '搜索资讯',
@@ -52,6 +53,7 @@ const i18nSimplifiedChinese = {
'account': '账号', 'account': '账号',
'accountPersonalize': '个性化', 'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountStickers': '贴图',
'accountFriend': '好友', 'accountFriend': '好友',
'accountFriendNew': '添加好友', 'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
@@ -305,6 +307,7 @@ const i18nSimplifiedChinese = {
'bsEstablishingConn': '部署连接中', 'bsEstablishingConn': '部署连接中',
'bsPreparingData': '正在准备用户资料', 'bsPreparingData': '正在准备用户资料',
'bsRegisteringPushNotify': '正在启用推送通知', 'bsRegisteringPushNotify': '正在启用推送通知',
'bsDismissibleErrorHint': '点击任意地方忽略此错误',
'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link', 'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link',
'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子', 'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子',
'themeColor': '全局主题色', 'themeColor': '全局主题色',
@@ -314,6 +317,18 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄', 'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉', 'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用', 'themeColorApplied': '全局主题颜色已应用',
'stickerDeletionConfirm': '确认删除贴图',
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
'attachmentSaved': '附件已保存到系统相册', 'attachmentSaved': '附件已保存到系统相册',
'cropImage': '裁剪图片', 'cropImage': '裁剪图片',
'stickerUploader': '上传贴图',
'stickerUploaderAttachmentNew': '上传附件',
'stickerUploaderAttachment': '附件序列号',
'stickerUploaderPack': '贴图包序号',
'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。',
'stickerUploaderAlias': '贴图别名',
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
'stickerUploaderName': '贴图名称',
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
'readMore': '阅读更多',
}; };

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@@ -18,23 +17,38 @@ class AccountProfilePopup extends StatefulWidget {
class _AccountProfilePopupState extends State<AccountProfilePopup> { class _AccountProfilePopupState extends State<AccountProfilePopup> {
bool _isBusy = true; bool _isBusy = true;
dynamic _hasError;
Account? _userinfo; Account? _userinfo;
void _getUserinfo() async { void _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try {
final client = ServiceFinder.configureClient('auth'); final client = ServiceFinder.configureClient('auth');
final resp = await client.get('/users/${widget.name}'); final resp = await client.get('/users/${widget.name}');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
setState(() {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
setState(() => _isBusy = false); _isBusy = false;
});
} else { } else {
context.showErrorDialog(resp.bodyString); setState(() {
Navigator.pop(context); _hasError = resp.bodyString;
_isBusy = false;
});
}
} catch (e) {
setState(() {
_hasError = e;
_isBusy = false;
});
} }
} }
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -43,13 +57,35 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _userinfo == null) { if (_isBusy) {
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.75, height: MediaQuery.of(context).size.height * 0.75,
child: const Center(child: CircularProgressIndicator()), child: const Center(child: CircularProgressIndicator()),
); );
} }
if (_hasError != null) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cancel, size: 24),
const SizedBox(height: 12),
Text(
_hasError.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
),
],
),
);
}
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.75, height: MediaQuery.of(context).size.height * 0.75,
child: Column( child: Column(

View File

@@ -226,7 +226,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
TextField( TextField(
controller: _clearAtController, controller: _clearAtController,
readOnly: true, readOnly: true,
@@ -238,7 +238,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
), ),
onTap: () => selectClearAt(), onTap: () => selectClearAt(),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(
@@ -281,7 +281,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
], ],
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(

View File

@@ -23,16 +23,26 @@ import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentEditorPopup extends StatefulWidget { class AttachmentEditorPopup extends StatefulWidget {
final String usage; 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) onAdd;
final void Function(int) onRemove; final void Function(int) onRemove;
const AttachmentEditorPopup({ const AttachmentEditorPopup({
super.key, super.key,
required this.usage, required this.usage,
required this.initialAttachments,
required this.onAdd, required this.onAdd,
required this.onRemove, required this.onRemove,
this.singleMode = false,
this.imageOnly = false,
this.autoUpload = false,
this.imageMaxWidth,
this.imageMaxHeight,
this.initialAttachments,
}); });
@override @override
@@ -43,7 +53,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
final AttachmentUploaderController _uploadController = Get.find(); final AttachmentUploaderController _uploadController = Get.find();
bool _isAutoUpload = false; late bool _isAutoUpload = widget.autoUpload;
bool _isBusy = false; bool _isBusy = false;
bool _isFirstTimeBusy = true; bool _isFirstTimeBusy = true;
@@ -54,13 +64,28 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
final medias = await _imagePicker.pickMultiImage(); if (widget.singleMode) {
final medias = await _imagePicker.pickMultiImage(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
if (medias.isEmpty) return; if (medias.isEmpty) return;
_enqueueTaskBatch(medias.map((x) { _enqueueTaskBatch(medias.map((x) {
final file = File(x.path); final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage); 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 { Future<void> _pickVideoToUpload() async {
@@ -164,6 +189,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (result != null) { if (result != null) {
widget.onAdd(result.id); widget.onAdd(result.id);
setState(() => _attachments.add(result)); setState(() => _attachments.add(result));
if (widget.singleMode) Navigator.pop(context);
} }
} }
@@ -179,9 +205,11 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
widget.usage, widget.usage,
null, null,
(item) { (item) {
if (item == null) return;
widget.onAdd(item.id); widget.onAdd(item.id);
if (mounted) { if (mounted) {
setState(() => _attachments.add(item)); setState(() => _attachments.add(item));
if (widget.singleMode) Navigator.pop(context);
} }
}, },
); );
@@ -209,12 +237,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
void _revertMetadataList() { void _revertMetadataList() {
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.initialAttachments.isEmpty) { if (widget.initialAttachments?.isEmpty ?? true) {
_isFirstTimeBusy = false; _isFirstTimeBusy = false;
return; return;
} else { } else {
_attachments = List.filled( _attachments = List.filled(
widget.initialAttachments.length, widget.initialAttachments!.length,
null, null,
growable: true, growable: true,
); );
@@ -222,7 +250,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
attach.listMetadata(widget.initialAttachments).then((result) { attach
.listMetadata(widget.initialAttachments ?? List.empty())
.then((result) {
setState(() { setState(() {
_attachments = result; _attachments = result;
_isBusy = false; _isBusy = false;
@@ -349,7 +379,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Icon(Icons.check), 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( Obx(
() => IconButton( () => IconButton(
color: Colors.teal, 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( Obx(
() => IconButton( () => IconButton(
color: Colors.green, color: Colors.green,
@@ -374,9 +414,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
_uploadController _uploadController
.performSingleTask(index) .performSingleTask(index)
.then((r) { .then((r) {
if (r == null) return;
widget.onAdd(r.id); widget.onAdd(r.id);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(r));
if (widget.singleMode) {
Navigator.pop(context);
}
} }
}); });
}, },
@@ -519,6 +563,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
widget.onAdd(r.id); widget.onAdd(r.id);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(r));
if (widget.singleMode) Navigator.pop(context);
} }
}); });
} }
@@ -551,9 +596,17 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Expanded(
child: Row(
children: [
Expanded(
child: Text(
'attachmentAdd'.tr, 'attachmentAdd'.tr,
style: Theme.of(context).textTheme.headlineSmall, style:
Theme.of(context).textTheme.headlineSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Obx(() { Obx(() {
@@ -563,7 +616,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
height: 18, height: 18,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2.5, strokeWidth: 2.5,
value: _uploadController.progressOfUpload.value, value: _uploadController
.progressOfUpload.value,
), ),
); );
} }
@@ -572,6 +626,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
], ],
), ),
), ),
],
),
),
const SizedBox(width: 20),
Text('attachmentAutoUpload'.tr), Text('attachmentAutoUpload'.tr),
const SizedBox(width: 8), const SizedBox(width: 8),
Switch( Switch(
@@ -670,6 +728,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
ignoring: _uploadController.isUploading.value, ignoring: _uploadController.isUploading.value,
child: Container( child: Container(
height: 64, height: 64,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
@@ -686,9 +745,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
children: [ children: [
if (PlatformInfo.isDesktop || if ((PlatformInfo.isDesktop ||
PlatformInfo.isIOS || PlatformInfo.isIOS ||
PlatformInfo.isWeb) PlatformInfo.isWeb) &&
!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.paste), icon: const Icon(Icons.paste),
label: Text('attachmentAddClipboard'.tr), label: Text('attachmentAddClipboard'.tr),
@@ -701,6 +761,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickPhotoToUpload(), onPressed: () => _pickPhotoToUpload(),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.add_road), icon: const Icon(Icons.add_road),
label: Text('attachmentAddGalleryVideo'.tr), label: Text('attachmentAddGalleryVideo'.tr),
@@ -713,18 +774,21 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(false), onPressed: () => _takeMediaToUpload(false),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.video_camera_back_outlined), icon: const Icon(Icons.video_camera_back_outlined),
label: Text('attachmentAddCameraVideo'.tr), label: Text('attachmentAddCameraVideo'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(true), onPressed: () => _takeMediaToUpload(true),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.file_present_rounded), icon: const Icon(Icons.file_present_rounded),
label: Text('attachmentAddFile'.tr), label: Text('attachmentAddFile'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickFileToUpload(), onPressed: () => _pickFileToUpload(),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
label: Text('attachmentAddFile'.tr), label: Text('attachmentAddFile'.tr),

View File

@@ -257,6 +257,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
child: Wrap( child: Wrap(
spacing: 6, spacing: 6,
children: [ children: [
Text(
'#${widget.item.id}',
style: metaTextStyle,
),
if (widget.item.metadata?['width'] != null && if (widget.item.metadata?['width'] != null &&
widget.item.metadata?['height'] != null) widget.item.metadata?['height'] != null)
Text( Text(

View File

@@ -57,7 +57,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void promptAddMember() async { void _promptAddMember() async {
final input = await showModalBottomSheet( final input = await showModalBottomSheet(
context: context, context: context,
builder: (context) { builder: (context) {
@@ -141,7 +141,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
'channelMembersAddHint' 'channelMembersAddHint'
.trParams({'channel': '#${widget.channel.alias}'}), .trParams({'channel': '#${widget.channel.alias}'}),
), ),
onTap: () => promptAddMember(), onTap: () => _promptAddMember(),
), ),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown_selectionarea/flutter_markdown.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' as markdown;
import 'package:markdown/markdown.dart'; import 'package:markdown/markdown.dart';
import 'package:solian/providers/stickers.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'account/account_profile_popup.dart'; import 'account/account_profile_popup.dart';
@@ -101,10 +103,15 @@ class _CustomEmoteInlineSyntax extends InlineSyntax {
@override @override
bool onMatch(markdown.InlineParser parser, Match match) { bool onMatch(markdown.InlineParser parser, Match match) {
// final alias = match[1]!; final StickerProvider sticker = Get.find();
// TODO mapping things... final alias = match[1]!;
if (sticker.aliasImageMapping[alias] == null) {
parser.advanceBy(1);
return false;
}
final element = markdown.Element.empty('img'); final element = markdown.Element.empty('img');
element.attributes['src'] = 'https://www.twitch.tv/creatorcamp/assets/uploads/lul.png'; element.attributes['src'] = sticker.aliasImageMapping[alias]!;
parser.addNode(element); parser.addNode(element);
return true; return true;

View File

@@ -7,6 +7,7 @@ import 'package:get/get.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/posts/post_editor.dart';
@@ -38,12 +39,24 @@ class _PostActionState extends State<PostAction> {
}); });
} }
Future<void> _doShare() async { Future<void> _doShare({bool noUri = false}) async {
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) {
await Share.shareUri(
Uri.parse('https://sn.solsynth.dev/posts/view/${widget.item.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
final extraContent = [
widget.item.body['title'],
widget.item.body['description'],
];
final isExtraNotEmpty = extraContent.any((x) => x != null);
await Share.share( await Share.share(
'postShareContent'.trParams({ 'postShareContent'.trParams({
'username': widget.item.author.nick, 'username': widget.item.author.nick,
'content': widget.item.body['content'] ?? 'no content', 'content':
'${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}',
'link': 'https://sn.solsynth.dev/posts/view/${widget.item.id}', 'link': 'https://sn.solsynth.dev/posts/view/${widget.item.id}',
}), }),
subject: 'postShareSubject'.trParams({ subject: 'postShareSubject'.trParams({
@@ -52,6 +65,7 @@ class _PostActionState extends State<PostAction> {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} }
}
@override @override
void initState() { void initState() {
@@ -86,6 +100,16 @@ class _PostActionState extends State<PostAction> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.share), leading: const Icon(Icons.share),
title: Text('share'.tr), title: Text('share'.tr),
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
? IconButton(
icon: const Icon(Icons.link_off),
tooltip: 'shareNoUri'.tr,
onPressed: () async {
await _doShare(noUri: true);
Navigator.pop(context);
},
)
: null,
onTap: () async { onTap: () async {
await _doShare(); await _doShare();
Navigator.pop(context); Navigator.pop(context);
@@ -97,13 +121,13 @@ class _PostActionState extends State<PostAction> {
leading: const FaIcon(FontAwesomeIcons.reply, size: 20), leading: const FaIcon(FontAwesomeIcons.reply, size: 20),
title: Text('reply'.tr), title: Text('reply'.tr),
onTap: () async { onTap: () async {
final value = await AppRouter.instance.pushNamed( Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(reply: widget.item), extra: PostPublishArguments(reply: widget.item),
),
); );
if (value != null) {
Navigator.pop(context, true);
}
}, },
), ),
if (!widget.noReact) if (!widget.noReact)
@@ -112,13 +136,13 @@ class _PostActionState extends State<PostAction> {
leading: const FaIcon(FontAwesomeIcons.retweet, size: 20), leading: const FaIcon(FontAwesomeIcons.retweet, size: 20),
title: Text('repost'.tr), title: Text('repost'.tr),
onTap: () async { onTap: () async {
final value = await AppRouter.instance.pushNamed( Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(repost: widget.item), extra: PostPublishArguments(repost: widget.item),
),
); );
if (value != null) {
Navigator.pop(context, true);
}
}, },
), ),
if (_canModifyContent && !widget.noReact) if (_canModifyContent && !widget.noReact)
@@ -146,13 +170,13 @@ class _PostActionState extends State<PostAction> {
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
title: Text('edit'.tr), title: Text('edit'.tr),
onTap: () async { onTap: () async {
final value = await AppRouter.instance.pushNamed( Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(edit: widget.item), extra: PostPublishArguments(edit: widget.item),
),
); );
if (value != null) {
Navigator.pop(context, true);
}
}, },
), ),
if (_canModifyContent) if (_canModifyContent)

View File

@@ -1,5 +1,6 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -22,7 +23,9 @@ class PostItem extends StatefulWidget {
final bool isReactable; final bool isReactable;
final bool isShowReply; final bool isShowReply;
final bool isShowEmbed; final bool isShowEmbed;
final bool isOverrideEmbedClickable;
final bool isFullDate; final bool isFullDate;
final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
@@ -35,7 +38,9 @@ class PostItem extends StatefulWidget {
this.isReactable = true, this.isReactable = true,
this.isShowReply = true, this.isShowReply = true,
this.isShowEmbed = true, this.isShowEmbed = true,
this.isOverrideEmbedClickable = false,
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
@@ -73,12 +78,13 @@ class _PostItemState extends State<PostItem> {
Widget _buildHeader() { Widget _buildHeader() {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (widget.isCompact) if (widget.isCompact)
AccountAvatar( AccountAvatar(
content: item.author.avatar.toString(), content: item.author.avatar.toString(),
radius: 10, radius: 10,
).paddingOnly(left: 2), ).paddingOnly(left: 2, top: 1),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -105,18 +111,26 @@ class _PostItemState extends State<PostItem> {
item.body['description'], item.body['description'],
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
if (item.body['description'] != null ||
item.body['title'] != null)
const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
),
], ],
).paddingOnly(left: widget.isCompact ? 6 : 12), ).paddingOnly(left: widget.isCompact ? 6 : 12),
), ),
if (widget.item.type == 'article')
Badge(
label: Text('article'.tr),
).paddingOnly(top: 3),
], ],
); );
} }
Widget _buildHeaderDivider() {
if (item.body['description'] != null || item.body['title'] != null) {
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
);
}
return const SizedBox();
}
Widget _buildFooter() { Widget _buildFooter() {
List<String> labels = List.empty(growable: true); List<String> labels = List.empty(growable: true);
if (widget.item.editedAt != null) { if (widget.item.editedAt != null) {
@@ -164,6 +178,7 @@ class _PostItemState extends State<PostItem> {
Widget _buildReply(BuildContext context) { Widget _buildReply(BuildContext context) {
return OpenContainer( return OpenContainer(
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
children: [ children: [
Row( Row(
@@ -202,13 +217,15 @@ class _PostItemState extends State<PostItem> {
), ),
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface,
); );
} }
Widget _buildRepost(BuildContext context) { Widget _buildRepost(BuildContext context) {
return OpenContainer( return OpenContainer(
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
children: [ children: [
Row( Row(
@@ -247,11 +264,14 @@ class _PostItemState extends State<PostItem> {
), ),
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface,
); );
} }
double _contentHeight = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<int> attachments = item.body['attachments'] is List final List<int> attachments = item.body['attachments'] is List
@@ -264,7 +284,17 @@ class _PostItemState extends State<PostItem> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildHeader().paddingSymmetric(horizontal: 12), _buildHeader().paddingSymmetric(horizontal: 12),
MarkdownTextContent( _buildHeaderDivider().paddingSymmetric(horizontal: 12),
Stack(
children: [
SizedContainer(
maxWidth: 640,
maxHeight: widget.isFullContent ? double.infinity : 80,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: MarkdownTextContent(
content: item.body['content'], content: item.body['content'],
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
).paddingOnly( ).paddingOnly(
@@ -273,6 +303,32 @@ class _PostItemState extends State<PostItem> {
top: 2, top: 2,
bottom: hasAttachment ? 4 : 0, bottom: hasAttachment ? 4 : 0,
), ),
),
),
if (_contentHeight >= 80 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surfaceContainerLow,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
],
),
),
),
),
),
],
),
_buildFooter().paddingOnly(left: 16), _buildFooter().paddingOnly(left: 16),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
@@ -295,6 +351,7 @@ class _PostItemState extends State<PostItem> {
} }
return OpenContainer( return OpenContainer(
tappable: widget.isClickable,
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -320,13 +377,47 @@ class _PostItemState extends State<PostItem> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildHeader(), _buildHeader(),
_buildHeaderDivider(),
Stack(
children: [
SizedContainer( SizedContainer(
maxWidth: 640, maxWidth: 640,
maxHeight:
widget.isFullContent ? double.infinity : 320,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: MarkdownTextContent( child: MarkdownTextContent(
content: item.body['content'], content: item.body['content'],
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
).paddingOnly(left: 12, right: 8), ).paddingOnly(left: 12, right: 8),
), ),
),
if (_contentHeight >= 320 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 320,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
],
),
),
),
),
),
],
),
if (widget.item.replyTo != null && widget.isShowEmbed) if (widget.item.replyTo != null && widget.isShowEmbed)
_buildReply(context).paddingOnly(top: 4), _buildReply(context).paddingOnly(top: 4),
if (widget.item.repostTo != null && widget.isShowEmbed) if (widget.item.repostTo != null && widget.isShowEmbed)
@@ -377,8 +468,52 @@ class _PostItemState extends State<PostItem> {
), ),
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface,
); );
} }
} }
typedef _OnWidgetSizeChange = void Function(Size size);
class _MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
_OnWidgetSizeChange onChange;
_MeasureSizeRenderObject(this.onChange);
@override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class _MeasureSize extends SingleChildRenderObjectWidget {
final _OnWidgetSizeChange onChange;
const _MeasureSize({
super.key,
required this.onChange,
required Widget super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return _MeasureSizeRenderObject(onChange);
}
@override
void updateRenderObject(
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}

View File

@@ -85,7 +85,13 @@ class PostListEntryWidget extends StatelessWidget {
context: context, context: context,
builder: (context) => PostAction(item: item), builder: (context) => PostAction(item: item),
).then((value) { ).then((value) {
if (value != null) onUpdate(); if (value is Future) {
value.then((_) {
onUpdate();
});
} else if (value != null) {
onUpdate();
}
}); });
}, },
); );

View File

@@ -6,18 +6,21 @@ import 'package:solian/widgets/posts/post_item.dart';
class PostOwnedListEntry extends StatelessWidget { class PostOwnedListEntry extends StatelessWidget {
final Post item; final Post item;
final Function onTap; final Function onTap;
final bool isFullContent;
final Color? backgroundColor;
const PostOwnedListEntry({ const PostOwnedListEntry({
super.key, super.key,
required this.item, required this.item,
required this.onTap, required this.onTap,
this.isFullContent = false,
this.backgroundColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
child: InkWell( child: GestureDetector(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -28,6 +31,8 @@ class PostOwnedListEntry extends StatelessWidget {
isClickable: false, isClickable: false,
isShowReply: false, isShowReply: false,
isReactable: false, isReactable: false,
isFullContent: isFullContent,
backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
], ],
), ),

View File

@@ -9,6 +9,7 @@ class PostWarpedListWidget extends StatelessWidget {
final bool isNestedClickable; final bool isNestedClickable;
final bool isPinned; final bool isPinned;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Function? onUpdate;
const PostWarpedListWidget({ const PostWarpedListWidget({
super.key, super.key,
@@ -17,6 +18,7 @@ class PostWarpedListWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.isPinned = true, this.isPinned = true,
this.onUpdate,
}); });
@override @override
@@ -35,9 +37,7 @@ class PostWarpedListWidget extends StatelessWidget {
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
item: item, item: item,
onUpdate: () { onUpdate: onUpdate ?? () {},
controller.refresh();
},
); );
}, },
), ),

View File

@@ -3,11 +3,13 @@ import 'package:flutter/material.dart';
class SizedContainer extends StatelessWidget { class SizedContainer extends StatelessWidget {
final Widget child; final Widget child;
final double maxWidth; final double maxWidth;
final double maxHeight;
const SizedContainer({ const SizedContainer({
super.key, super.key,
required this.child, required this.child,
this.maxWidth = 720, this.maxWidth = 720,
this.maxHeight = double.infinity,
}); });
@override @override
@@ -15,7 +17,7 @@ class SizedContainer extends StatelessWidget {
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight),
child: child, child: child,
), ),
); );

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

@@ -385,6 +385,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
field_suggestion:
dependency: "direct main"
description:
name: field_suggestion
sha256: "596362ed67a661a18e01e4f51b8b92424f333baf37cebd48cc647a4c82c858d5"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
file: file:
dependency: transitive dependency: transitive
description: description:
@@ -736,6 +744,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
highlightable:
dependency: transitive
description:
name: highlightable
sha256: "526793e148c91977b694d75d99cd34401ea3b65efd223e7b539c76916af86ffd"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
http: http:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.0+9 version: 1.2.1+6
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@@ -66,6 +66,7 @@ dependencies:
animations: ^2.0.11 animations: ^2.0.11
avatar_stack: ^1.2.0 avatar_stack: ^1.2.0
async: ^2.11.0 async: ^2.11.0
field_suggestion: ^0.2.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: