Edit profile

This commit is contained in:
LittleSheep 2024-11-10 01:04:39 +08:00
parent ed2e44cc54
commit abc1d5a9d7
20 changed files with 928 additions and 60 deletions

View File

@ -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"

View File

@ -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": "同步账户信息"

View File

@ -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

View File

@ -50,6 +50,14 @@
<string>en</string>
<string>zh_CN</string>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Photo Library will allow Solian take photo or video for your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Photo Library will allow Solian record audio for your post.</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIStatusBarHidden</key>
<false/>
</dict>

View File

@ -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,
],

View File

@ -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<String, SnAttachment> _cache = {};
@ -43,4 +50,157 @@ class SnAttachmentProvider {
}
return rids.map((rid) => _cache[rid]!).toList();
}
static Map<String, String> mimetypeOverrides = {
'mov': 'video/quicktime',
'mp4': 'video/mp4'
};
Future<SnAttachment> directUploadOne(
Uint8List data,
String filename,
String pool,
Map<String, dynamic>? 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<String, dynamic>? 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<SnAttachment> _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<SnAttachment> chunkedUploadParts(
XFile file,
SnAttachment place,
int chunkSize, {
Function(double progress)? onProgress,
}) async {
final Map<String, dynamic> chunks = place.fileChunks ?? {};
var currentTask = 0;
final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[];
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;
}
}

View File

@ -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',

View File

@ -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(),

View File

@ -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<ProfileEditScreen> createState() => _ProfileEditScreenState();
}
class _ProfileEditScreenState extends State<ProfileEditScreen> {
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<UserProvider>();
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<DateTime?>(
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<void> _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<SnAttachmentProvider>();
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<SnNetworkProvider>();
await sn.client.put(
'/cgi/id/users/me/$place',
data: {'attachment': attachment.rid},
);
if (!mounted) return;
final ua = context.read<UserProvider>();
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<SnNetworkProvider>();
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<UserProvider>();
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<SnNetworkProvider>();
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),
],
),
);
}
}

View File

@ -134,7 +134,7 @@ class _AccountPublisherEditScreenState
const Gap(4),
TextField(
controller: _descriptionController,
maxLines: 3,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
labelText: 'fieldDescription'.tr(),

View File

@ -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<SnNetworkProvider>();
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,

View File

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage/flutter_secure_storage_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
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);

View File

@ -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
)

View File

@ -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"))

View File

@ -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:

View File

@ -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:

View File

@ -7,11 +7,14 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -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
)