Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
c5258cb9ca | |||
47c535910d | |||
66f2f33394 | |||
f5fbe1f483 | |||
fcf4dc7a2d | |||
43b7059957 | |||
11c913af60 | |||
db8f0d63e1 | |||
4036a79995 | |||
859bbd09e0 | |||
60033fdef3 | |||
9c3d181deb | |||
9e6829bd5a | |||
f50461a7f7 | |||
147879e4d8 | |||
f353c05cb5 | |||
ac60043ca7 | |||
8d79274b0c | |||
ad4e4071fa | |||
c59f77c877 | |||
16047a7d57 | |||
fdc68fc5e1 | |||
bbee825cf4 | |||
2673c11046 |
@ -4,3 +4,4 @@ android.enableJetifier=true
|
|||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
|
kotlin.jvm.target.validation.mode = IGNORE
|
||||||
|
@ -98,6 +98,8 @@
|
|||||||
"accountFriendBlocked": "Friend blocklist",
|
"accountFriendBlocked": "Friend blocklist",
|
||||||
"accountFriendListHint": "Swipe left to decline, right to approve",
|
"accountFriendListHint": "Swipe left to decline, right to approve",
|
||||||
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
|
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
|
||||||
|
"accountBlocked": "Account has been blocked",
|
||||||
|
"accountUnblocked": "Account has been unblocked",
|
||||||
"accountSuspended": "Account was suspended",
|
"accountSuspended": "Account was suspended",
|
||||||
"accountSuspendedAt": "Account was suspended since @date",
|
"accountSuspendedAt": "Account was suspended since @date",
|
||||||
"aspectRatio": "Aspect Ratio",
|
"aspectRatio": "Aspect Ratio",
|
||||||
@ -457,5 +459,23 @@
|
|||||||
"serviceStatus": "Status of Service",
|
"serviceStatus": "Status of Service",
|
||||||
"firstBootTime": "First boot at @time",
|
"firstBootTime": "First boot at @time",
|
||||||
"rateTheApp": "Rate the app",
|
"rateTheApp": "Rate the app",
|
||||||
"rateTheAppDesc": "Rate Solar Network on the App Store to let us serve you better!"
|
"rateTheAppDesc": "Rate Solar Network on the App Store to let us serve you better!",
|
||||||
|
"friendAdd": "Add as friend",
|
||||||
|
"blockUser": "Block user",
|
||||||
|
"unblockUser": "Unblock user",
|
||||||
|
"learnMoreAboutPerson": "Learn more about that person",
|
||||||
|
"global": "Global",
|
||||||
|
"all": "All",
|
||||||
|
"unablePreview": "Unable to preview",
|
||||||
|
"dashboardNav": "Dash",
|
||||||
|
"accountNav": "You",
|
||||||
|
"performance": "Performance",
|
||||||
|
"animatedMessageList": "Non-animated message list",
|
||||||
|
"animatedMessageListDesc": "Remove animation effects in message list, to reduce cause lag",
|
||||||
|
"theme": "Theme",
|
||||||
|
"globalTheme": "Global theme",
|
||||||
|
"agedTheme": "Old school style theme",
|
||||||
|
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
|
||||||
|
"appBackgroundImage": "Global background image",
|
||||||
|
"appBackgroundImageDesc": "The global background image will be displayed on all pages"
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,8 @@
|
|||||||
"accountFriendBlocked": "好友黑名单",
|
"accountFriendBlocked": "好友黑名单",
|
||||||
"accountFriendListHint": "左滑来拒绝,右滑来接受",
|
"accountFriendListHint": "左滑来拒绝,右滑来接受",
|
||||||
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
|
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
|
||||||
|
"accountBlocked": "已屏蔽账号",
|
||||||
|
"accountUnblocked": "已解除屏蔽账号",
|
||||||
"accountSuspended": "帐号被停用",
|
"accountSuspended": "帐号被停用",
|
||||||
"accountSuspendedAt": "该帐号自 @date 起被停用",
|
"accountSuspendedAt": "该帐号自 @date 起被停用",
|
||||||
"aspectRatio": "纵横比",
|
"aspectRatio": "纵横比",
|
||||||
@ -264,7 +266,7 @@
|
|||||||
"channelMembersAddHint": "到 @channel",
|
"channelMembersAddHint": "到 @channel",
|
||||||
"channelType": "频道类型",
|
"channelType": "频道类型",
|
||||||
"channelTypeCommon": "普通频道",
|
"channelTypeCommon": "普通频道",
|
||||||
"channelTypeDirect": "私信聊天",
|
"channelTypeDirect": "私信",
|
||||||
"channelAdjust": "调整频道",
|
"channelAdjust": "调整频道",
|
||||||
"channelDetail": "频道详情",
|
"channelDetail": "频道详情",
|
||||||
"channelSettings": "频道设置",
|
"channelSettings": "频道设置",
|
||||||
@ -453,5 +455,23 @@
|
|||||||
"serviceStatus": "服务状态",
|
"serviceStatus": "服务状态",
|
||||||
"firstBootTime": "首次启动于 @time",
|
"firstBootTime": "首次启动于 @time",
|
||||||
"rateTheApp": "给应用评分",
|
"rateTheApp": "给应用评分",
|
||||||
"rateTheAppDesc": "在 App Store 上给 Solar Network 评分,让我们更好地为您服务吧!"
|
"rateTheAppDesc": "在 App Store 上给 Solar Network 评分,让我们更好地为您服务吧!",
|
||||||
|
"friendAdd": "添加好友",
|
||||||
|
"blockUser": "屏蔽用户",
|
||||||
|
"unblockUser": "解除屏蔽用户",
|
||||||
|
"learnMoreAboutPerson": "了解关于 TA 的更多",
|
||||||
|
"global": "全局",
|
||||||
|
"all": "全部",
|
||||||
|
"unablePreview": "无法预览",
|
||||||
|
"dashboardNav": "仪表盘",
|
||||||
|
"accountNav": "您",
|
||||||
|
"performance": "性能",
|
||||||
|
"animatedMessageList": "无动画消息列表",
|
||||||
|
"animatedMessageListDesc": "在消息列表中禁用动画效果",
|
||||||
|
"theme": "主题",
|
||||||
|
"globalTheme": "全局应用主题",
|
||||||
|
"agedTheme": "过时主题",
|
||||||
|
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
||||||
|
"appBackgroundImage": "全局背景图片",
|
||||||
|
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
@ -198,8 +198,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
try {
|
try {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
if (auth.isAuthorized.isTrue)
|
|
||||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
@ -258,8 +256,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _isErrored) {
|
if (_isBusy || _isErrored) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Material(
|
child: RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
@ -57,6 +57,8 @@ void main() async {
|
|||||||
|
|
||||||
Future<void> _initializeFirebase() async {
|
Future<void> _initializeFirebase() async {
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
if (PlatformInfo.isIOS || PlatformInfo.isAndroid || PlatformInfo.isMacOS) {
|
||||||
|
// Initialize firebase crashlytics for the platform that supported
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||||
};
|
};
|
||||||
@ -64,6 +66,7 @@ Future<void> _initializeFirebase() async {
|
|||||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeBackgroundNotificationService() async {
|
Future<void> _initializeBackgroundNotificationService() async {
|
||||||
|
50
lib/models/theme.dart
Normal file
50
lib/models/theme.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'theme.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(converters: [ColorConverter()])
|
||||||
|
class SolianThemeData {
|
||||||
|
String id;
|
||||||
|
Color seedColor;
|
||||||
|
String? fontFamily;
|
||||||
|
List<String>? fontFamilyFallback;
|
||||||
|
|
||||||
|
SolianThemeData({
|
||||||
|
required this.id,
|
||||||
|
required this.seedColor,
|
||||||
|
this.fontFamily,
|
||||||
|
this.fontFamilyFallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SolianThemeData.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SolianThemeDataFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$SolianThemeDataToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is SolianThemeData) {
|
||||||
|
return id == other.id;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ColorConverter extends JsonConverter<Color, int> {
|
||||||
|
const ColorConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color fromJson(int json) {
|
||||||
|
return Color(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int toJson(Color object) {
|
||||||
|
return object.value;
|
||||||
|
}
|
||||||
|
}
|
26
lib/models/theme.g.dart
Normal file
26
lib/models/theme.g.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'theme.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
SolianThemeData _$SolianThemeDataFromJson(Map<String, dynamic> json) =>
|
||||||
|
SolianThemeData(
|
||||||
|
id: json['id'] as String,
|
||||||
|
seedColor:
|
||||||
|
const ColorConverter().fromJson((json['seed_color'] as num).toInt()),
|
||||||
|
fontFamily: json['font_family'] as String?,
|
||||||
|
fontFamilyFallback: (json['font_family_fallback'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SolianThemeDataToJson(SolianThemeData instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'seed_color': const ColorConverter().toJson(instance.seedColor),
|
||||||
|
'font_family': instance.fontFamily,
|
||||||
|
'font_family_fallback': instance.fontFamilyFallback,
|
||||||
|
};
|
@ -29,6 +29,8 @@ abstract class PlatformInfo {
|
|||||||
|
|
||||||
static bool get canRateTheApp => isIOS || isMacOS;
|
static bool get canRateTheApp => isIOS || isMacOS;
|
||||||
|
|
||||||
|
static bool get canCropImage => isIOS || isAndroid || isWeb;
|
||||||
|
|
||||||
static bool get canRecord => (isMobile || isMacOS);
|
static bool get canRecord => (isMobile || isMacOS);
|
||||||
|
|
||||||
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
||||||
|
@ -392,7 +392,7 @@ class ChatCallProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future gotoScreen(BuildContext context) {
|
Future gotoScreen(BuildContext context) {
|
||||||
return Navigator.of(context, rootNavigator: true).push(
|
return Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (context) => const CallScreen()),
|
MaterialPageRoute(builder: (context) => const CallScreen()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,25 +9,6 @@ import 'package:uuid/uuid.dart';
|
|||||||
|
|
||||||
class ChannelProvider extends GetxController {
|
class ChannelProvider extends GetxController {
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
RxList<Channel> availableChannels = RxList.empty(growable: true);
|
|
||||||
|
|
||||||
List<Channel> get groupChannels =>
|
|
||||||
availableChannels.where((x) => x.type == 0).toList();
|
|
||||||
List<Channel> get directChannels =>
|
|
||||||
availableChannels.where((x) => x.type == 1).toList();
|
|
||||||
|
|
||||||
Future<void> refreshAvailableChannel() async {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
final resp = await listAvailableChannel();
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
availableChannels.value =
|
|
||||||
resp.body.map((x) => Channel.fromJson(x)).toList().cast<Channel>();
|
|
||||||
availableChannels.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
@ -89,18 +70,22 @@ class ChannelProvider extends GetxController {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listAvailableChannel({String scope = 'global'}) async {
|
Future<List<Channel>> listAvailableChannel({
|
||||||
|
String scope = 'global',
|
||||||
|
bool isDirect = false,
|
||||||
|
}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||||
|
|
||||||
final client = await auth.configureClient('messaging');
|
final client = await auth.configureClient('messaging');
|
||||||
|
|
||||||
final resp = await client.get('/channels/$scope/me/available');
|
final resp =
|
||||||
|
await client.get('/channels/$scope/me/available?direct=$isDirect');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return List.from(resp.body.map((x) => Channel.fromJson(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> createChannel(String scope, dynamic payload) async {
|
Future<Response> createChannel(String scope, dynamic payload) async {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:get/get.dart' hide Value;
|
import 'package:get/get.dart' hide Value;
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
@ -182,4 +185,26 @@ class MessagesFetchingProvider extends GetxController {
|
|||||||
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<int, List<LocalMessageEventTableData>>>
|
||||||
|
getLastInAllChannels() async {
|
||||||
|
final database = Get.find<DatabaseProvider>().database;
|
||||||
|
final rows = await database.customSelect('''
|
||||||
|
SELECT id, channel_id, data, created_at
|
||||||
|
FROM ${database.localMessageEventTable.actualTableName}
|
||||||
|
WHERE (channel_id, created_at) IN (
|
||||||
|
SELECT channel_id, MAX(created_at)
|
||||||
|
FROM ${database.localMessageEventTable.actualTableName}
|
||||||
|
GROUP BY channel_id
|
||||||
|
)
|
||||||
|
''', readsFrom: {database.localMessageEventTable}).get();
|
||||||
|
return rows.map((row) {
|
||||||
|
return LocalMessageEventTableData(
|
||||||
|
id: row.read<int>('id'),
|
||||||
|
channelId: row.read<int>('channel_id'),
|
||||||
|
data: Event.fromJson(jsonDecode(row.read<String>('data'))),
|
||||||
|
createdAt: row.read<DateTime>('created_at'),
|
||||||
|
);
|
||||||
|
}).groupListsBy((x) => x.channelId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,19 @@ class RelationshipProvider extends GetxController {
|
|||||||
return _friends.any((x) => x.relatedId == account.id);
|
return _friends.any((x) => x.relatedId == account.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Relationship?> getRelationship(int relatedId) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
final resp = await client.get('/users/me/relations/$relatedId');
|
||||||
|
if (resp.statusCode == 404) {
|
||||||
|
return null;
|
||||||
|
} else if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Relationship.fromJson(resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> listRelation() async {
|
Future<Response> listRelation() async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final client = await auth.configureClient('auth');
|
final client = await auth.configureClient('auth');
|
||||||
@ -38,7 +51,19 @@ class RelationshipProvider extends GetxController {
|
|||||||
return client.get('/users/me/relations?status=$status');
|
return client.get('/users/me/relations?status=$status');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> makeFriend(String username) async {
|
Future<Relationship?> blockUser(String username) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final client = await auth.configureClient('auth');
|
||||||
|
final resp =
|
||||||
|
await client.post('/users/me/relations/block?related=$username', {});
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Relationship.fromJson(resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Relationship?> makeFriend(String username) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final client = await auth.configureClient('auth');
|
final client = await auth.configureClient('auth');
|
||||||
final resp = await client.post('/users/me/relations?related=$username', {});
|
final resp = await client.post('/users/me/relations?related=$username', {});
|
||||||
@ -46,7 +71,7 @@ class RelationshipProvider extends GetxController {
|
|||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return Relationship.fromJson(resp.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> handleRelation(
|
Future<Response> handleRelation(
|
||||||
@ -64,17 +89,17 @@ class RelationshipProvider extends GetxController {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> editRelation(Relationship relationship, int status) async {
|
Future<Relationship?> editRelation(int relatedId, int status) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final client = await auth.configureClient('auth');
|
final client = await auth.configureClient('auth');
|
||||||
final resp = await client.patch(
|
final resp = await client.put(
|
||||||
'/users/me/relations/${relationship.relatedId}',
|
'/users/me/relations/$relatedId',
|
||||||
{'status': status},
|
{'status': status},
|
||||||
);
|
);
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return Relationship.fromJson(resp.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/models/theme.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
|
|
||||||
class ThemeSwitcher extends ChangeNotifier {
|
class ThemeSwitcher extends ChangeNotifier {
|
||||||
@ -13,11 +16,21 @@ class ThemeSwitcher extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> restoreTheme() async {
|
Future<void> restoreTheme() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('global_theme_color')) {
|
if (prefs.containsKey('global_theme')) {
|
||||||
final value = prefs.getInt('global_theme_color')!;
|
final value = SolianThemeData.fromJson(
|
||||||
final color = Color(value);
|
jsonDecode(prefs.getString('global_theme')!),
|
||||||
lightThemeData = AppTheme.build(Brightness.light, seedColor: color);
|
);
|
||||||
darkThemeData = AppTheme.build(Brightness.dark, seedColor: color);
|
final agedTheme = prefs.getBool('aged_theme');
|
||||||
|
lightThemeData = AppTheme.buildFromData(
|
||||||
|
Brightness.light,
|
||||||
|
value,
|
||||||
|
useMaterial3: agedTheme == null ? true : !agedTheme,
|
||||||
|
);
|
||||||
|
darkThemeData = AppTheme.buildFromData(
|
||||||
|
Brightness.dark,
|
||||||
|
value,
|
||||||
|
useMaterial3: agedTheme == null ? true : !agedTheme,
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,4 +40,25 @@ class ThemeSwitcher extends ChangeNotifier {
|
|||||||
darkThemeData = dark;
|
darkThemeData = dark;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setThemeData(SolianThemeData? data) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (data == null) {
|
||||||
|
prefs.remove('global_theme');
|
||||||
|
} else {
|
||||||
|
prefs.setString(
|
||||||
|
'global_theme',
|
||||||
|
jsonEncode(data.toJson()),
|
||||||
|
);
|
||||||
|
lightThemeData = AppTheme.buildFromData(Brightness.light, data);
|
||||||
|
darkThemeData = AppTheme.buildFromData(Brightness.dark, data);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setAgedTheme(bool enabled) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setBool('aged_theme', enabled);
|
||||||
|
await restoreTheme();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ import 'package:solian/screens/posts/post_editor.dart';
|
|||||||
import 'package:solian/screens/settings.dart';
|
import 'package:solian/screens/settings.dart';
|
||||||
import 'package:solian/shells/root_shell.dart';
|
import 'package:solian/shells/root_shell.dart';
|
||||||
import 'package:solian/shells/title_shell.dart';
|
import 'package:solian/shells/title_shell.dart';
|
||||||
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||||
|
|
||||||
abstract class AppRouter {
|
abstract class AppRouter {
|
||||||
static GoRouter instance = GoRouter(
|
static GoRouter instance = GoRouter(
|
||||||
@ -137,12 +139,15 @@ abstract class AppRouter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static final ShellRoute _chatRoute = ShellRoute(
|
static final ShellRoute _chatRoute = ShellRoute(
|
||||||
builder: (context, state, child) => child,
|
builder: (context, state, child) =>
|
||||||
|
AppTheme.isLargeScreen(context) ? ChatListShell(child: child) : child,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
builder: (context, state) => const ChatScreen(),
|
builder: (context, state) => AppTheme.isLargeScreen(context)
|
||||||
|
? const EmptyPagePlaceholder()
|
||||||
|
: const ChatScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat/organize',
|
path: '/chat/organize',
|
||||||
|
@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -15,8 +16,7 @@ class AboutScreen extends StatelessWidget {
|
|||||||
const denseButtonStyle =
|
const denseButtonStyle =
|
||||||
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -7,6 +7,7 @@ import 'package:solian/providers/account_status.dart';
|
|||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/account/account_heading.dart';
|
import 'package:solian/widgets/account/account_heading.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
|
||||||
@ -49,8 +50,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
|
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
if (auth.isAuthorized.isFalse) {
|
if (auth.isAuthorized.isFalse) {
|
||||||
|
@ -6,6 +6,7 @@ import 'package:solian/models/relations.dart';
|
|||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/relative_list.dart';
|
import 'package:solian/widgets/account/relative_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class FriendScreen extends StatefulWidget {
|
class FriendScreen extends StatefulWidget {
|
||||||
const FriendScreen({super.key});
|
const FriendScreen({super.key});
|
||||||
@ -117,8 +118,7 @@ class _FriendScreenState extends State<FriendScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
|
@ -6,6 +6,7 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class NotificationPreferencesScreen extends StatefulWidget {
|
class NotificationPreferencesScreen extends StatefulWidget {
|
||||||
const NotificationPreferencesScreen({super.key});
|
const NotificationPreferencesScreen({super.key});
|
||||||
@ -74,8 +75,7 @@ class _NotificationPreferencesScreenState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -9,10 +7,12 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class PersonalizeScreen extends StatefulWidget {
|
class PersonalizeScreen extends StatefulWidget {
|
||||||
const PersonalizeScreen({super.key});
|
const PersonalizeScreen({super.key});
|
||||||
@ -77,9 +77,12 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
XFile file;
|
||||||
|
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
|
if (PlatformInfo.canCropImage) {
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
sourcePath: image.path,
|
sourcePath: image.path,
|
||||||
uiSettings: [
|
uiSettings: [
|
||||||
@ -106,7 +109,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) return;
|
||||||
final file = File(croppedFile.path);
|
file = XFile(croppedFile.path);
|
||||||
|
} else {
|
||||||
|
file = XFile(image.path);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@ -181,8 +187,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const double padding = 32;
|
const double padding = 32;
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
|
@ -13,6 +13,7 @@ import 'package:solian/models/attachment.dart';
|
|||||||
import 'package:solian/models/daily_sign.dart';
|
import 'package:solian/models/daily_sign.dart';
|
||||||
import 'package:solian/models/pagination.dart';
|
import 'package:solian/models/pagination.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/models/relations.dart';
|
||||||
import 'package:solian/models/subscription.dart';
|
import 'package:solian/models/subscription.dart';
|
||||||
import 'package:solian/providers/account_status.dart';
|
import 'package:solian/providers/account_status.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
@ -26,6 +27,8 @@ import 'package:solian/widgets/attachments/attachment_list.dart';
|
|||||||
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
||||||
import 'package:solian/widgets/posts/post_list.dart';
|
import 'package:solian/widgets/posts/post_list.dart';
|
||||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||||
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
class AccountProfilePage extends StatefulWidget {
|
class AccountProfilePage extends StatefulWidget {
|
||||||
@ -50,6 +53,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
|
|
||||||
Account? _userinfo;
|
Account? _userinfo;
|
||||||
Subscription? _subscription;
|
Subscription? _subscription;
|
||||||
|
Relationship? _relationship;
|
||||||
List<Post> _pinnedPosts = List.empty();
|
List<Post> _pinnedPosts = List.empty();
|
||||||
List<DailySignRecord> _dailySignRecords = List.empty();
|
List<DailySignRecord> _dailySignRecords = List.empty();
|
||||||
int _totalUpvote = 0, _totalDownvote = 0;
|
int _totalUpvote = 0, _totalDownvote = 0;
|
||||||
@ -61,6 +65,15 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
setState(() => _isSubscribing = false);
|
setState(() => _isSubscribing = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _getRelationship() async {
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final relations = Get.find<RelationshipProvider>();
|
||||||
|
_relationship = await relations.getRelationship(_userinfo!.id);
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _getUserinfo() async {
|
Future<void> _getUserinfo() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@ -120,6 +133,63 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _subscribeToUser() async {
|
||||||
|
setState(() => _isSubscribing = true);
|
||||||
|
_subscription =
|
||||||
|
await Get.find<SubscriptionProvider>().subscribeToUser(_userinfo!.id);
|
||||||
|
setState(() => _isSubscribing = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unsubscribeFromUser() async {
|
||||||
|
setState(() => _isSubscribing = true);
|
||||||
|
await Get.find<SubscriptionProvider>().unsubscribeFromUser(_userinfo!.id);
|
||||||
|
_subscription = null;
|
||||||
|
setState(() => _isSubscribing = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _makeFriend() async {
|
||||||
|
setState(() => _isMakingFriend = true);
|
||||||
|
try {
|
||||||
|
_relationship = await _relationshipProvider.makeFriend(widget.name);
|
||||||
|
context.showSnackbar(
|
||||||
|
'accountFriendRequestSent'.tr,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isMakingFriend = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _blockUser() async {
|
||||||
|
setState(() => _isMakingFriend = true);
|
||||||
|
try {
|
||||||
|
_relationship = await _relationshipProvider.blockUser(widget.name);
|
||||||
|
context.showSnackbar(
|
||||||
|
'accountBlocked'.tr,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isMakingFriend = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unblockUser() async {
|
||||||
|
setState(() => _isMakingFriend = true);
|
||||||
|
try {
|
||||||
|
_relationship =
|
||||||
|
await _relationshipProvider.editRelation(_userinfo!.id, 1);
|
||||||
|
context.showSnackbar(
|
||||||
|
'accountUnblocked'.tr,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
context.showErrorDialog(e);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isMakingFriend = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int get _userSocialCreditPoints {
|
int get _userSocialCreditPoints {
|
||||||
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
|
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
|
||||||
}
|
}
|
||||||
@ -151,37 +221,20 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_getUserinfo().then((_) {
|
_getUserinfo().then((_) {
|
||||||
|
_getRelationship();
|
||||||
_getSubscription();
|
_getSubscription();
|
||||||
_getPinnedPosts();
|
_getPinnedPosts();
|
||||||
_getDailySignRecords();
|
_getDailySignRecords();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatisticsEntry(String label, String content) {
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
content,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _userinfo == null) {
|
if (_isBusy || _userinfo == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 3,
|
length: 3,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
@ -221,59 +274,31 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_userinfo != null && _subscription == null)
|
if (_userinfo != null && _subscription == null)
|
||||||
OutlinedButton(
|
IconButton(
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
visualDensity:
|
visualDensity:
|
||||||
VisualDensity(horizontal: -4, vertical: -2),
|
VisualDensity(horizontal: -4, vertical: -2),
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing
|
onPressed: _isSubscribing ? null : _subscribeToUser,
|
||||||
? null
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
: () async {
|
tooltip: 'subscribe'.tr,
|
||||||
setState(() => _isSubscribing = true);
|
|
||||||
_subscription =
|
|
||||||
await Get.find<SubscriptionProvider>()
|
|
||||||
.subscribeToUser(_userinfo!.id);
|
|
||||||
setState(() => _isSubscribing = false);
|
|
||||||
},
|
|
||||||
child: Text('subscribe'.tr),
|
|
||||||
)
|
)
|
||||||
else if (_userinfo != null)
|
else if (_userinfo != null)
|
||||||
OutlinedButton(
|
IconButton(
|
||||||
style: const ButtonStyle(
|
style: const ButtonStyle(
|
||||||
visualDensity:
|
visualDensity:
|
||||||
VisualDensity(horizontal: -4, vertical: -2),
|
VisualDensity(horizontal: -4, vertical: -2),
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing
|
onPressed:
|
||||||
? null
|
_isSubscribing ? null : _unsubscribeFromUser,
|
||||||
: () async {
|
icon: const Icon(Icons.remove_circle_outline),
|
||||||
setState(() => _isSubscribing = true);
|
tooltip: 'unsubscribe'.tr,
|
||||||
await Get.find<SubscriptionProvider>()
|
|
||||||
.unsubscribeFromUser(_userinfo!.id);
|
|
||||||
_subscription = null;
|
|
||||||
setState(() => _isSubscribing = false);
|
|
||||||
},
|
|
||||||
child: Text('unsubscribe'.tr),
|
|
||||||
),
|
),
|
||||||
if (_userinfo != null &&
|
if (_userinfo != null && _relationship == null)
|
||||||
!_relationshipProvider.hasFriend(_userinfo!))
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.person_add),
|
icon: const Icon(Icons.person_add),
|
||||||
onPressed: _isMakingFriend
|
onPressed: _isMakingFriend ? null : _makeFriend,
|
||||||
? null
|
tooltip: 'friendAdd'.tr,
|
||||||
: () async {
|
|
||||||
setState(() => _isMakingFriend = true);
|
|
||||||
try {
|
|
||||||
await _relationshipProvider
|
|
||||||
.makeFriend(widget.name);
|
|
||||||
context.showSnackbar(
|
|
||||||
'accountFriendRequestSent'.tr,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
} finally {
|
|
||||||
setState(() => _isMakingFriend = false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const IconButton(
|
const IconButton(
|
||||||
@ -300,8 +325,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
ListView(
|
ListView(
|
||||||
|
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||||
children: [
|
children: [
|
||||||
const Gap(16),
|
|
||||||
CenteredContainer(
|
CenteredContainer(
|
||||||
child: AccountHeadingWidget(
|
child: AccountHeadingWidget(
|
||||||
name: _userinfo!.name,
|
name: _userinfo!.name,
|
||||||
@ -421,9 +446,82 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
).marginOnly(
|
).marginOnly(
|
||||||
right: 24, left: 12, bottom: 8, top: 24),
|
right: 24,
|
||||||
|
left: 12,
|
||||||
|
bottom: 8,
|
||||||
|
top: 24,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
appendWidgets: [
|
||||||
|
Card(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 4,
|
||||||
|
horizontal: 8,
|
||||||
|
),
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AbuseReportDialog(
|
||||||
|
resourceId: 'user:${_userinfo!.id}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.flag,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text('reportAbuse'.tr),
|
||||||
|
),
|
||||||
|
if (_relationship?.status != 2)
|
||||||
|
TextButton.icon(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
_isMakingFriend ? null : _blockUser,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.block,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text('blockUser'.tr),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextButton.icon(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
_isMakingFriend ? null : _unblockUser,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.add_circle_outline,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: Text('unblockUser'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -440,7 +538,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_buildStatisticsEntry(
|
_StatsWidget(
|
||||||
'totalSocialCreditPoints'.tr,
|
'totalSocialCreditPoints'.tr,
|
||||||
_userinfo != null
|
_userinfo != null
|
||||||
? _userSocialCreditPoints.toString()
|
? _userSocialCreditPoints.toString()
|
||||||
@ -453,16 +551,16 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
Obx(
|
||||||
() => _buildStatisticsEntry(
|
() => _StatsWidget(
|
||||||
'totalPostCount'.tr,
|
'totalPostCount'.tr,
|
||||||
_postController.postTotal.value.toString(),
|
_postController.postTotal.value.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildStatisticsEntry(
|
_StatsWidget(
|
||||||
'totalUpvote'.tr,
|
'totalUpvote'.tr,
|
||||||
_totalUpvote.toString(),
|
_totalUpvote.toString(),
|
||||||
),
|
),
|
||||||
_buildStatisticsEntry(
|
_StatsWidget(
|
||||||
'totalDownvote'.tr,
|
'totalDownvote'.tr,
|
||||||
_totalDownvote.toString(),
|
_totalDownvote.toString(),
|
||||||
),
|
),
|
||||||
@ -560,3 +658,28 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StatsWidget extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
const _StatsWidget(this.label, this.content);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,11 +7,11 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/auth.dart';
|
import 'package:solian/models/auth.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
import 'package:solian/providers/content/realm.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -177,7 +177,6 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
await auth.refreshAuthorizeStatus();
|
await auth.refreshAuthorizeStatus();
|
||||||
await auth.refreshUserProfile();
|
await auth.refreshUserProfile();
|
||||||
|
|
||||||
Get.find<ChannelProvider>().refreshAvailableChannel();
|
|
||||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||||
@ -218,8 +217,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: CenteredContainer(
|
child: CenteredContainer(
|
||||||
maxWidth: 360,
|
maxWidth: 360,
|
||||||
child: PageTransitionSwitcher(
|
child: PageTransitionSwitcher(
|
||||||
|
@ -3,6 +3,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
@ -65,8 +66,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: CenteredContainer(
|
child: CenteredContainer(
|
||||||
maxWidth: 360,
|
maxWidth: 360,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
@ -11,6 +11,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/chat/call/call_controls.dart';
|
import 'package:solian/widgets/chat/call/call_controls.dart';
|
||||||
import 'package:solian/widgets/chat/call/call_participant.dart';
|
import 'package:solian/widgets/chat/call/call_participant.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart' as livekit;
|
import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class CallScreen extends StatefulWidget {
|
class CallScreen extends StatefulWidget {
|
||||||
final bool hideAppBar;
|
final bool hideAppBar;
|
||||||
@ -197,8 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ChatCallProvider ctrl = Get.find();
|
final ChatCallProvider ctrl = Get.find();
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: widget.hideAppBar
|
appBar: widget.hideAppBar
|
||||||
? null
|
? null
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/call.dart';
|
import 'package:solian/models/call.dart';
|
||||||
@ -25,6 +26,7 @@ import 'package:solian/widgets/chat/chat_event_list.dart';
|
|||||||
import 'package:solian/widgets/chat/chat_message_input.dart';
|
import 'package:solian/widgets/chat/chat_message_input.dart';
|
||||||
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
|
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class ChannelChatScreen extends StatefulWidget {
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -179,6 +181,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late SharedPreferences _prefs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -189,11 +193,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
_chatController = ChatEventController();
|
_chatController = ChatEventController();
|
||||||
_chatController.initialize();
|
_chatController.initialize();
|
||||||
|
|
||||||
|
SharedPreferences.getInstance().then((inst) {
|
||||||
|
_prefs = inst;
|
||||||
_getOngoingCall();
|
_getOngoingCall();
|
||||||
_getChannel().then((_) {
|
_getChannel().then((_) {
|
||||||
_chatController.getInitialEvents(_channel!, widget.realm);
|
_chatController.getInitialEvents(_channel!, widget.realm);
|
||||||
_listenMessages();
|
_listenMessages();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -201,16 +208,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
String title = _channel?.name ?? 'loading'.tr;
|
String title = _channel?.name ?? 'loading'.tr;
|
||||||
String? placeholder;
|
String? placeholder;
|
||||||
|
|
||||||
if (_channel?.type == 1) {
|
|
||||||
final otherside =
|
final otherside =
|
||||||
_channel!.members!.where((e) => e.account.id != _accountId).first;
|
_channel?.members!.where((e) => e.account.id != _accountId).firstOrNull;
|
||||||
|
|
||||||
|
if (_channel?.type == 1 && otherside != null) {
|
||||||
title = otherside.account.nick;
|
title = otherside.account.nick;
|
||||||
placeholder = 'messageInputPlaceholder'.trParams(
|
placeholder = 'messageInputPlaceholder'.trParams(
|
||||||
{'channel': '@${otherside.account.name}'},
|
{'channel': '@${otherside.account.name}'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return RootContainer(
|
||||||
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
title: AppBarTitle(title),
|
title: AppBarTitle(title),
|
||||||
@ -246,7 +255,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
.then((value) {
|
.then((value) {
|
||||||
if (value == false) AppRouter.instance.pop();
|
if (value == false) AppRouter.instance.pop();
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
final resp = Channel.fromJson(value as Map<String, dynamic>);
|
final resp =
|
||||||
|
Channel.fromJson(value as Map<String, dynamic>);
|
||||||
_getChannel(alias: resp.alias);
|
_getChannel(alias: resp.alias);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -274,7 +284,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
channel: _channel!,
|
channel: _channel!,
|
||||||
ongoingCall: _ongoingCall!,
|
ongoingCall: _ongoingCall!,
|
||||||
onJoin: () {
|
onJoin: () {
|
||||||
if (!AppTheme.isLargeScreen(context)) {
|
if (!AppTheme.isUltraLargeScreen(context)) {
|
||||||
final ChatCallProvider call = Get.find();
|
final ChatCallProvider call = Get.find();
|
||||||
call.gotoScreen(context);
|
call.gotoScreen(context);
|
||||||
}
|
}
|
||||||
@ -282,6 +292,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ChatEventList(
|
child: ChatEventList(
|
||||||
|
noAnimated:
|
||||||
|
_prefs.getBool('non_animated_message_list') ??
|
||||||
|
false,
|
||||||
scope: widget.realm,
|
scope: widget.realm,
|
||||||
channel: _channel!,
|
channel: _channel!,
|
||||||
chatController: _chatController,
|
chatController: _chatController,
|
||||||
@ -328,7 +341,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
),
|
),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final ChatCallProvider call = Get.find();
|
final ChatCallProvider call = Get.find();
|
||||||
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
|
if (call.isMounted.value &&
|
||||||
|
AppTheme.isUltraLargeScreen(context)) {
|
||||||
return const Expanded(
|
return const Expanded(
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||||
@ -346,6 +360,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import 'package:solian/providers/content/channel.dart';
|
|||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChannelOrganizeArguments {
|
class ChannelOrganizeArguments {
|
||||||
@ -114,8 +115,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: AppBarTitle('channelOrganizing'.tr),
|
title: AppBarTitle('channelOrganizing'.tr),
|
||||||
|
@ -1,50 +1,158 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
import 'package:solian/providers/content/channel.dart';
|
||||||
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatelessWidget {
|
||||||
const ChatScreen({super.key});
|
const ChatScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
Widget build(BuildContext context) {
|
||||||
|
return const RootContainer(
|
||||||
|
child: ChatList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class ChatListShell extends StatelessWidget {
|
||||||
late final ChannelProvider _channels;
|
final Widget? child;
|
||||||
|
|
||||||
|
const ChatListShell({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RootContainer(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 360,
|
||||||
|
child: ChatList(),
|
||||||
|
),
|
||||||
|
const VerticalDivider(thickness: 0.3, width: 0.3),
|
||||||
|
Expanded(child: child ?? const EmptyPagePlaceholder()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatList extends StatefulWidget {
|
||||||
|
const ChatList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatList> createState() => _ChatListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatListState extends State<ChatList> {
|
||||||
|
List<Channel> _normalChannels = List.empty();
|
||||||
|
List<Channel> _directChannels = List.empty();
|
||||||
|
final Map<String, List<Channel>> _realmChannels = {};
|
||||||
|
|
||||||
|
late final ChannelProvider _channels = Get.find();
|
||||||
|
|
||||||
|
List<Channel> _sortChannels(List<Channel> channels) {
|
||||||
|
channels.sort(
|
||||||
|
(a, b) =>
|
||||||
|
_lastMessages?[b.id]?.createdAt.compareTo(
|
||||||
|
_lastMessages?[a.id]?.createdAt ??
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
|
) ??
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadNormalChannels() async {
|
||||||
|
final resp = await _channels.listAvailableChannel(isDirect: false);
|
||||||
|
setState(() {
|
||||||
|
_normalChannels = _sortChannels(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadDirectChannels() async {
|
||||||
|
final resp = await _channels.listAvailableChannel(isDirect: true);
|
||||||
|
setState(() {
|
||||||
|
_directChannels = _sortChannels(resp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRealmChannels(String realm) async {
|
||||||
|
final resp = await _channels.listAvailableChannel(scope: realm);
|
||||||
|
setState(() {
|
||||||
|
_realmChannels[realm] = _sortChannels(List.from(resp));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAllChannels() async {
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
Future.wait([
|
||||||
|
_loadNormalChannels(),
|
||||||
|
_loadDirectChannels(),
|
||||||
|
...realms.availableRealms.map((x) => _loadRealmChannels(x.alias)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
|
|
||||||
|
Future<void> _loadLastMessages() async {
|
||||||
|
final ctrl = ChatEventController();
|
||||||
|
await ctrl.initialize();
|
||||||
|
final messages = await ctrl.src.getLastInAllChannels();
|
||||||
|
setState(() {
|
||||||
|
_lastMessages = messages
|
||||||
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
|
.cast<int, LocalMessageEventTableData>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
try {
|
_loadLastMessages().then((_) {
|
||||||
_channels = Get.find();
|
_loadAllChannels();
|
||||||
_channels.refreshAvailableChannel();
|
});
|
||||||
} catch (e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
|
||||||
return Material(
|
return Obx(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
() => DefaultTabController(
|
||||||
|
length: 2 + realms.availableRealms.length,
|
||||||
|
child: RootContainer(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: Obx(() {
|
||||||
|
final adaptive = AppBarLeadingButton.adaptive(context);
|
||||||
|
if (adaptive != null) return adaptive;
|
||||||
|
if (_channels.isLoading.value) {
|
||||||
|
return const CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
).paddingAll(18);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}),
|
||||||
title: AppBarTitle('chat'.tr),
|
title: AppBarTitle('chat'.tr),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
@ -58,13 +166,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text('channelOrganizeCommon'.tr),
|
title: Text('channelOrganizeCommon'.tr),
|
||||||
leading: const Icon(Icons.tag),
|
leading: const Icon(Icons.tag),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||||
(value) {
|
(value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
_channels.refreshAvailableChannel();
|
_loadAllChannels();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -77,7 +186,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
FontAwesomeIcons.userGroup,
|
FontAwesomeIcons.userGroup,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final ChannelProvider channels = Get.find();
|
final ChannelProvider channels = Get.find();
|
||||||
@ -85,7 +195,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
.createDirectChannel(context, 'global')
|
.createDirectChannel(context, 'global')
|
||||||
.then((resp) {
|
.then((resp) {
|
||||||
if (resp != null) {
|
if (resp != null) {
|
||||||
_channels.refreshAvailableChannel();
|
_loadAllChannels();
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
context.showErrorDialog(e);
|
context.showErrorDialog(e);
|
||||||
@ -98,11 +208,70 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
|
dividerHeight: 0.3,
|
||||||
|
tabAlignment: TabAlignment.startOffset,
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.forum,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('all'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
child: Icon(
|
||||||
|
Icons.chat_bubble,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('channelTypeDirect'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...realms.availableRealms.map((x) => Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(
|
||||||
|
content: x.avatar,
|
||||||
|
radius: 14,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(x.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Obx(() {
|
body: Obx(() {
|
||||||
if (auth.isAuthorized.isFalse) {
|
if (auth.isAuthorized.isFalse) {
|
||||||
return SigninRequiredOverlay(
|
return SigninRequiredOverlay(
|
||||||
onDone: () => _channels.refreshAvailableChannel(),
|
onDone: () => _loadAllChannels(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,37 +279,49 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
|
||||||
if (_channels.isLoading.isFalse) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
} else {
|
|
||||||
return const LinearProgressIndicator();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
const ChatCallCurrentIndicator(),
|
const ChatCallCurrentIndicator(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CenteredContainer(
|
child: TabBarView(
|
||||||
child: RefreshIndicator(
|
children: [
|
||||||
onRefresh: _channels.refreshAvailableChannel,
|
RefreshIndicator(
|
||||||
child: Obx(
|
onRefresh: _loadNormalChannels,
|
||||||
() => ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
noCategory: true,
|
channels: _sortChannels([
|
||||||
channels: List.from([
|
..._normalChannels,
|
||||||
..._channels.groupChannels
|
..._directChannels,
|
||||||
.where((x) => x.realmId == null),
|
..._realmChannels.values.expand((x) => x),
|
||||||
..._channels.directChannels
|
|
||||||
]),
|
]),
|
||||||
selfId: selfId,
|
selfId: selfId,
|
||||||
useReplace: true,
|
useReplace: AppTheme.isLargeScreen(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: _loadDirectChannels,
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _directChannels,
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: AppTheme.isLargeScreen(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...realms.availableRealms.map(
|
||||||
|
(x) => RefreshIndicator(
|
||||||
|
onRefresh: () => _loadRealmChannels(x.alias),
|
||||||
|
child: ChannelListWidget(
|
||||||
|
channels: _realmChannels[x.alias] ?? [],
|
||||||
|
selfId: selfId,
|
||||||
|
useReplace: AppTheme.isLargeScreen(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,7 +354,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_forward),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AppRouter.instance.goNamed('feed');
|
AppRouter.instance.goNamed('explore');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -10,11 +10,12 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
|
import 'package:solian/widgets/navigation/realm_switcher.dart';
|
||||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class ExploreScreen extends StatefulWidget {
|
class ExploreScreen extends StatefulWidget {
|
||||||
const ExploreScreen({super.key});
|
const ExploreScreen({super.key});
|
||||||
@ -55,10 +56,8 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
@ -82,8 +81,14 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return [
|
return [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
title: AppBarTitle('explore'.tr),
|
flexibleSpace: SizedBox(
|
||||||
centerTitle: false,
|
height: 48,
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
RealmSwitcher(),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 8),
|
||||||
|
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||||
floating: true,
|
floating: true,
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -96,10 +101,39 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
|
dividerHeight: 0.3,
|
||||||
|
tabAlignment: TabAlignment.fill,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: 'postListNews'.tr),
|
Tab(
|
||||||
Tab(text: 'postListFriends'.tr),
|
child: Row(
|
||||||
Tab(text: 'postListShuffle'.tr),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.feed, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListNews'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.people, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListFriends'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.shuffle_on_outlined, size: 20),
|
||||||
|
const Gap(8),
|
||||||
|
Text('postListShuffle'.tr),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -114,16 +148,6 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (navState.focusedRealm.value != null)
|
|
||||||
MaterialBanner(
|
|
||||||
leading: const Icon(Icons.layers),
|
|
||||||
content: Text(
|
|
||||||
'postBrowsingIn'.trParams({
|
|
||||||
'region': '#${navState.focusedRealm.value!.alias}',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
actions: const [SizedBox.shrink()],
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
@ -9,6 +9,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/posts/post_action.dart';
|
import 'package:solian/widgets/posts/post_action.dart';
|
||||||
import 'package:solian/widgets/posts/post_owned_list.dart';
|
import 'package:solian/widgets/posts/post_owned_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class DraftBoxScreen extends StatefulWidget {
|
class DraftBoxScreen extends StatefulWidget {
|
||||||
const DraftBoxScreen({super.key});
|
const DraftBoxScreen({super.key});
|
||||||
@ -54,8 +55,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
|
@ -6,6 +6,7 @@ import 'package:solian/providers/content/posts.dart';
|
|||||||
import 'package:solian/providers/last_read.dart';
|
import 'package:solian/providers/last_read.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:solian/widgets/posts/post_replies.dart';
|
import 'package:solian/widgets/posts/post_replies.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class PostDetailScreen extends StatefulWidget {
|
class PostDetailScreen extends StatefulWidget {
|
||||||
final String id;
|
final String id;
|
||||||
@ -47,8 +48,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: FutureBuilder(
|
child: FutureBuilder(
|
||||||
future: getDetail(),
|
future: getDetail(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
@ -19,6 +19,7 @@ import 'package:solian/widgets/app_bar_title.dart';
|
|||||||
import 'package:solian/widgets/markdown_text_content.dart';
|
import 'package:solian/widgets/markdown_text_content.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class PostPublishArguments {
|
class PostPublishArguments {
|
||||||
final Post? edit;
|
final Post? edit;
|
||||||
@ -151,8 +152,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -376,6 +376,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
|
isAutoWarp: _editorController.mode.value == 0,
|
||||||
content: _editorController.contentController.text,
|
content: _editorController.contentController.text,
|
||||||
parentId: 'post-editor-preview',
|
parentId: 'post-editor-preview',
|
||||||
).paddingOnly(top: 12, right: 16),
|
).paddingOnly(top: 12, right: 16),
|
||||||
|
@ -15,6 +15,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
class RealmListScreen extends StatefulWidget {
|
class RealmListScreen extends StatefulWidget {
|
||||||
@ -58,8 +59,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
|
@ -7,6 +7,7 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/screens/realms/realm_organize.dart';
|
import 'package:solian/screens/realms/realm_organize.dart';
|
||||||
import 'package:solian/widgets/realms/realm_deletion.dart';
|
import 'package:solian/widgets/realms/realm_deletion.dart';
|
||||||
import 'package:solian/widgets/realms/realm_member.dart';
|
import 'package:solian/widgets/realms/realm_member.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class RealmDetailScreen extends StatefulWidget {
|
class RealmDetailScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -86,7 +87,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(
|
return RootContainer(
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
@ -141,6 +143,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -8,12 +6,14 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class RealmOrganizeArguments {
|
class RealmOrganizeArguments {
|
||||||
@ -84,9 +84,12 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
XFile file;
|
||||||
|
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
|
if (PlatformInfo.canCropImage) {
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
sourcePath: image.path,
|
sourcePath: image.path,
|
||||||
uiSettings: [
|
uiSettings: [
|
||||||
@ -113,7 +116,10 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) return;
|
||||||
final file = File(croppedFile.path);
|
file = XFile(croppedFile.path);
|
||||||
|
} else {
|
||||||
|
file = XFile(image.path);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@ -184,8 +190,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
|
@ -16,6 +16,7 @@ import 'package:solian/theme.dart';
|
|||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
import 'package:solian/widgets/posts/post_list.dart';
|
import 'package:solian/widgets/posts/post_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class RealmViewScreen extends StatefulWidget {
|
class RealmViewScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -68,12 +69,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
|||||||
_channels.addAll(
|
_channels.addAll(
|
||||||
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
||||||
);
|
);
|
||||||
_channels.addAll(
|
_channels.addAll(availableResp);
|
||||||
availableResp.body
|
|
||||||
.map((e) => Channel.fromJson(e))
|
|
||||||
.toList()
|
|
||||||
.cast<Channel>(),
|
|
||||||
);
|
|
||||||
_channels.retainWhere((x) => channelIdx.add(x.id));
|
_channels.retainWhere((x) => channelIdx.add(x.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,8 +87,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 2,
|
length: 2,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
@ -260,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget {
|
|||||||
child: ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
channels: channels,
|
channels: channels,
|
||||||
selfId: auth.userProfile.value!['id'],
|
selfId: auth.userProfile.value!['id'],
|
||||||
noCategory: true,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:in_app_review/in_app_review.dart';
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/theme.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/database/database.dart';
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class SettingScreen extends StatefulWidget {
|
class SettingScreen extends StatefulWidget {
|
||||||
const SettingScreen({super.key});
|
const SettingScreen({super.key});
|
||||||
@ -22,6 +30,7 @@ class SettingScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SettingScreenState extends State<SettingScreen> {
|
class _SettingScreenState extends State<SettingScreen> {
|
||||||
SharedPreferences? _prefs;
|
SharedPreferences? _prefs;
|
||||||
|
String _docBasepath = '/';
|
||||||
|
|
||||||
Widget _buildCaptionHeader(String title) {
|
Widget _buildCaptionHeader(String title) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -32,39 +41,38 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThemeColorButton(String label, Color color) {
|
static final List<SolianThemeData> _presentTheme = [
|
||||||
return IconButton(
|
SolianThemeData(
|
||||||
icon: Icon(Icons.circle, color: color),
|
id: 'themeColorRed',
|
||||||
tooltip: label,
|
seedColor: const Color.fromRGBO(154, 98, 91, 1),
|
||||||
onPressed: () {
|
|
||||||
context.read<ThemeSwitcher>().setTheme(
|
|
||||||
AppTheme.build(
|
|
||||||
Brightness.light,
|
|
||||||
seedColor: color,
|
|
||||||
),
|
),
|
||||||
AppTheme.build(
|
SolianThemeData(
|
||||||
Brightness.dark,
|
id: 'themeColorBlue',
|
||||||
seedColor: color,
|
seedColor: const Color.fromRGBO(103, 96, 193, 1),
|
||||||
|
),
|
||||||
|
SolianThemeData(
|
||||||
|
id: 'themeColorMiku',
|
||||||
|
seedColor: const Color.fromRGBO(56, 120, 126, 1),
|
||||||
|
),
|
||||||
|
SolianThemeData(
|
||||||
|
id: 'themeColorKagamine',
|
||||||
|
seedColor: const Color.fromRGBO(244, 183, 63, 1),
|
||||||
|
),
|
||||||
|
SolianThemeData(
|
||||||
|
id: 'themeColorLuka',
|
||||||
|
seedColor: const Color.fromRGBO(243, 174, 218, 1),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
_prefs?.setInt('global_theme_color', color.value);
|
|
||||||
context.clearSnackbar();
|
|
||||||
context.showSnackbar('themeColorApplied'.tr);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final List<(String, Color)> _presentTheme = [
|
|
||||||
('themeColorRed', const Color.fromRGBO(154, 98, 91, 1)),
|
|
||||||
('themeColorBlue', const Color.fromRGBO(103, 96, 193, 1)),
|
|
||||||
('themeColorMiku', const Color.fromRGBO(56, 120, 126, 1)),
|
|
||||||
('themeColorKagamine', const Color.fromRGBO(244, 183, 63, 1)),
|
|
||||||
('themeColorLuka', const Color.fromRGBO(243, 174, 218, 1)),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
getApplicationDocumentsDirectory().then((dir) {
|
||||||
|
_docBasepath = dir.path;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
SharedPreferences.getInstance().then((inst) {
|
SharedPreferences.getInstance().then((inst) {
|
||||||
_prefs = inst;
|
_prefs = inst;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -75,19 +83,100 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildCaptionHeader('themeColor'.tr),
|
_buildCaptionHeader('theme'.tr),
|
||||||
SizedBox(
|
ListTile(
|
||||||
height: 56,
|
leading: const Icon(Icons.palette),
|
||||||
child: ListView(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
scrollDirection: Axis.horizontal,
|
title: Text('globalTheme'.tr),
|
||||||
children: _presentTheme
|
trailing: DropdownButtonHideUnderline(
|
||||||
.map((x) => _buildThemeColorButton(x.$1, x.$2))
|
child: DropdownButton2<SolianThemeData>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
'theme'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: _presentTheme
|
||||||
|
.map((SolianThemeData item) =>
|
||||||
|
DropdownMenuItem<SolianThemeData>(
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.circle, color: item.seedColor),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
item.id.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
value: (_prefs?.containsKey('global_theme') ?? false)
|
||||||
|
? SolianThemeData.fromJson(
|
||||||
|
jsonDecode(_prefs!.getString('global_theme')!),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onChanged: (SolianThemeData? value) {
|
||||||
|
context.read<ThemeSwitcher>().setThemeData(value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
height: 40,
|
||||||
|
width: 140,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Icons.military_tech),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
title: Text('agedTheme'.tr),
|
||||||
|
subtitle: Text('agedThemeDesc'.tr),
|
||||||
|
value: _prefs?.getBool('aged_theme') ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
context.read<ThemeSwitcher>().setAgedTheme(value);
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!PlatformInfo.isWeb)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.wallpaper),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 22, right: 31),
|
||||||
|
title: Text('appBackgroundImage'.tr),
|
||||||
|
subtitle: Text('appBackgroundImageDesc'.tr),
|
||||||
|
trailing: File('$_docBasepath/app_background_image').existsSync()
|
||||||
|
? const Icon(Icons.check_box)
|
||||||
|
: const Icon(Icons.check_box_outline_blank),
|
||||||
|
onTap: () async {
|
||||||
|
if (File('$_docBasepath/app_background_image').existsSync()) {
|
||||||
|
File('$_docBasepath/app_background_image').deleteSync();
|
||||||
|
} else {
|
||||||
|
final image = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
await File(image.path)
|
||||||
|
.copy('$_docBasepath/app_background_image');
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_buildCaptionHeader('notification'.tr),
|
_buildCaptionHeader('notification'.tr),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@ -181,6 +270,21 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
_buildCaptionHeader('performance'.tr),
|
||||||
|
CheckboxListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
secondary: const Icon(Icons.message),
|
||||||
|
title: Text('animatedMessageList'.tr),
|
||||||
|
subtitle: Text('animatedMessageListDesc'.tr),
|
||||||
|
value: _prefs?.getBool('non_animated_message_list') ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
_prefs
|
||||||
|
?.setBool('non_animated_message_list', value ?? false)
|
||||||
|
.then((_) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
_buildCaptionHeader('more'.tr),
|
_buildCaptionHeader('more'.tr),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep),
|
leading: const Icon(Icons.delete_sweep),
|
||||||
|
@ -2,7 +2,9 @@ import 'package:firebase_analytics/firebase_analytics.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/navigation/app_navigation_drawer.dart';
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation_bottom.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation_rail.dart';
|
||||||
|
|
||||||
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
@ -39,17 +41,28 @@ class RootShell extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final showRailNavigation = AppTheme.isLargeScreen(context);
|
||||||
|
|
||||||
|
final destNames = AppNavigation.destinations.map((x) => x.page).toList();
|
||||||
|
final showBottomNavigation =
|
||||||
|
destNames.contains(routeName) && !showRailNavigation;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
key: rootScaffoldKey,
|
||||||
drawer: AppTheme.isLargeScreen(context)
|
bottomNavigationBar: showBottomNavigation
|
||||||
? null
|
? AppNavigationBottom(
|
||||||
: AppNavigationDrawer(routeName: routeName),
|
initialIndex: destNames.indexOf(routeName ?? 'page'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
body: AppTheme.isLargeScreen(context)
|
body: AppTheme.isLargeScreen(context)
|
||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
if (showNavigation) AppNavigationDrawer(routeName: routeName),
|
if (showRailNavigation) const AppNavigationRail(),
|
||||||
if (showNavigation)
|
if (showRailNavigation)
|
||||||
const VerticalDivider(thickness: 0.3, width: 1),
|
const VerticalDivider(
|
||||||
|
width: 0.3,
|
||||||
|
thickness: 0.3,
|
||||||
|
),
|
||||||
Expanded(child: child),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
|
||||||
import 'package:solian/widgets/sidebar/sidebar_placeholder.dart';
|
|
||||||
|
|
||||||
class SidebarShell extends StatelessWidget {
|
|
||||||
final bool showAppBar;
|
|
||||||
final GoRouterState state;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
final bool sidebarFirst;
|
|
||||||
final Widget? sidebar;
|
|
||||||
|
|
||||||
const SidebarShell({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.state,
|
|
||||||
this.showAppBar = true,
|
|
||||||
this.sidebarFirst = false,
|
|
||||||
this.sidebar,
|
|
||||||
});
|
|
||||||
|
|
||||||
List<Widget> buildContent(BuildContext context) {
|
|
||||||
return [
|
|
||||||
Flexible(
|
|
||||||
flex: 2,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
if (AppTheme.isExtraLargeScreen(context))
|
|
||||||
const VerticalDivider(thickness: 0.3, width: 1),
|
|
||||||
if (AppTheme.isExtraLargeScreen(context))
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: sidebar ?? const SidebarPlaceholder(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: showAppBar
|
|
||||||
? AppBar(
|
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
|
||||||
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr),
|
|
||||||
centerTitle: false,
|
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
body: AppTheme.isLargeScreen(context)
|
|
||||||
? Row(
|
|
||||||
children: sidebarFirst
|
|
||||||
? buildContent(context).reversed.toList()
|
|
||||||
: buildContent(context),
|
|
||||||
)
|
|
||||||
: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import 'package:solian/theme.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class TitleShell extends StatelessWidget {
|
class TitleShell extends StatelessWidget {
|
||||||
final bool showAppBar;
|
final bool showAppBar;
|
||||||
@ -26,7 +27,8 @@ class TitleShell extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(state != null || title != null);
|
assert(state != null || title != null);
|
||||||
|
|
||||||
return Scaffold(
|
return RootContainer(
|
||||||
|
child: Scaffold(
|
||||||
appBar: showAppBar
|
appBar: showAppBar
|
||||||
? AppBar(
|
? AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -44,6 +46,7 @@ class TitleShell extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: child,
|
body: child,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:solian/models/theme.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
|
|
||||||
abstract class AppTheme {
|
abstract class AppTheme {
|
||||||
@ -6,7 +7,10 @@ abstract class AppTheme {
|
|||||||
MediaQuery.of(context).size.width > 640;
|
MediaQuery.of(context).size.width > 640;
|
||||||
|
|
||||||
static bool isExtraLargeScreen(BuildContext context) =>
|
static bool isExtraLargeScreen(BuildContext context) =>
|
||||||
MediaQuery.of(context).size.width > 720;
|
MediaQuery.of(context).size.width > 920;
|
||||||
|
|
||||||
|
static bool isUltraLargeScreen(BuildContext context) =>
|
||||||
|
MediaQuery.of(context).size.width > 1200;
|
||||||
|
|
||||||
static bool isSpecializedMacOS(BuildContext context) =>
|
static bool isSpecializedMacOS(BuildContext context) =>
|
||||||
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
|
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
|
||||||
@ -35,6 +39,7 @@ abstract class AppTheme {
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
|
||||||
),
|
),
|
||||||
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
@ -52,4 +57,36 @@ abstract class AppTheme {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ThemeData buildFromData(
|
||||||
|
Brightness brightness,
|
||||||
|
SolianThemeData data, {
|
||||||
|
bool useMaterial3 = true,
|
||||||
|
}) {
|
||||||
|
return ThemeData(
|
||||||
|
brightness: brightness,
|
||||||
|
useMaterial3: useMaterial3,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
brightness: brightness,
|
||||||
|
seedColor: data.seedColor,
|
||||||
|
),
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
|
fontFamily: data.fontFamily ?? 'Comfortaa',
|
||||||
|
fontFamilyFallback: data.fontFamilyFallback ??
|
||||||
|
[
|
||||||
|
'NotoSansSC',
|
||||||
|
'NotoSansHK',
|
||||||
|
'NotoSansJP',
|
||||||
|
if (PlatformInfo.isWeb) 'NotoSansEmoji',
|
||||||
|
],
|
||||||
|
typography: Typography.material2021(
|
||||||
|
colorScheme: brightness == Brightness.light
|
||||||
|
? const ColorScheme.light()
|
||||||
|
: const ColorScheme.dark(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final Color? feColor;
|
final Color? feColor;
|
||||||
final double? radius;
|
final double? radius;
|
||||||
|
final Widget? fallbackWidget;
|
||||||
|
|
||||||
const AccountAvatar({
|
const AccountAvatar({
|
||||||
super.key,
|
super.key,
|
||||||
@ -14,6 +15,7 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
this.bgColor,
|
this.bgColor,
|
||||||
this.feColor,
|
this.feColor,
|
||||||
this.radius,
|
this.radius,
|
||||||
|
this.fallbackWidget,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,11 +37,12 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
|
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
|
||||||
child: isEmpty
|
child: isEmpty
|
||||||
? Icon(
|
? (fallbackWidget ??
|
||||||
|
Icon(
|
||||||
Icons.account_circle,
|
Icons.account_circle,
|
||||||
size: radius != null ? radius! * 1.2 : 24,
|
size: radius != null ? radius! * 1.2 : 24,
|
||||||
color: feColor,
|
color: feColor,
|
||||||
)
|
))
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
final AccountProfile? profile;
|
final AccountProfile? profile;
|
||||||
final List<AccountBadge>? badges;
|
final List<AccountBadge>? badges;
|
||||||
final List<Widget>? extraWidgets;
|
final List<Widget>? extraWidgets;
|
||||||
|
final List<Widget>? appendWidgets;
|
||||||
|
|
||||||
final Future<Response>? status;
|
final Future<Response>? status;
|
||||||
final Function? onEditStatus;
|
final Function? onEditStatus;
|
||||||
@ -39,6 +40,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
this.profile,
|
this.profile,
|
||||||
this.status,
|
this.status,
|
||||||
this.extraWidgets,
|
this.extraWidgets,
|
||||||
|
this.appendWidgets,
|
||||||
this.onEditStatus,
|
this.onEditStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -257,6 +259,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddingSymmetric(horizontal: 16),
|
).paddingSymmetric(horizontal: 16),
|
||||||
|
...?appendWidgets?.map((x) => x.paddingSymmetric(horizontal: 16)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -106,10 +106,14 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
extraWidgets: [
|
extraWidgets: [
|
||||||
Card(
|
Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.contact_page_outlined,
|
||||||
|
),
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
title: Text('visitProfilePage'.tr),
|
title: Text('visitProfilePage'.tr),
|
||||||
|
subtitle: Text('learnMoreAboutPerson'.tr),
|
||||||
visualDensity:
|
visualDensity:
|
||||||
const VisualDensity(horizontal: -4, vertical: -2),
|
const VisualDensity(horizontal: -4, vertical: -2),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
@ -28,13 +28,9 @@ class SilverRelativeList extends StatelessWidget {
|
|||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Theme
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface,
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
builder: (context) => AccountProfilePopup(
|
||||||
AccountProfilePopup(
|
|
||||||
name: element.related.name,
|
name: element.related.name,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -43,27 +39,35 @@ class SilverRelativeList extends StatelessWidget {
|
|||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if(element.status != 1 && element.status != 3)
|
if (element.status != 1 && element.status != 3)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.check),
|
icon: const Icon(Icons.check),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final RelationshipProvider provider = Get.find();
|
final RelationshipProvider provider = Get.find();
|
||||||
if (element.status == 0) {
|
if (element.status == 0) {
|
||||||
provider.handleRelation(element, true).then((_) => onUpdate());
|
provider
|
||||||
|
.handleRelation(element, true)
|
||||||
|
.then((_) => onUpdate());
|
||||||
} else {
|
} else {
|
||||||
provider.editRelation(element, 1).then((_) => onUpdate());
|
provider
|
||||||
|
.editRelation(element.relatedId, 1)
|
||||||
|
.then((_) => onUpdate());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if(element.status != 2 && element.status != 3)
|
if (element.status != 2 && element.status != 3)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final RelationshipProvider provider = Get.find();
|
final RelationshipProvider provider = Get.find();
|
||||||
if (element.status == 0) {
|
if (element.status == 0) {
|
||||||
provider.handleRelation(element, false).then((_) => onUpdate());
|
provider
|
||||||
|
.handleRelation(element, false)
|
||||||
|
.then((_) => onUpdate());
|
||||||
} else {
|
} else {
|
||||||
provider.editRelation(element, 2).then((_) => onUpdate());
|
provider
|
||||||
|
.editRelation(element.relatedId, 2)
|
||||||
|
.then((_) => onUpdate());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -396,7 +396,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
),
|
),
|
||||||
if (!element.isCompleted &&
|
if (!element.isCompleted &&
|
||||||
element.error == null &&
|
element.error == null &&
|
||||||
canBeCrop)
|
canBeCrop &&
|
||||||
|
PlatformInfo.canCropImage)
|
||||||
Obx(
|
Obx(
|
||||||
() => IconButton(
|
() => IconButton(
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
@ -744,8 +745,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: _uploadController.isUploading.value,
|
ignoring: _uploadController.isUploading.value,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 64,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
@ -754,11 +755,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 0,
|
runSpacing: 8,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
runAlignment: WrapAlignment.center,
|
runAlignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -766,55 +765,62 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
PlatformInfo.isIOS ||
|
PlatformInfo.isIOS ||
|
||||||
PlatformInfo.isWeb) &&
|
PlatformInfo.isWeb) &&
|
||||||
!widget.imageOnly)
|
!widget.imageOnly)
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.paste),
|
icon: const Icon(Icons.paste),
|
||||||
label: Text('attachmentAddClipboard'.tr),
|
tooltip: 'attachmentAddClipboard'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _pasteFileToUpload(),
|
onPressed: () => _pasteFileToUpload(),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.add_photo_alternate),
|
icon: const Icon(Icons.add_photo_alternate),
|
||||||
label: Text('attachmentAddGalleryPhoto'.tr),
|
tooltip: 'attachmentAddGalleryPhoto'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _pickPhotoToUpload(),
|
onPressed: () => _pickPhotoToUpload(),
|
||||||
),
|
),
|
||||||
if (!widget.imageOnly)
|
if (!widget.imageOnly)
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.add_road),
|
icon: const Icon(Icons.add_road),
|
||||||
label: Text('attachmentAddGalleryVideo'.tr),
|
tooltip: 'attachmentAddGalleryVideo'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _pickVideoToUpload(),
|
onPressed: () => _pickVideoToUpload(),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
if (PlatformInfo.isMobile)
|
||||||
|
IconButton(
|
||||||
icon: const Icon(Icons.photo_camera_back),
|
icon: const Icon(Icons.photo_camera_back),
|
||||||
label: Text('attachmentAddCameraPhoto'.tr),
|
tooltip: 'attachmentAddCameraPhoto'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _takeMediaToUpload(false),
|
onPressed: () => _takeMediaToUpload(false),
|
||||||
),
|
),
|
||||||
if (!widget.imageOnly)
|
if (!widget.imageOnly && PlatformInfo.isMobile)
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.video_camera_back_outlined),
|
icon: const Icon(Icons.video_camera_back_outlined),
|
||||||
label: Text('attachmentAddCameraVideo'.tr),
|
tooltip: 'attachmentAddCameraVideo'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _takeMediaToUpload(true),
|
onPressed: () => _takeMediaToUpload(true),
|
||||||
),
|
),
|
||||||
if (!widget.imageOnly)
|
if (!widget.imageOnly)
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.file_present_rounded),
|
icon: const Icon(Icons.file_present_rounded),
|
||||||
label: Text('attachmentAddFile'.tr),
|
tooltip: 'attachmentAddFile'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _pickFileToUpload(),
|
onPressed: () => _pickFileToUpload(),
|
||||||
),
|
),
|
||||||
if (!widget.imageOnly)
|
if (!widget.imageOnly)
|
||||||
ElevatedButton.icon(
|
IconButton(
|
||||||
icon: const Icon(Icons.link),
|
icon: const Icon(Icons.link),
|
||||||
label: Text('attachmentAddFile'.tr),
|
tooltip: 'attachmentAddLink'.tr,
|
||||||
style: const ButtonStyle(visualDensity: density),
|
style: const ButtonStyle(visualDensity: density),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
onPressed: () => _linkAttachments(),
|
onPressed: () => _linkAttachments(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 12),
|
).paddingSymmetric(horizontal: 12),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.animate(
|
.animate(
|
||||||
target: _uploadController.isUploading.value ? 0 : 1,
|
target: _uploadController.isUploading.value ? 0 : 1,
|
||||||
|
@ -177,9 +177,6 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
if (element == null) return const SizedBox.shrink();
|
if (element == null) return const SizedBox.shrink();
|
||||||
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
maxWidth: widget.columnMaxWidth,
|
||||||
maxHeight: 640,
|
maxHeight: 640,
|
||||||
@ -247,7 +244,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
maxHeight: widget.flatMaxHeight,
|
maxHeight: widget.flatMaxHeight,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Colors.transparent,
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
horizontal: BorderSide(
|
horizontal: BorderSide(
|
||||||
width: 0.3,
|
width: 0.3,
|
||||||
@ -257,6 +254,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
),
|
),
|
||||||
child: CarouselSlider.builder(
|
child: CarouselSlider.builder(
|
||||||
options: CarouselOptions(
|
options: CarouselOptions(
|
||||||
|
animateToClosest: true,
|
||||||
aspectRatio: _aspectRatio,
|
aspectRatio: _aspectRatio,
|
||||||
viewportFraction:
|
viewportFraction:
|
||||||
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1),
|
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1),
|
||||||
@ -319,6 +317,7 @@ class AttachmentListEntry extends StatelessWidget {
|
|||||||
width: width ?? MediaQuery.of(context).size.width,
|
width: width ?? MediaQuery.of(context).size.width,
|
||||||
height: height,
|
height: height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
border: showBorder
|
border: showBorder
|
||||||
? Border.symmetric(
|
? Border.symmetric(
|
||||||
vertical: BorderSide(
|
vertical: BorderSide(
|
||||||
|
@ -98,12 +98,12 @@ class ChannelCallIndicator extends StatelessWidget {
|
|||||||
child: Text('callJoin'.tr),
|
child: Text('callJoin'.tr),
|
||||||
);
|
);
|
||||||
} else if (call.channel.value?.id == channel.id &&
|
} else if (call.channel.value?.id == channel.id &&
|
||||||
!AppTheme.isLargeScreen(context)) {
|
!AppTheme.isUltraLargeScreen(context)) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: () => onJoin(),
|
onPressed: () => onJoin(),
|
||||||
child: Text('callResume'.tr),
|
child: Text('callResume'.tr),
|
||||||
);
|
);
|
||||||
} else if (!AppTheme.isLargeScreen(context)) {
|
} else if (!AppTheme.isUltraLargeScreen(context)) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
child: Text('callJoin'.tr),
|
child: Text('callJoin'.tr),
|
||||||
|
@ -4,18 +4,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
|
||||||
class ChannelListWidget extends StatefulWidget {
|
class ChannelListWidget extends StatefulWidget {
|
||||||
final List<Channel> channels;
|
final List<Channel> channels;
|
||||||
final int selfId;
|
final int selfId;
|
||||||
final bool isDense;
|
|
||||||
final bool isCollapsed;
|
|
||||||
final bool noCategory;
|
|
||||||
final bool useReplace;
|
final bool useReplace;
|
||||||
final Function(Channel)? onSelected;
|
final Function(Channel)? onSelected;
|
||||||
|
|
||||||
@ -23,9 +23,6 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.channels,
|
required this.channels,
|
||||||
required this.selfId,
|
required this.selfId,
|
||||||
this.isDense = false,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
this.noCategory = false,
|
|
||||||
this.useReplace = false,
|
this.useReplace = false,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
});
|
});
|
||||||
@ -35,43 +32,25 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||||
final List<Channel> _globalChannels = List.empty(growable: true);
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
final Map<String, List<Channel>> _inRealms = {};
|
|
||||||
|
|
||||||
final ChatEventController _eventController = ChatEventController();
|
final ChatEventController _eventController = ChatEventController();
|
||||||
|
|
||||||
void _mapChannels() {
|
Future<void> _loadLastMessages() async {
|
||||||
_inRealms.clear();
|
final messages = await _eventController.src.getLastInAllChannels();
|
||||||
_globalChannels.clear();
|
setState(() {
|
||||||
|
_lastMessages = messages
|
||||||
if (widget.noCategory) {
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
_globalChannels.addAll(widget.channels);
|
.cast<int, LocalMessageEventTableData>();
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
for (final channel in widget.channels) {
|
|
||||||
if (channel.realmId != null) {
|
|
||||||
if (_inRealms[channel.realm!.alias] == null) {
|
|
||||||
_inRealms[channel.realm!.alias] = List.empty(growable: true);
|
|
||||||
}
|
|
||||||
_inRealms[channel.realm!.alias]!.add(channel);
|
|
||||||
} else {
|
|
||||||
_globalChannels.add(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant ChannelListWidget oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
setState(() => _mapChannels());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mapChannels();
|
_eventController.initialize().then((_) {
|
||||||
_eventController.initialize();
|
_loadLastMessages();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _gotoChannel(Channel item) {
|
void _gotoChannel(Channel item) {
|
||||||
@ -98,107 +77,183 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDirectMessageDescription(Channel item, ChannelMember otherside) {
|
Widget _buildTitle(Channel item, ChannelMember? otherside) {
|
||||||
if (PlatformInfo.isWeb) {
|
if (otherside != null) {
|
||||||
return Text('channelDirectDescription'.trParams(
|
return Row(
|
||||||
{'username': '@${otherside.account.name}'},
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
));
|
children: [
|
||||||
}
|
Expanded(child: Text(otherside.account.nick)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
return FutureBuilder(
|
Text(
|
||||||
future: Future.delayed(
|
DateFormat('MM/dd').format(
|
||||||
const Duration(milliseconds: 500),
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
() => _eventController.src.getLastInChannel(item),
|
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
style: TextStyle(
|
||||||
if (!snapshot.hasData && snapshot.data == null) {
|
fontSize: 12,
|
||||||
return Text('channelDirectDescription'.trParams(
|
color:
|
||||||
{'username': '@${otherside.account.name}'},
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(item.name)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(
|
||||||
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = snapshot.data!.data!;
|
Widget _buildSubtitle(Channel item, ChannelMember? otherside) {
|
||||||
return Text(
|
if (PlatformInfo.isWeb) {
|
||||||
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}',
|
return otherside != null
|
||||||
|
? Text(
|
||||||
|
'channelDirectDescription'.trParams(
|
||||||
|
{'username': '@${otherside.account.name}'},
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.easeIn,
|
||||||
|
switchOutCurve: Curves.easeOut,
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return FadeTransition(opacity: animation, child: child);
|
||||||
|
},
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: (_lastMessages == null || _lastMessages![item.id] == null)
|
||||||
|
? Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return otherside != null
|
||||||
|
? Text(
|
||||||
|
'channelDirectDescription'.trParams(
|
||||||
|
{'username': '@${otherside.account.name}'},
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final data = _lastMessages![item.id]!.data!;
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item.type == 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(data.sender.account.nick),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
textColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
if (item.type == 0) const Gap(6),
|
||||||
|
if (data.body['text'] != null)
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
data.body['text'],
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Badge(label: Text('unablePreview'.tr)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
layoutBuilder: (currentChild, previousChildren) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: <Widget>[
|
||||||
|
...previousChildren,
|
||||||
|
if (currentChild != null) currentChild,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntry(Channel item) {
|
Widget _buildEntry(Channel item) {
|
||||||
final padding = widget.isDense
|
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||||
? const EdgeInsets.symmetric(horizontal: 20)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 16);
|
|
||||||
|
|
||||||
if (item.type == 1) {
|
|
||||||
final otherside =
|
final otherside =
|
||||||
item.members!.where((e) => e.account.id != widget.selfId).first;
|
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
||||||
|
|
||||||
|
if (item.type == 1 && otherside != null) {
|
||||||
final avatar = AccountAvatar(
|
final avatar = AccountAvatar(
|
||||||
content: otherside.account.avatar,
|
content: otherside.account.avatar,
|
||||||
radius: widget.isDense ? 12 : 20,
|
radius: 20,
|
||||||
bgColor: Theme.of(context).colorScheme.primary,
|
bgColor: Theme.of(context).colorScheme.primary,
|
||||||
feColor: Theme.of(context).colorScheme.onPrimary,
|
feColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: otherside.account.nick,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: avatar,
|
leading: avatar,
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(otherside.account.nick),
|
title: _buildTitle(item, otherside),
|
||||||
subtitle: !widget.isDense
|
subtitle: _buildSubtitle(item, otherside),
|
||||||
? _buildDirectMessageDescription(item, otherside)
|
|
||||||
: null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final avatar = CircleAvatar(
|
final avatar = CircleAvatar(
|
||||||
backgroundColor: item.realmId == null
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
? Theme.of(context).colorScheme.primary
|
radius: 20,
|
||||||
: Colors.transparent,
|
|
||||||
radius: widget.isDense ? 12 : 20,
|
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.hashtag,
|
FontAwesomeIcons.hashtag,
|
||||||
color: item.realmId == null
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
size: 16,
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: item.name,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
minTileHeight: widget.isDense ? 48 : null,
|
minTileHeight: null,
|
||||||
leading: avatar,
|
leading: item.realmId == null
|
||||||
|
? avatar
|
||||||
|
: badges.Badge(
|
||||||
|
position: badges.BadgePosition.bottomEnd(bottom: -4, end: -6),
|
||||||
|
badgeStyle: badges.BadgeStyle(
|
||||||
|
badgeColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
badgeContent: AccountAvatar(
|
||||||
|
content: item.realm?.avatar,
|
||||||
|
radius: 10,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(item.name),
|
title: _buildTitle(item, null),
|
||||||
subtitle: !widget.isDense
|
subtitle: _buildSubtitle(item, null),
|
||||||
? Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -206,13 +261,12 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.noCategory) {
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: _globalChannels.length,
|
itemCount: widget.channels.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = _globalChannels[index];
|
final element = widget.channels[index];
|
||||||
return _buildEntry(element);
|
return _buildEntry(element);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -220,36 +274,4 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: _globalChannels.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = _globalChannels[index];
|
|
||||||
return _buildEntry(element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SliverList.list(
|
|
||||||
children: _inRealms.entries.map((element) {
|
|
||||||
return ExpansionTile(
|
|
||||||
tilePadding: const EdgeInsets.only(left: 20, right: 24),
|
|
||||||
minTileHeight: 48,
|
|
||||||
title: Text(element.value.first.realm!.name),
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Colors.teal,
|
|
||||||
radius: widget.isDense ? 12 : 24,
|
|
||||||
child: Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: element.value.map((x) => _buildEntry(x)).toList(),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
@ -9,6 +10,7 @@ import 'package:solian/widgets/chat/chat_event_action.dart';
|
|||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class ChatEventList extends StatelessWidget {
|
class ChatEventList extends StatelessWidget {
|
||||||
|
final bool noAnimated;
|
||||||
final String scope;
|
final String scope;
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
final ChatEventController chatController;
|
final ChatEventController chatController;
|
||||||
@ -23,6 +25,7 @@ class ChatEventList extends StatelessWidget {
|
|||||||
required this.chatController,
|
required this.chatController,
|
||||||
required this.onEdit,
|
required this.onEdit,
|
||||||
required this.onReply,
|
required this.onReply,
|
||||||
|
this.noAnimated = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool _checkMessageMergeable(Event? a, Event? b) {
|
bool _checkMessageMergeable(Event? a, Event? b) {
|
||||||
@ -63,7 +66,8 @@ class ChatEventList extends StatelessWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: ChatEvent(
|
child: Builder(builder: (context) {
|
||||||
|
final widget = ChatEvent(
|
||||||
key: Key('m${item!.uuid}'),
|
key: Key('m${item!.uuid}'),
|
||||||
item: item,
|
item: item,
|
||||||
isMerged: isMerged,
|
isMerged: isMerged,
|
||||||
@ -71,7 +75,23 @@ class ChatEventList extends StatelessWidget {
|
|||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
top: !isMerged ? 8 : 0,
|
top: !isMerged ? 8 : 0,
|
||||||
bottom: !hasMerged ? 8 : 0,
|
bottom: !hasMerged ? 8 : 0,
|
||||||
),
|
);
|
||||||
|
|
||||||
|
if (noAnimated) {
|
||||||
|
return widget;
|
||||||
|
} else {
|
||||||
|
return widget
|
||||||
|
.animate(
|
||||||
|
key: Key('animated-m${item.uuid}'),
|
||||||
|
)
|
||||||
|
.slideY(
|
||||||
|
curve: Curves.fastLinearToSlowEaseIn,
|
||||||
|
duration: 250.ms,
|
||||||
|
begin: 0.5,
|
||||||
|
end: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
@ -79,7 +99,7 @@ class ChatEventList extends StatelessWidget {
|
|||||||
builder: (context) => ChatEventAction(
|
builder: (context) => ChatEventAction(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
realm: channel.realm,
|
realm: channel.realm,
|
||||||
item: item,
|
item: item!,
|
||||||
onEdit: () {
|
onEdit: () {
|
||||||
onEdit(item);
|
onEdit(item);
|
||||||
},
|
},
|
||||||
|
80
lib/widgets/navigation/app_account_widget.dart
Normal file
80
lib/widgets/navigation/app_account_widget.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/account_status.dart';
|
||||||
|
import 'package:solian/providers/account_status.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/relation.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
|
||||||
|
class AppAccountWidget extends StatefulWidget {
|
||||||
|
const AppAccountWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppAccountWidget> createState() => _AppAccountWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppAccountWidgetState extends State<AppAccountWidget> {
|
||||||
|
AccountStatus? _accountStatus;
|
||||||
|
|
||||||
|
Future<void> _getStatus() async {
|
||||||
|
final StatusProvider provider = Get.find();
|
||||||
|
|
||||||
|
final resp = await provider.getCurrentStatus();
|
||||||
|
final status = AccountStatus.fromJson(resp.body);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_accountStatus = status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
||||||
|
return const Icon(Icons.account_circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusBadgeColor = _accountStatus != null
|
||||||
|
? StatusProvider.determineStatus(_accountStatus!).$2
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
|
final RelationshipProvider relations = Get.find();
|
||||||
|
final accountNotifications = relations.friendRequestCount.value;
|
||||||
|
|
||||||
|
return badges.Badge(
|
||||||
|
badgeContent: Text(
|
||||||
|
accountNotifications.toString(),
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
showBadge: accountNotifications > 0,
|
||||||
|
position: badges.BadgePosition.topEnd(
|
||||||
|
top: -10,
|
||||||
|
end: -6,
|
||||||
|
),
|
||||||
|
child: badges.Badge(
|
||||||
|
showBadge: _accountStatus != null,
|
||||||
|
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||||
|
position: badges.BadgePosition.bottomEnd(
|
||||||
|
bottom: 0,
|
||||||
|
end: -2,
|
||||||
|
),
|
||||||
|
child: AccountAvatar(
|
||||||
|
radius: 14,
|
||||||
|
content: auth.userProfile.value!['avatar'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,33 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/utils.dart';
|
import 'package:get/utils.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_account_widget.dart';
|
||||||
|
|
||||||
abstract class AppNavigation {
|
abstract class AppNavigation {
|
||||||
static List<AppNavigationDestination> destinations = [
|
static List<AppNavigationDestination> destinations = [
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.dashboard,
|
icon: const Icon(Icons.dashboard),
|
||||||
label: 'dashboard'.tr,
|
label: 'dashboardNav'.tr,
|
||||||
page: 'dashboard',
|
page: 'dashboard',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.explore,
|
icon: const Icon(Icons.explore),
|
||||||
label: 'explore'.tr,
|
label: 'explore'.tr,
|
||||||
page: 'explore',
|
page: 'explore',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.workspaces,
|
icon: const Icon(Icons.forum),
|
||||||
|
label: 'chat'.tr,
|
||||||
|
page: 'chat',
|
||||||
|
),
|
||||||
|
AppNavigationDestination(
|
||||||
|
icon: const Icon(Icons.workspaces),
|
||||||
label: 'realms'.tr,
|
label: 'realms'.tr,
|
||||||
page: 'realms',
|
page: 'realms',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.forum,
|
icon: const AppAccountWidget(),
|
||||||
label: 'chat'.tr,
|
label: 'accountNav'.tr,
|
||||||
page: 'chat',
|
page: 'account',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -30,7 +36,7 @@ abstract class AppNavigation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppNavigationDestination {
|
class AppNavigationDestination {
|
||||||
final IconData icon;
|
final Widget icon;
|
||||||
final String label;
|
final String label;
|
||||||
final String page;
|
final String page;
|
||||||
|
|
||||||
|
47
lib/widgets/navigation/app_navigation_bottom.dart
Normal file
47
lib/widgets/navigation/app_navigation_bottom.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationBottom extends StatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const AppNavigationBottom({super.key, this.initialIndex = 0});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationBottom> createState() => _AppNavigationBottomState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationBottomState extends State<AppNavigationBottom> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialIndex >= 0) {
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BottomNavigationBar(
|
||||||
|
currentIndex: _currentIndex,
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
showSelectedLabels: true,
|
||||||
|
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
|
||||||
|
items: AppNavigation.destinations
|
||||||
|
.map(
|
||||||
|
(x) => BottomNavigationBarItem(
|
||||||
|
icon: x.icon,
|
||||||
|
label: x.label,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onTap: (idx) {
|
||||||
|
setState(() => _currentIndex = idx);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,330 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/account_status.dart';
|
|
||||||
import 'package:solian/providers/account_status.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
|
||||||
import 'package:solian/providers/relation.dart';
|
|
||||||
import 'package:solian/router.dart';
|
|
||||||
import 'package:solian/shells/root_shell.dart';
|
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/account/account_status_action.dart';
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation.dart';
|
|
||||||
import 'package:badges/badges.dart' as badges;
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation_region.dart';
|
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
|
||||||
final String? routeName;
|
|
||||||
|
|
||||||
const AppNavigationDrawer({super.key, this.routeName});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
bool _isCollapsed = true;
|
|
||||||
|
|
||||||
late final AnimationController _drawerAnimationController =
|
|
||||||
AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
late final Animation<double> _drawerAnimation = Tween<double>(
|
|
||||||
begin: 80.0,
|
|
||||||
end: 304.0,
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: _drawerAnimationController,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
));
|
|
||||||
|
|
||||||
AccountStatus? _accountStatus;
|
|
||||||
|
|
||||||
Future<void> _getStatus() async {
|
|
||||||
final StatusProvider provider = Get.find();
|
|
||||||
|
|
||||||
final resp = await provider.getCurrentStatus();
|
|
||||||
final status = AccountStatus.fromJson(resp.body);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_accountStatus = status;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get _unFocusColor =>
|
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
|
||||||
|
|
||||||
Widget _buildUserInfo() {
|
|
||||||
return Obx(() {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
|
||||||
if (_isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: const Icon(Icons.account_circle).paddingSymmetric(
|
|
||||||
horizontal: 28,
|
|
||||||
vertical: 20,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
|
||||||
leading: const Icon(Icons.account_circle),
|
|
||||||
title: !_isCollapsed ? Text('guest'.tr) : null,
|
|
||||||
subtitle: !_isCollapsed ? Text('unsignedIn'.tr) : null,
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final leading = Obx(() {
|
|
||||||
final statusBadgeColor = _accountStatus != null
|
|
||||||
? StatusProvider.determineStatus(_accountStatus!).$2
|
|
||||||
: Colors.grey;
|
|
||||||
|
|
||||||
final RelationshipProvider relations = Get.find();
|
|
||||||
final accountNotifications = relations.friendRequestCount.value;
|
|
||||||
|
|
||||||
return badges.Badge(
|
|
||||||
badgeContent: Text(
|
|
||||||
accountNotifications.toString(),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
showBadge: accountNotifications > 0,
|
|
||||||
position: badges.BadgePosition.topEnd(
|
|
||||||
top: -10,
|
|
||||||
end: -6,
|
|
||||||
),
|
|
||||||
child: badges.Badge(
|
|
||||||
showBadge: _accountStatus != null,
|
|
||||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
|
||||||
position: badges.BadgePosition.bottomEnd(
|
|
||||||
bottom: 0,
|
|
||||||
end: -2,
|
|
||||||
),
|
|
||||||
child: AccountAvatar(
|
|
||||||
content: auth.userProfile.value!['avatar'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
child: !_isCollapsed
|
|
||||||
? Row(
|
|
||||||
children: [
|
|
||||||
leading,
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
auth.userProfile.value!['nick'],
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
).paddingOnly(left: 16),
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (_accountStatus == null) {
|
|
||||||
return Text(
|
|
||||||
'loading'.tr,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 16);
|
|
||||||
}
|
|
||||||
final info = StatusProvider.determineStatus(
|
|
||||||
_accountStatus!,
|
|
||||||
);
|
|
||||||
return Text(
|
|
||||||
info.$3,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 16);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: 20, vertical: 16)
|
|
||||||
: leading.paddingSymmetric(horizontal: 20, vertical: 16),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AccountStatusAction(
|
|
||||||
currentStatus: _accountStatus!.status,
|
|
||||||
),
|
|
||||||
).then((val) {
|
|
||||||
if (val == true) _getStatus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _expandDrawer() {
|
|
||||||
_drawerAnimationController.animateTo(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _collapseDrawer() {
|
|
||||||
_drawerAnimationController.animateTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeDrawer() {
|
|
||||||
_autoResize();
|
|
||||||
rootScaffoldKey.currentState!.closeDrawer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _autoResize() {
|
|
||||||
if (AppTheme.isExtraLargeScreen(context)) {
|
|
||||||
_expandDrawer();
|
|
||||||
} else if (AppTheme.isLargeScreen(context)) {
|
|
||||||
_collapseDrawer();
|
|
||||||
} else {
|
|
||||||
_drawerAnimationController.value = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.value) _getStatus();
|
|
||||||
Future.delayed(Duration.zero, () => _autoResize());
|
|
||||||
_drawerAnimationController.addListener(() {
|
|
||||||
if (_drawerAnimation.value > 180 && _isCollapsed) {
|
|
||||||
setState(() => _isCollapsed = false);
|
|
||||||
} else if (_drawerAnimation.value < 180 && !_isCollapsed) {
|
|
||||||
setState(() => _isCollapsed = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_drawerAnimationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _drawerAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Drawer(
|
|
||||||
width: _drawerAnimation.value,
|
|
||||||
backgroundColor:
|
|
||||||
AppTheme.isLargeScreen(context) ? Colors.transparent : null,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
bottom: false,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildUserInfo().paddingSymmetric(vertical: 8),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.spaceAround,
|
|
||||||
children: AppNavigation.destinations
|
|
||||||
.map(
|
|
||||||
(e) => Tooltip(
|
|
||||||
message: e.label,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Icon(
|
|
||||||
e.icon,
|
|
||||||
size: 22,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
).paddingAll(16),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed(e.page);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
).paddingSymmetric(vertical: 8, horizontal: 12),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: AppNavigationRegion(
|
|
||||||
isCollapsed: _isCollapsed,
|
|
||||||
onSelected: () {
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
if (_isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: 'expand'.tr,
|
|
||||||
child: InkWell(
|
|
||||||
child: const Icon(Icons.chevron_right, size: 20)
|
|
||||||
.paddingSymmetric(
|
|
||||||
horizontal: 28,
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_expandDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
),
|
|
||||||
leading:
|
|
||||||
const Icon(Icons.chevron_left, size: 20).paddingAll(2),
|
|
||||||
title: Text('collapse'.tr),
|
|
||||||
onTap: () {
|
|
||||||
_collapseDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(
|
|
||||||
top: 8,
|
|
||||||
bottom: math.max(8, MediaQuery.of(context).padding.bottom),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
65
lib/widgets/navigation/app_navigation_rail.dart
Normal file
65
lib/widgets/navigation/app_navigation_rail.dart
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationRail extends StatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const AppNavigationRail({super.key, this.initialIndex = 0});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationRail> createState() => _AppNavigationRailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||||
|
int? _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialIndex >= 0) {
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return NavigationRail(
|
||||||
|
selectedIndex: _currentIndex,
|
||||||
|
labelType: NavigationRailLabelType.selected,
|
||||||
|
groupAlignment: -1,
|
||||||
|
destinations: AppNavigation.destinations
|
||||||
|
.sublist(0, AppNavigation.destinations.length - 1)
|
||||||
|
.map(
|
||||||
|
(x) => NavigationRailDestination(
|
||||||
|
icon: x.icon,
|
||||||
|
label: Text(x.label),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
trailing: Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IconButton(
|
||||||
|
icon: AppNavigation.destinations.last.icon,
|
||||||
|
tooltip: AppNavigation.destinations.last.label,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _currentIndex = null);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations.last.page);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDestinationSelected: (idx) {
|
||||||
|
setState(() => _currentIndex = idx);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||||
|
},
|
||||||
|
).paddingOnly(
|
||||||
|
top: max(16, MediaQuery.of(context).padding.top),
|
||||||
|
bottom: max(16, MediaQuery.of(context).padding.bottom),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,230 +0,0 @@
|
|||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/realm.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/content/realm.dart';
|
|
||||||
import 'package:solian/providers/navigation.dart';
|
|
||||||
import 'package:solian/services.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
|
||||||
|
|
||||||
class AppNavigationRegion extends StatefulWidget {
|
|
||||||
final bool isCollapsed;
|
|
||||||
final Function onSelected;
|
|
||||||
|
|
||||||
const AppNavigationRegion({
|
|
||||||
super.key,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationRegion> createState() => _AppNavigationRegionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
|
||||||
bool _isTryingExit = false;
|
|
||||||
|
|
||||||
void _focusRealm(Realm item) {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = item,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unFocusRealm() {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRealmFocusAvatar() {
|
|
||||||
final focusedRealm = Get.find<NavigationStateProvider>().focusedRealm.value;
|
|
||||||
return GestureDetector(
|
|
||||||
child: MouseRegion(
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, animation) {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: animation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _isTryingExit
|
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
)
|
|
||||||
: _buildEntryAvatar(focusedRealm!),
|
|
||||||
),
|
|
||||||
onEnter: (_) => setState(() => _isTryingExit = true),
|
|
||||||
onExit: (_) => setState(() => _isTryingExit = false),
|
|
||||||
),
|
|
||||||
onTap: () => _unFocusRealm(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntryAvatar(Realm item) {
|
|
||||||
return Hero(
|
|
||||||
tag: Key('region-realm-avatar-${item.id}'),
|
|
||||||
child: (item.avatar?.isNotEmpty ?? false)
|
|
||||||
? AccountAvatar(content: item.avatar)
|
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntry(BuildContext context, Realm item) {
|
|
||||||
const padding = EdgeInsets.symmetric(horizontal: 20, vertical: 8);
|
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: _buildEntryAvatar(item).paddingSymmetric(vertical: 8),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
leading: _buildEntryAvatar(item),
|
|
||||||
contentPadding: padding,
|
|
||||||
title: Text(item.name),
|
|
||||||
subtitle: Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final RealmProvider realms = Get.find();
|
|
||||||
final ChannelProvider channels = Get.find();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Obx(
|
|
||||||
() => PageTransitionSwitcher(
|
|
||||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: navState.focusedRealm.value == null
|
|
||||||
? widget.isCollapsed
|
|
||||||
? CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return Tooltip(
|
|
||||||
message: element.name,
|
|
||||||
child: _buildEntry(context, element),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return _buildEntry(context, element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
if (!widget.isCollapsed &&
|
|
||||||
(navState.focusedRealm.value!.banner?.isNotEmpty ??
|
|
||||||
false))
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: AutoCacheImage(
|
|
||||||
ServiceFinder.buildUrl(
|
|
||||||
'uc',
|
|
||||||
'/attachments/${navState.focusedRealm.value!.banner}',
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: navState.focusedRealm.value!.name,
|
|
||||||
child: _buildRealmFocusAvatar().paddingOnly(
|
|
||||||
top: 24,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
tileColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
leading: _buildRealmFocusAvatar(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 8),
|
|
||||||
title: Text(navState.focusedRealm.value!.name),
|
|
||||||
subtitle: Text(
|
|
||||||
navState.focusedRealm.value!.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(
|
|
||||||
() => ChannelListWidget(
|
|
||||||
useReplace: true,
|
|
||||||
channels: channels.availableChannels
|
|
||||||
.where((x) =>
|
|
||||||
x.realm?.id == navState.focusedRealm.value?.id)
|
|
||||||
.toList(),
|
|
||||||
isCollapsed: widget.isCollapsed,
|
|
||||||
selfId: auth.userProfile.value!['id'],
|
|
||||||
noCategory: true,
|
|
||||||
onSelected: (_) => widget.onSelected(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
92
lib/widgets/navigation/realm_switcher.dart
Normal file
92
lib/widgets/navigation/realm_switcher.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/navigation.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
|
||||||
|
class RealmSwitcher extends StatelessWidget {
|
||||||
|
const RealmSwitcher({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final realms = Get.find<RealmProvider>();
|
||||||
|
final navState = Get.find<NavigationStateProvider>();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<Realm?>(
|
||||||
|
iconStyleData: const IconStyleData(iconSize: 0),
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
'Realm Region',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [null, ...realms.availableRealms]
|
||||||
|
.map((Realm? item) => DropdownMenuItem<Realm?>(
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item != null)
|
||||||
|
AccountAvatar(
|
||||||
|
content: item.avatar,
|
||||||
|
radius: 14,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
radius: 14,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.public,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item?.name ?? 'global'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
value: navState.focusedRealm.value,
|
||||||
|
onChanged: (Realm? value) {
|
||||||
|
navState.focusedRealm.value = value;
|
||||||
|
},
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
height: 48,
|
||||||
|
width: max(200, MediaQuery.of(context).size.width * 0.4),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
_attachmentController.text = value.toString();
|
_attachmentController.text = value.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
widget.controller.thumbnail.value = value;
|
widget.controller.thumbnail.value = value.isEmpty ? null : value;
|
||||||
},
|
},
|
||||||
initialAttachments: const [],
|
initialAttachments: const [],
|
||||||
onRemove: (_) {},
|
onRemove: (_) {},
|
||||||
@ -91,7 +91,8 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.controller.thumbnail.value = _attachmentController.text;
|
final text = _attachmentController.text;
|
||||||
|
widget.controller.thumbnail.value = text.isEmpty ? null : text;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Text('confirm'.tr),
|
child: Text('confirm'.tr),
|
||||||
|
@ -117,29 +117,15 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
if (_contentHeight >= 80 && !widget.isFullContent)
|
if (_contentHeight >= 80 && !widget.isFullContent)
|
||||||
Align(
|
Opacity(
|
||||||
alignment: Alignment.bottomCenter,
|
opacity: 0.8,
|
||||||
child: IgnorePointer(
|
child: InkWell(child: Text('readMore'.tr)),
|
||||||
child: Container(
|
).paddingOnly(
|
||||||
height: 80,
|
left: 12,
|
||||||
decoration: BoxDecoration(
|
top: 4,
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.bottomCenter,
|
|
||||||
end: Alignment.topCenter,
|
|
||||||
colors: [
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface
|
|
||||||
.withOpacity(0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
LinkExpansion(content: item.body['content']).paddingOnly(
|
LinkExpansion(content: item.body['content']).paddingOnly(
|
||||||
left: 8,
|
left: 8,
|
||||||
@ -225,33 +211,15 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
if (_contentHeight >= 320 && !widget.isFullContent)
|
||||||
Align(
|
Opacity(
|
||||||
alignment: Alignment.bottomCenter,
|
opacity: 0.8,
|
||||||
child: IgnorePointer(
|
child: InkWell(child: Text('readMore'.tr)),
|
||||||
child: Container(
|
).paddingOnly(
|
||||||
height: 320,
|
left: 12,
|
||||||
decoration: BoxDecoration(
|
top: 4,
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.bottomCenter,
|
|
||||||
end: Alignment.topCenter,
|
|
||||||
colors: [
|
|
||||||
(widget.backgroundColor ??
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface),
|
|
||||||
(widget.backgroundColor ??
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface)
|
|
||||||
.withOpacity(0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
if (widget.item.replyTo != null && widget.isShowEmbed)
|
if (widget.item.replyTo != null && widget.isShowEmbed)
|
||||||
Container(
|
Container(
|
||||||
@ -336,8 +304,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedColor:
|
closedColor: Colors.transparent,
|
||||||
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -574,7 +541,7 @@ class _PostEmbedWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedColor: Theme.of(context).colorScheme.surface,
|
closedColor: Colors.transparent,
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
48
lib/widgets/root_container.dart
Normal file
48
lib/widgets/root_container.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
|
|
||||||
|
class RootContainer extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
const RootContainer({super.key, this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: PlatformInfo.isWeb
|
||||||
|
? Future.value(null)
|
||||||
|
: getApplicationDocumentsDirectory(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final path = '${snapshot.data!.path}/app_background_image';
|
||||||
|
final file = File(path);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
backgroundBlendMode: BlendMode.darken,
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
image: DecorationImage(
|
||||||
|
opacity: 0.2,
|
||||||
|
image: FileImage(file),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class EmptyPagePlaceholder extends StatelessWidget {
|
class EmptyPagePlaceholder extends StatelessWidget {
|
||||||
const EmptyPagePlaceholder({super.key});
|
const EmptyPagePlaceholder({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Center(
|
child: Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SidebarPlaceholder extends StatelessWidget {
|
|
||||||
const SidebarPlaceholder({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(Icons.menu_open, size: 50),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,8 +12,6 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.bluetooth</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
@ -57,5 +57,11 @@
|
|||||||
<string>INStartCallIntent</string>
|
<string>INStartCallIntent</string>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Allow you take photo/video for your message or post</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Allow you record audio for your message or post</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Allow you add photo to your message or post</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -10,8 +10,6 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.bluetooth</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
@ -2,7 +2,7 @@ name: solian
|
|||||||
description: "The Solar Network App"
|
description: "The Solar Network App"
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
|
|
||||||
version: 1.2.4+1
|
version: 1.3.6+3
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: ">=3.3.4 <4.0.0"
|
||||||
|
Reference in New Issue
Block a user