Compare commits

..

No commits in common. "657f36c1f83a020af8b938663b9eb5e3e43450ce" and "1eb42fa351317863aece3a9aecdde2aecd8b92a8" have entirely different histories.

48 changed files with 250 additions and 2403 deletions

View File

@ -34,8 +34,6 @@ PODS:
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- image_picker_ios (0.0.1):
@ -43,8 +41,6 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- SDWebImage (5.19.2):
- SDWebImage/Core (= 5.19.2)
- SDWebImage/Core (5.19.2)
@ -55,11 +51,9 @@ PODS:
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
@ -74,16 +68,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
@ -92,11 +82,9 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe

View File

@ -199,7 +199,6 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -271,23 +270,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;

View File

@ -1,12 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/providers/account.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/post.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/translations.dart';
@ -33,19 +28,7 @@ class SolianApp extends StatelessWidget {
fallbackLocale: const Locale('en', 'US'),
onInit: () {
Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => FriendProvider());
Get.lazyPut(() => PostProvider());
Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => AccountProvider());
Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider());
final AuthProvider auth = Get.find();
auth.isAuthorized.then((value) async {
if (value) {
Get.find<AccountProvider>().connect();
}
});
},
builder: (context, child) {
return ScaffoldMessenger(

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:solian/models/account.dart';
class Channel {
@ -62,15 +60,6 @@ class Channel {
'realm_id': realmId,
'is_encrypted': isEncrypted,
};
IconData get icon {
switch (type) {
case 1:
return FontAwesomeIcons.userGroup;
default:
return FontAwesomeIcons.hashtag;
}
}
}
class ChannelMember {

View File

@ -1,161 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:web_socket_channel/io.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class AccountProvider extends GetxController {
final FlutterLocalNotificationsPlugin localNotify =
FlutterLocalNotificationsPlugin();
RxBool isConnected = false.obs;
RxBool isConnecting = false.obs;
RxInt notificationUnread = 0.obs;
RxList<Notification> notifications =
List<Notification>.empty(growable: true).obs;
IOWebSocketChannel? websocket;
@override
onInit() {
Permission.notification.request().then((status) {
notifyInitialization();
notifyPrefetch();
});
super.onInit();
}
void connect({noRetry = false}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
if (auth.credentials == null) await auth.loadCredentials();
final uri = Uri.parse(
'${ServiceFinder.services['passport']}/api/ws?tk=${auth.credentials!.accessToken}'
.replaceFirst('http', 'ws'),
);
isConnecting.value = true;
try {
websocket = IOWebSocketChannel.connect(uri);
await websocket?.ready;
} catch (e) {
if (!noRetry) {
await auth.refreshCredentials();
return connect(noRetry: true);
}
}
listen();
isConnected.value = true;
isConnecting.value = false;
}
void disconnect() {
websocket?.sink.close(WebSocketStatus.normalClosure);
isConnected.value = false;
}
void listen() {
websocket?.stream.listen(
(event) {
final packet = NetworkPackage.fromJson(jsonDecode(event));
switch (packet.method) {
case 'notifications.new':
final notification = Notification.fromJson(packet.payload!);
notificationUnread++;
notifications.add(notification);
notifyMessage(notification.subject, notification.content);
break;
}
},
onDone: () {
isConnected.value = false;
},
onError: (err) {
isConnected.value = false;
},
);
}
void notifyInitialization() {
const androidSettings = AndroidInitializationSettings('app_icon');
const darwinSettings = DarwinInitializationSettings(
notificationCategories: [
DarwinNotificationCategory('general'),
],
);
const linuxSettings =
LinuxInitializationSettings(defaultActionName: 'Open notification');
const InitializationSettings initializationSettings =
InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
macOS: darwinSettings,
linux: linuxSettings,
);
localNotify.initialize(initializationSettings);
}
void notifyMessage(String title, String body) {
const androidSettings = AndroidNotificationDetails(
'general',
'General',
importance: Importance.high,
priority: Priority.high,
silent: true,
);
const darwinSettings = DarwinNotificationDetails(
presentAlert: true,
presentBanner: true,
presentBadge: true,
presentSound: false,
);
const linuxSettings = LinuxNotificationDetails();
localNotify.show(
math.max(1, math.Random().nextInt(100000000)),
title,
body,
const NotificationDetails(
android: androidSettings,
iOS: darwinSettings,
macOS: darwinSettings,
linux: linuxSettings,
),
);
}
Future<void> notifyPrefetch() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.get('/api/notifications?skip=0&take=100');
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
if (data != null) {
notifications.addAll(data);
notificationUnread.value = data.length;
}
}
}
}

View File

