✨ Can edit avatar and banner
This commit is contained in:
		@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -73,7 +73,7 @@
 | 
			
		||||
  "reactionAdded": "你的反应已被添加。",
 | 
			
		||||
  "reactionRemoved": "你的反应已被移除。",
 | 
			
		||||
  "chatNew": "新聊天",
 | 
			
		||||
  "chatManage": "管理聊天",
 | 
			
		||||
  "chatDetail": "聊天详情",
 | 
			
		||||
  "chatMember": "成员",
 | 
			
		||||
  "chatNotifySetting": "通知设定",
 | 
			
		||||
  "chatNewCreate": "新建频道",
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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<PersonalizeScreenWidget> {
 | 
			
		||||
  final _imagePicker = ImagePicker();
 | 
			
		||||
 | 
			
		||||
  final _usernameController = TextEditingController();
 | 
			
		||||
  final _nicknameController = TextEditingController();
 | 
			
		||||
  final _firstNameController = TextEditingController();
 | 
			
		||||
@@ -38,6 +44,8 @@ class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
 | 
			
		||||
  final _descriptionController = TextEditingController();
 | 
			
		||||
  final _birthdayController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  String? _avatar;
 | 
			
		||||
  String? _banner;
 | 
			
		||||
  DateTime? _birthday;
 | 
			
		||||
 | 
			
		||||
  bool _isSubmitting = false;
 | 
			
		||||
@@ -65,6 +73,12 @@ class _PersonalizeScreenWidgetState extends State<PersonalizeScreenWidget> {
 | 
			
		||||
    _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<PersonalizeScreenWidget> {
 | 
			
		||||
    );
 | 
			
		||||
    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<PersonalizeScreenWidget> {
 | 
			
		||||
    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
 | 
			
		||||
  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(
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return IndentScaffold(
 | 
			
		||||
      title: AppLocalizations.of(context)!.chatManage,
 | 
			
		||||
      title: AppLocalizations.of(context)!.chatDetail,
 | 
			
		||||
      hideDrawer: true,
 | 
			
		||||
      noSafeArea: true,
 | 
			
		||||
      child: Column(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								lib/utils/file.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/utils/file.dart
									
									
									
									
									
										Normal 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();
 | 
			
		||||
}
 | 
			
		||||
@@ -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<AttachmentEditor> {
 | 
			
		||||
    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<AttachmentEditor> {
 | 
			
		||||
 | 
			
		||||
    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<AttachmentEditor> {
 | 
			
		||||
    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<AttachmentEditor> {
 | 
			
		||||
    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) {
 | 
			
		||||
    return item.filename.replaceAll(RegExp(r'\.[^/.]+$'), '');
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user