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/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.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/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';
@ -112,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<StickerProvider>().refreshAvailableStickers(),
Get.find<RealmProvider>().refreshAvailableRealms(), 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(),
]);
}, },
), ),
( (
@ -142,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++);
} }
@ -171,19 +173,20 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
height: 280, height: 280,
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Image.asset('assets/logo.png', width: 80, height: 80) child: ClipRRect(
.animate(onPlay: (c) => c.repeat()) borderRadius: const BorderRadius.all(Radius.circular(16)),
.rotate(duration: 850.ms, curve: Curves.easeInOut), child: Image.asset('assets/logo.png', width: 80, height: 80),
),
), ),
), ),
GestureDetector( GestureDetector(
child: 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,

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;
},
); );
queueOfUpload.removeAt(queueIndex); if (queueOfUpload[queueIndex].error == null) {
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,20 @@ 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).toList(growable: true);
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@ -135,7 +151,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 +169,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 +191,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 +205,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) {

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';
@ -226,6 +227,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

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -18,9 +17,11 @@ class AboutScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Image.asset('assets/logo.png', width: 64, height: 64) ClipRRect(
.animate(onPlay: (c) => c.repeat()) borderRadius: const BorderRadius.all(Radius.circular(16)),
.rotate(duration: 1000.ms), child: Image.asset('assets/logo.png', width: 120, height: 120),
),
const SizedBox(height: 8),
Text( Text(
'Solian', 'Solian',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
@ -56,10 +57,9 @@ class AboutScreen extends StatelessWidget {
applicationVersion: '${info.version} (${info.buildNumber})', applicationVersion: '${info.version} (${info.buildNumber})',
applicationLegalese: 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.', '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( applicationIcon: ClipRRect(
'assets/logo.png', borderRadius: const BorderRadius.all(Radius.circular(16)),
width: 56, child: Image.asset('assets/logo.png', width: 60, height: 60),
height: 56,
), ),
); );
}, },

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

@ -40,7 +40,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
var client = ServiceFinder.configureClient('auth'); var client = ServiceFinder.configureClient('auth');
@ -114,7 +114,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
}); });
getUserinfo(); _getUserinfo();
getPinnedPosts(); getPinnedPosts();
} }
@ -189,8 +189,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
: () async { : () async {
setState(() => _isMakingFriend = true); setState(() => _isMakingFriend = true);
try { try {
await _relationshipProvider.makeFriend(widget.name); await _relationshipProvider
context.showSnackbar('accountFriendRequestSent'.tr); .makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} finally { } finally {
@ -274,6 +277,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
color: color:
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
child: PostListEntryWidget( child: PostListEntryWidget(
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
item: element, item: element,
isClickable: true, isClickable: true,
isNestedClickable: 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 @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 +227,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
item: _replyTo!, constraints: const BoxConstraints(maxHeight: 280),
isReactable: false, child: SingleChildScrollView(
).paddingOnly(bottom: 8), child: PostItem(
item: _replyTo!,
isReactable: false,
).paddingOnly(bottom: 8),
),
),
], ],
), ),
if (_repostTo != null) if (_repostTo != null)
@ -237,10 +250,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
item: _repostTo!, constraints: const BoxConstraints(maxHeight: 280),
isReactable: false, child: SingleChildScrollView(
).paddingOnly(bottom: 8), child: PostItem(
item: _repostTo!,
isReactable: false,
).paddingOnly(bottom: 8),
),
),
], ],
), ),
Expanded( Expanded(

View File

@ -52,6 +52,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 +163,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 +176,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 +321,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',
@ -336,7 +342,22 @@ 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.',
}; };

View File