@ -5,13 +5,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/providers/account.dart';
import 'package:solian/services.dart';
import 'package:oauth2/oauth2.dart' as oauth2;
class AuthProvider extends GetConnect {
final deviceEndpoint = Uri.parse(
'${ServiceFinder.services['passport']}/api/notifications/subscribe');
final tokenEndpoint =
Uri.parse('${ServiceFinder.services['passport']}/api/auth/token');
final userinfoEndpoint =
Uri.parse('${ServiceFinder.services['passport']}/api/users/me');
final redirectUrl = Uri.parse('solian://auth');
static const clientId = 'solian';
static const clientSecret = '_F4%q2Eea3';
@ -21,35 +25,30 @@ class AuthProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['passport'];
loadCredentials();
applyAuthenticator();
}
oauth2.Credentials? credentials;
Future<void> refreshCredentials() async {
final resp = await post('/api/auth/token', {
'refresh_token': credentials!.refreshToken,
'grant_type': 'refresh_token',
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
credentials = oauth2.Credentials(
resp.body['access_token'],
refreshToken: resp.body['refresh_token'],
idToken: resp.body['access_token'],
tokenEndpoint: tokenEndpoint,
expiration: DateTime.now().add(const Duration(minutes: 3)),
);
storage.write(
key: 'auth_credentials',
value: jsonEncode(credentials!.toJson()),
);
}
Future<Request<T?>> requestAuthenticator<T>(Request<T?> request) async {
Future<Request<T?>> reqAuthenticator<T>(Request<T?> request) async {
if (credentials != null && credentials!.isExpired) {
refreshCredentials();
final resp = await post('/api/auth/token', {
'refresh_token': credentials!.refreshToken,
'grant_type': 'refresh_token',
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
credentials = oauth2.Credentials(
resp.body['access_token'],
refreshToken: resp.body['refresh_token'],
idToken: resp.body['access_token'],
tokenEndpoint: tokenEndpoint,
expiration: DateTime.now().add(const Duration(minutes: 3)),
);
storage.write(
key: 'auth_credentials', value: jsonEncode(credentials!.toJson()));
}
if (credentials != null) {
@ -59,20 +58,18 @@ class AuthProvider extends GetConnect {
return request;
}
Future<void> loadCredentials() async {
if (await isAuthorized) {
final content = await storage.read(key: 'auth_credentials');
credentials = oauth2.Credentials.fromJson(jsonDecode(content!));
}
void applyAuthenticator() {
isAuthorized.then((status) async {
if (status) {
final content = await storage.read(key: 'auth_credentials');
credentials = oauth2.Credentials.fromJson(jsonDecode(content!));
httpClient.addAuthenticator(reqAuthenticator);
}
});
}
Future<oauth2.Credentials> signin(
BuildContext context,
String username,
String password,
) async {
_cacheUserProfileResponse = null;
BuildContext context, String username, String password) async {
final resp = await oauth2.resourceOwnerPasswordGrant(
tokenEndpoint,
username,
@ -92,23 +89,13 @@ class AuthProvider extends GetConnect {
);
storage.write(
key: 'auth_credentials',
value: jsonEncode(credentials!.toJson()),
);
Get.find<AccountProvider>().connect();
Get.find<AccountProvider>().notifyPrefetch();
key: 'auth_credentials', value: jsonEncode(credentials!.toJson()));
applyAuthenticator();
return credentials!;
}
void signout() {
_cacheUserProfileResponse = null;
Get.find<AccountProvider>().disconnect();
Get.find<AccountProvider>().notifications.clear();
Get.find<AccountProvider>().notificationUnread.value = 0;
storage.deleteAll();
}
@ -121,11 +108,7 @@ class AuthProvider extends GetConnect {
return _cacheUserProfileResponse!;
}
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(requestAuthenticator);
final resp = await client.get('/api/users/me');
final resp = await get('/api/users/me');
_cacheUserProfileResponse = resp;
return resp;
}

View File

@ -39,7 +39,7 @@ class AttachmentProvider extends GetConnect {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
final filePayload =
MultipartFile(await file.readAsBytes(), filename: basename(file.path));
@ -78,7 +78,7 @@ class AttachmentProvider extends GetConnect {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
var resp = await client.put('/api/attachments/$id', {
'metadata': {
@ -102,7 +102,7 @@ class AttachmentProvider extends GetConnect {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
var resp = await client.delete('/api/attachments/$id');
if (resp.statusCode != 200) {

View File

@ -1,21 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class ChannelProvider extends GetxController {
Future<Response> listAvailableChannel({String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.get('/api/channels/$realm/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@ -1,36 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/services.dart';
class PostProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['interactive'];
}
Future<Response> listPost(int page) async {
final resp = await get('/api/feed?take=${10}&offset=$page');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/api/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
Future<Response> getPost(String alias) async {
final resp = await get('/api/posts/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
return resp;
}
}

View File

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import 'package:solian/services.dart';
class PostExploreProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.services['interactive'];
}
Future<Response> listPost(int page) => get('/api/feed?take=${10}&offset=$page');
}

View File

@ -1,21 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class RealmProvider extends GetxController {
Future<Response> listAvailableRealm() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) throw Exception('unauthorized');
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@ -1,43 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class FriendProvider extends GetConnect {
@override
void onInit() {
final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.services['passport'];
httpClient.addAuthenticator(auth.requestAuthenticator);
}
Future<Response> listFriendship() => get('/api/users/me/friends');
Future<Response> listFriendshipWithStatus(int status) =>
get('/api/users/me/friends?status=$status');
Future<Response> createFriendship(String username) async {
final resp = await post('/api/users/me/friends?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> updateFriendship(Friendship relationship, int status) async {
final AuthProvider auth = Get.find();
final prof = await auth.getProfile();
final otherside = relationship.getOtherside(prof.body['id']);
final resp = await put('/api/users/me/friends/${otherside.id}', {
'status': status,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@ -1,12 +1,10 @@
import 'package:go_router/go_router.dart';
import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/contact.dart';
import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/social.dart';
import 'package:solian/screens/posts/post_publish.dart';
import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart';
import 'package:solian/screens/home.dart';
import 'package:solian/screens/posts/publish.dart';
import 'package:solian/shells/basic_shell.dart';
import 'package:solian/shells/nav_shell.dart';
@ -15,17 +13,12 @@ abstract class AppRouter {
routes: [
ShellRoute(
builder: (context, state, child) =>
NavShell(state: state, child: child, showAppBar: false),
NavShell(state: state, child: child),
routes: [
GoRoute(
path: '/',
name: 'social',
builder: (context, state) => const SocialScreen(),
),
GoRoute(
path: '/contact',
name: 'contact',
builder: (context, state) => const ContactScreen(),
name: 'home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/account',
@ -38,29 +31,21 @@ abstract class AppRouter {
builder: (context, state, child) =>
BasicShell(state: state, child: child),
routes: [
GoRoute(
path: '/posts/:alias',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
alias: state.pathParameters['alias']!,
),
),
],
),
ShellRoute(
builder: (context, state, child) =>
BasicShell(state: state, child: child),
routes: [
GoRoute(
path: '/account/friend',
name: 'accountFriend',
builder: (context, state) => const FriendScreen(),
),
GoRoute(
path: '/account/personalize',
name: 'accountPersonalize',
builder: (context, state) => const PersonalizeScreen(),
),
GoRoute(
path: '/auth/sign-in',
name: 'signin',
builder: (context, state) => const SignInScreen(),
),
GoRoute(
path: '/auth/sign-up',
name: 'signup',
builder: (context, state) => const SignUpScreen(),
),
],
),
GoRoute(
@ -76,17 +61,6 @@ abstract class AppRouter {
);
},
),
GoRoute(
path: '/chat/organize',
name: 'channelOrganizing',
builder: (context, state) {
final arguments = state.extra as ChannelOrganizeArguments?;
return ChannelOrganizeScreen(
edit: arguments?.edit,
realm: state.uri.queryParameters['realm'],
);
},
),
],
);
}

View File

@ -2,9 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart';
import 'package:solian/services.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
class AccountScreen extends StatefulWidget {
@ -43,14 +41,7 @@ class _AccountScreenState extends State<AccountScreen> {
title: 'signin'.tr,
caption: 'signinCaption'.tr,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isDismissible: false,
isScrollControlled: true,
context: context,
builder: (context) => const SignInPopup(),
).then((_) async {
await provider.getProfile(noCache: true);
AppRouter.instance.pushNamed('signin').then((_) {
setState(() {});
});
},
@ -60,15 +51,7 @@ class _AccountScreenState extends State<AccountScreen> {
title: 'signup'.tr,
caption: 'signupCaption'.tr,
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isDismissible: false,
isScrollControlled: true,
context: context,
builder: (context) => const SignUpPopup(),
).then((_) {
setState(() {});
});
AppRouter.instance.pushNamed('signup');
},
),
],
@ -85,9 +68,7 @@ class _AccountScreenState extends State<AccountScreen> {
leading: x.$1,
title: Text(x.$2),
onTap: () {
AppRouter.instance
.pushNamed(x.$3)
.then((_) => setState(() {}));
AppRouter.instance.pushNamed(x.$3);
},
),
)),
@ -119,72 +100,25 @@ class AccountNameCard extends StatelessWidget {
future: provider.getProfile(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
return const Center(
child: CircularProgressIndicator(),
);
}
final prof = snapshot.data!;
return Material(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
if (prof.body['banner'] != null)
Image.network(
'${ServiceFinder.services['paperclip']}/api/attachments/${prof.body['banner']}',
fit: BoxFit.cover,
),
Positioned(
bottom: -30,
left: 18,
child: AccountAvatar(
content: prof.body['avatar'],
radius: 48,
),
),
],
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
prof.body['nick'],
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
).paddingOnly(right: 4),
Text(
'@${prof.body['name']}',
style: const TextStyle(
fontSize: 15,
),
),
],
).paddingOnly(left: 120, top: 8),
SizedBox(
width: double.infinity,
child: Card(
child: ListTile(
title: Text('description'.tr),
subtitle: Text(
prof.body['description']?.isNotEmpty
? prof.body['description']
: 'No description yet.',
),
),
),
).paddingOnly(left: 24, right: 24, top: 8),
],
child: Card(
child: ListTile(
contentPadding:
const EdgeInsets.only(left: 22, right: 34, top: 4, bottom: 4),
leading: AccountAvatar(
content: snapshot.data!.body?['avatar'], radius: 24),
title: Text(snapshot.data!.body?['nick']),
subtitle: Text(snapshot.data!.body?['email']),
),
).paddingOnly(
left: 16,
right: 16,
top: SolianTheme.isLargeScreen(context) ? 8 : 0,
),
);
},

View File

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/widgets/account/friend_list.dart';
class FriendScreen extends StatefulWidget {
const FriendScreen({super.key});
@override
State<FriendScreen> createState() => _FriendScreenState();
}
class _FriendScreenState extends State<FriendScreen> {
bool _isBusy = false;
int? _accountId;
List<Friendship> _friendships = List.empty();
List<Friendship> filterWithStatus(int status) {
return _friendships.where((x) => x.status == status).toList();
}
Future<void> getFriendship() async {
setState(() => _isBusy = true);
final FriendProvider provider = Get.find();
final resp = await provider.listFriendship();
setState(() {
_friendships = resp.body
.map((e) => Friendship.fromJson(e))
.toList()
.cast<Friendship>();
_isBusy = false;
});
}
void showScopedListPopup(String title, int status) {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: CustomScrollView(
slivers: [
SliverFriendList(
accountId: _accountId!,
items: filterWithStatus(status),
onUpdate: () {
getFriendship();
},
),
],
),
),
],
),
);
},
);
}
void promptAddFriend() async {
final FriendProvider provider = Get.find();
final controller = TextEditingController();
final input = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('accountFriendNew'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('accountFriendNewHint'.tr, textAlign: TextAlign.left),
const SizedBox(height: 18),
TextField(
controller: controller,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'username'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
child: Text('next'.tr),
onPressed: () {
Navigator.pop(context, controller.text);
},
),
],
);
},
);
WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose());
if (input == null || input.isEmpty) return;
try {
setState(() => _isBusy = true);
await provider.createFriendship(input);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
Get.find<AuthProvider>().getProfile().then((value) {
_accountId = value.body['id'];
});
super.initState();
Future.delayed(Duration.zero, () => getFriendship());
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => promptAddFriend(),
),
body: RefreshIndicator(
onRefresh: () => getFriendship(),
child: CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.person_add),
trailing: const Icon(Icons.chevron_right),
title: Text(
'${'accountFriendPending'.tr} (${filterWithStatus(0).length})',
),
onTap: () =>
showScopedListPopup('accountFriendPending'.tr, 0),
),
),
SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.block),
trailing: const Icon(Icons.chevron_right),
title: Text(
'${'accountFriendBlocked'.tr} (${filterWithStatus(2).length})',
),
onTap: () =>
showScopedListPopup('accountFriendBlocked'.tr, 2),
),
),
SliverFriendList(
accountId: _accountId!,
items: filterWithStatus(1),
onUpdate: () {
getFriendship();
},
),
const SliverToBoxAdapter(
child: Divider(thickness: 0.3, height: 0.3),
),
SliverToBoxAdapter(
child: Text(
'accountFriendListHint'.tr,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
).paddingOnly(top: 16, bottom: 32),
),
],
),
),
),
);
}
}

