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