Compare commits
10 Commits
1eb42fa351
...
657f36c1f8
Author | SHA1 | Date | |
---|---|---|---|
657f36c1f8 | |||
9eae49128e | |||
daee3e8074 | |||
f376603482 | |||
806ae602d5 | |||
15ed75b04e | |||
3e640768c8 | |||
d98d167f4c | |||
05f88fe3f3 | |||
a291e8af66 |
@ -34,6 +34,8 @@ 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):
|
||||
@ -41,6 +43,8 @@ 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)
|
||||
@ -51,9 +55,11 @@ 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:
|
||||
@ -68,12 +74,16 @@ 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"
|
||||
|
||||
@ -82,9 +92,11 @@ 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
|
||||
|
@ -199,6 +199,7 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
|
||||
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -270,6 +271,23 @@
|
||||
/* 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;
|
||||
|
@ -1,7 +1,12 @@
|
||||
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';
|
||||
@ -28,7 +33,19 @@ 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(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
class Channel {
|
||||
@ -60,6 +62,15 @@ class Channel {
|
||||
'realm_id': realmId,
|
||||
'is_encrypted': isEncrypted,
|
||||
};
|
||||
|
||||
IconData get icon {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return FontAwesomeIcons.userGroup;
|
||||
default:
|
||||
return FontAwesomeIcons.hashtag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelMember {
|
||||
|
161
lib/providers/account.dart
Normal file
161
lib/providers/account.dart
Normal file
@ -0,0 +1,161 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,17 +5,13 @@ 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';
|
||||
@ -25,14 +21,12 @@ class AuthProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||
|
||||
applyAuthenticator();
|
||||
loadCredentials();
|
||||
}
|
||||
|
||||
oauth2.Credentials? credentials;
|
||||
|
||||
Future<Request<T?>> reqAuthenticator<T>(Request<T?> request) async {
|
||||
if (credentials != null && credentials!.isExpired) {
|
||||
Future<void> refreshCredentials() async {
|
||||
final resp = await post('/api/auth/token', {
|
||||
'refresh_token': credentials!.refreshToken,
|
||||
'grant_type': 'refresh_token',
|
||||
@ -48,7 +42,14 @@ class AuthProvider extends GetConnect {
|
||||
expiration: DateTime.now().add(const Duration(minutes: 3)),
|
||||
);
|
||||
storage.write(
|
||||
key: 'auth_credentials', value: jsonEncode(credentials!.toJson()));
|
||||
key: 'auth_credentials',
|
||||
value: jsonEncode(credentials!.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Request<T?>> requestAuthenticator<T>(Request<T?> request) async {
|
||||
if (credentials != null && credentials!.isExpired) {
|
||||
refreshCredentials();
|
||||
}
|
||||
|
||||
if (credentials != null) {
|
||||
@ -58,18 +59,20 @@ class AuthProvider extends GetConnect {
|
||||
return request;
|
||||
}
|
||||
|
||||
void applyAuthenticator() {
|
||||
isAuthorized.then((status) async {
|
||||
if (status) {
|
||||
Future<void> loadCredentials() async {
|
||||
if (await isAuthorized) {
|
||||
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 {
|
||||
BuildContext context,
|
||||
String username,
|
||||
String password,
|
||||
) async {
|
||||
_cacheUserProfileResponse = null;
|
||||
|
||||
final resp = await oauth2.resourceOwnerPasswordGrant(
|
||||
tokenEndpoint,
|
||||
username,
|
||||
@ -89,13 +92,23 @@ class AuthProvider extends GetConnect {
|
||||
);
|
||||
|
||||
storage.write(
|
||||
key: 'auth_credentials', value: jsonEncode(credentials!.toJson()));
|
||||
applyAuthenticator();
|
||||
key: 'auth_credentials',
|
||||
value: jsonEncode(credentials!.toJson()),
|
||||
);
|
||||
|
||||
Get.find<AccountProvider>().connect();
|
||||
Get.find<AccountProvider>().notifyPrefetch();
|
||||
|
||||
return credentials!;
|
||||
}
|
||||
|
||||
void signout() {
|
||||
_cacheUserProfileResponse = null;
|
||||
|
||||
Get.find<AccountProvider>().disconnect();
|
||||
Get.find<AccountProvider>().notifications.clear();
|
||||
Get.find<AccountProvider>().notificationUnread.value = 0;
|
||||
|
||||
storage.deleteAll();
|
||||
}
|
||||
|
||||
@ -108,7 +121,11 @@ class AuthProvider extends GetConnect {
|
||||
return _cacheUserProfileResponse!;
|
||||
}
|
||||
|
||||
final resp = await get('/api/users/me');
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||
client.httpClient.addAuthenticator(requestAuthenticator);
|
||||
|
||||
final resp = await client.get('/api/users/me');
|
||||
_cacheUserProfileResponse = resp;
|
||||
return resp;
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class AttachmentProvider extends GetConnect {
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
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.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
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.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
var resp = await client.delete('/api/attachments/$id');
|
||||
if (resp.statusCode != 200) {
|
||||
|
21
lib/providers/content/channel.dart
Normal file
21
lib/providers/content/channel.dart
Normal file
@ -0,0 +1,21 @@
|
||||
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;
|
||||
}
|
||||
}
|
36
lib/providers/content/post.dart
Normal file
36
lib/providers/content/post.dart
Normal file
@ -0,0 +1,36 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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');
|
||||
}
|
21
lib/providers/content/realm.dart
Normal file
21
lib/providers/content/realm.dart
Normal file
@ -0,0 +1,21 @@
|
||||
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;
|
||||
}
|
||||
}
|
43
lib/providers/friend.dart
Normal file
43
lib/providers/friend.dart
Normal file
@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
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/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/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/shells/basic_shell.dart';
|
||||
import 'package:solian/shells/nav_shell.dart';
|
||||
|
||||
@ -13,12 +15,17 @@ abstract class AppRouter {
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) =>
|
||||
NavShell(state: state, child: child),
|
||||
NavShell(state: state, child: child, showAppBar: false),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
name: 'social',
|
||||
builder: (context, state) => const SocialScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/contact',
|
||||
name: 'contact',
|
||||
builder: (context, state) => const ContactScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
@ -31,21 +38,29 @@ 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(
|
||||
@ -61,6 +76,17 @@ 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'],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
|
||||
class AccountScreen extends StatefulWidget {
|
||||
@ -41,7 +43,14 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
title: 'signin'.tr,
|
||||
caption: 'signinCaption'.tr,
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('signin').then((_) {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const SignInPopup(),
|
||||
).then((_) async {
|
||||
await provider.getProfile(noCache: true);
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
@ -51,7 +60,15 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
title: 'signup'.tr,
|
||||
caption: 'signupCaption'.tr,
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('signup');
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const SignUpPopup(),
|
||||
).then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -68,7 +85,9 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
leading: x.$1,
|
||||
title: Text(x.$2),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed(x.$3);
|
||||
AppRouter.instance
|
||||
.pushNamed(x.$3)
|
||||
.then((_) => setState(() {}));
|
||||
},
|
||||
),
|
||||
)),
|
||||
@ -100,25 +119,72 @@ class AccountNameCard extends StatelessWidget {
|
||||
future: provider.getProfile(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return Container();
|
||||
}
|
||||
|
||||
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(
|
||||
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']),
|
||||
title: Text('description'.tr),
|
||||
subtitle: Text(
|
||||
prof.body['description']?.isNotEmpty
|
||||
? prof.body['description']
|
||||
: 'No description yet.',
|
||||
),
|
||||
).paddingOnly(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: SolianTheme.isLargeScreen(context) ? 8 : 0,
|
||||
),
|
||||
),
|
||||
).paddingOnly(left: 24, right: 24, top: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
212
lib/screens/account/friend.dart
Normal file
212
lib/screens/account/friend.dart
Normal file
@ -0,0 +1,212 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
210
lib/screens/account/notification.dart
Normal file
210
lib/screens/account/notification.dart
Normal file
@ -0,0 +1,210 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -37,15 +37,14 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
void selectBirthday() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _birthday,
|
||||
initialDate: _birthday?.toLocal(),
|
||||
firstDate: DateTime(DateTime.now().year - 200),
|
||||
lastDate: DateTime(DateTime.now().year + 200),
|
||||
lastDate: DateTime(DateTime.now().year),
|
||||
);
|
||||
if (picked != null && picked != _birthday) {
|
||||
setState(() {
|
||||
_birthday = picked;
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd hh:mm').format(_birthday!);
|
||||
_birthdayController.text = DateFormat('yyyy-MM-dd').format(_birthday!);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -66,7 +65,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 hh:mm').format(_birthday!);
|
||||
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
|
||||
}
|
||||
|
||||
_isBusy = false;
|
||||
@ -101,7 +100,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
final resp = await client.put(
|
||||
'/api/users/me/$position',
|
||||
@ -109,7 +108,37 @@ 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);
|
||||
@ -277,11 +306,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: null,
|
||||
onPressed: _isBusy ? null : () => syncWidget(),
|
||||
child: Text('reset'.tr),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: null,
|
||||
onPressed: _isBusy ? null : () => updatePersonalize(),
|
||||
child: Text('apply'.tr),
|
||||
),
|
||||
],
|
||||
|
@ -2,18 +2,17 @@ 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 SignInScreen extends StatefulWidget {
|
||||
const SignInScreen({super.key});
|
||||
class SignInPopup extends StatefulWidget {
|
||||
const SignInPopup({super.key});
|
||||
|
||||
@override
|
||||
State<SignInScreen> createState() => _SignInScreenState();
|
||||
State<SignInPopup> createState() => _SignInPopupState();
|
||||
}
|
||||
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
class _SignInPopupState extends State<SignInPopup> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
@ -23,15 +22,16 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
final username = _usernameController.value.text;
|
||||
final password = _passwordController.value.text;
|
||||
if (username.isEmpty || password.isEmpty) return;
|
||||
provider.signin(context, username, password).then((_) {
|
||||
AppRouter.instance.pop(true);
|
||||
provider.signin(context, username, password).then((_) async {
|
||||
Navigator.pop(context, 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,9 +46,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
launchUrlString(
|
||||
'${ServiceFinder.services['passport']}/mfa?ticket=${ticketId!.group(1)}',
|
||||
);
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -56,28 +54,32 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(messages.last),
|
||||
));
|
||||
context.showErrorDialog(messages.last);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
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: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Image.asset('assets/logo.png', width: 72, height: 72),
|
||||
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),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
@ -107,10 +109,19 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) => performAction(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
child: Text('signin'.tr),
|
||||
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),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -4,14 +4,14 @@ import 'package:solian/exts.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
const SignUpScreen({super.key});
|
||||
class SignUpPopup extends StatefulWidget {
|
||||
const SignUpPopup({super.key});
|
||||
|
||||
@override
|
||||
State<SignUpScreen> createState() => _SignUpScreenState();
|
||||
State<SignUpPopup> createState() => _SignUpPopupState();
|
||||
}
|
||||
|
||||
class _SignUpScreenState extends State<SignUpScreen> {
|
||||
class _SignUpPopupState extends State<SignUpPopup> {
|
||||
final _emailController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _nicknameController = TextEditingController();
|
||||
@ -61,19 +61,25 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
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: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Image.asset('assets/logo.png', width: 72, height: 72),
|
||||
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),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
@ -132,9 +138,18 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
onSubmitted: (_) => performAction(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
child: Text('signup'.tr),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onPressed: () => performAction(context),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
292
lib/screens/channel/channel_organize.dart
Normal file
292
lib/screens/channel/channel_organize.dart
Normal file
@ -0,0 +1,292 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
127
lib/screens/contact.dart
Normal file
127
lib/screens/contact.dart
Normal file
@ -0,0 +1,127 @@
|
||||
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: () {},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
69
lib/screens/posts/post_detail.dart
Normal file
69
lib/screens/posts/post_detail.dart
Normal file
@ -0,0 +1,69 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
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';
|
||||
@ -40,7 +41,7 @@ class PostPublishingScreen extends StatefulWidget {
|
||||
class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
final _contentController = TextEditingController();
|
||||
|
||||
bool _isSubmitting = false;
|
||||
bool _isBusy = false;
|
||||
|
||||
List<int> _attachments = List.empty();
|
||||
|
||||
@ -60,11 +61,11 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
if (!await auth.isAuthorized) return;
|
||||
if (_contentController.value.text.isEmpty) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
final payload = {
|
||||
'content': _contentController.value.text,
|
||||
@ -86,7 +87,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
AppRouter.instance.pop(resp.body);
|
||||
}
|
||||
|
||||
setState(() => _isSubmitting = false);
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
@ -125,8 +126,8 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
leading: const PrevPageButton(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => applyPost(),
|
||||
child: Text('postAction'.tr.toUpperCase()),
|
||||
onPressed: () => applyPost(),
|
||||
)
|
||||
],
|
||||
),
|
||||
@ -134,8 +135,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isSubmitting)
|
||||
const LinearProgressIndicator().animate().scaleX(),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
@ -150,7 +150,10 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.reply),
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 18,
|
||||
),
|
||||
leadingPadding:
|
||||
const EdgeInsets.only(left: 10, right: 20),
|
||||
backgroundColor: Colors.transparent,
|
||||
@ -181,7 +184,10 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.redo),
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 18,
|
||||
),
|
||||
leadingPadding:
|
||||
const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
@ -4,27 +4,31 @@ 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_explore.dart';
|
||||
import 'package:solian/providers/content/post.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
class SocialScreen extends StatefulWidget {
|
||||
const SocialScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
State<SocialScreen> createState() => _SocialScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
class _SocialScreenState extends State<SocialScreen> {
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
getPosts(int pageKey) async {
|
||||
final PostExploreProvider provider = Get.find();
|
||||
final resp = await provider.listPost(pageKey);
|
||||
if (resp.statusCode != 200) {
|
||||
_pagingController.error = resp.bodyString;
|
||||
final PostProvider provider = Get.find();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
resp = await provider.listPost(pageKey);
|
||||
} catch (e) {
|
||||
_pagingController.error = e;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -39,7 +43,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Get.lazyPut(() => PostExploreProvider());
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(getPosts);
|
||||
@ -69,32 +72,36 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: MediaQuery.removePadding(
|
||||
removeTop: true,
|
||||
context: context,
|
||||
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,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
@ -2,9 +2,11 @@ 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:8446' : 'https://im.solsynth.dev',
|
||||
'interactive':
|
||||
devFlag ? 'http://localhost:8445' : 'https://co.solsynth.dev',
|
||||
'messaging': devFlag ? 'http://localhost:8447' : 'https://im.solsynth.dev',
|
||||
};
|
||||
}
|
@ -8,24 +8,32 @@ 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});
|
||||
const NavShell({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.state,
|
||||
this.showAppBar = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = AppRouter.instance.canPop();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
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,
|
||||
bottomNavigationBar: SolianTheme.isLargeScreen(context)
|
||||
? null
|
||||
: const AppNavigationBottomBar(),
|
||||
|
@ -9,14 +9,17 @@ class SolianMessages extends Translations {
|
||||
'next': 'Next',
|
||||
'reset': 'Reset',
|
||||
'page': 'Page',
|
||||
'home': 'Home',
|
||||
'social': 'Social',
|
||||
'contact': 'Contact',
|
||||
'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',
|
||||
@ -31,16 +34,24 @@ 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',
|
||||
@ -48,12 +59,18 @@ 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.',
|
||||
@ -73,6 +90,16 @@ 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': '隐藏',
|
||||
@ -84,10 +111,13 @@ class SolianMessages extends Translations {
|
||||
'edit': '编辑',
|
||||
'delete': '删除',
|
||||
'page': '页面',
|
||||
'home': '首页',
|
||||
'social': '社交',
|
||||
'contact': '联系',
|
||||
'apply': '应用',
|
||||
'search': '搜索',
|
||||
'reply': '回复',
|
||||
'repost': '转帖',
|
||||
'notification': '通知',
|
||||
'errorHappened': '发生错误了',
|
||||
'email': '邮件地址',
|
||||
'username': '用户名',
|
||||
@ -101,25 +131,38 @@ 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': '你正在编辑一个你发布的帖子',
|
||||
@ -138,6 +181,16 @@ class SolianMessages extends Translations {
|
||||
'attachmentAddFile': '附加文件',
|
||||
'attachmentSetting': '调整附件',
|
||||
'attachmentAlt': '替代文字',
|
||||
'channelOrganizing': '组织频道',
|
||||
'channelEditingNotify': '你正在编辑频道 @channel',
|
||||
'channelAlias': '别称(标识符)',
|
||||
'channelName': '显示名称',
|
||||
'channelDescription': '频道简介',
|
||||
'channelEncrypted': '加密频道',
|
||||
'channelMember': '频道成员',
|
||||
'channelType': '频道类型',
|
||||
'channelTypeCommon': '普通频道',
|
||||
'channelTypeDirect': '私信聊天',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ class AccountAvatar extends StatelessWidget {
|
||||
bool isEmpty = content == null;
|
||||
if (content is String) {
|
||||
direct = content.startsWith('http');
|
||||
isEmpty = content.endsWith('/api/attachments/0');
|
||||
if (!isEmpty) isEmpty = content.isEmpty;
|
||||
if (!isEmpty) isEmpty = content.endsWith('/api/attachments/0');
|
||||
}
|
||||
|
||||
return CircleAvatar(
|
||||
|
73
lib/widgets/account/friend_list.dart
Normal file
73
lib/widgets/account/friend_list.dart
Normal file
@ -0,0 +1,73 @@
|
||||
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(_, __),
|
||||
);
|
||||
}
|
||||
}
|
81
lib/widgets/account/friend_select.dart
Normal file
81
lib/widgets/account/friend_select.dart
Normal file
@ -0,0 +1,81 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ 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;
|
||||
@ -13,6 +14,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
|
||||
const AttachmentItem({
|
||||
super.key,
|
||||
required this.parentId,
|
||||
required this.item,
|
||||
this.badge,
|
||||
this.fit = BoxFit.cover,
|
||||
@ -24,7 +26,7 @@ class AttachmentItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Hero(
|
||||
tag: Key('a${item.uuid}'),
|
||||
tag: Key('a${item.uuid}p$parentId'),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@ -48,8 +50,10 @@ 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!();
|
||||
|
@ -9,9 +9,11 @@ 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.attachmentsId});
|
||||
const AttachmentList(
|
||||
{super.key, required this.parentId, required this.attachmentsId});
|
||||
|
||||
@override
|
||||
State<AttachmentList> createState() => _AttachmentListState();
|
||||
@ -126,6 +128,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AttachmentItem(
|
||||
parentId: widget.parentId,
|
||||
key: Key('a${element!.uuid}'),
|
||||
item: element,
|
||||
badge: _attachmentsMeta.length > 1
|
||||
@ -180,7 +183,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AttachmentListFullscreen(
|
||||
builder: (context) => AttachmentListFullScreen(
|
||||
parentId: widget.parentId,
|
||||
attachment: element,
|
||||
),
|
||||
),
|
||||
|
@ -2,17 +2,19 @@ import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
|
||||
class AttachmentListFullscreen extends StatefulWidget {
|
||||
class AttachmentListFullScreen extends StatefulWidget {
|
||||
final String parentId;
|
||||
final Attachment attachment;
|
||||
|
||||
const AttachmentListFullscreen({super.key, required this.attachment});
|
||||
const AttachmentListFullScreen(
|
||||
{super.key, required this.parentId, 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();
|
||||
@ -33,6 +35,7 @@ class _AttachmentListFullscreenState extends State<AttachmentListFullscreen> {
|
||||
panEnabled: true,
|
||||
scaleEnabled: true,
|
||||
child: AttachmentItem(
|
||||
parentId: widget.parentId,
|
||||
showHideButton: false,
|
||||
item: widget.attachment,
|
||||
fit: BoxFit.contain,
|
||||
|
@ -4,9 +4,14 @@ import 'package:get/utils.dart';
|
||||
abstract class AppNavigation {
|
||||
static List<AppNavigationDestination> destinations = [
|
||||
AppNavigationDestination(
|
||||
icon: const Icon(Icons.home),
|
||||
label: 'home'.tr,
|
||||
page: 'home',
|
||||
icon: const Icon(Icons.public),
|
||||
label: 'social'.tr,
|
||||
page: 'social',
|
||||
),
|
||||
AppNavigationDestination(
|
||||
icon: const Icon(Icons.contacts),
|
||||
label: 'contact'.tr,
|
||||
page: 'contact',
|
||||
),
|
||||
AppNavigationDestination(
|
||||
icon: const Icon(Icons.account_circle),
|
||||
@ -21,5 +26,6 @@ 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});
|
||||
}
|
||||
|
@ -15,13 +15,15 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigationRail(
|
||||
destinations: AppNavigation.destinations.map(
|
||||
destinations: AppNavigation.destinations
|
||||
.map(
|
||||
(e) => NavigationRailDestination(
|
||||
icon: e.icon,
|
||||
label: Text(e.label),
|
||||
),
|
||||
).toList(),
|
||||
labelType: NavigationRailLabelType.selected,
|
||||
)
|
||||
.toList(),
|
||||
labelType: NavigationRailLabelType.all,
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (idx) {
|
||||
setState(() => _selectedIndex = idx);
|
||||
|
@ -2,12 +2,13 @@ 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/publish.dart';
|
||||
import 'package:solian/screens/posts/post_publish.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class PostAction extends StatefulWidget {
|
||||
@ -68,7 +69,7 @@ class _PostActionState extends State<PostAction> {
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.reply),
|
||||
leading: const FaIcon(FontAwesomeIcons.reply, size: 20),
|
||||
title: Text('reply'.tr),
|
||||
onTap: () async {
|
||||
final value = await AppRouter.instance.pushNamed(
|
||||
@ -82,7 +83,7 @@ class _PostActionState extends State<PostAction> {
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.redo),
|
||||
leading: const FaIcon(FontAwesomeIcons.retweet, size: 20),
|
||||
title: Text('repost'.tr),
|
||||
onTap: () async {
|
||||
final value = await AppRouter.instance.pushNamed(
|
||||
@ -155,7 +156,7 @@ class _PostDeletionDialogState extends State<PostDeletionDialog> {
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
final resp = await client.delete('/api/posts/${widget.item.id}');
|
||||
|
@ -1,8 +1,10 @@
|
||||
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';
|
||||
@ -10,14 +12,22 @@ 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
|
||||
@ -38,14 +48,14 @@ class _PostItemState extends State<PostItem> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.reply,
|
||||
size: 18,
|
||||
FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
),
|
||||
Text(
|
||||
'postRepliedNotify'.trParams(
|
||||
{'username': '@${widget.item.author.name}'},
|
||||
{'username': '@${widget.item.replyTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(
|
||||
color:
|
||||
@ -59,6 +69,7 @@ class _PostItemState extends State<PostItem> {
|
||||
child: PostItem(
|
||||
item: widget.item.replyTo!,
|
||||
isCompact: true,
|
||||
overrideAttachmentParent: widget.item.alias,
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
@ -70,14 +81,14 @@ class _PostItemState extends State<PostItem> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.redo,
|
||||
size: 18,
|
||||
FaIcon(
|
||||
FontAwesomeIcons.retweet,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
),
|
||||
Text(
|
||||
'postRepostedNotify'.trParams(
|
||||
{'username': '@${widget.item.author.name}'},
|
||||
{'username': '@${widget.item.repostTo!.author.name}'},
|
||||
),
|
||||
style: TextStyle(
|
||||
color:
|
||||
@ -91,6 +102,7 @@ class _PostItemState extends State<PostItem> {
|
||||
child: PostItem(
|
||||
item: widget.item.repostTo!,
|
||||
isCompact: true,
|
||||
overrideAttachmentParent: widget.item.alias,
|
||||
).paddingSymmetric(vertical: 8),
|
||||
),
|
||||
],
|
||||
@ -126,7 +138,10 @@ class _PostItemState extends State<PostItem> {
|
||||
top: 2,
|
||||
bottom: hasAttachment ? 4 : 0,
|
||||
),
|
||||
AttachmentList(attachmentsId: item.attachments ?? List.empty()),
|
||||
AttachmentList(
|
||||
parentId: widget.overrideAttachmentParent ?? widget.item.alias,
|
||||
attachmentsId: item.attachments ?? List.empty(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -150,14 +165,38 @@ 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,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -168,8 +207,12 @@ class _PostItemState extends State<PostItem> {
|
||||
right: 16,
|
||||
left: 16,
|
||||
),
|
||||
AttachmentList(attachmentsId: item.attachments ?? List.empty()),
|
||||
AttachmentList(
|
||||
parentId: widget.item.alias,
|
||||
attachmentsId: item.attachments ?? List.empty(),
|
||||
),
|
||||
PostQuickAction(
|
||||
isShowReply: widget.isShowReply,
|
||||
isReactable: widget.isReactable,
|
||||
item: widget.item,
|
||||
onReact: (symbol, changes) {
|
||||
|
63
lib/widgets/posts/post_list.dart
Normal file
63
lib/widgets/posts/post_list.dart
Normal file
@ -0,0 +1,63 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,15 +6,18 @@ 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,
|
||||
});
|
||||
@ -50,7 +53,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
@ -93,17 +96,25 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.isReactable)
|
||||
if (widget.isReactable && widget.isShowReply)
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.comment),
|
||||
label: Text(widget.item.replyCount.toString()),
|
||||
visualDensity: density,
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return PostReplyListPopup(item: widget.item);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.isReactable)
|
||||
if (widget.isReactable && widget.isShowReply)
|
||||
const VerticalDivider(
|
||||
thickness: 0.3, width: 0.3, indent: 8, endIndent: 8)
|
||||
.paddingOnly(left: 8),
|
||||
.paddingSymmetric(horizontal: 8),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
@ -132,7 +143,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
onPressed: () => showReactMenu(),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 8),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
86
lib/widgets/posts/post_replies.dart
Normal file
86
lib/widgets/posts/post_replies.dart
Normal file
@ -0,0 +1,86 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -6,12 +6,14 @@ 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"))
|
||||
|
41
macos/Podfile.lock
Normal file
41
macos/Podfile.lock
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
@ -21,12 +21,14 @@
|
||||
/* 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 */
|
||||
@ -60,11 +62,13 @@
|
||||
/* 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>"; };
|
||||
@ -76,8 +80,14 @@
|
||||
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 */
|
||||
@ -85,6 +95,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7EA791CAE9710BE5D384F1BB /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -92,12 +103,27 @@
|
||||
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 = (
|
||||
@ -125,6 +151,7 @@
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
27286574DE31F9C9A78B355D /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -175,6 +202,8 @@
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2738AEB242CF49091607FEDA /* Pods_Runner.framework */,
|
||||
55FB45492936527B3666668F /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -186,6 +215,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
291CAD95BC748648C4154147 /* [CP] Check Pods Manifest.lock */,
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
@ -204,11 +234,13 @@
|
||||
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 = (
|
||||
);
|
||||
@ -291,6 +323,28 @@
|
||||
/* 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;
|
||||
@ -329,6 +383,45 @@
|
||||
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 */
|
||||
@ -380,6 +473,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3793F8988B03A8D2EAECDCBD /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@ -394,6 +488,7 @@
|
||||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@ -408,6 +503,7 @@
|
||||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
170
pubspec.lock
170
pubspec.lock
@ -89,6 +89,22 @@ 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:
|
||||
@ -145,6 +161,14 @@ 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
|
||||
@ -166,6 +190,30 @@ 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:
|
||||
@ -186,10 +234,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "8496a89eea74e23f92581885f876455d9d460e71201405dffe5f55dfe1155864"
|
||||
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.1"
|
||||
version: "9.2.2"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -202,10 +250,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: b768a7dab26d6186b68e2831b3104f8968154f0f4fdbf66e7c2dd7bdf299daaf
|
||||
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -256,6 +304,14 @@ 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:
|
||||
@ -268,10 +324,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "7685acd06244ba4be60f455c5cafe5790c63dc91fc03f7385b1e922a6b85b17c"
|
||||
sha256: "6ad5662b014c06c20fa46ab78715c96b2222a7fe4f87bf77e0289592c2539e86"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.1.1"
|
||||
version: "14.1.3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -292,10 +348,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.7"
|
||||
version: "4.2.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -528,6 +584,54 @@ 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:
|
||||
@ -573,6 +677,14 @@ 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:
|
||||
@ -621,6 +733,14 @@ 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:
|
||||
@ -641,10 +761,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
|
||||
sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -693,6 +813,14 @@ 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:
|
||||
@ -717,14 +845,30 @@ 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: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0"
|
||||
version: "5.5.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -742,5 +886,5 @@ packages:
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.3.4 <4.0.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
@ -51,6 +51,12 @@ 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:
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#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) {
|
||||
@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user