View File

@ -1,210 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/providers/account.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:solian/models/notification.dart' as notify;
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart';
class NotificationScreen extends StatefulWidget {
const NotificationScreen({super.key});
@override
State<NotificationScreen> createState() => _NotificationScreenState();
}
class _NotificationScreenState extends State<NotificationScreen> {
bool _isBusy = false;
Future<void> markAllRead() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
setState(() => _isBusy = true);
final AccountProvider provider = Get.find();
List<int> markList = List.empty(growable: true);
for (final element in provider.notifications) {
if (element.isRealtime) continue;
markList.add(element.id);
}
if (markList.isNotEmpty) {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
await client.put('/api/notifications/batch/read', {'messages': markList});
}
provider.notifications.clear();
setState(() => _isBusy = false);
}
Future<void> markOneRead(notify.Notification element, int index) async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
final AccountProvider provider = Get.find();
if (element.isRealtime) {
provider.notifications.removeAt(index);
return;
}
setState(() => _isBusy = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
await client.put('/api/notifications/${element.id}/read', {});
provider.notifications.removeAt(index);
setState(() => _isBusy = false);
}
@override
Widget build(BuildContext context) {
final AccountProvider provider = Get.find();
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'notification'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: Obx(() {
return CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
if (provider.notifications.isEmpty)
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
color:
Theme.of(context).colorScheme.surfaceContainerHigh,
child: ListTile(
leading: const Icon(Icons.check),
title: Text('notifyEmpty'.tr),
subtitle: Text('notifyEmptyCaption'.tr),
),
),
),
if (provider.notifications.isNotEmpty)
SliverToBoxAdapter(
child: ListTile(
tileColor:
Theme.of(context).colorScheme.secondaryContainer,
leading: const Icon(Icons.checklist),
title: Text('notifyAllRead'.tr),
contentPadding:
const EdgeInsets.symmetric(horizontal: 28),
onTap: _isBusy ? null : () => markAllRead(),
),
),
SliverList.separated(
itemCount: provider.notifications.length,
itemBuilder: (BuildContext context, int index) {
var element = provider.notifications[index];
return Dismissible(
key: Key(const Uuid().v4()),
background: Container(
color: Colors.lightBlue,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: const Icon(Icons.check, color: Colors.white),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8,
),
title: Text(element.subject),
subtitle: Column(
children: [
Text(element.content),
if (element.links != null)
Row(
children: element.links!
.map((e) => InkWell(
child: Text(
e.label,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
decoration:
TextDecoration.underline,
),
),
onTap: () {
launchUrlString(e.url);
},
).paddingOnly(right: 5))
.toList(),
),
],
),
),
onDismissed: (_) => markOneRead(element, index),
);
},
separatorBuilder: (_, __) =>
const Divider(thickness: 0.3, height: 0.3),
),
],
);
}),
),
],
),
);
}
}
class NotificationButton extends StatelessWidget {
const NotificationButton({super.key});
@override
Widget build(BuildContext context) {
final AccountProvider provider = Get.find();
final button = IconButton(
icon: const Icon(Icons.notifications),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) => const NotificationScreen(),
).then((_) => provider.notificationUnread.value = 0);
},
);
return Obx(() {
if (provider.notificationUnread.value > 0) {
return Badge(
isLabelVisible: true,
offset: const Offset(-8, 2),
label: Text(provider.notificationUnread.value.toString()),
child: button,
);
} else {
return button;
}
});
}
}

View File

@ -37,14 +37,15 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
void selectBirthday() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _birthday?.toLocal(),
initialDate: _birthday,
firstDate: DateTime(DateTime.now().year - 200),
lastDate: DateTime(DateTime.now().year),
lastDate: DateTime(DateTime.now().year + 200),
);
if (picked != null && picked != _birthday) {
setState(() {
_birthday = picked;
_birthdayController.text = DateFormat('yyyy-MM-dd').format(_birthday!);
_birthdayController.text =
DateFormat('yyyy-MM-dd hh:mm').format(_birthday!);
});
}
}
@ -65,7 +66,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
if (prof.body['profile']['birthday'] != null) {
_birthday = DateTime.parse(prof.body['profile']['birthday']);
_birthdayController.text =
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
DateFormat('yyyy-MM-dd hh:mm').format(_birthday!);
}
_isBusy = false;
@ -100,7 +101,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
final resp = await client.put(
'/api/users/me/$position',
@ -108,37 +109,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
);
if (resp.statusCode == 200) {
syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr);
} else {
context.showErrorDialog(resp.bodyString);
}
setState(() => _isBusy = false);
}
void updatePersonalize() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
setState(() => _isBusy = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['passport'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
_birthday?.toIso8601String();
final resp = await client.put(
'/api/users/me',
{
'nick': _nicknameController.value.text,
'description': _descriptionController.value.text,
'first_name': _firstNameController.value.text,
'last_name': _lastNameController.value.text,
'birthday': _birthday?.toUtc().toIso8601String(),
},
);
if (resp.statusCode == 200) {
syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr);
} else {
context.showErrorDialog(resp.bodyString);
@ -306,11 +277,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isBusy ? null : () => syncWidget(),
onPressed: null,
child: Text('reset'.tr),
),
ElevatedButton(
onPressed: _isBusy ? null : () => updatePersonalize(),
onPressed: null,
child: Text('apply'.tr),
),
],

View File

@ -2,17 +2,18 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/services.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInPopup extends StatefulWidget {
const SignInPopup({super.key});
class SignInScreen extends StatefulWidget {
const SignInScreen({super.key});
@override
State<SignInPopup> createState() => _SignInPopupState();
State<SignInScreen> createState() => _SignInScreenState();
}
class _SignInPopupState extends State<SignInPopup> {
class _SignInScreenState extends State<SignInScreen> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
@ -22,16 +23,15 @@ class _SignInPopupState extends State<SignInPopup> {
final username = _usernameController.value.text;
final password = _passwordController.value.text;
if (username.isEmpty || password.isEmpty) return;
provider.signin(context, username, password).then((_) async {
Navigator.pop(context, true);
provider.signin(context, username, password).then((_) {
AppRouter.instance.pop(true);
}).catchError((e) {
List<String> messages = e.toString().split('\n');
if (messages.last.contains('risk')) {
final ticketId = RegExp(r'ticketId=(\d+)').firstMatch(messages.last);
if (ticketId == null) {
context.showErrorDialog(
'Requested to multi-factor authenticate, but the ticket id was not found',
);
'Requested to multi-factor authenticate, but the ticket id was not found');
}
showDialog(
context: context,
@ -46,7 +46,9 @@ class _SignInPopupState extends State<SignInPopup> {
launchUrlString(
'${ServiceFinder.services['passport']}/mfa?ticket=${ticketId!.group(1)}',
);
Navigator.pop(context);
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
)
],
@ -54,32 +56,28 @@ class _SignInPopupState extends State<SignInPopup> {
},
);
} else {
context.showErrorDialog(messages.last);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(messages.last),
));
}
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.9,
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/logo.png', width: 64, height: 64)
.paddingOnly(bottom: 4),
Text(
'signinGreeting'.tr,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
),
).paddingOnly(left: 4, bottom: 16),
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Image.asset('assets/logo.png', width: 72, height: 72),
),
TextField(
autocorrect: false,
enableSuggestions: false,
@ -109,19 +107,10 @@ class _SignInPopupState extends State<SignInPopup> {
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performAction(context),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: TextButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next'.tr),
const Icon(Icons.chevron_right),
],
),
onPressed: () => performAction(context),
),
const SizedBox(height: 16),
ElevatedButton(
child: Text('signin'.tr),
onPressed: () => performAction(context),
)
],
),

