Profile editing & file upload

This commit is contained in:
LittleSheep 2025-04-25 23:04:26 +08:00
parent 7b8ee81f03
commit aed2160760
20 changed files with 611 additions and 39 deletions

View File

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

View File

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

View File

@ -51,5 +51,15 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>NSCameraUsageDescription</key>
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
</dict>
</plist>

View File

@ -60,7 +60,6 @@ class IslandApp extends HookConsumerWidget {
final userNotifier = ref.read(userInfoProvider.notifier);
Future(() {
userNotifier.fetchUser();
print('user fetched');
});
return null;
}, []);

View File

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

View File

@ -121,8 +121,8 @@ Future<ThemeData> createAppTheme(
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
},
),
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
sliderTheme: SliderThemeData(year2023: false),
progressIndicatorTheme: ProgressIndicatorThemeData(),
sliderTheme: SliderThemeData(),
);
}

View File

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

View File

@ -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'),
];
}

View File

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

View File

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

View 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()));
}
}

View 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
View 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;
}

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
@ -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);

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
@ -17,6 +18,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterPlatformAlertPluginRegisterWithRegistrar(

View File

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