@ -52,6 +52,7 @@ const i18nSimplifiedChinese = {
'account': '账号', 'account': '账号',
'accountPersonalize': '个性化', 'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountStickers': '贴图',
'accountFriend': '好友', 'accountFriend': '好友',
'accountFriendNew': '添加好友', 'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
@ -314,6 +315,17 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄', 'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉', 'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用', 'themeColorApplied': '全局主题颜色已应用',
'stickerDeletionConfirm': '确认删除贴图',
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
'attachmentSaved': '附件已保存到系统相册', 'attachmentSaved': '附件已保存到系统相册',
'cropImage': '裁剪图片', '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'; import 'package:solian/widgets/account/account_heading.dart';
class AccountProfilePopup extends StatefulWidget { 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 @override
State<AccountProfilePopup> createState() => _AccountProfilePopupState(); State<AccountProfilePopup> createState() => _AccountProfilePopupState();
@ -21,11 +21,11 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
Account? _userinfo; Account? _userinfo;
void getUserinfo() async { void _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = ServiceFinder.configureClient('auth'); 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) { if (resp.statusCode == 200) {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
setState(() => _isBusy = false); setState(() => _isBusy = false);
@ -38,7 +38,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getUserinfo(); _getUserinfo();
} }
@override @override

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

@ -35,7 +35,7 @@ class SilverRelativeList extends StatelessWidget {
context: context, context: context,
builder: (context) => builder: (context) =>
AccountProfilePopup( 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 { 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) {
if (medias.isEmpty) return; final medias = await _imagePicker.pickMultiImage(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
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,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Expanded(
'attachmentAdd'.tr, child: Text(
style: Theme.of(context).textTheme.headlineSmall, 'attachmentAdd'.tr,
style: Theme.of(context).textTheme.headlineSmall,
maxLines: 2,
overflow: TextOverflow.fade,
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Obx(() { Obx(() {
@ -670,6 +719,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 +736,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,36 +752,40 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickPhotoToUpload(), onPressed: () => _pickPhotoToUpload(),
), ),
ElevatedButton.icon( if (!widget.imageOnly)
icon: const Icon(Icons.add_road), ElevatedButton.icon(
label: Text('attachmentAddGalleryVideo'.tr), icon: const Icon(Icons.add_road),
style: const ButtonStyle(visualDensity: density), label: Text('attachmentAddGalleryVideo'.tr),
onPressed: () => _pickVideoToUpload(), style: const ButtonStyle(visualDensity: density),
), onPressed: () => _pickVideoToUpload(),
),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.photo_camera_back), icon: const Icon(Icons.photo_camera_back),
label: Text('attachmentAddCameraPhoto'.tr), label: Text('attachmentAddCameraPhoto'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(false), onPressed: () => _takeMediaToUpload(false),
), ),
ElevatedButton.icon( if (!widget.imageOnly)
icon: const Icon(Icons.video_camera_back_outlined), ElevatedButton.icon(
label: Text('attachmentAddCameraVideo'.tr), icon: const Icon(Icons.video_camera_back_outlined),
style: const ButtonStyle(visualDensity: density), label: Text('attachmentAddCameraVideo'.tr),
onPressed: () => _takeMediaToUpload(true), style: const ButtonStyle(visualDensity: density),
), onPressed: () => _takeMediaToUpload(true),
ElevatedButton.icon( ),
icon: const Icon(Icons.file_present_rounded), if (!widget.imageOnly)
label: Text('attachmentAddFile'.tr), ElevatedButton.icon(
style: const ButtonStyle(visualDensity: density), icon: const Icon(Icons.file_present_rounded),
onPressed: () => _pickFileToUpload(), label: Text('attachmentAddFile'.tr),
), style: const ButtonStyle(visualDensity: density),
ElevatedButton.icon( onPressed: () => _pickFileToUpload(),
icon: const Icon(Icons.link), ),
label: Text('attachmentAddFile'.tr), if (!widget.imageOnly)
style: const ButtonStyle(visualDensity: density), ElevatedButton.icon(
onPressed: () => _linkAttachments(), icon: const Icon(Icons.link),
), label: Text('attachmentAddFile'.tr),
style: const ButtonStyle(visualDensity: density),
onPressed: () => _linkAttachments(),
),
], ],
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
), ),

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(
@ -160,7 +160,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( 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, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( 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/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: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';
class MarkdownTextContent extends StatelessWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final bool isSelectable; final bool isSelectable;
@ -34,6 +39,8 @@ class MarkdownTextContent extends StatelessWidget {
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.EmojiSyntax(), markdown.EmojiSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
@ -41,6 +48,23 @@ class MarkdownTextContent extends StatelessWidget {
), ),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
if (href == null) return; 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( await launchUrlString(
href, href,
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
@ -57,3 +81,39 @@ class MarkdownTextContent extends StatelessWidget {
return _buildContent(context); 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, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( 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, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( 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/UserDefaults (7.13.3):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- livekit_client (2.2.2): - livekit_client (2.2.3):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- macos_window_utils (1.0.0): - macos_window_utils (1.0.0):
@ -108,7 +108,7 @@ PODS:
- screen_brightness_macos (0.1.0): - screen_brightness_macos (0.1.0):
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (8.32.0) - Sentry/HybridSDK (8.32.0)
- sentry_flutter (8.5.0): - sentry_flutter (8.6.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (= 8.32.0) - Sentry/HybridSDK (= 8.32.0)
@ -240,7 +240,7 @@ SPEC CHECKSUMS:
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
livekit_client: c24af2b8474a39325596e714118e05551ec5eacc livekit_client: a59d8778582019242d96fe9da69d4ec48833b5ca
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
@ -253,7 +253,7 @@ SPEC CHECKSUMS:
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5 sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec 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" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.0+9 version: 1.2.1+2
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -113,7 +113,7 @@ flutter:
flutter_launcher_icons: flutter_launcher_icons:
android: android:
generate: "launcher_icon" generate: "launcher_icon"
image_path: "assets/icon-fit.png" image_path: "assets/icon.png"
ios: true ios: true
image_path: "assets/icon.png" image_path: "assets/icon.png"
min_sdk_android: 21 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