View File

@ -4,14 +4,14 @@ import 'package:solian/exts.dart';
import 'package:solian/router.dart';
import 'package:solian/services.dart';
class SignUpPopup extends StatefulWidget {
const SignUpPopup({super.key});
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
State<SignUpPopup> createState() => _SignUpPopupState();
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpPopupState extends State<SignUpPopup> {
class _SignUpScreenState extends State<SignUpScreen> {
final _emailController = TextEditingController();
final _usernameController = TextEditingController();
final _nicknameController = TextEditingController();
@ -61,25 +61,19 @@ class _SignUpPopupState extends State<SignUpPopup> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.9,
return Material(
color: Theme.of(context).colorScheme.surface,
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(maxWidth: 360),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/logo.png', width: 64, height: 64)
.paddingOnly(bottom: 4),
Text(
'signupGreeting'.tr,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w900,
),
).paddingOnly(left: 4, bottom: 16),
Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Image.asset('assets/logo.png', width: 72, height: 72),
),
TextField(
autocorrect: false,
enableSuggestions: false,
@ -138,18 +132,9 @@ class _SignUpPopupState extends State<SignUpPopup> {
onSubmitted: (_) => performAction(context),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next'.tr),
const Icon(Icons.chevron_right),
],
),
onPressed: () => performAction(context),
),
ElevatedButton(
child: Text('signup'.tr),
onPressed: () => performAction(context),
)
],
),

View File

@ -1,292 +0,0 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/friend_select.dart';
import 'package:solian/widgets/prev_page.dart';
import 'package:uuid/uuid.dart';
class ChannelOrganizeArguments {
final Channel? edit;
ChannelOrganizeArguments({this.edit});
}
class ChannelOrganizeScreen extends StatefulWidget {
final Channel? edit;
final String? realm;
const ChannelOrganizeScreen({super.key, this.edit, this.realm});
@override
State<ChannelOrganizeScreen> createState() => _ChannelOrganizeScreenState();
}
class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
static Map<int, String> channelTypes = {
0: 'channelTypeCommon'.tr,
1: 'channelTypeDirect'.tr,
};
bool _isBusy = false;
final _aliasController = TextEditingController();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
bool _isEncrypted = false;
int _channelType = 0;
List<Account> _initialMembers = List.empty(growable: true);
void selectInitialMembers() async {
final input = await showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) => FriendSelect(
title: 'channelMember'.tr,
trailingBuilder: (item) {
if (_initialMembers.any((e) => e.id == item.id)) {
return const Icon(Icons.check);
} else {
return null;
}
},
),
);
if (input == null) return;
setState(() {
if (_initialMembers.any((e) => e.id == input.id)) {
_initialMembers = _initialMembers
.where((e) => e.id != input.id)
.toList(growable: true);
} else {
_initialMembers.add(input as Account);
}
});
}
void applyChannel() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
if (_aliasController.value.text.isEmpty) randomizeAlias();
setState(() => _isBusy = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
final scope = (widget.realm?.isNotEmpty ?? false) ? widget.realm : 'global';
final payload = {
'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text,
'description': _descriptionController.value.text,
'is_encrypted': _isEncrypted,
if (_channelType == 1)
'members': _initialMembers.map((e) => e.id).toList(),
};
Response resp;
if (widget.edit != null) {
resp = await client.put(
'/api/channels/$scope/${widget.edit!.id}',
payload,
);
} else if (_channelType == 1) {
resp = await client.post('/api/channels/$scope/dm', payload);
} else {
resp = await client.post('/api/channels/$scope', payload);
}
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
AppRouter.instance.pop(resp.body);
}
setState(() => _isBusy = false);
}
void randomizeAlias() {
_aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12);
}
void syncWidget() {
if (widget.edit != null) {
_aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description;
_isEncrypted = widget.edit!.isEncrypted;
_channelType = widget.edit!.type;
}
}
void cancelAction() {
AppRouter.instance.pop();
}
@override
void initState() {
syncWidget();
super.initState();
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
appBar: AppBar(
title: Text('channelOrganizing'.tr),
leading: const PrevPageButton(),
actions: [
TextButton(
onPressed: _isBusy ? null : () => applyChannel(),
child: Text('apply'.tr.toUpperCase()),
)
],
),
body: SafeArea(
top: false,
child: Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
if (widget.edit != null)
MaterialBanner(
leading: const Icon(Icons.edit),
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,
content: Text(
'channelEditingNotify'
.trParams({'channel': '#${widget.edit!.alias}'}),
),
actions: [
TextButton(
onPressed: cancelAction,
child: Text('cancel'.tr),
),
],
),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _aliasController,
decoration: InputDecoration.collapsed(
hintText: 'channelAlias'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: () => randomizeAlias(),
child: const Icon(Icons.refresh),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
const Divider(thickness: 0.3),
TextField(
autocorrect: true,
controller: _nameController,
decoration: InputDecoration.collapsed(
hintText: 'channelName'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 8),
const Divider(thickness: 0.3),
Expanded(
child: TextField(
minLines: 5,
maxLines: null,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: _descriptionController,
decoration: InputDecoration.collapsed(
hintText: 'channelDescription'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 12),
),
const Divider(thickness: 0.3),
if (_channelType == 1)
ListTile(
leading: const Icon(Icons.supervisor_account)
.paddingSymmetric(horizontal: 8),
title: Text('channelMember'.tr),
subtitle: _initialMembers.isNotEmpty
? Text(_initialMembers.map((e) => e.name).join(' '))
: null,
trailing: const Icon(Icons.chevron_right),
onTap: () => selectInitialMembers(),
).animate().fadeIn().slideY(
begin: 1,
end: 0,
curve: Curves.fastEaseInToSlowEaseOut,
),
ListTile(
leading: const Icon(Icons.mode).paddingSymmetric(horizontal: 8),
title: Text('channelType'.tr),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>(
isExpanded: true,
items: channelTypes.entries
.map((item) => DropdownMenuItem<int>(
value: item.key,
child: Text(
item.value,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
value: _channelType,
onChanged: (int? value) {
setState(() => _channelType = value ?? 0);
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.only(left: 16, right: 1),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
),
),
CheckboxListTile(
title: Text('channelEncrypted'.tr),
value: _isEncrypted,
onChanged: (widget.edit?.isEncrypted ?? false)
? null
: (newValue) =>
setState(() => _isEncrypted = newValue ?? false),
controlAffinity: ListTileControlAffinity.leading,
),
],
),
),
),
);
}
}

View File

@ -1,127 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart';
class ContactScreen extends StatefulWidget {
const ContactScreen({super.key});
@override
State<ContactScreen> createState() => _ContactScreenState();
}
class _ContactScreenState extends State<ContactScreen> {
bool _isBusy = true;
final List<Channel> _channels = List.empty(growable: true);
getChannels() async {
setState(() => _isBusy = true);
final ChannelProvider provider = Get.find();
final resp = await provider.listAvailableChannel();
setState(() {
_channels.clear();
_channels.addAll(
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
);
});
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
getChannels();
}
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
// TODO Un signed in tip
return Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text('contact'.tr),
centerTitle: false,
titleSpacing: SolianTheme.isLargeScreen(context) ? null : 24,
forceElevated: innerBoxIsScrolled,
actions: [
const NotificationButton(),
IconButton(
icon: const Icon(Icons.add_circle),
onPressed: () {
AppRouter.instance.pushNamed('channelOrganizing').then(
(value) {
if (value != null) {
getChannels();
}
},
);
},
),
if (!SolianTheme.isLargeScreen(context))
const SizedBox(width: 16),
],
),
),
];
},
body: MediaQuery.removePadding(
removeTop: true,
context: context,
child: Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded(
child: RefreshIndicator(
onRefresh: () => getChannels(),
child: ListView.builder(
itemCount: _channels.length,
itemBuilder: (context, index) {
final element = _channels[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.indigo,
child: FaIcon(
element.icon,
color: Colors.white,
size: 16,
),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
title: Text(element.name),
subtitle: Text(element.description),
onTap: () {},
);
},
),
),
),
],
),
),
),
),
);
}
}

View File

