✨ Profile editing & file upload
This commit is contained in:
@ -60,7 +60,6 @@ class IslandApp extends HookConsumerWidget {
|
||||
final userNotifier = ref.read(userInfoProvider.notifier);
|
||||
Future(() {
|
||||
userNotifier.fetchUser();
|
||||
print('user fetched');
|
||||
});
|
||||
return null;
|
||||
}, []);
|
||||
|
@ -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<String>((ref) async {
|
||||
final String platformInfo;
|
||||
if (kIsWeb) {
|
||||
@ -64,7 +67,14 @@ final apiClientProvider = Provider<Dio>((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<AppTokenPair?>((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<String?>? _refreshCompleter;
|
||||
|
||||
Future<String?> getFreshAtk(Ref ref) async {
|
||||
final tkPair = ref.watch(tokenPairProvider);
|
||||
Future<String?> getFreshAtk(
|
||||
AppTokenPair? tkPair,
|
||||
String baseUrl, {
|
||||
Function(String, String)? onRefreshed,
|
||||
}) async {
|
||||
var atk = tkPair?.accessToken;
|
||||
var rtk = tkPair?.refreshToken;
|
||||
|
||||
@ -147,10 +156,11 @@ Future<String?> 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;
|
||||
}
|
||||
}
|
||||
|
@ -121,8 +121,8 @@ Future<ThemeData> createAppTheme(
|
||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
||||
sliderTheme: SliderThemeData(year2023: false),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(),
|
||||
sliderTheme: SliderThemeData(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
}
|
||||
|
||||
Future<void> fetchUser() async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final client = _ref.read(apiClientProvider);
|
||||
final response = await client.get('/accounts/me');
|
||||
|
@ -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'),
|
||||
];
|
||||
}
|
||||
|
@ -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<void> {
|
||||
const AccountRoute({List<_i6.PageRouteInfo>? children})
|
||||
class AccountRoute extends _i8.PageRouteInfo<void> {
|
||||
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<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.CreateAccountScreen]
|
||||
class CreateAccountRoute extends _i6.PageRouteInfo<void> {
|
||||
const CreateAccountRoute({List<_i6.PageRouteInfo>? children})
|
||||
class CreateAccountRoute extends _i8.PageRouteInfo<void> {
|
||||
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<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.ExploreScreen]
|
||||
class ExploreRoute extends _i6.PageRouteInfo<void> {
|
||||
const ExploreRoute({List<_i6.PageRouteInfo>? children})
|
||||
class ExploreRoute extends _i8.PageRouteInfo<void> {
|
||||
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<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.LoginScreen]
|
||||
class LoginRoute extends _i6.PageRouteInfo<void> {
|
||||
const LoginRoute({List<_i6.PageRouteInfo>? children})
|
||||
class LoginRoute extends _i8.PageRouteInfo<void> {
|
||||
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<void> {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i5.TabsScreen]
|
||||
class TabsRoute extends _i6.PageRouteInfo<void> {
|
||||
const TabsRoute({List<_i6.PageRouteInfo>? children})
|
||||
/// [_i5.MyselfProfileScreen]
|
||||
class MyselfProfileRoute extends _i8.PageRouteInfo<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
|
13
lib/screens/auth/account/me.dart
Normal file
13
lib/screens/auth/account/me.dart
Normal file
@ -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()));
|
||||
}
|
||||
}
|
238
lib/screens/auth/account/me/update.dart
Normal file
238
lib/screens/auth/account/me/update.dart
Normal file
@ -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<FormState>.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<FormState>.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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
65
lib/services/file.dart
Normal file
65
lib/services/file.dart
Normal file
@ -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<SnCloudFile?> putMediaToCloud({
|
||||
required dynamic fileData, // Can be XFile or List<int> (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<int> || fileData is Uint8List) {
|
||||
byteData = fileData is List<int> ? 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<int> (Uint8List).',
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, String> metadata = {
|
||||
'filename': actualFilename,
|
||||
'content-type': actualMimetype,
|
||||
};
|
||||
|
||||
final completer = Completer<SnCloudFile?>();
|
||||
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user