✨ Chat basis
This commit is contained in:
parent
657f36c1f8
commit
5b45718ebd
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
|
||||
class Channel {
|
||||
int id;
|
||||
@ -13,6 +14,7 @@ class Channel {
|
||||
int type;
|
||||
Account account;
|
||||
int accountId;
|
||||
Realm? realm;
|
||||
int? realmId;
|
||||
bool isEncrypted;
|
||||
|
||||
@ -30,6 +32,7 @@ class Channel {
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
required this.isEncrypted,
|
||||
this.realm,
|
||||
this.realmId,
|
||||
});
|
||||
|
||||
@ -44,6 +47,7 @@ class Channel {
|
||||
type: json['type'],
|
||||
account: Account.fromJson(json['account']),
|
||||
accountId: json['account_id'],
|
||||
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
|
||||
realmId: json['realm_id'],
|
||||
isEncrypted: json['is_encrypted'],
|
||||
);
|
||||
@ -57,8 +61,9 @@ class Channel {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'type': type,
|
||||
'account': account,
|
||||
'account': account.toJson(),
|
||||
'account_id': accountId,
|
||||
'realm': realm?.toJson(),
|
||||
'realm_id': realmId,
|
||||
'is_encrypted': isEncrypted,
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ class Message {
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String rawContent;
|
||||
Map<String, dynamic> content;
|
||||
Map<String, dynamic>? metadata;
|
||||
String type;
|
||||
List<String>? attachments;
|
||||
@ -21,16 +21,12 @@ class Message {
|
||||
|
||||
bool isSending = false;
|
||||
|
||||
Map<String, dynamic> get decodedContent {
|
||||
return jsonDecode(utf8.fuse(base64).decode(rawContent));
|
||||
}
|
||||
|
||||
Message({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.deletedAt,
|
||||
required this.rawContent,
|
||||
required this.content,
|
||||
required this.metadata,
|
||||
required this.type,
|
||||
this.attachments,
|
||||
@ -47,14 +43,16 @@ class Message {
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'],
|
||||
rawContent: json['content'],
|
||||
content: json['content'],
|
||||
metadata: json['metadata'],
|
||||
type: json['type'],
|
||||
attachments: json['attachments'],
|
||||
channel: Channel.fromJson(json['channel']),
|
||||
sender: Sender.fromJson(json['sender']),
|
||||
replyId: json['reply_id'],
|
||||
replyTo: json['reply_to'] != null ? Message.fromJson(json['reply_to']) : null,
|
||||
replyTo: json['reply_to'] != null
|
||||
? Message.fromJson(json['reply_to'])
|
||||
: null,
|
||||
channelId: json['channel_id'],
|
||||
senderId: json['sender_id'],
|
||||
);
|
||||
@ -64,7 +62,7 @@ class Message {
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt,
|
||||
'content': rawContent,
|
||||
'content': content,
|
||||
'metadata': metadata,
|
||||
'type': type,
|
||||
'attachments': attachments,
|
||||
|
@ -10,7 +10,7 @@ class Realm {
|
||||
String description;
|
||||
bool isPublic;
|
||||
bool isCommunity;
|
||||
int accountId;
|
||||
int? accountId;
|
||||
|
||||
Realm({
|
||||
required this.id,
|
||||
@ -22,14 +22,16 @@ class Realm {
|
||||
required this.description,
|
||||
required this.isPublic,
|
||||
required this.isCommunity,
|
||||
required this.accountId,
|
||||
this.accountId,
|
||||
});
|
||||
|
||||
factory Realm.fromJson(Map<String, dynamic> json) => Realm(
|
||||
id: json['id'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
|
||||
deletedAt: json['deleted_at'] != null
|
||||
? DateTime.parse(json['deleted_at'])
|
||||
: null,
|
||||
alias: json['alias'],
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
@ -77,7 +79,9 @@ class RealmMember {
|
||||
id: json['id'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
|
||||
deletedAt: json['deleted_at'] != null
|
||||
? DateTime.parse(json['deleted_at'])
|
||||
: null,
|
||||
realmId: json['realm_id'],
|
||||
accountId: json['account_id'],
|
||||
account: Account.fromJson(json['account']),
|
||||
|
@ -3,6 +3,21 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class ChannelProvider extends GetxController {
|
||||
Future<Response> getChannel(String alias, {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'];
|
||||
|
||||
final resp = await client.get('/api/channels/$realm/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> listAvailableChannel({String realm = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||
|
@ -2,6 +2,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/account/friend.dart';
|
||||
import 'package:solian/screens/account/personalize.dart';
|
||||
import 'package:solian/screens/channel/channel_chat.dart';
|
||||
import 'package:solian/screens/channel/channel_organize.dart';
|
||||
import 'package:solian/screens/contact.dart';
|
||||
import 'package:solian/screens/posts/post_detail.dart';
|
||||
@ -87,6 +88,16 @@ abstract class AppRouter {
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat/:alias',
|
||||
name: 'channelChat',
|
||||
builder: (context, state) {
|
||||
return ChannelChatScreen(
|
||||
alias: state.pathParameters['alias']!,
|
||||
realm: state.uri.queryParameters['realm'] ?? 'global',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
171
lib/screens/channel/channel_chat.dart
Normal file
171
lib/screens/channel/channel_chat.dart
Normal file
@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/chat/chat_message.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
final String realm;
|
||||
|
||||
const ChannelChatScreen({
|
||||
super.key,
|
||||
required this.alias,
|
||||
this.realm = 'global',
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||
}
|
||||
|
||||
class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Channel? _channel;
|
||||
|
||||
final PagingController<int, Message> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
getChannel() async {
|
||||
final ChannelProvider provider = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final resp = await provider.getChannel(widget.alias, realm: widget.realm);
|
||||
setState(() => _channel = Channel.fromJson(resp.body));
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> getMessages(int pageKey) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) return;
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
final resp = await client.get(
|
||||
'/api/channels/${widget.realm}/${widget.alias}/messages?take=10&offset=$pageKey');
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
final parsed = result.data?.map((e) => Message.fromJson(e)).toList();
|
||||
|
||||
if (parsed != null && parsed.length >= 10) {
|
||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||
} else if (parsed != null) {
|
||||
_pagingController.appendLastPage(parsed);
|
||||
}
|
||||
} else if (resp.statusCode == 403) {
|
||||
_pagingController.appendLastPage([]);
|
||||
} else {
|
||||
_pagingController.error = resp.bodyString;
|
||||
}
|
||||
}
|
||||
|
||||
bool checkMessageMergeable(Message? a, Message? b) {
|
||||
if (a?.replyTo != null) return false;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.senderId != b.senderId) return false;
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||
}
|
||||
|
||||
Widget chatHistoryBuilder(context, item, index) {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
hasMerged = checkMessageMergeable(
|
||||
_pagingController.itemList?[index - 1],
|
||||
item,
|
||||
);
|
||||
}
|
||||
if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
|
||||
isMerged = checkMessageMergeable(
|
||||
item,
|
||||
_pagingController.itemList?[index + 1],
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: !isMerged ? 8 : 0,
|
||||
bottom: !hasMerged ? 8 : 0,
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
child: ChatMessage(
|
||||
item: item,
|
||||
isCompact: isMerged,
|
||||
),
|
||||
),
|
||||
onLongPress: () {},
|
||||
).animate(key: Key('m${item.id}'), autoPlay: true).slideY(
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
duration: 350.ms,
|
||||
begin: 0.25,
|
||||
end: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
getChannel().then((_) {
|
||||
_pagingController.addPageRequestListener(getMessages);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isBusy) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_channel?.name ?? 'loading'.tr),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {},
|
||||
),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PagedListView<int, Message>(
|
||||
reverse: true,
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Message>(
|
||||
animateTransitions: true,
|
||||
transitionDuration: 350.ms,
|
||||
itemBuilder: chatHistoryBuilder,
|
||||
noItemsFoundIndicatorBuilder: (_) => Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -78,8 +78,9 @@ class _ContactScreenState extends State<ContactScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!SolianTheme.isLargeScreen(context))
|
||||
const SizedBox(width: 16),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -111,7 +112,16 @@ class _ContactScreenState extends State<ContactScreen> {
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(element.name),
|
||||
subtitle: Text(element.description),
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed(
|
||||
'channelChat',
|
||||
pathParameters: {'alias': element.alias},
|
||||
queryParameters: {
|
||||
if (element.realmId != null)
|
||||
'realm': element.realm!.alias,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -87,8 +87,9 @@ class _SocialScreenState extends State<SocialScreen> {
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
actions: [
|
||||
const NotificationButton(),
|
||||
if (!SolianTheme.isLargeScreen(context))
|
||||
const SizedBox(width: 16),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -14,6 +14,7 @@ class SolianMessages extends Translations {
|
||||
'apply': 'Apply',
|
||||
'cancel': 'Cancel',
|
||||
'confirm': 'Confirm',
|
||||
'loading': 'Loading...',
|
||||
'edit': 'Edit',
|
||||
'delete': 'Delete',
|
||||
'search': 'Search',
|
||||
@ -100,6 +101,8 @@ class SolianMessages extends Translations {
|
||||
'channelType': 'Channel type',
|
||||
'channelTypeCommon': 'Regular',
|
||||
'channelTypeDirect': 'DM',
|
||||
'messageDecoding': 'Decoding...',
|
||||
'messageDecodeFailed': 'Unable to decode: @message',
|
||||
},
|
||||
'zh_CN': {
|
||||
'hide': '隐藏',
|
||||
@ -108,6 +111,7 @@ class SolianMessages extends Translations {
|
||||
'reset': '重置',
|
||||
'cancel': '取消',
|
||||
'confirm': '确认',
|
||||
'loading': '载入中…',
|
||||
'edit': '编辑',
|
||||
'delete': '删除',
|
||||
'page': '页面',
|
||||
@ -191,6 +195,8 @@ class SolianMessages extends Translations {
|
||||
'channelType': '频道类型',
|
||||
'channelTypeCommon': '普通频道',
|
||||
'channelTypeDirect': '私信聊天',
|
||||
'messageDecoding': '解码信息中…',
|
||||
'messageDecodeFailed': '解码信息失败:@message',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
104
lib/widgets/chat/chat_message.dart
Normal file
104
lib/widgets/chat/chat_message.dart
Normal file
@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:timeago/timeago.dart' show format;
|
||||
|
||||
class ChatMessage extends StatelessWidget {
|
||||
final Message item;
|
||||
final bool isCompact;
|
||||
|
||||
const ChatMessage({super.key, required this.item, required this.isCompact});
|
||||
|
||||
Future<String?> decodeContent(Map<String, dynamic> content) async {
|
||||
String? text;
|
||||
if (item.type == 'm.text') {
|
||||
switch (content['algorithm']) {
|
||||
case 'plain':
|
||||
text = content['value'];
|
||||
default:
|
||||
throw Exception('Unsupported algorithm');
|
||||
// TODO Impl AES algorithm
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasAttachment = item.attachments?.isNotEmpty ?? false;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountAvatar(content: item.sender.account.avatar),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
item.sender.account.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(format(item.createdAt, locale: 'en_short'))
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
FutureBuilder(
|
||||
future: decodeContent(item.content),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Opacity(
|
||||
opacity: 0.8,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.more_horiz),
|
||||
Text('messageDecoding'.tr)
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate(onPlay: (c) => c.repeat())
|
||||
.fade(begin: 0, end: 1);
|
||||
} else if (snapshot.hasError) {
|
||||
return Opacity(
|
||||
opacity: 0.9,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.close),
|
||||
Text(
|
||||
'messageDecodeFailed'.trParams(
|
||||
{'message': snapshot.error.toString()}),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Markdown(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
data: snapshot.data ?? '',
|
||||
padding: const EdgeInsets.all(0),
|
||||
).paddingOnly(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 2,
|
||||
bottom: hasAttachment ? 4 : 0,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -15,18 +15,21 @@ class _AppNavigationBottomBarState extends State<AppNavigationBottomBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomNavigationBar(
|
||||
items: AppNavigation.destinations.map(
|
||||
(e) => BottomNavigationBarItem(
|
||||
icon: e.icon,
|
||||
label: e.label,
|
||||
),
|
||||
).toList(),
|
||||
items: AppNavigation.destinations
|
||||
.map(
|
||||
(e) => BottomNavigationBarItem(
|
||||
icon: e.icon,
|
||||
label: e.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
|
||||
currentIndex: _selectedIndex,
|
||||
showUnselectedLabels: false,
|
||||
onTap: (idx) {
|
||||
setState(() => _selectedIndex = idx);
|
||||
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||
AppRouter.instance
|
||||
.pushReplacementNamed(AppNavigation.destinations[idx].page);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -27,7 +27,8 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (idx) {
|
||||
setState(() => _selectedIndex = idx);
|
||||
AppRouter.instance.pushNamed(AppNavigation.destinations[idx].page);
|
||||
AppRouter.instance
|
||||
.pushReplacementNamed(AppNavigation.destinations[idx].page);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
PODS:
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (6.1.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
@ -12,6 +14,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/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`)
|
||||
@ -20,6 +23,8 @@ DEPENDENCIES:
|
||||
EXTERNAL SOURCES:
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
flutter_local_notifications:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||
flutter_secure_storage_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
|
||||
FlutterMacOS:
|
||||
@ -31,6 +36,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
|
@ -120,7 +120,6 @@
|
||||
1B3B8DF79807852D828EBE0C /* Pods-RunnerTests.release.xcconfig */,
|
||||
BA5247A2B03173FDFDFCFF93 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -724,13 +723,17 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_HARDENED_RUNTIME = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
@ -22,6 +22,11 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
|
@ -4,5 +4,13 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Loading…
Reference in New Issue
Block a user