@ -4,31 +4,27 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/post.dart';
import 'package:solian/providers/content/post_explore.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart';
class SocialScreen extends StatefulWidget {
const SocialScreen({super.key});
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<SocialScreen> createState() => _SocialScreenState();
State<HomeScreen> createState() => _HomeScreenState();
}
class _SocialScreenState extends State<SocialScreen> {
class _HomeScreenState extends State<HomeScreen> {
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
getPosts(int pageKey) async {
final PostProvider provider = Get.find();
Response resp;
try {
resp = await provider.listPost(pageKey);
} catch (e) {
_pagingController.error = e;
final PostExploreProvider provider = Get.find();
final resp = await provider.listPost(pageKey);
if (resp.statusCode != 200) {
_pagingController.error = resp.bodyString;
return;
}
@ -43,6 +39,7 @@ class _SocialScreenState extends State<SocialScreen> {
@override
void initState() {
Get.lazyPut(() => PostExploreProvider());
super.initState();
_pagingController.addPageRequestListener(getPosts);
@ -72,36 +69,32 @@ class _SocialScreenState extends State<SocialScreen> {
}),
body: Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
SliverOverlapAbsorber(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text('social'.tr),
centerTitle: false,
titleSpacing:
SolianTheme.isLargeScreen(context) ? null : 24,
forceElevated: innerBoxIsScrolled,
actions: [
const NotificationButton(),
if (!SolianTheme.isLargeScreen(context))
const SizedBox(width: 16),
],
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: PagedListView<int, Post>.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) {
return GestureDetector(
child: PostItem(key: Key('p${item.alias}'), item: item)
.paddingSymmetric(
vertical: (item.attachments?.isEmpty ?? false) ? 8 : 0,
),
),
];
},
body: MediaQuery.removePadding(
removeTop: true,
context: context,
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: PostListWidget(controller: _pagingController),
),
onTap: () {},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(item: item),
).then((value) {
if (value == true) _pagingController.refresh();
});
},
);
},
),
separatorBuilder: (_, __) =>
const Divider(thickness: 0.3, height: 0.3),
),
),
),

