diff --git a/android/settings.gradle b/android/settings.gradle index 1d6d19b..1744d99 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -20,7 +20,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "1.9.23" apply false } include ":app" diff --git a/lib/i18n/app_en.arb b/lib/i18n/app_en.arb index 6fd321a..fbba2ba 100644 --- a/lib/i18n/app_en.arb +++ b/lib/i18n/app_en.arb @@ -75,7 +75,7 @@ "chatNew": "New Chat", "chatNewCreate": "Create a channel", "chatNewJoin": "Join a exists channel", - "chatManage": "Manage Chat", + "chatDetail": "Chat Details", "chatMember": "Member", "chatNotifySetting": "Notify Settings", "chatChannelUsage": "Channel", diff --git a/lib/i18n/app_zh.arb b/lib/i18n/app_zh.arb index c50360a..c353e38 100644 --- a/lib/i18n/app_zh.arb +++ b/lib/i18n/app_zh.arb @@ -73,7 +73,7 @@ "reactionAdded": "你的反应已被添加。", "reactionRemoved": "你的反应已被移除。", "chatNew": "新聊天", - "chatManage": "管理聊天", + "chatDetail": "聊天详情", "chatMember": "成员", "chatNotifySetting": "通知设定", "chatNewCreate": "新建频道", diff --git a/lib/screens/account.dart b/lib/screens/account.dart index d520de3..00fca02 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/router.dart'; +import 'package:solian/utils/theme.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:solian/widgets/scaffold.dart'; @@ -14,7 +15,7 @@ class AccountScreen extends StatelessWidget { return IndentScaffold( title: AppLocalizations.of(context)!.account, noSafeArea: true, - fixedAppBarColor: true, + fixedAppBarColor: SolianTheme.isLargeScreen(context), child: AccountScreenWidget( onSelect: (item) { SolianRouter.router.pushNamed(item); diff --git a/lib/screens/account/personalize.dart b/lib/screens/account/personalize.dart index 2ccc162..f3deee2 100644 --- a/lib/screens/account/personalize.dart +++ b/lib/screens/account/personalize.dart @@ -1,11 +1,15 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:http/http.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/exts.dart'; import 'package:solian/widgets/scaffold.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -31,6 +35,8 @@ class PersonalizeScreenWidget extends StatefulWidget { } class _PersonalizeScreenWidgetState extends State { + final _imagePicker = ImagePicker(); + final _usernameController = TextEditingController(); final _nicknameController = TextEditingController(); final _firstNameController = TextEditingController(); @@ -38,6 +44,8 @@ class _PersonalizeScreenWidgetState extends State { final _descriptionController = TextEditingController(); final _birthdayController = TextEditingController(); + String? _avatar; + String? _banner; DateTime? _birthday; bool _isSubmitting = false; @@ -65,6 +73,12 @@ class _PersonalizeScreenWidgetState extends State { _descriptionController.text = prof['description']; _firstNameController.text = prof['profile']['first_name']; _lastNameController.text = prof['profile']['last_name']; + if (prof['avatar'] != null && prof['avatar'].isNotEmpty) { + _avatar = getRequestUri('passport', '/api/avatar/${prof['avatar']}').toString(); + } + if (prof['banner'] != null && prof['banner'].isNotEmpty) { + _banner = getRequestUri('passport', '/api/avatar/${prof['banner']}').toString(); + } if (prof['profile']['birthday'] != null) { _birthday = DateTime.parse(prof['profile']['birthday']); _birthdayController.text = DateFormat('yyyy-MM-dd hh:mm').format(_birthday!); @@ -93,7 +107,9 @@ class _PersonalizeScreenWidgetState extends State { ); if (res.statusCode == 200) { await auth.fetchProfiles(); - resetInputs(); + setState(() { + resetInputs(); + }); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.personalizeApplied), @@ -106,20 +122,115 @@ class _PersonalizeScreenWidgetState extends State { setState(() => _isSubmitting = false); } + Future applyAvatar(String position) async { + final auth = context.read(); + if (!await auth.isAuthorized()) return; + + final image = await _imagePicker.pickImage(source: ImageSource.gallery); + if (image == null) return; + + setState(() => _isSubmitting = true); + + final file = File(image.path); + try { + final req = MultipartRequest('PUT', getRequestUri('passport', '/api/users/me/$position')); + req.files.add(await MultipartFile.fromPath(position, file.path)); + + var res = await auth.client!.send(req); + if (res.statusCode == 200) { + await auth.fetchProfiles(); + setState(() { + resetInputs(); + }); + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)!.personalizeApplied), + )); + } else { + throw Exception(utf8.decode(await res.stream.toBytes())); + } + } catch (err) { + context.showErrorDialog(err); + } + + setState(() => _isSubmitting = false); + } + @override void initState() { super.initState(); - Future.delayed(Duration.zero, () => resetInputs()); + Future.delayed(Duration.zero, () { + setState(() { + resetInputs(); + }); + }); } @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 32), - child: Column( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: ListView( children: [ _isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), + const SizedBox(height: 24), + Stack( + children: [ + AccountAvatar(source: _avatar ?? '', radius: 40, direct: true), + Positioned( + bottom: 0, + left: 40, + child: FloatingActionButton.small( + onPressed: () => applyAvatar('avatar'), + child: const Icon( + Icons.camera, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Container( + color: Theme.of(context).colorScheme.surfaceVariant, + child: _banner != null + ? Image.network( + _banner!, + fit: BoxFit.cover, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ) + : Container(), + ), + ), + ), + Positioned( + bottom: 16, + right: 16, + child: FloatingActionButton( + onPressed: () => applyAvatar('banner'), + child: const Icon( + Icons.camera_alt, + ), + ), + ), + ], + ), + const SizedBox(height: 24), Row( children: [ Flexible( diff --git a/lib/screens/chat/chat_detail.dart b/lib/screens/chat/chat_detail.dart index c273b1d..42baf8b 100644 --- a/lib/screens/chat/chat_detail.dart +++ b/lib/screens/chat/chat_detail.dart @@ -63,7 +63,7 @@ class _ChatDetailScreenState extends State { ]; return IndentScaffold( - title: AppLocalizations.of(context)!.chatManage, + title: AppLocalizations.of(context)!.chatDetail, hideDrawer: true, noSafeArea: true, child: Column( diff --git a/lib/utils/file.dart b/lib/utils/file.dart new file mode 100644 index 0000000..e469d48 --- /dev/null +++ b/lib/utils/file.dart @@ -0,0 +1,9 @@ +import 'dart:io'; + +import 'package:crypto/crypto.dart'; + +Future calculateFileSha256(File file) async { + final bytes = await file.readAsBytes(); + final digest = sha256.convert(bytes); + return digest.toString(); +} \ No newline at end of file diff --git a/lib/widgets/posts/attachment_editor.dart b/lib/widgets/posts/attachment_editor.dart index b9c02ea..ca837c2 100755 --- a/lib/widgets/posts/attachment_editor.dart +++ b/lib/widgets/posts/attachment_editor.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; -import 'package:crypto/crypto.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -11,6 +10,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:solian/models/post.dart'; import 'package:solian/providers/auth.dart'; +import 'package:solian/utils/file.dart'; import 'package:solian/utils/service_url.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:solian/widgets/exts.dart'; @@ -62,7 +62,7 @@ class _AttachmentEditorState extends State { bool isPopped = false; for (final media in medias) { final file = File(media.path); - final hashcode = await calculateSha256(file); + final hashcode = await calculateFileSha256(file); try { await uploadAttachment(file, hashcode); } catch (err) { @@ -90,7 +90,7 @@ class _AttachmentEditorState extends State { bool isPopped = false; for (final file in files) { - final hashcode = await calculateSha256(file); + final hashcode = await calculateFileSha256(file); try { await uploadAttachment(file, hashcode); } catch (err) { @@ -120,7 +120,7 @@ class _AttachmentEditorState extends State { setState(() => _isSubmitting = true); final file = File(media.path); - final hashcode = await calculateSha256(file); + final hashcode = await calculateFileSha256(file); if (Navigator.canPop(context)) { Navigator.pop(context); @@ -171,12 +171,6 @@ class _AttachmentEditorState extends State { setState(() => _isSubmitting = false); } - Future calculateSha256(File file) async { - final bytes = await file.readAsBytes(); - final digest = sha256.convert(bytes); - return digest.toString(); - } - String getFileName(Attachment item) { return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), ''); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2392b32..f69939b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,6 +2,8 @@ PODS: - connectivity_plus (0.0.1): - Flutter - FlutterMacOS + - desktop_drop (0.0.1): + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -30,6 +32,9 @@ PODS: - FlutterMacOS - screen_brightness_macos (0.1.0): - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - wakelock_plus (0.0.1): @@ -38,6 +43,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) @@ -51,6 +57,7 @@ DEPENDENCIES: - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) @@ -61,6 +68,8 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: @@ -87,6 +96,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin screen_brightness_macos: :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos wakelock_plus: @@ -94,6 +105,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 @@ -107,6 +119,7 @@ SPEC CHECKSUMS: package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 WebRTC-SDK: efc3e67e0355b1ee14bfe3c91188cada6000cb94