Can edit avatar and banner

This commit is contained in:
LittleSheep 2024-05-03 16:16:42 +08:00
parent e742338d92
commit 740c704fb8
9 changed files with 147 additions and 19 deletions

View File

@ -20,7 +20,7 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false 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" include ":app"

View File

@ -75,7 +75,7 @@
"chatNew": "New Chat", "chatNew": "New Chat",
"chatNewCreate": "Create a channel", "chatNewCreate": "Create a channel",
"chatNewJoin": "Join a exists channel", "chatNewJoin": "Join a exists channel",
"chatManage": "Manage Chat", "chatDetail": "Chat Details",
"chatMember": "Member", "chatMember": "Member",
"chatNotifySetting": "Notify Settings", "chatNotifySetting": "Notify Settings",
"chatChannelUsage": "Channel", "chatChannelUsage": "Channel",

View File

@ -73,7 +73,7 @@
"reactionAdded": "你的反应已被添加。", "reactionAdded": "你的反应已被添加。",
"reactionRemoved": "你的反应已被移除。", "reactionRemoved": "你的反应已被移除。",
"chatNew": "新聊天", "chatNew": "新聊天",
"chatManage": "管理聊天", "chatDetail": "聊天详情",
"chatMember": "成员", "chatMember": "成员",
"chatNotifySetting": "通知设定", "chatNotifySetting": "通知设定",
"chatNewCreate": "新建频道", "chatNewCreate": "新建频道",

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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/utils/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/widgets/scaffold.dart'; import 'package:solian/widgets/scaffold.dart';
@ -14,7 +15,7 @@ class AccountScreen extends StatelessWidget {
return IndentScaffold( return IndentScaffold(
title: AppLocalizations.of(context)!.account, title: AppLocalizations.of(context)!.account,
noSafeArea: true, noSafeArea: true,
fixedAppBarColor: true, fixedAppBarColor: SolianTheme.isLargeScreen(context),
child: AccountScreenWidget( child: AccountScreenWidget(
onSelect: (item) { onSelect: (item) {
SolianRouter.router.pushNamed(item); SolianRouter.router.pushNamed(item);

View File

@ -1,11 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/utils/service_url.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/exts.dart';
import 'package:solian/widgets/scaffold.dart'; import 'package:solian/widgets/scaffold.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -31,6 +35,8 @@ class PersonalizeScreenWidget extends StatefulWidget {
} }
class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> { class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
final _imagePicker = ImagePicker();
final _usernameController = TextEditingController(); final _usernameController = TextEditingController();
final _nicknameController = TextEditingController(); final _nicknameController = TextEditingController();
final _firstNameController = TextEditingController(); final _firstNameController = TextEditingController();
@ -38,6 +44,8 @@ class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
final _birthdayController = TextEditingController(); final _birthdayController = TextEditingController();
String? _avatar;
String? _banner;
DateTime? _birthday; DateTime? _birthday;
bool _isSubmitting = false; bool _isSubmitting = false;
@ -65,6 +73,12 @@ class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
_descriptionController.text = prof['description']; _descriptionController.text = prof['description'];
_firstNameController.text = prof['profile']['first_name']; _firstNameController.text = prof['profile']['first_name'];
_lastNameController.text = prof['profile']['last_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) { if (prof['profile']['birthday'] != null) {
_birthday = DateTime.parse(prof['profile']['birthday']); _birthday = DateTime.parse(prof['profile']['birthday']);
_birthdayController.text = DateFormat('yyyy-MM-dd hh:mm').format(_birthday!); _birthdayController.text = DateFormat('yyyy-MM-dd hh:mm').format(_birthday!);
@ -93,7 +107,9 @@ class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
); );
if (res.statusCode == 200) { if (res.statusCode == 200) {
await auth.fetchProfiles(); await auth.fetchProfiles();
resetInputs(); setState(() {
resetInputs();
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context)!.personalizeApplied), content: Text(AppLocalizations.of(context)!.personalizeApplied),
@ -106,20 +122,115 @@ class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
} }
Future<void> applyAvatar(String position) async {
final auth = context.read<AuthProvider>();
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 @override
void initState() { void initState() {
super.initState(); super.initState();
Future.delayed(Duration.zero, () => resetInputs()); Future.delayed(Duration.zero, () {
setState(() {
resetInputs();
});
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column( child: ListView(
children: [ children: [
_isSubmitting ? const LinearProgressIndicator().animate().scaleX() : Container(), _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( Row(
children: [ children: [
Flexible( Flexible(

View File

@ -63,7 +63,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
]; ];
return IndentScaffold( return IndentScaffold(
title: AppLocalizations.of(context)!.chatManage, title: AppLocalizations.of(context)!.chatDetail,
hideDrawer: true, hideDrawer: true,
noSafeArea: true, noSafeArea: true,
child: Column( child: Column(

9
lib/utils/file.dart Normal file
View File

@ -0,0 +1,9 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
Future<String> calculateFileSha256(File file) async {
final bytes = await file.readAsBytes();
final digest = sha256.convert(bytes);
return digest.toString();
}

View File

@ -2,7 +2,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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:provider/provider.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/utils/file.dart';
import 'package:solian/utils/service_url.dart'; import 'package:solian/utils/service_url.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:solian/widgets/exts.dart'; import 'package:solian/widgets/exts.dart';
@ -62,7 +62,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
bool isPopped = false; bool isPopped = false;
for (final media in medias) { for (final media in medias) {
final file = File(media.path); final file = File(media.path);
final hashcode = await calculateSha256(file); final hashcode = await calculateFileSha256(file);
try { try {
await uploadAttachment(file, hashcode); await uploadAttachment(file, hashcode);
} catch (err) { } catch (err) {
@ -90,7 +90,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
bool isPopped = false; bool isPopped = false;
for (final file in files) { for (final file in files) {
final hashcode = await calculateSha256(file); final hashcode = await calculateFileSha256(file);
try { try {
await uploadAttachment(file, hashcode); await uploadAttachment(file, hashcode);
} catch (err) { } catch (err) {
@ -120,7 +120,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final file = File(media.path); final file = File(media.path);
final hashcode = await calculateSha256(file); final hashcode = await calculateFileSha256(file);
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
Navigator.pop(context); Navigator.pop(context);
@ -171,12 +171,6 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
} }
Future<String> calculateSha256(File file) async {
final bytes = await file.readAsBytes();
final digest = sha256.convert(bytes);
return digest.toString();
}
String getFileName(Attachment item) { String getFileName(Attachment item) {
return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), ''); return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), '');
} }

View File

@ -2,6 +2,8 @@ PODS:
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- desktop_drop (0.0.1):
- FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1): - file_selector_macos (0.0.1):
@ -30,6 +32,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- screen_brightness_macos (0.1.0): - screen_brightness_macos (0.1.0):
- FlutterMacOS - FlutterMacOS
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1): - url_launcher_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
@ -38,6 +43,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - 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`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/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`) - 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`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - 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`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -61,6 +68,8 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
connectivity_plus: connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
desktop_drop:
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
device_info_plus: device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_selector_macos: file_selector_macos:
@ -87,6 +96,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_brightness_macos: screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
url_launcher_macos: url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
wakelock_plus: wakelock_plus:
@ -94,6 +105,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
@ -107,6 +119,7 @@ SPEC CHECKSUMS:
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: efc3e67e0355b1ee14bfe3c91188cada6000cb94 WebRTC-SDK: efc3e67e0355b1ee14bfe3c91188cada6000cb94