View File

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/post.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart';
class PostDetailScreen extends StatefulWidget {
final String alias;
const PostDetailScreen({super.key, required this.alias});
@override
State<PostDetailScreen> createState() => _PostDetailScreenState();
}
class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item;
Future<Post?> getDetail() async {
final PostProvider provider = Get.find();
try {
final resp = await provider.getPost(widget.alias);
item = Post.fromJson(resp.body);
} catch (e) {
context.showErrorDialog(e).then((_) => Navigator.pop(context));
}
return item;
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: FutureBuilder(
future: getDetail(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView(
children: [
PostItem(
item: item!,
isClickable: true,
isShowReply: false,
),
const Divider(thickness: 0.3, height: 0.3),
Text(
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 16),
PostReplyList(
item: item!,
shrinkWrap: true,
),
],
);
},
),
);
}
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/post.dart';
@ -41,7 +40,7 @@ class PostPublishingScreen extends StatefulWidget {
class _PostPublishingScreenState extends State<PostPublishingScreen> {
final _contentController = TextEditingController();
bool _isBusy = false;
bool _isSubmitting = false;
List<int> _attachments = List.empty();
@ -61,11 +60,11 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
if (!await auth.isAuthorized) return;
if (_contentController.value.text.isEmpty) return;
setState(() => _isBusy = true);
setState(() => _isSubmitting = true);
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
final payload = {
'content': _contentController.value.text,
@ -87,7 +86,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
AppRouter.instance.pop(resp.body);
}
setState(() => _isBusy = false);
setState(() => _isSubmitting = false);
}
void syncWidget() {
@ -126,8 +125,8 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
leading: const PrevPageButton(),
actions: [
TextButton(
onPressed: _isBusy ? null : () => applyPost(),
child: Text('postAction'.tr.toUpperCase()),
onPressed: () => applyPost(),
)
],
),
@ -135,7 +134,8 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
top: false,
child: Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
if (_isSubmitting)
const LinearProgressIndicator().animate().scaleX(),
if (widget.edit != null)
MaterialBanner(
leading: const Icon(Icons.edit),
@ -150,10 +150,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
child: Column(
children: [
MaterialBanner(
leading: const FaIcon(
FontAwesomeIcons.reply,
size: 18,
),
leading: const Icon(Icons.reply),
leadingPadding:
const EdgeInsets.only(left: 10, right: 20),
backgroundColor: Colors.transparent,
@ -184,10 +181,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
child: Column(
children: [
MaterialBanner(
leading: const FaIcon(
FontAwesomeIcons.retweet,
size: 18,
),
leading: const Icon(Icons.redo),
leadingPadding:
const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,

View File

@ -2,11 +2,9 @@ abstract class ServiceFinder {
static const bool devFlag = true;
static Map<String, String> services = {
'paperclip':
devFlag ? 'http://localhost:8443' : 'https://usercontent.solsynth.dev',
'paperclip': devFlag ? 'http://localhost:8443' : 'https://usercontent.solsynth.dev',
'passport': devFlag ? 'http://localhost:8444' : 'https://id.solsynth.dev',
'interactive':
devFlag ? 'http://localhost:8445' : 'https://co.solsynth.dev',
'messaging': devFlag ? 'http://localhost:8447' : 'https://im.solsynth.dev',
'interactive': devFlag ? 'http://localhost:8445' : 'https://co.solsynth.dev',
'messaging': devFlag ? 'http://localhost:8446' : 'https://im.solsynth.dev',
};
}

View File

@ -8,32 +8,24 @@ import 'package:solian/widgets/navigation/app_navigation_bottom_bar.dart';
import 'package:solian/widgets/navigation/app_navigation_rail.dart';
class NavShell extends StatelessWidget {
final bool showAppBar;
final GoRouterState state;
final Widget child;
const NavShell({
super.key,
required this.child,
required this.state,
this.showAppBar = true,
});
const NavShell({super.key, required this.child, required this.state});
@override
Widget build(BuildContext context) {
final canPop = AppRouter.instance.canPop();
return Scaffold(
appBar: showAppBar
? AppBar(
title: Text(state.topRoute?.name?.tr ?? 'page'.tr),
centerTitle: false,
titleSpacing: canPop ? null : 24,
elevation: SolianTheme.isLargeScreen(context) ? 1 : 0,
leading: canPop ? const PrevPageButton() : null,
automaticallyImplyLeading: false,
)
: null,
appBar: AppBar(
title: Text(state.topRoute?.name?.tr ?? 'page'.tr),
centerTitle: false,
titleSpacing: canPop ? null : 24,
elevation: SolianTheme.isLargeScreen(context) ? 1 : 0,
leading: canPop ? const PrevPageButton() : null,
automaticallyImplyLeading: false,
),
bottomNavigationBar: SolianTheme.isLargeScreen(context)
? null
: const AppNavigationBottomBar(),

View File

@ -9,17 +9,14 @@ class SolianMessages extends Translations {
'next': 'Next',
'reset': 'Reset',
'page': 'Page',
'social': 'Social',
'contact': 'Contact',
'home': 'Home',
'apply': 'Apply',
'cancel': 'Cancel',
'confirm': 'Confirm',
'edit': 'Edit',
'delete': 'Delete',
'search': 'Search',
'reply': 'Reply',
'repost': 'Repost',
'notification': 'Notification',
'errorHappened': 'An error occurred',
'email': 'Email',
'username': 'Username',
@ -34,24 +31,16 @@ class SolianMessages extends Translations {
'accountPersonalizeApplied':
'Account personalize settings has been saved.',
'accountFriend': 'Friend',
'accountFriendNew': 'New friend',
'accountFriendNewHint':
'Use someone\'s username to send a request of making friends with them!',
'accountFriendPending': 'Friend requests',
'accountFriendBlocked': 'Friend blocklist',
'accountFriendListHint': 'Swipe left to decline, right to approve',
'aspectRatio': 'Aspect Ratio',
'aspectRatioSquare': 'Square',
'aspectRatioPortrait': 'Portrait',
'aspectRatioLandscape': 'Landscape',
'signin': 'Sign in',
'signinGreeting': 'Welcome back\nSolar Network',
'signinCaption':
'Sign in to create post, start a realm, message your friend and more!',
'signinRiskDetected':
'Risk detected, click Next to open a webpage and signin through it to pass security check.',
'signup': 'Sign up',
'signupGreeting': 'Welcome onboard',
'signupCaption':
'Create an account on Solarpass and then get the access of entire Solar Network!',
'signout': 'Sign out',
@ -59,18 +48,12 @@ class SolianMessages extends Translations {
'matureContent': 'Mature Content',
'matureContentCaption':
'The content is rated and may not suitable for everyone to view',
'notifyAllRead': 'Mark all as read',
'notifyEmpty': 'All notifications read',
'notifyEmptyCaption': 'It seems like nothing happened recently',
'postAction': 'Post',
'postDetail': 'Post',
'postReplies': 'Replies',
'postPublishing': 'Post a post',
'postIdentityNotify': 'You will post this post as',
'postContentPlaceholder': 'What\'s happened?!',
'postReaction': 'Reactions of the Post',
'postActionList': 'Actions of Post',
'postReplyAction': 'Make a reply',
'postRepliedNotify': 'Replied a post from @username.',
'postRepostedNotify': 'Reposted a post from @username.',
'postEditingNotify': 'You\'re editing as post from you.',
@ -90,16 +73,6 @@ class SolianMessages extends Translations {
'attachmentAddFile': 'Attach file',
'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text',
'channelOrganizing': 'Organize a channel',
'channelEditingNotify': 'You\'re editing channel @channel',
'channelAlias': 'Alias (Identifier)',
'channelName': 'Name',
'channelDescription': 'Description',
'channelEncrypted': 'Encrypted Channel',
'channelMember': 'Channel member',
'channelType': 'Channel type',
'channelTypeCommon': 'Regular',
'channelTypeDirect': 'DM',
},
'zh_CN': {
'hide': '隐藏',
@ -111,13 +84,10 @@ class SolianMessages extends Translations {
'edit': '编辑',
'delete': '删除',
'page': '页面',
'social': '社交',
'contact': '联系',
'home': '首页',
'apply': '应用',
'search': '搜索',
'reply': '回复',
'repost': '转帖',
'notification': '通知',
'errorHappened': '发生错误了',
'email': '邮件地址',
'username': '用户名',
@ -131,38 +101,25 @@ class SolianMessages extends Translations {
'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountFriend': '好友',
'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
'accountFriendPending': '好友请求',
'accountFriendBlocked': '好友黑名单',
'accountFriendListHint': '左滑来拒绝,右滑来接受',
'aspectRatio': '纵横比',
'aspectRatioSquare': '方型',
'aspectRatioPortrait': '竖型',
'aspectRatioLandscape': '横型',
'signin': '登录',
'signinGreeting': '欢迎回来\nSolar Network',
'signinCaption': '登录以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!',
'signinRiskDetected': '检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。',
'signup': '注册',
'signupGreeting': '欢迎加入\nSolar Network',
'signupCaption': '在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!',
'signout': '登出',
'riskDetection': '检测到风险',
'matureContent': '评级内容',
'matureContentCaption': '该内容已被评级为家长指导级或以上,这可能说明内容包含一系列不友好的成分',
'notifyAllRead': '已读所有通知',
'notifyEmpty': '通知箱为空',
'notifyEmptyCaption': '看起来最近没发生什么呢',
'postAction': '发表',
'postDetail': '帖子详情',
'postReplies': '帖子回复',
'postPublishing': '发表帖子',
'postIdentityNotify': '你将会以本身份发表帖子',
'postContentPlaceholder': '发生什么事了?!',
'postReaction': '帖子的反应',
'postActionList': '帖子的操作',
'postReplyAction': '发表一则回复',
'postRepliedNotify': '回了一个 @username 的帖子',
'postRepostedNotify': '转了一个 @username 的帖子',
'postEditingNotify': '你正在编辑一个你发布的帖子',
@ -181,16 +138,6 @@ class SolianMessages extends Translations {
'attachmentAddFile': '附加文件',
'attachmentSetting': '调整附件',
'attachmentAlt': '替代文字',
'channelOrganizing': '组织频道',
'channelEditingNotify': '你正在编辑频道 @channel',
'channelAlias': '别称(标识符)',
'channelName': '显示名称',
'channelDescription': '频道简介',
'channelEncrypted': '加密频道',
'channelMember': '频道成员',
'channelType': '频道类型',
'channelTypeCommon': '普通频道',
'channelTypeDirect': '私信聊天',
}
};
}

View File

@ -15,8 +15,7 @@ class AccountAvatar extends StatelessWidget {
bool isEmpty = content == null;
if (content is String) {
direct = content.startsWith('http');
if (!isEmpty) isEmpty = content.isEmpty;
if (!isEmpty) isEmpty = content.endsWith('/api/attachments/0');
isEmpty = content.endsWith('/api/attachments/0');
}
return CircleAvatar(

View File

@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/widgets/account/account_avatar.dart';
class SliverFriendList extends StatelessWidget {
final int accountId;
final List<Friendship> items;
final Function onUpdate;
const SliverFriendList({
super.key,
required this.accountId,
required this.items,
required this.onUpdate,
});
DismissDirection getDismissDirection(Friendship relation) {
if (relation.status == 2) return DismissDirection.endToStart;
if (relation.status == 1) return DismissDirection.startToEnd;
if (relation.status == 0 && relation.relatedId != accountId) {
return DismissDirection.startToEnd;
}
return DismissDirection.horizontal;
}
Widget buildItem(context, index) {
final element = items[index];
final otherside = element.getOtherside(accountId);
final randomId = DateTime.now().microsecondsSinceEpoch >> 10;
return Dismissible(
key: Key(randomId.toString()),
background: Container(
color: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: const Icon(Icons.close, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerRight,
child: const Icon(Icons.check, color: Colors.white),
),
direction: getDismissDirection(element),
child: ListTile(
title: Text(otherside.nick),
subtitle: Text(otherside.name),
leading: AccountAvatar(content: otherside.avatar),
),
onDismissed: (direction) {
final FriendProvider provider = Get.find();
if (direction == DismissDirection.startToEnd) {
provider.updateFriendship(element, 2).then((_) => onUpdate());
}
if (direction == DismissDirection.endToStart) {
provider.updateFriendship(element, 1).then((_) => onUpdate());
}
},
);
}
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: items.length,
itemBuilder: (_, __) => buildItem(_, __),
);
}
}

View File

@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/widgets/account/account_avatar.dart';
class FriendSelect extends StatefulWidget {
final String title;
final Widget? Function(Account item)? trailingBuilder;
const FriendSelect({super.key, required this.title, this.trailingBuilder});
@override
State<FriendSelect> createState() => _FriendSelectState();
}
class _FriendSelectState extends State<FriendSelect> {
int _accountId = 0;
List<Friendship> _friends = List.empty(growable: true);
getFriends() async {
final AuthProvider auth = Get.find();
final prof = await auth.getProfile();
_accountId = prof.body['id'];
final FriendProvider provider = Get.find();
final resp = await provider.listFriendshipWithStatus(1);
setState(() {
_friends.addAll(resp.body
.map((e) => Friendship.fromJson(e))
.toList()
.cast<Friendship>());
});
}
@override
void initState() {
super.initState();
getFriends();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: ListView.builder(
itemCount: _friends.length,
itemBuilder: (context, index) {
var element = _friends[index].getOtherside(_accountId);
return ListTile(
title: Text(element.nick),
subtitle: Text(element.name),
leading: AccountAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element)
: null,
onTap: () {
Navigator.pop(context, element);
},
);
},
),
),
],
),
);
}
}

View File

@ -4,7 +4,6 @@ import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart';
class AttachmentItem extends StatelessWidget {
final String parentId;
final Attachment item;
final bool showBadge;
final bool showHideButton;
@ -14,7 +13,6 @@ class AttachmentItem extends StatelessWidget {
const AttachmentItem({
super.key,
required this.parentId,
required this.item,
this.badge,
this.fit = BoxFit.cover,
@ -26,7 +24,7 @@ class AttachmentItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Hero(
tag: Key('a${item.uuid}p$parentId'),
tag: Key('a${item.uuid}'),
child: Stack(
fit: StackFit.expand,
children: [
@ -50,10 +48,8 @@ class AttachmentItem extends StatelessWidget {
child: Material(
color: Colors.transparent,
child: ActionChip(
visualDensity:
const VisualDensity(vertical: -4, horizontal: -4),
avatar: Icon(Icons.visibility_off,
color: Theme.of(context).colorScheme.onSurfaceVariant),
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
avatar: Icon(Icons.visibility_off, color: Theme.of(context).colorScheme.onSurfaceVariant),
label: Text('hide'.tr),
onPressed: () {
if (onHide != null) onHide!();

View File

@ -9,11 +9,9 @@ import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_list_fullscreen.dart';
class AttachmentList extends StatefulWidget {
final String parentId;
final List<int> attachmentsId;
const AttachmentList(
{super.key, required this.parentId, required this.attachmentsId});
const AttachmentList({super.key, required this.attachmentsId});
@override
State<AttachmentList> createState() => _AttachmentListState();
@ -128,7 +126,6 @@ class _AttachmentListState extends State<AttachmentList> {
fit: StackFit.expand,
children: [
AttachmentItem(
parentId: widget.parentId,
key: Key('a${element!.uuid}'),
item: element,
badge: _attachmentsMeta.length > 1
@ -183,8 +180,7 @@ class _AttachmentListState extends State<AttachmentList> {
} else {
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (context) => AttachmentListFullScreen(
parentId: widget.parentId,
builder: (context) => AttachmentListFullscreen(
attachment: element,
),
),

View File

@ -2,19 +2,17 @@ import 'package:flutter/material.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/widgets/attachments/attachment_item.dart';
class AttachmentListFullScreen extends StatefulWidget {
final String parentId;
class AttachmentListFullscreen extends StatefulWidget {
final Attachment attachment;
const AttachmentListFullScreen(
{super.key, required this.parentId, required this.attachment});
const AttachmentListFullscreen({super.key, required this.attachment});
@override
State<AttachmentListFullScreen> createState() =>
_AttachmentListFullScreenState();
State<AttachmentListFullscreen> createState() =>
_AttachmentListFullscreenState();
}
class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
class _AttachmentListFullscreenState extends State<AttachmentListFullscreen> {
@override
void initState() {
super.initState();
@ -35,7 +33,6 @@ class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
panEnabled: true,
scaleEnabled: true,
child: AttachmentItem(
parentId: widget.parentId,
showHideButton: false,
item: widget.attachment,
fit: BoxFit.contain,

View File

@ -4,14 +4,9 @@ import 'package:get/utils.dart';
abstract class AppNavigation {
static List<AppNavigationDestination> destinations = [
AppNavigationDestination(
icon: const Icon(Icons.public),
label: 'social'.tr,
page: 'social',
),
AppNavigationDestination(
icon: const Icon(Icons.contacts),
label: 'contact'.tr,
page: 'contact',
icon: const Icon(Icons.home),
label: 'home'.tr,
page: 'home',
),
AppNavigationDestination(
icon: const Icon(Icons.account_circle),
@ -26,6 +21,5 @@ class AppNavigationDestination {
final String label;
final String page;
AppNavigationDestination(
{required this.icon, required this.label, required this.page});
AppNavigationDestination({required this.icon, required this.label, required this.page});
}

View File

@ -15,15 +15,13 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
@override
Widget build(BuildContext context) {
return NavigationRail(
destinations: AppNavigation.destinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
)
.toList(),
labelType: NavigationRailLabelType.all,
destinations: AppNavigation.destinations.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
).toList(),
labelType: NavigationRailLabelType.selected,
selectedIndex: _selectedIndex,
onDestinationSelected: (idx) {
setState(() => _selectedIndex = idx);

View File

@ -2,13 +2,12 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_publish.dart';
import 'package:solian/screens/posts/publish.dart';
import 'package:solian/services.dart';
class PostAction extends StatefulWidget {
@ -69,7 +68,7 @@ class _PostActionState extends State<PostAction> {
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const FaIcon(FontAwesomeIcons.reply, size: 20),
leading: const Icon(Icons.reply),
title: Text('reply'.tr),
onTap: () async {
final value = await AppRouter.instance.pushNamed(
@ -83,7 +82,7 @@ class _PostActionState extends State<PostAction> {
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const FaIcon(FontAwesomeIcons.retweet, size: 20),
leading: const Icon(Icons.redo),
title: Text('repost'.tr),
onTap: () async {
final value = await AppRouter.instance.pushNamed(
@ -156,7 +155,7 @@ class _PostDeletionDialogState extends State<PostDeletionDialog> {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
setState(() => _isBusy = true);
final resp = await client.delete('/api/posts/${widget.item.id}');

View File

@ -1,10 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get_utils/get_utils.dart';
import 'package:solian/models/post.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/posts/post_quick_action.dart';
@ -12,22 +10,14 @@ import 'package:timeago/timeago.dart' show format;
class PostItem extends StatefulWidget {
final Post item;
final bool isClickable;
final bool isCompact;
final bool isReactable;
final bool isShowReply;
final bool isShowEmbed;
final String? overrideAttachmentParent;
const PostItem({
super.key,
required this.item,
this.isClickable = false,
this.isCompact = false,
this.isReactable = true,
this.isShowReply = true,
this.isShowEmbed = true,
this.overrideAttachmentParent,
});
@override
@ -48,14 +38,14 @@ class _PostItemState extends State<PostItem> {
children: [
Row(
children: [
FaIcon(
FontAwesomeIcons.reply,
size: 16,
Icon(
Icons.reply,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
),
Text(
'postRepliedNotify'.trParams(
{'username': '@${widget.item.replyTo!.author.name}'},
{'username': '@${widget.item.author.name}'},
),
style: TextStyle(
color:
@ -69,7 +59,6 @@ class _PostItemState extends State<PostItem> {
child: PostItem(
item: widget.item.replyTo!,
isCompact: true,
overrideAttachmentParent: widget.item.alias,
).paddingSymmetric(vertical: 8),
),
],
@ -81,14 +70,14 @@ class _PostItemState extends State<PostItem> {
children: [
Row(
children: [
FaIcon(
FontAwesomeIcons.retweet,
size: 16,
Icon(
Icons.redo,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
),
Text(
'postRepostedNotify'.trParams(
{'username': '@${widget.item.repostTo!.author.name}'},
{'username': '@${widget.item.author.name}'},
),
style: TextStyle(
color:
@ -102,7 +91,6 @@ class _PostItemState extends State<PostItem> {
child: PostItem(
item: widget.item.repostTo!,
isCompact: true,
overrideAttachmentParent: widget.item.alias,
).paddingSymmetric(vertical: 8),
),
],
@ -138,10 +126,7 @@ class _PostItemState extends State<PostItem> {
top: 2,
bottom: hasAttachment ? 4 : 0,
),
AttachmentList(
parentId: widget.overrideAttachmentParent ?? widget.item.alias,
attachmentsId: item.attachments ?? List.empty(),
),
AttachmentList(attachmentsId: item.attachments ?? List.empty()),
],
);
}
@ -165,38 +150,14 @@ class _PostItemState extends State<PostItem> {
.paddingOnly(left: 4),
],
),
if (widget.item.replyTo != null) buildReply(context),
if (widget.item.repostTo != null) buildRepost(context),
Markdown(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
data: item.content,
padding: const EdgeInsets.all(0),
).paddingOnly(left: 12, right: 8),
if (widget.item.replyTo != null && widget.isShowEmbed)
GestureDetector(
child: buildReply(context).paddingOnly(top: 4),
onTap: () {
if (!widget.isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {
'alias': widget.item.replyTo!.alias,
},
);
},
),
if (widget.item.repostTo != null && widget.isShowEmbed)
GestureDetector(
child: buildRepost(context).paddingOnly(top: 4),
onTap: () {
if (!widget.isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {
'alias': widget.item.repostTo!.alias,
},
);
},
),
],
),
)
@ -207,12 +168,8 @@ class _PostItemState extends State<PostItem> {
right: 16,
left: 16,
),
AttachmentList(
parentId: widget.item.alias,
attachmentsId: item.attachments ?? List.empty(),
),
AttachmentList(attachmentsId: item.attachments ?? List.empty()),
PostQuickAction(
isShowReply: widget.isShowReply,
isReactable: widget.isReactable,
item: widget.item,
onReact: (symbol, changes) {

View File

@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart';
class PostListWidget extends StatelessWidget {
final bool shrinkWrap;
final bool isShowEmbed;
final bool isClickable;
final bool isNestedClickable;
final PagingController<int, Post> controller;
const PostListWidget({
super.key,
required this.controller,
this.shrinkWrap = false,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
});
@override
Widget build(BuildContext context) {
return PagedListView<int, Post>.separated(
shrinkWrap: shrinkWrap,
pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) {
return GestureDetector(
child: PostItem(
key: Key('p${item.alias}'),
item: item,
isShowEmbed: isShowEmbed,
isClickable: isNestedClickable,
).paddingSymmetric(
vertical: (item.attachments?.isEmpty ?? false) ? 8 : 0,
),
onTap: () {
if (!isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {'alias': item.alias},
);
},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(item: item),
).then((value) {
if (value == true) controller.refresh();
});
},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

View File

@ -6,18 +6,15 @@ import 'package:solian/models/reaction.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/posts/post_reaction.dart';
import 'package:solian/widgets/posts/post_replies.dart';
class PostQuickAction extends StatefulWidget {
final Post item;
final bool isReactable;
final bool isShowReply;
final void Function(String symbol, int num) onReact;
const PostQuickAction({
super.key,
required this.item,
this.isShowReply = true,
this.isReactable = true,
required this.onReact,
});
@ -53,7 +50,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
final client = GetConnect();
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
client.httpClient.addAuthenticator(auth.requestAuthenticator);
client.httpClient.addAuthenticator(auth.reqAuthenticator);
setState(() => _isSubmitting = true);
@ -96,25 +93,17 @@ class _PostQuickActionState extends State<PostQuickAction> {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.isReactable && widget.isShowReply)
if (widget.isReactable)
ActionChip(
avatar: const Icon(Icons.comment),
label: Text(widget.item.replyCount.toString()),
visualDensity: density,
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) {
return PostReplyListPopup(item: widget.item);
},
);
},
onPressed: () {},
),
if (widget.isReactable && widget.isShowReply)
if (widget.isReactable)
const VerticalDivider(
thickness: 0.3, width: 0.3, indent: 8, endIndent: 8)
.paddingSymmetric(horizontal: 8),
.paddingOnly(left: 8),
Expanded(
child: ListView(
shrinkWrap: true,
@ -143,7 +132,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
onPressed: () => showReactMenu(),
),
],
),
).paddingOnly(left: 8),
)
],
),

View File

@ -1,86 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/post.dart';
import 'package:solian/widgets/posts/post_list.dart';
class PostReplyList extends StatefulWidget {
final Post item;
final bool shrinkWrap;
const PostReplyList({
super.key,
required this.item,
this.shrinkWrap = false,
});
@override
State<PostReplyList> createState() => _PostReplyListState();
}
class _PostReplyListState extends State<PostReplyList> {
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
Future<void> getReplies(int pageKey) async {
final PostProvider provider = Get.find();
Response resp;
try {
resp = await provider.listPostReplies(widget.item.alias, pageKey);
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(getReplies);
}
@override
Widget build(BuildContext context) {
return PostListWidget(
isShowEmbed: false,
shrinkWrap: widget.shrinkWrap,
controller: _pagingController,
);
}
}
class PostReplyListPopup extends StatelessWidget {
final Post item;
const PostReplyListPopup({super.key, required this.item});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: PostReplyList(
item: item,
),
),
],
);
}
}

View File

@ -6,14 +6,12 @@ import FlutterMacOS
import Foundation
import file_selector_macos
import flutter_local_notifications
import flutter_secure_storage_macos
import path_provider_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@ -1,41 +0,0 @@
PODS:
- file_selector_macos (0.0.1):
- FlutterMacOS
- flutter_secure_storage_macos (6.1.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
flutter_secure_storage_macos:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
FlutterMacOS:
:path: Flutter/ephemeral
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
COCOAPODS: 1.15.2

View File

@ -21,14 +21,12 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
32E4047D78077CA8C6FE31E6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2738AEB242CF49091607FEDA /* Pods_Runner.framework */; };
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
7EA791CAE9710BE5D384F1BB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55FB45492936527B3666668F /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -62,13 +60,11 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
2738AEB242CF49091607FEDA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* solian.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = solian.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* solian.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "solian.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@ -80,14 +76,8 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
3793F8988B03A8D2EAECDCBD /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
4BD0A28F73121D6B6DD462AF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
55FB45492936527B3666668F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
8AB030F50C089DAB14480DBD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
A9258CF6356D15783726DC84 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -95,7 +85,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7EA791CAE9710BE5D384F1BB /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -103,27 +92,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
32E4047D78077CA8C6FE31E6 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27286574DE31F9C9A78B355D /* Pods */ = {
isa = PBXGroup;
children = (
A9258CF6356D15783726DC84 /* Pods-Runner.debug.xcconfig */,
4BD0A28F73121D6B6DD462AF /* Pods-Runner.release.xcconfig */,
8AB030F50C089DAB14480DBD /* Pods-Runner.profile.xcconfig */,
3793F8988B03A8D2EAECDCBD /* Pods-RunnerTests.debug.xcconfig */,
1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */,
BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
@ -151,7 +125,6 @@
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
27286574DE31F9C9A78B355D /* Pods */,
);
sourceTree = "<group>";
};
@ -202,8 +175,6 @@
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
2738AEB242CF49091607FEDA /* Pods_Runner.framework */,
55FB45492936527B3666668F /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -215,7 +186,6 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
291CAD95BC748648C4154147 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
@ -234,13 +204,11 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
C1C3653094F6B3FFFFCFD12B /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
C5DDC734703B72E778163C68 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -323,28 +291,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
291CAD95BC748648C4154147 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -383,45 +329,6 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
C1C3653094F6B3FFFFCFD12B /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
C5DDC734703B72E778163C68 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -473,7 +380,6 @@
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3793F8988B03A8D2EAECDCBD /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -488,7 +394,6 @@
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
@ -503,7 +408,6 @@
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;

View File

@ -4,7 +4,4 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -89,22 +89,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dbus:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
dropdown_button2:
dependency: "direct main"
description:
name: dropdown_button2
sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1
url: "https://pub.dev"
source: hosted
version: "2.3.9"
fake_async:
dependency: transitive
description:
@ -161,14 +145,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.3+1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -190,30 +166,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef"
url: "https://pub.dev"
source: hosted
version: "17.1.2"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
url: "https://pub.dev"
source: hosted
version: "4.0.0+1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
flutter_markdown:
dependency: "direct main"
description:
@ -234,10 +186,10 @@ packages:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
sha256: "8496a89eea74e23f92581885f876455d9d460e71201405dffe5f55dfe1155864"
url: "https://pub.dev"
source: hosted
version: "9.2.2"
version: "9.2.1"
flutter_secure_storage_linux:
dependency: transitive
description:
@ -250,10 +202,10 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
sha256: b768a7dab26d6186b68e2831b3104f8968154f0f4fdbf66e7c2dd7bdf299daaf
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
@ -304,14 +256,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f"
url: "https://pub.dev"
source: hosted
version: "10.7.0"
get:
dependency: "direct main"
description:
@ -324,10 +268,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "6ad5662b014c06c20fa46ab78715c96b2222a7fe4f87bf77e0289592c2539e86"
sha256: "7685acd06244ba4be60f455c5cafe5790c63dc91fc03f7385b1e922a6b85b17c"
url: "https://pub.dev"
source: hosted
version: "14.1.3"
version: "14.1.1"
http:
dependency: transitive
description:
@ -348,10 +292,10 @@ packages:
dependency: "direct main"
description:
name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
version: "4.1.7"
image_picker:
dependency: "direct main"
description:
@ -584,54 +528,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.dev"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5"
url: "https://pub.dev"
source: hosted
version: "12.0.6"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
url: "https://pub.dev"
source: hosted
version: "9.4.4"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
@ -677,14 +573,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
@ -733,14 +621,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.6.1"
timezone:
dependency: transitive
description:
name: timezone
sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5
url: "https://pub.dev"
source: hosted
version: "0.9.3"
typed_data:
dependency: transitive
description:
@ -761,10 +641,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
version: "6.3.1"
url_launcher_ios:
dependency: transitive
description:
@ -813,14 +693,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
vector_math:
dependency: transitive
description:
@ -845,30 +717,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712"
url: "https://pub.dev"
source: hosted
version: "0.1.4"
web_socket_channel:
dependency: "direct main"
description:
name: web_socket_channel
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
url: "https://pub.dev"
source: hosted
version: "3.0.0"
win32:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.1"
version: "5.5.0"
xdg_directories:
dependency: transitive
description:
@ -886,5 +742,5 @@ packages:
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.4.0 <4.0.0"
dart: ">=3.3.4 <4.0.0"
flutter: ">=3.19.0"

View File

@ -51,12 +51,6 @@ dependencies:
path: ^1.9.0
intl: ^0.19.0
image: ^4.1.7
font_awesome_flutter: ^10.7.0
web_socket_channel: ^3.0.0
flutter_local_notifications: ^17.1.2
permission_handler: ^11.3.1
uuid: ^4.4.0
dropdown_button2: ^2.3.9
dev_dependencies:
flutter_test:

View File

@ -8,7 +8,6 @@
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -16,8 +15,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
permission_handler_windows
url_launcher_windows
)