diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 296d5e5..bd3c2ca 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -10,6 +10,7 @@ "loginSuccess": "Logged in as {}", "loginGreeting": "Welcome back!", "username": "Username", + "usernameCannotChangeHint": "Username cannot be updated after created.", "usernameLookupHint": "We also take your email address.", "unknown": "Unknown", "termAcceptNextWithAgree": "By continuing, you agree to our terms of services and other terms and conditions.", @@ -20,7 +21,12 @@ "createAccount": "Create an Account", "nickname": "Nickname", "email": "Email", + "bio": "Bio", "fieldCannotBeEmpty": "This field cannot be empty.", "fieldEmailAddressMustBeValid": "The email address must be valid.", - "logout": "Logout" + "logout": "Logout", + "updateYourProfile": "Edit Profile", + "accountBasicInfo": "Basic Info", + "accountProfile": "Profile", + "saveChanges": "Save Changes" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1daeec6..e04bbc8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,6 +1,40 @@ PODS: - device_info_plus (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_inappwebview_ios (0.0.1): - Flutter @@ -11,6 +45,8 @@ PODS: - OrderedSet (~> 6.0.3) - flutter_platform_alert (0.0.1): - Flutter + - image_picker_ios (0.0.1): + - Flutter - Kingfisher (8.3.1) - media_kit_libs_ios_video (1.0.4): - Flutter @@ -22,12 +58,16 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - SDWebImage (5.21.0): + - SDWebImage/Core (= 5.21.0) + - SDWebImage/Core (5.21.0) - 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 - volume_controller (0.0.1): @@ -37,9 +77,11 @@ PODS: DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - Kingfisher (~> 8.0) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) @@ -53,18 +95,26 @@ DEPENDENCIES: SPEC REPOS: trunk: + - DKImagePickerController + - DKPhotoGallery - Kingfisher - OrderedSet + - SDWebImage + - SwiftyGif EXTERNAL SOURCES: device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_platform_alert: :path: ".symlinks/plugins/flutter_platform_alert/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" media_kit_libs_ios_video: :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" media_kit_video: @@ -86,17 +136,23 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f94f4b3..5f16a87 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -51,5 +51,15 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSCalendarsUsageDescription + Grant access to Calander help us to shows Solar Calander with your own events. + NSCameraUsageDescription + Grant access to Camera will allow Solian take photo or video for your post. + NSMicrophoneUsageDescription + Grant access to Microphone will allow Solian record audio for your post. + NSPhotoLibraryAddUsageDescription + Grant access to Photo Library will allow Solian download photo to album for you. + NSPhotoLibraryUsageDescription + Grant access to Photo Library will allow Solian upload photo or video for your post. diff --git a/lib/main.dart b/lib/main.dart index 899d44d..4fefbc5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -60,7 +60,6 @@ class IslandApp extends HookConsumerWidget { final userNotifier = ref.read(userInfoProvider.notifier); Future(() { userNotifier.fetchUser(); - print('user fetched'); }); return null; }, []); diff --git a/lib/pods/network.dart b/lib/pods/network.dart index 4bcad40..b86a573 100644 --- a/lib/pods/network.dart +++ b/lib/pods/network.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:island/models/auth.dart'; @@ -13,6 +14,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'config.dart'; +final imagePickerProvider = Provider((ref) => ImagePicker()); + final userAgentProvider = FutureProvider((ref) async { final String platformInfo; if (kIsWeb) { @@ -64,7 +67,14 @@ final apiClientProvider = Provider((ref) { RequestOptions options, RequestInterceptorHandler handler, ) async { - final atk = await getFreshAtk(ref); + final atk = await getFreshAtk( + ref.watch(tokenPairProvider), + ref.watch(serverUrlProvider), + onRefreshed: (atk, rtk) { + setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); + ref.invalidate(tokenPairProvider); + }, + ); if (atk != null) { options.headers['Authorization'] = 'Bearer $atk'; } @@ -87,11 +97,9 @@ final tokenPairProvider = Provider((ref) { return AppTokenPair.fromJson(jsonDecode(tkPairString)); }); -Future<(String, String)?> refreshToken(Ref ref, String? rtk) async { +Future<(String, String)?> refreshToken(String baseUrl, String? rtk) async { if (rtk == null) return null; - final baseUrl = ref.watch(serverUrlProvider); - final dio = Dio(); dio.options.baseUrl = baseUrl; @@ -102,16 +110,17 @@ Future<(String, String)?> refreshToken(Ref ref, String? rtk) async { final String atk = resp.data['access_token']; final String nRtk = resp.data['refresh_token']; - setTokenPair(ref.watch(sharedPreferencesProvider), atk, nRtk); - ref.invalidate(tokenPairProvider); return (atk, nRtk); } Completer? _refreshCompleter; -Future getFreshAtk(Ref ref) async { - final tkPair = ref.watch(tokenPairProvider); +Future getFreshAtk( + AppTokenPair? tkPair, + String baseUrl, { + Function(String, String)? onRefreshed, +}) async { var atk = tkPair?.accessToken; var rtk = tkPair?.refreshToken; @@ -147,10 +156,11 @@ Future getFreshAtk(Ref ref) async { final exp = jsonDecode(payload)['exp']; if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) { log('[Auth] Access token need refresh, doing it at ${DateTime.now()}'); - final result = await refreshToken(ref, rtk); + final result = await refreshToken(baseUrl, rtk); if (result == null) { atk = null; } else { + onRefreshed?.call(result.$1, result.$2); atk = result.$1; } } diff --git a/lib/pods/theme.dart b/lib/pods/theme.dart index 0380245..c7b3a2c 100644 --- a/lib/pods/theme.dart +++ b/lib/pods/theme.dart @@ -121,8 +121,8 @@ Future createAppTheme( TargetPlatform.windows: ZoomPageTransitionsBuilder(), }, ), - progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false), - sliderTheme: SliderThemeData(year2023: false), + progressIndicatorTheme: ProgressIndicatorThemeData(), + sliderTheme: SliderThemeData(), ); } diff --git a/lib/pods/userinfo.dart b/lib/pods/userinfo.dart index 405a2e8..aa778a6 100644 --- a/lib/pods/userinfo.dart +++ b/lib/pods/userinfo.dart @@ -15,7 +15,6 @@ class UserInfoNotifier extends StateNotifier> { } Future fetchUser() async { - state = const AsyncValue.loading(); try { final client = _ref.read(apiClientProvider); final response = await client.get('/accounts/me'); diff --git a/lib/route.dart b/lib/route.dart index 85532c3..60ce490 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -19,5 +19,7 @@ class AppRouter extends RootStackRouter { ), AutoRoute(page: LoginRoute.page, path: '/auth/login'), AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'), + AutoRoute(page: MyselfProfileRoute.page, path: '/account/me'), + AutoRoute(page: UpdateProfileRoute.page, path: '/account/me/update'), ]; } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 7a08e98..333116a 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -9,22 +9,24 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i6; +import 'package:auto_route/auto_route.dart' as _i8; import 'package:island/screens/account.dart' as _i1; +import 'package:island/screens/auth/account/me.dart' as _i5; +import 'package:island/screens/auth/account/me/update.dart' as _i7; import 'package:island/screens/auth/create_account.dart' as _i2; import 'package:island/screens/auth/login.dart' as _i4; -import 'package:island/screens/auth/tabs.dart' as _i5; +import 'package:island/screens/auth/tabs.dart' as _i6; import 'package:island/screens/explore.dart' as _i3; /// generated route for /// [_i1.AccountScreen] -class AccountRoute extends _i6.PageRouteInfo { - const AccountRoute({List<_i6.PageRouteInfo>? children}) +class AccountRoute extends _i8.PageRouteInfo { + const AccountRoute({List<_i8.PageRouteInfo>? children}) : super(AccountRoute.name, initialChildren: children); static const String name = 'AccountRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i8.PageInfo page = _i8.PageInfo( name, builder: (data) { return const _i1.AccountScreen(); @@ -34,13 +36,13 @@ class AccountRoute extends _i6.PageRouteInfo { /// generated route for /// [_i2.CreateAccountScreen] -class CreateAccountRoute extends _i6.PageRouteInfo { - const CreateAccountRoute({List<_i6.PageRouteInfo>? children}) +class CreateAccountRoute extends _i8.PageRouteInfo { + const CreateAccountRoute({List<_i8.PageRouteInfo>? children}) : super(CreateAccountRoute.name, initialChildren: children); static const String name = 'CreateAccountRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i8.PageInfo page = _i8.PageInfo( name, builder: (data) { return const _i2.CreateAccountScreen(); @@ -50,13 +52,13 @@ class CreateAccountRoute extends _i6.PageRouteInfo { /// generated route for /// [_i3.ExploreScreen] -class ExploreRoute extends _i6.PageRouteInfo { - const ExploreRoute({List<_i6.PageRouteInfo>? children}) +class ExploreRoute extends _i8.PageRouteInfo { + const ExploreRoute({List<_i8.PageRouteInfo>? children}) : super(ExploreRoute.name, initialChildren: children); static const String name = 'ExploreRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i8.PageInfo page = _i8.PageInfo( name, builder: (data) { return const _i3.ExploreScreen(); @@ -66,13 +68,13 @@ class ExploreRoute extends _i6.PageRouteInfo { /// generated route for /// [_i4.LoginScreen] -class LoginRoute extends _i6.PageRouteInfo { - const LoginRoute({List<_i6.PageRouteInfo>? children}) +class LoginRoute extends _i8.PageRouteInfo { + const LoginRoute({List<_i8.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i8.PageInfo page = _i8.PageInfo( name, builder: (data) { return const _i4.LoginScreen(); @@ -81,17 +83,49 @@ class LoginRoute extends _i6.PageRouteInfo { } /// generated route for -/// [_i5.TabsScreen] -class TabsRoute extends _i6.PageRouteInfo { - const TabsRoute({List<_i6.PageRouteInfo>? children}) +/// [_i5.MyselfProfileScreen] +class MyselfProfileRoute extends _i8.PageRouteInfo { + const MyselfProfileRoute({List<_i8.PageRouteInfo>? children}) + : super(MyselfProfileRoute.name, initialChildren: children); + + static const String name = 'MyselfProfileRoute'; + + static _i8.PageInfo page = _i8.PageInfo( + name, + builder: (data) { + return const _i5.MyselfProfileScreen(); + }, + ); +} + +/// generated route for +/// [_i6.TabsScreen] +class TabsRoute extends _i8.PageRouteInfo { + const TabsRoute({List<_i8.PageRouteInfo>? children}) : super(TabsRoute.name, initialChildren: children); static const String name = 'TabsRoute'; - static _i6.PageInfo page = _i6.PageInfo( + static _i8.PageInfo page = _i8.PageInfo( name, builder: (data) { - return const _i5.TabsScreen(); + return const _i6.TabsScreen(); + }, + ); +} + +/// generated route for +/// [_i7.UpdateProfileScreen] +class UpdateProfileRoute extends _i8.PageRouteInfo { + const UpdateProfileRoute({List<_i8.PageRouteInfo>? children}) + : super(UpdateProfileRoute.name, initialChildren: children); + + static const String name = 'UpdateProfileRoute'; + + static _i8.PageInfo page = _i8.PageInfo( + name, + builder: (data) { + return const _i7.UpdateProfileScreen(); }, ); } diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 4932f9d..0ea2cf1 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -72,6 +72,16 @@ class AccountScreen extends HookConsumerWidget { ], ), ), + ListTile( + leading: const Icon(LucideIcons.edit), + trailing: const Icon(LucideIcons.chevronRight), + contentPadding: EdgeInsets.symmetric(horizontal: 24), + title: Text('accountProfile').tr(), + subtitle: Text('Update your profile.'), + onTap: () { + context.router.push(UpdateProfileRoute()); + }, + ), ListTile( leading: const Icon(LucideIcons.logOut), trailing: const Icon(LucideIcons.chevronRight), diff --git a/lib/screens/auth/account/me.dart b/lib/screens/auth/account/me.dart new file mode 100644 index 0000000..d501cd2 --- /dev/null +++ b/lib/screens/auth/account/me.dart @@ -0,0 +1,13 @@ +import 'package:auto_route/annotations.dart'; +import 'package:flutter/material.dart'; +import 'package:island/widgets/app_scaffold.dart'; + +@RoutePage() +class MyselfProfileScreen extends StatelessWidget { + const MyselfProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return AppScaffold(appBar: AppBar(leading: const PageBackButton())); + } +} diff --git a/lib/screens/auth/account/me/update.dart b/lib/screens/auth/account/me/update.dart new file mode 100644 index 0000000..263055c --- /dev/null +++ b/lib/screens/auth/account/me/update.dart @@ -0,0 +1,238 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/pods/userinfo.dart'; +import 'package:island/services/file.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:styled_widget/styled_widget.dart'; + +@RoutePage() +class UpdateProfileScreen extends HookConsumerWidget { + const UpdateProfileScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userInfoProvider); + + final submitting = useState(false); + + void updateProfilePicture(String position) async { + final result = await ref + .read(imagePickerProvider) + .pickImage(source: ImageSource.gallery); + if (result == null) return; + + submitting.value = true; + try { + final baseUrl = ref.watch(serverUrlProvider); + final atk = await getFreshAtk( + ref.watch(tokenPairProvider), + baseUrl, + onRefreshed: (atk, rtk) { + setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk); + ref.invalidate(tokenPairProvider); + }, + ); + if (atk == null) throw ArgumentError('Access token is null'); + final cloudFile = + await putMediaToCloud( + fileData: result, + atk: atk, + baseUrl: baseUrl, + filename: result.name, + mimetype: result.mimeType ?? 'image/jpeg', + ).future; + if (cloudFile == null) { + throw ArgumentError('Failed to upload the file...'); + } + final client = ref.watch(apiClientProvider); + await client.patch( + '/accounts/me/profile', + data: {'${position}_id': cloudFile.id}, + ); + final userNotifier = ref.read(userInfoProvider.notifier); + userNotifier.fetchUser(); + } catch (err) { + showErrorAlert(err); + } finally { + submitting.value = false; + } + } + + final formKeyBasicInfo = useMemoized(GlobalKey.new, const []); + final usernameController = useTextEditingController(text: user.value!.name); + final nicknameController = useTextEditingController(text: user.value!.nick); + + void updateBasicInfo() async { + if (!formKeyBasicInfo.currentState!.validate()) return; + + submitting.value = true; + try { + final client = ref.watch(apiClientProvider); + await client.patch( + '/accounts/me', + data: { + 'name': usernameController.text, + 'nick': nicknameController.text, + }, + ); + final userNotifier = ref.read(userInfoProvider.notifier); + userNotifier.fetchUser(); + } catch (err) { + showErrorAlert(err); + } finally { + submitting.value = false; + } + } + + final formKeyProfile = useMemoized(GlobalKey.new, const []); + final bioController = useTextEditingController( + text: user.value!.profile.bio, + ); + + void updateProfile() async { + if (!formKeyProfile.currentState!.validate()) return; + + submitting.value = true; + try { + final client = ref.watch(apiClientProvider); + await client.patch( + '/accounts/me/profile', + data: {'bio': bioController.text}, + ); + final userNotifier = ref.read(userInfoProvider.notifier); + userNotifier.fetchUser(); + } catch (err) { + showErrorAlert(err); + } finally { + submitting.value = false; + } + } + + return AppScaffold( + appBar: AppBar( + title: Text('updateYourProfile').tr(), + leading: const PageBackButton(), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 16 / 7, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + GestureDetector( + child: Container( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + child: + user.value!.profile.background != null + ? CloudFileWidget( + item: user.value!.profile.background!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), + ), + onTap: () { + updateProfilePicture('background'); + }, + ), + Positioned( + left: 20, + bottom: -32, + child: GestureDetector( + child: ProfilePictureWidget( + item: user.value!.profile.picture, + radius: 40, + ), + onTap: () { + updateProfilePicture('picture'); + }, + ), + ), + ], + ), + ).padding(bottom: 32), + Text('accountBasicInfo') + .tr() + .bold() + .fontSize(18) + .padding(horizontal: 24, top: 16, bottom: 12), + Form( + key: formKeyBasicInfo, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: 'username'.tr(), + helperText: 'usernameCannotChangeHint'.tr(), + prefixText: '@', + ), + controller: usernameController, + readOnly: true, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + TextFormField( + decoration: InputDecoration(labelText: 'nickname'.tr()), + controller: nicknameController, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: submitting.value ? null : updateBasicInfo, + label: Text('saveChanges').tr(), + icon: const Icon(LucideIcons.save), + ), + ), + ], + ).padding(horizontal: 24), + ), + Text('accountProfile') + .tr() + .bold() + .fontSize(18) + .padding(horizontal: 24, top: 16, bottom: 8), + Form( + key: formKeyProfile, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 16, + children: [ + TextFormField( + decoration: InputDecoration(labelText: 'bio'.tr()), + maxLines: null, + minLines: 3, + controller: bioController, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), + ), + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: submitting.value ? null : updateProfile, + label: Text('saveChanges').tr(), + icon: const Icon(LucideIcons.save), + ), + ), + ], + ).padding(horizontal: 24), + ), + ], + ), + ); + } +} diff --git a/lib/services/file.dart b/lib/services/file.dart new file mode 100644 index 0000000..f1ad42d --- /dev/null +++ b/lib/services/file.dart @@ -0,0 +1,65 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cross_file/cross_file.dart'; +import 'package:island/models/file.dart'; +import 'package:tus_client_dart/tus_client_dart.dart'; + +Completer putMediaToCloud({ + required dynamic fileData, // Can be XFile or List (Uint8List) + required String atk, + required String baseUrl, + String? filename, + String? mimetype, + Function(double progress, Duration estimate)? onProgress, +}) { + XFile file; + String actualFilename = filename ?? 'randomly_file'; + String actualMimetype = mimetype ?? ''; + Uint8List? byteData; + + if (fileData is XFile) { + file = fileData; + actualFilename = filename ?? fileData.name; + actualMimetype = mimetype ?? fileData.mimeType ?? ''; + } else if (fileData is List || fileData is Uint8List) { + byteData = fileData is List ? Uint8List.fromList(fileData) : fileData; + actualFilename = filename ?? 'uploaded_file'; + actualMimetype = mimetype ?? 'application/octet-stream'; + if (mimetype == null) { + throw ArgumentError('Mimetype is required when providing raw bytes.'); + } + file = XFile.fromData(byteData!, mimeType: actualMimetype); + } else { + throw ArgumentError( + 'Invalid fileData type. Expected XFile or List (Uint8List).', + ); + } + + final Map metadata = { + 'filename': actualFilename, + 'content-type': actualMimetype, + }; + + final completer = Completer(); + + final client = TusClient(file); + client + .upload( + uri: Uri.parse('$baseUrl/files/tus'), + headers: {'Authorization': 'Bearer $atk'}, + metadata: metadata, + onComplete: (lastResponse) { + final resp = jsonDecode(lastResponse!.headers['x-fileinfo']!); + completer.complete(SnCloudFile.fromJson(resp)); + }, + onProgress: (double progress, Duration estimate) { + onProgress?.call(progress, estimate); + }, + measureUploadSpeed: true, + ) + .catchError(completer.completeError); + + return completer; +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 0623e53..3268e87 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -16,6 +17,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); + 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_platform_alert_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin"); flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e552af4..38256a7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux + file_selector_linux flutter_platform_alert media_kit_libs_linux media_kit_video diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index eb32abd..3e36600 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,8 @@ import Foundation import bitsdojo_window_macos import device_info_plus +import file_picker +import file_selector_macos import flutter_inappwebview_macos import flutter_platform_alert import media_kit_libs_macos_video @@ -22,6 +24,8 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b16402f..6d28159 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -250,7 +250,7 @@ packages: source: hosted version: "3.1.2" cross_file: - dependency: transitive + dependency: "direct main" description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" @@ -369,6 +369,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "8986dec4581b4bcd4b6df5d75a2ea0bede3db802f500635d05fa8be298f9467f" + url: "https://pub.dev" + source: hosted + version: "10.1.2" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + 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: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" fixnum: dependency: transitive description: @@ -523,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" flutter_riverpod: dependency: "direct main" description: @@ -653,6 +701,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + 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: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" + url: "https://pub.dev" + source: hosted + version: "0.8.12+23" + 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: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + url: "https://pub.dev" + source: hosted + version: "0.8.12+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + 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: @@ -1325,10 +1437,11 @@ packages: tus_client_dart: dependency: "direct main" description: - name: tus_client_dart - sha256: dd6bb9f53b0c330480bbd91b7e4f46663b879f836e3b450c17a988e9dd959170 - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "55fd380bcca8c984773711062ac7dfdbfa87c9d1" + url: "https://github.com/LittleSheep2Code/tus_client.git" + source: git version: "2.5.0" typed_data: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index b08dd3b..f580eb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,7 +70,11 @@ dependencies: package_info_plus: ^8.3.0 device_info_plus: ^11.4.0 lucide_icons: ^0.257.0 - tus_client_dart: ^2.5.0 + tus_client_dart: + git: https://github.com/LittleSheep2Code/tus_client.git + cross_file: ^0.3.4+2 + image_picker: ^1.1.2 + file_picker: ^10.1.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index a47c644..16c40b5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterPlatformAlertPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 90fdd8d..3a6ea80 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows + file_selector_windows flutter_inappwebview_windows flutter_platform_alert media_kit_libs_windows_video