diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
index 0fac258..2a70256 100644
--- a/assets/translations/en-US.json
+++ b/assets/translations/en-US.json
@@ -11,6 +11,7 @@
"screenAccountPublishers": "Publishers",
"screenAccountPublisherNew": "New Publisher",
"screenAccountPublisherEdit": "Edit Publisher",
+ "screenAccountProfileEdit": "Edit Profile",
"dialogOkay": "Okay",
"dialogCancel": "Cancel",
"dialogConfirm": "Confirm",
@@ -36,6 +37,10 @@
"fieldDescription": "Description",
"fieldUsernameCannotEditHint": "Username cannot be edited after created",
"fieldUsernameLookupHint": "You can use username, phone number or email to login",
+ "fieldFirstName": "First name",
+ "fieldLastName": "Last name",
+ "fieldBirthday": "Birthday",
+ "fieldImageHint": "You can click those profile pictures to edit them.",
"forgotPassword": "Forgot password",
"loginPickFactor": "Pick a factor",
"loginMultiFactor": {
@@ -54,6 +59,9 @@
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
"accountPublishers": "Your publishers",
"accountPublishersSubtitle": "Manage your publish identities.",
+ "accountProfileEdit": "Edit your profile",
+ "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
+ "accountProfileEditApplied": "Profile modification applied.",
"publishersNew": "New Publisher",
"publisherNewSubtitle": "Create a new publisher identity.",
"publisherSyncWithAccount": "Sync with account"
diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json
index e37e5ea..6c42db4 100644
--- a/assets/translations/zh-CN.json
+++ b/assets/translations/zh-CN.json
@@ -11,6 +11,7 @@
"screenAccountPublishers": "发布者",
"screenAccountPublisherNew": "新建发布者",
"screenAccountPublisherEdit": "编辑发布者",
+ "screenAccountProfileEdit": "编辑资料",
"dialogOkay": "好的",
"dialogCancel": "取消",
"dialogConfirm": "确认",
@@ -35,6 +36,10 @@
"fieldPassword": "密码",
"fieldUsernameCannotEditHint": "用户名在创建后无法修改",
"fieldUsernameLookupHint": "支持用户名、电话号码或邮箱地址",
+ "fieldFirstName": "名",
+ "fieldLastName": "姓",
+ "fieldBirthday": "生日",
+ "fieldImageHint": "你可以点击这些个人头像来编辑它们。",
"fieldDescription": "简介",
"forgotPassword": "忘记密码",
"loginPickFactor": "选择方式验证",
@@ -54,6 +59,9 @@
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
"accountPublishers": "你的发布者",
"accountPublishersSubtitle": "管理你的公共形象。",
+ "accountProfileEdit": "编辑资料",
+ "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
+ "accountProfileEditApplied": "个人资料修改已被应用。",
"publishersNew": "新发布者",
"publisherNewSubtitle": "创建一个新的公共身份。",
"publisherSyncWithAccount": "同步账户信息"
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 0e88c5a..1f5c230 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -2,47 +2,134 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
+ - croppy (0.0.1):
+ - Flutter
- cupertino_http (0.0.1):
- Flutter
+ - DKImagePickerController/Core (4.3.9):
+ - DKImagePickerController/ImageDataManager
+ - DKImagePickerController/Resource
+ - DKImagePickerController/ImageDataManager (4.3.9)
+ - DKImagePickerController/PhotoGallery (4.3.9):
+ - DKImagePickerController/Core
+ - DKPhotoGallery
+ - DKImagePickerController/Resource (4.3.9)
+ - DKPhotoGallery (0.0.19):
+ - DKPhotoGallery/Core (= 0.0.19)
+ - DKPhotoGallery/Model (= 0.0.19)
+ - DKPhotoGallery/Preview (= 0.0.19)
+ - DKPhotoGallery/Resource (= 0.0.19)
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Core (0.0.19):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Preview
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Model (0.0.19):
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Preview (0.0.19):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Resource
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Resource (0.0.19):
+ - SDWebImage
+ - SwiftyGif
+ - file_picker (0.0.1):
+ - DKImagePickerController/PhotoGallery
+ - Flutter
- Flutter (1.0.0)
+ - flutter_image_compress_common (1.0.0):
+ - Flutter
+ - Mantle
+ - SDWebImage
+ - SDWebImageWebPCoder
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (3.3.1):
- Flutter
+ - image_picker_ios (0.0.1):
+ - Flutter
+ - libwebp (1.3.2):
+ - libwebp/demux (= 1.3.2)
+ - libwebp/mux (= 1.3.2)
+ - libwebp/sharpyuv (= 1.3.2)
+ - libwebp/webp (= 1.3.2)
+ - libwebp/demux (1.3.2):
+ - libwebp/webp
+ - libwebp/mux (1.3.2):
+ - libwebp/demux
+ - libwebp/sharpyuv (1.3.2)
+ - libwebp/webp (1.3.2):
+ - libwebp/sharpyuv
+ - Mantle (2.2.0):
+ - Mantle/extobjc (= 2.2.0)
+ - Mantle/extobjc (2.2.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - SDWebImage (5.19.7):
+ - SDWebImage/Core (= 5.19.7)
+ - SDWebImage/Core (5.19.7)
+ - SDWebImageWebPCoder (0.14.6):
+ - libwebp (~> 1.0)
+ - SDWebImage/Core (~> 5.17)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
+ - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
+ - croppy (from `.symlinks/plugins/croppy/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
+ - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
+ - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+SPEC REPOS:
+ trunk:
+ - DKImagePickerController
+ - DKPhotoGallery
+ - libwebp
+ - Mantle
+ - SDWebImage
+ - SDWebImageWebPCoder
+ - SwiftyGif
+
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
+ croppy:
+ :path: ".symlinks/plugins/croppy/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/ios"
+ file_picker:
+ :path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
+ flutter_image_compress_common:
+ :path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
@@ -54,13 +141,24 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
+ croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
+ DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
+ DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
+ file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
+ image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
+ libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
+ Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
+ SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
+ SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
+ SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index e48f56e..2e1f684 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -1,56 +1,64 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Surface
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- surface
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- CADisableMinimumFrameDurationOnPhone
-
- UIApplicationSupportsIndirectInputEvents
-
- CFBundleLocalizations
-
- en
- zh_CN
-
- UIStatusBarHidden
-
-
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Surface
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ surface
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+ CFBundleLocalizations
+
+ en
+ zh_CN
+
+ NSPhotoLibraryUsageDescription
+ Grant access to Photo Library will allow Solian upload photo or video for your post.
+ NSCameraUsageDescription
+ Grant access to Photo Library will allow Solian take photo or video for your post.
+ NSMicrophoneUsageDescription
+ Grant access to Photo Library will allow Solian record audio for your post.
+ ITSAppUsesNonExemptEncryption
+
+ UIStatusBarHidden
+
+
diff --git a/lib/main.dart b/lib/main.dart
index 637b18a..710fb32 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,3 +1,4 @@
+import 'package:croppy/croppy.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:flutter/material.dart';
@@ -48,6 +49,7 @@ class SolianApp extends StatelessWidget {
locale: context.locale,
supportedLocales: context.supportedLocales,
localizationsDelegates: [
+ CroppyLocalizations.delegate,
RelativeTimeLocalizations.delegate,
...context.localizationDelegates,
],
diff --git a/lib/providers/sn_attachment.dart b/lib/providers/sn_attachment.dart
index 8a739e1..2323e07 100644
--- a/lib/providers/sn_attachment.dart
+++ b/lib/providers/sn_attachment.dart
@@ -1,8 +1,15 @@
+import 'dart:collection';
+import 'dart:typed_data';
+
+import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
+import 'package:cross_file/cross_file.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
+const kConcurrentUploadChunks = 5;
+
class SnAttachmentProvider {
late final SnNetworkProvider _sn;
final Map _cache = {};
@@ -43,4 +50,157 @@ class SnAttachmentProvider {
}
return rids.map((rid) => _cache[rid]!).toList();
}
+
+ static Map mimetypeOverrides = {
+ 'mov': 'video/quicktime',
+ 'mp4': 'video/mp4'
+ };
+
+ Future directUploadOne(
+ Uint8List data,
+ String filename,
+ String pool,
+ Map? metadata, {
+ String? mimetype,
+ Function(double progress)? onProgress,
+ }) async {
+ final filePayload = MultipartFile.fromBytes(data, filename: filename);
+ final fileAlt = filename.contains('.')
+ ? filename.substring(0, filename.lastIndexOf('.'))
+ : filename;
+ final fileExt =
+ filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+
+ String? mimetypeOverride;
+ if (mimetype != null) {
+ mimetypeOverride = mimetype;
+ } else if (mimetypeOverrides.keys.contains(fileExt)) {
+ mimetypeOverride = mimetypeOverrides[fileExt];
+ }
+
+ final formData = FormData.fromMap({
+ 'alt': fileAlt,
+ 'file': filePayload,
+ 'pool': pool,
+ 'metadata': metadata,
+ if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
+ });
+ final resp = await _sn.client.post(
+ '/cgi/uc/attachments',
+ data: formData,
+ onSendProgress: (count, total) {
+ if (onProgress != null) {
+ onProgress(count / total);
+ }
+ },
+ );
+
+ return SnAttachment.fromJson(resp.data);
+ }
+
+ Future<(SnAttachment, int)> chunkedUploadInitialize(
+ int size,
+ String filename,
+ String pool,
+ Map? metadata,
+ ) async {
+ final fileAlt = filename.contains('.')
+ ? filename.substring(0, filename.lastIndexOf('.'))
+ : filename;
+ final fileExt =
+ filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+
+ String? mimetypeOverride;
+ if (mimetypeOverrides.keys.contains(fileExt)) {
+ mimetypeOverride = mimetypeOverrides[fileExt];
+ }
+
+ final resp = await _sn.client.post('/cgi/uc/attachments/multipart', data: {
+ 'alt': fileAlt,
+ 'name': filename,
+ 'pool': pool,
+ 'metadata': metadata,
+ 'size': size,
+ if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
+ });
+
+ return (
+ SnAttachment.fromJson(resp.data['meta']),
+ resp.data['chunk_size'] as int
+ );
+ }
+
+ Future _chunkedUploadOnePart(
+ Uint8List data,
+ String rid,
+ String cid, {
+ Function(double progress)? onProgress,
+ }) async {
+ final resp = await _sn.client.post(
+ '/cgi/uc/attachments/multipart/$rid/$cid',
+ data: data,
+ options: Options(headers: {'Content-Type': 'application/octet-stream'}),
+ onSendProgress: (count, total) {
+ if (onProgress != null) {
+ onProgress(count / total);
+ }
+ },
+ );
+
+ return SnAttachment.fromJson(resp.data);
+ }
+
+ Future chunkedUploadParts(
+ XFile file,
+ SnAttachment place,
+ int chunkSize, {
+ Function(double progress)? onProgress,
+ }) async {
+ final Map chunks = place.fileChunks ?? {};
+ var currentTask = 0;
+
+ final queue = Queue>();
+ final activeTasks = >[];
+
+ for (final entry in chunks.entries) {
+ queue.add(() async {
+ final beginCursor = entry.value * chunkSize;
+ final endCursor = (entry.value + 1) * chunkSize;
+ final data = Uint8List.fromList(await file
+ .openRead(beginCursor, endCursor)
+ .expand((chunk) => chunk)
+ .toList());
+
+ place = await _chunkedUploadOnePart(
+ data,
+ place.rid,
+ entry.key,
+ onProgress: (chunkProgress) {
+ final overallProgress =
+ (currentTask + chunkProgress) / chunks.length;
+ if (onProgress != null) {
+ onProgress(overallProgress);
+ }
+ },
+ );
+
+ currentTask++;
+ }());
+ }
+
+ while (queue.isNotEmpty || activeTasks.isNotEmpty) {
+ while (activeTasks.length < kConcurrentUploadChunks && queue.isNotEmpty) {
+ final task = queue.removeFirst();
+ activeTasks.add(task);
+
+ task.then((_) => activeTasks.remove(task));
+ }
+
+ if (activeTasks.isNotEmpty) {
+ await Future.any(activeTasks);
+ }
+ }
+
+ return place;
+ }
}
diff --git a/lib/router.dart b/lib/router.dart
index fe1ab37..6ea6d7c 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -1,8 +1,9 @@
import 'package:go_router/go_router.dart';
import 'package:surface/screens/account.dart';
-import 'package:surface/screens/account/publisher_edit.dart';
-import 'package:surface/screens/account/publisher_new.dart';
-import 'package:surface/screens/account/publishers.dart';
+import 'package:surface/screens/account/profile_edit.dart';
+import 'package:surface/screens/account/publishers/publisher_edit.dart';
+import 'package:surface/screens/account/publishers/publisher_new.dart';
+import 'package:surface/screens/account/publishers/publishers.dart';
import 'package:surface/screens/auth/login.dart';
import 'package:surface/screens/auth/register.dart';
import 'package:surface/screens/explore.dart';
@@ -50,6 +51,11 @@ final appRouter = GoRouter(
name: 'authRegister',
builder: (context, state) => const RegisterScreen(),
),
+ GoRoute(
+ path: '/account/profile/edit',
+ name: 'accountProfileEdit',
+ builder: (context, state) => const ProfileEditScreen(),
+ ),
GoRoute(
path: '/account/publishers',
name: 'accountPublishers',
diff --git a/lib/screens/account.dart b/lib/screens/account.dart
index 846c2c3..eac66c0 100644
--- a/lib/screens/account.dart
+++ b/lib/screens/account.dart
@@ -74,6 +74,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
);
}).padding(all: 20),
).padding(horizontal: 8, top: 16, bottom: 4),
+ ListTile(
+ title: Text('accountProfileEdit').tr(),
+ subtitle: Text('accountProfileEditSubtitle').tr(),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+ leading: const Icon(Symbols.contact_page),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () {
+ GoRouter.of(context).pushNamed('accountProfileEdit');
+ },
+ ),
ListTile(
title: Text('accountPublishers').tr(),
subtitle: Text('accountPublishersSubtitle').tr(),
diff --git a/lib/screens/account/profile_edit.dart b/lib/screens/account/profile_edit.dart
new file mode 100644
index 0000000..695b2d7
--- /dev/null
+++ b/lib/screens/account/profile_edit.dart
@@ -0,0 +1,348 @@
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:croppy/croppy.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:path/path.dart' show basename;
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+import 'package:surface/providers/sn_attachment.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/providers/userinfo.dart';
+import 'package:surface/widgets/account/account_image.dart';
+import 'package:surface/widgets/dialog.dart';
+import 'package:surface/widgets/loading_indicator.dart';
+import 'package:surface/widgets/universal_image.dart';
+
+class ProfileEditScreen extends StatefulWidget {
+ const ProfileEditScreen({super.key});
+
+ @override
+ State createState() => _ProfileEditScreenState();
+}
+
+class _ProfileEditScreenState extends State {
+ final _imagePicker = ImagePicker();
+
+ final _usernameController = TextEditingController();
+ final _nicknameController = TextEditingController();
+ final _firstNameController = TextEditingController();
+ final _lastNameController = TextEditingController();
+ final _descriptionController = TextEditingController();
+ final _birthdayController = TextEditingController();
+
+ String? _avatar;
+ String? _banner;
+ DateTime? _birthday;
+
+ bool _isBusy = false;
+
+ static const _kDateFormat = 'y/M/d';
+
+ void _syncWidget() async {
+ final ua = context.read();
+ final prof = ua.user!;
+ _usernameController.text = prof.name;
+ _nicknameController.text = prof.nick;
+ _descriptionController.text = prof.description;
+ _firstNameController.text = prof.profile!.firstName;
+ _lastNameController.text = prof.profile!.lastName;
+ _avatar = prof.avatar;
+ _banner = prof.banner;
+ if (prof.profile!.birthday != null) {
+ _birthdayController.text = DateFormat(_kDateFormat).format(
+ prof.profile!.birthday!.toLocal(),
+ );
+ }
+ }
+
+ void _selectBirthday() async {
+ await showCupertinoModalPopup(
+ context: context,
+ builder: (BuildContext context) => Container(
+ height: 216,
+ padding: const EdgeInsets.only(top: 6.0),
+ margin: EdgeInsets.only(
+ bottom: MediaQuery.of(context).viewInsets.bottom,
+ ),
+ color: Theme.of(context).colorScheme.surface,
+ child: SafeArea(
+ top: false,
+ child: CupertinoDatePicker(
+ initialDateTime: _birthday?.toLocal(),
+ mode: CupertinoDatePickerMode.date,
+ use24hFormat: true,
+ onDateTimeChanged: (DateTime newDate) {
+ setState(() {
+ _birthday = newDate;
+ _birthdayController.text =
+ DateFormat(_kDateFormat).format(_birthday!);
+ });
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
+ Future _updateImage(String place) async {
+ final image = await _imagePicker.pickImage(source: ImageSource.gallery);
+ if (image == null) return;
+ if (!mounted) return;
+
+ final ImageProvider imageProvider =
+ kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
+ final aspectRatios = place == 'banner'
+ ? [CropAspectRatio(width: 16, height: 7)]
+ : [CropAspectRatio(width: 1, height: 1)];
+ final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
+ ? await showCupertinoImageCropper(
+ // ignore: use_build_context_synchronously
+ context,
+ allowedAspectRatios: aspectRatios,
+ imageProvider: imageProvider,
+ )
+ : await showMaterialImageCropper(
+ // ignore: use_build_context_synchronously
+ context,
+ allowedAspectRatios: aspectRatios,
+ imageProvider: imageProvider,
+ );
+
+ if (result == null) return;
+
+ if (!mounted) return;
+ final attach = context.read();
+
+ setState(() => _isBusy = true);
+
+ final rawBytes =
+ (await result.uiImage.toByteData(format: ImageByteFormat.png))!
+ .buffer
+ .asUint8List();
+
+ try {
+ final attachment = await attach.directUploadOne(
+ rawBytes,
+ basename(image.path),
+ 'avatar',
+ null,
+ mimetype: 'image/png',
+ );
+
+ if (!mounted) return;
+ final sn = context.read();
+ await sn.client.put(
+ '/cgi/id/users/me/$place',
+ data: {'attachment': attachment.rid},
+ );
+
+ if (!mounted) return;
+ final ua = context.read();
+ await ua.refreshUser();
+
+ if (!mounted) return;
+ context.showSnackbar('accountProfileEditApplied'.tr());
+ _syncWidget();
+ } catch (err) {
+ if (!mounted) return;
+ context.showErrorDialog(err);
+ } finally {
+ setState(() => _isBusy = false);
+ }
+ }
+
+ void _updateUserInfo() async {
+ setState(() => _isBusy = true);
+
+ final sn = context.read();
+
+ try {
+ await sn.client.put(
+ '/cgi/id/users/me',
+ data: {
+ 'nick': _nicknameController.value.text,
+ 'description': _descriptionController.value.text,
+ 'first_name': _firstNameController.value.text,
+ 'last_name': _lastNameController.value.text,
+ 'birthday': _birthday?.toUtc().toIso8601String(),
+ },
+ );
+
+ if (!mounted) return;
+ final ua = context.read();
+ await ua.refreshUser();
+
+ if (!mounted) return;
+ context.showSnackbar('accountProfileEditApplied'.tr());
+ _syncWidget();
+ } catch (err) {
+ context.showErrorDialog(err);
+ } finally {
+ setState(() => _isBusy = false);
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _syncWidget();
+ }
+
+ @override
+ void dispose() {
+ _usernameController.dispose();
+ _nicknameController.dispose();
+ _firstNameController.dispose();
+ _lastNameController.dispose();
+ _descriptionController.dispose();
+ _birthdayController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ const double padding = 24;
+
+ final sn = context.read();
+
+ return SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ LoadingIndicator(isActive: _isBusy),
+ const Gap(24),
+ Stack(
+ clipBehavior: Clip.none,
+ children: [
+ Material(
+ elevation: 0,
+ child: InkWell(
+ child: ClipRRect(
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
+ child: AspectRatio(
+ aspectRatio: 16 / 9,
+ child: Container(
+ color:
+ Theme.of(context).colorScheme.surfaceContainerHigh,
+ child: _banner != null
+ ? UniversalImage(
+ sn.getAttachmentUrl(_banner!),
+ fit: BoxFit.cover,
+ )
+ : const SizedBox.shrink(),
+ ),
+ ),
+ ),
+ onTap: () {
+ _updateImage('banner');
+ },
+ ),
+ ),
+ Positioned(
+ bottom: -28,
+ left: 16,
+ child: Material(
+ elevation: 2,
+ borderRadius: const BorderRadius.all(Radius.circular(40)),
+ child: InkWell(
+ child: AccountImage(content: _avatar, radius: 40),
+ onTap: () {
+ _updateImage('avatar');
+ },
+ ),
+ ),
+ ),
+ ],
+ ).padding(horizontal: padding),
+ const Gap(8 + 28),
+ Column(
+ children: [
+ TextField(
+ readOnly: true,
+ controller: _usernameController,
+ decoration: InputDecoration(
+ border: const UnderlineInputBorder(),
+ labelText: 'fieldUsername'.tr(),
+ helperText: 'fieldUsernameCannotEditHint'.tr(),
+ ),
+ ),
+ const Gap(4),
+ TextField(
+ controller: _nicknameController,
+ decoration: InputDecoration(
+ border: const UnderlineInputBorder(),
+ labelText: 'fieldNickname'.tr(),
+ ),
+ ),
+ const Gap(4),
+ Row(
+ children: [
+ Flexible(
+ flex: 1,
+ child: TextField(
+ controller: _firstNameController,
+ decoration: InputDecoration(
+ border: const UnderlineInputBorder(),
+ labelText: 'fieldFirstName'.tr(),
+ ),
+ ),
+ ),
+ const Gap(8),
+ Flexible(
+ flex: 1,
+ child: TextField(
+ controller: _lastNameController,
+ decoration: InputDecoration(
+ border: const UnderlineInputBorder(),
+ labelText: 'fieldLastName'.tr(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ const Gap(4),
+ TextField(
+ controller: _descriptionController,
+ keyboardType: TextInputType.multiline,
+ maxLines: null,
+ minLines: 3,
+ decoration: InputDecoration(
+ border: const UnderlineInputBorder(),
+ labelText: 'fieldDescription'.tr(),
+ ),
+ ),
+ const Gap(4),
+ TextField(
+ controller: _birthdayController,
+ readOnly: true,
+ decoration: InputDecoration(
+ border: const UnderlineInputBorder(),
+ labelText: 'fieldBirthday'.tr(),
+ ),
+ onTap: () => _selectBirthday(),
+ ),
+ ],
+ ).padding(horizontal: padding + 8),
+ const Gap(12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ ElevatedButton.icon(
+ onPressed: _isBusy ? null : _updateUserInfo,
+ icon: const Icon(Symbols.save),
+ label: Text('apply').tr(),
+ ),
+ ],
+ ).padding(horizontal: padding),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/screens/account/publisher_edit.dart b/lib/screens/account/publishers/publisher_edit.dart
similarity index 99%
rename from lib/screens/account/publisher_edit.dart
rename to lib/screens/account/publishers/publisher_edit.dart
index 1c0095b..81d9b50 100644
--- a/lib/screens/account/publisher_edit.dart
+++ b/lib/screens/account/publishers/publisher_edit.dart
@@ -134,7 +134,7 @@ class _AccountPublisherEditScreenState
const Gap(4),
TextField(
controller: _descriptionController,
- maxLines: 3,
+ maxLines: null,
minLines: 3,
decoration: InputDecoration(
labelText: 'fieldDescription'.tr(),
diff --git a/lib/screens/account/publisher_new.dart b/lib/screens/account/publishers/publisher_new.dart
similarity index 100%
rename from lib/screens/account/publisher_new.dart
rename to lib/screens/account/publishers/publisher_new.dart
diff --git a/lib/screens/account/publishers.dart b/lib/screens/account/publishers/publishers.dart
similarity index 100%
rename from lib/screens/account/publishers.dart
rename to lib/screens/account/publishers/publishers.dart
diff --git a/lib/widgets/account/account_image.dart b/lib/widgets/account/account_image.dart
index ccb62fe..d5aaa22 100644
--- a/lib/widgets/account/account_image.dart
+++ b/lib/widgets/account/account_image.dart
@@ -4,7 +4,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/widgets/universal_image.dart';
class AccountImage extends StatelessWidget {
- final String content;
+ final String? content;
final Color? backgroundColor;
final Color? foregroundColor;
final double? radius;
@@ -22,21 +22,21 @@ class AccountImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final sn = context.read();
- final url = sn.getAttachmentUrl(content);
+ final url = sn.getAttachmentUrl(content ?? '');
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return CircleAvatar(
key: Key('attachment-${content.hashCode}'),
radius: radius,
backgroundColor: backgroundColor,
- backgroundImage: content.isNotEmpty
+ backgroundImage: (content?.isNotEmpty ?? false)
? ResizeImage(
UniversalImage.provider(url),
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
)
: null,
- child: content.isEmpty
+ child: (content?.isEmpty ?? true)
? (fallbackWidget ??
Icon(
Icons.account_circle,
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 80f0c37..0a1f571 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
+ file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin");
flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 3d4708b..aa8db8f 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -3,11 +3,13 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ file_selector_linux
flutter_secure_storage
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ croppy
jni
)
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 311f992..de38c1e 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -6,6 +6,8 @@ import FlutterMacOS
import Foundation
import connectivity_plus
+import file_selector_macos
+import flutter_image_compress_macos
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
@@ -13,6 +15,8 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
+ FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
+ FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index b028ed2..c1d952c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -158,6 +158,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
+ cassowary:
+ dependency: transitive
+ description:
+ name: cassowary
+ sha256: f304452beaf93b9349daaeeda23f853578c9dd8674c06c6100fda0319c46b967
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.3"
characters:
dependency: transitive
description:
@@ -230,6 +238,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
+ croppy:
+ dependency: "direct main"
+ description:
+ name: croppy
+ sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.1"
+ cross_file:
+ dependency: "direct main"
+ description:
+ name: cross_file
+ sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.4+2"
crypto:
dependency: transitive
description:
@@ -334,6 +358,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.2"
+ equatable:
+ dependency: transitive
+ description:
+ name: equatable
+ sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.5"
fake_async:
dependency: transitive
description:
@@ -358,6 +390,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
+ file_picker:
+ dependency: "direct main"
+ description:
+ name: file_picker
+ sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.1.3"
+ file_selector_linux:
+ dependency: transitive
+ description:
+ name: file_selector_linux
+ sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3"
+ file_selector_macos:
+ dependency: transitive
+ description:
+ name: file_selector_macos
+ sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.4+2"
+ file_selector_platform_interface:
+ dependency: transitive
+ description:
+ name: file_selector_platform_interface
+ sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.6.2"
+ file_selector_windows:
+ dependency: transitive
+ description:
+ name: file_selector_windows
+ sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.9.3+3"
fixnum:
dependency: transitive
description:
@@ -387,6 +459,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
+ flutter_image_compress:
+ dependency: "direct main"
+ description:
+ name: flutter_image_compress
+ sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ flutter_image_compress_common:
+ dependency: transitive
+ description:
+ name: flutter_image_compress_common
+ sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
+ flutter_image_compress_macos:
+ dependency: transitive
+ description:
+ name: flutter_image_compress_macos
+ sha256: "26df6385512e92b3789dc76b613b54b55c457a7f1532e59078b04bf189782d47"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
+ flutter_image_compress_ohos:
+ dependency: transitive
+ description:
+ name: flutter_image_compress_ohos
+ sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.3"
+ flutter_image_compress_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_image_compress_platform_interface
+ sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
+ flutter_image_compress_web:
+ dependency: transitive
+ description:
+ name: flutter_image_compress_web
+ sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.4+1"
flutter_lints:
dependency: "direct dev"
description:
@@ -416,6 +536,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.2"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.23"
flutter_secure_storage:
dependency: "direct main"
description:
@@ -562,6 +690,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.3.0"
+ image_picker:
+ dependency: "direct main"
+ description:
+ name: image_picker
+ sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ image_picker_android:
+ dependency: transitive
+ description:
+ name: image_picker_android
+ sha256: "8faba09ba361d4b246dc0a17cb4289b3324c2b9f6db7b3d457ee69106a86bd32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.12+17"
+ image_picker_for_web:
+ dependency: transitive
+ description:
+ name: image_picker_for_web
+ sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.6"
+ image_picker_ios:
+ dependency: transitive
+ description:
+ name: image_picker_ios
+ sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.12+1"
+ image_picker_linux:
+ dependency: transitive
+ description:
+ name: image_picker_linux
+ sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
+ image_picker_macos:
+ dependency: transitive
+ description:
+ name: image_picker_macos
+ sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
+ image_picker_platform_interface:
+ dependency: transitive
+ description:
+ name: image_picker_platform_interface
+ sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.10.0"
+ image_picker_windows:
+ dependency: transitive
+ description:
+ name: image_picker_windows
+ sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1+1"
intl:
dependency: transitive
description:
@@ -1263,6 +1455,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.8.0"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 9ae35c2..de6553a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -59,6 +59,11 @@ dependencies:
path: ^1.9.0
relative_time: ^5.0.0
flutter_secure_storage: ^4.2.1
+ image_picker: ^1.1.2
+ cross_file: ^0.3.4+2
+ file_picker: ^8.1.3
+ flutter_image_compress: ^2.3.0
+ croppy: ^1.3.1
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 5777988..384ea41 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
+ FileSelectorWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FileSelectorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 0b0bda0..a8a1fe3 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -4,10 +4,12 @@
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
+ file_selector_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ croppy
jni
)