✨ Better theme & background image
This commit is contained in:
parent
fcf4dc7a2d
commit
f5fbe1f483
@ -471,5 +471,11 @@
|
|||||||
"accountNav": "You",
|
"accountNav": "You",
|
||||||
"performance": "Performance",
|
"performance": "Performance",
|
||||||
"animatedMessageList": "Non-animated message list",
|
"animatedMessageList": "Non-animated message list",
|
||||||
"animatedMessageListDesc": "Remove animation effects in message list, to reduce cause lag"
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -467,5 +467,11 @@
|
|||||||
"accountNav": "您",
|
"accountNav": "您",
|
||||||
"performance": "性能",
|
"performance": "性能",
|
||||||
"animatedMessageList": "无动画消息列表",
|
"animatedMessageList": "无动画消息列表",
|
||||||
"animatedMessageListDesc": "在消息列表中禁用动画效果"
|
"animatedMessageListDesc": "在消息列表中禁用动画效果",
|
||||||
|
"theme": "主题",
|
||||||
|
"globalTheme": "全局应用主题",
|
||||||
|
"agedTheme": "过时主题",
|
||||||
|
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
||||||
|
"appBackgroundImage": "全局背景图片",
|
||||||
|
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
};
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,181 +140,186 @@ class _ChatListState extends State<ChatList> {
|
|||||||
return Obx(
|
return Obx(
|
||||||
() => DefaultTabController(
|
() => DefaultTabController(
|
||||||
length: 2 + realms.availableRealms.length,
|
length: 2 + realms.availableRealms.length,
|
||||||
child: Scaffold(
|
child: RootContainer(
|
||||||
appBar: AppBar(
|
child: Scaffold(
|
||||||
leading: Obx(() {
|
appBar: AppBar(
|
||||||
final adaptive = AppBarLeadingButton.adaptive(context);
|
leading: Obx(() {
|
||||||
if (adaptive != null) return adaptive;
|
final adaptive = AppBarLeadingButton.adaptive(context);
|
||||||
if (_channels.isLoading.value) {
|
if (adaptive != null) return adaptive;
|
||||||
return const CircularProgressIndicator(
|
if (_channels.isLoading.value) {
|
||||||
strokeWidth: 3,
|
return const CircularProgressIndicator(
|
||||||
).paddingAll(18);
|
strokeWidth: 3,
|
||||||
}
|
).paddingAll(18);
|
||||||
return const SizedBox.shrink();
|
}
|
||||||
}),
|
return const SizedBox.shrink();
|
||||||
title: AppBarTitle('chat'.tr),
|
}),
|
||||||
centerTitle: true,
|
title: AppBarTitle('chat'.tr),
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
centerTitle: true,
|
||||||
actions: [
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
const BackgroundStateWidget(),
|
actions: [
|
||||||
const NotificationButton(),
|
const BackgroundStateWidget(),
|
||||||
PopupMenuButton(
|
const NotificationButton(),
|
||||||
icon: const Icon(Icons.add_circle),
|
PopupMenuButton(
|
||||||
itemBuilder: (BuildContext context) => [
|
icon: const Icon(Icons.add_circle),
|
||||||
PopupMenuItem(
|
itemBuilder: (BuildContext context) => [
|
||||||
child: ListTile(
|
PopupMenuItem(
|
||||||
title: Text('channelOrganizeCommon'.tr),
|
child: ListTile(
|
||||||
leading: const Icon(Icons.tag),
|
title: Text('channelOrganizeCommon'.tr),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
leading: const Icon(Icons.tag),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||||
|
(value) {
|
||||||
|
if (value != null) {
|
||||||
|
_loadAllChannels();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
PopupMenuItem(
|
||||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
child: ListTile(
|
||||||
(value) {
|
title: Text('channelOrganizeDirect'.tr),
|
||||||
if (value != null) {
|
leading: const FaIcon(
|
||||||
|
FontAwesomeIcons.userGroup,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
final ChannelProvider channels = Get.find();
|
||||||
|
channels
|
||||||
|
.createDirectChannel(context, 'global')
|
||||||
|
.then((resp) {
|
||||||
|
if (resp != null) {
|
||||||
_loadAllChannels();
|
_loadAllChannels();
|
||||||
}
|
}
|
||||||
},
|
}).catchError((e) {
|
||||||
);
|
context.showErrorDialog(e);
|
||||||
},
|
});
|
||||||
),
|
},
|
||||||
PopupMenuItem(
|
),
|
||||||
child: ListTile(
|
],
|
||||||
title: Text('channelOrganizeDirect'.tr),
|
),
|
||||||
leading: const FaIcon(
|
SizedBox(
|
||||||
FontAwesomeIcons.userGroup,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
size: 16,
|
),
|
||||||
),
|
],
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
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),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
final ChannelProvider channels = Get.find();
|
|
||||||
channels
|
|
||||||
.createDirectChannel(context, 'global')
|
|
||||||
.then((resp) {
|
|
||||||
if (resp != null) {
|
|
||||||
_loadAllChannels();
|
|
||||||
}
|
|
||||||
}).catchError((e) {
|
|
||||||
context.showErrorDialog(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
|
||||||
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: () => _loadAllChannels(),
|
||||||
onDone: () => _loadAllChannels(),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
final selfId = auth.userProfile.value!['id'];
|
final selfId = auth.userProfile.value!['id'];
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const ChatCallCurrentIndicator(),
|
const ChatCallCurrentIndicator(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
RefreshIndicator(
|
RefreshIndicator(
|
||||||
onRefresh: _loadNormalChannels,
|
onRefresh: _loadNormalChannels,
|
||||||
child: ChannelListWidget(
|
|
||||||
channels: _sortChannels([
|
|
||||||
..._normalChannels,
|
|
||||||
..._directChannels,
|
|
||||||
..._realmChannels.values.expand((x) => x),
|
|
||||||
]),
|
|
||||||
selfId: selfId,
|
|
||||||
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(
|
child: ChannelListWidget(
|
||||||
channels: _realmChannels[x.alias] ?? [],
|
channels: _sortChannels([
|
||||||
|
..._normalChannels,
|
||||||
|
..._directChannels,
|
||||||
|
..._realmChannels.values.expand((x) => x),
|
||||||
|
]),
|
||||||
selfId: selfId,
|
selfId: selfId,
|
||||||
useReplace: AppTheme.isLargeScreen(context),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
}),
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,61 +87,63 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(
|
return RootContainer(
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
Padding(
|
||||||
child: Row(
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
children: [
|
child: Row(
|
||||||
const CircleAvatar(
|
children: [
|
||||||
radius: 28,
|
const CircleAvatar(
|
||||||
backgroundColor: Colors.teal,
|
radius: 28,
|
||||||
child: Icon(Icons.group, color: Colors.white),
|
backgroundColor: Colors.teal,
|
||||||
),
|
child: Icon(Icons.group, color: Colors.white),
|
||||||
const Gap(16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(widget.realm.name,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge),
|
|
||||||
Text(widget.realm.description,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
Text(
|
|
||||||
'#${widget.realm.id.toString().padLeft(8, '0')} · ${widget.realm.alias}',
|
|
||||||
style: const TextStyle(fontSize: 11),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
const Gap(16),
|
||||||
],
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.realm.name,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge),
|
||||||
|
Text(widget.realm.description,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
Text(
|
||||||
|
'#${widget.realm.id.toString().padLeft(8, '0')} · ${widget.realm.alias}',
|
||||||
|
style: const TextStyle(fontSize: 11),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const Divider(thickness: 0.3),
|
||||||
const Divider(thickness: 0.3),
|
Expanded(
|
||||||
Expanded(
|
child: ListView(
|
||||||
child: ListView(
|
children: [
|
||||||
children: [
|
ListTile(
|
||||||
ListTile(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
leading: const Icon(Icons.supervisor_account),
|
||||||
leading: const Icon(Icons.supervisor_account),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
title: Text('realmMembers'.tr),
|
||||||
title: Text('realmMembers'.tr),
|
onTap: () => showMemberList(),
|
||||||
onTap: () => showMemberList(),
|
),
|
||||||
),
|
...(_isOwned ? ownerActions : List.empty()),
|
||||||
...(_isOwned ? ownerActions : List.empty()),
|
const Divider(thickness: 0.3),
|
||||||
const Divider(thickness: 0.3),
|
ListTile(
|
||||||
ListTile(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
leading: _isOwned
|
||||||
leading: _isOwned
|
? const Icon(Icons.delete)
|
||||||
? const Icon(Icons.delete)
|
: const Icon(Icons.exit_to_app),
|
||||||
: const Icon(Icons.exit_to_app),
|
title: Text(_isOwned ? 'delete'.tr : 'leave'.tr),
|
||||||
title: Text(_isOwned ? 'delete'.tr : 'leave'.tr),
|
onTap: () => promptLeaveChannel(),
|
||||||
onTap: () => promptLeaveChannel(),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
|
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';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
@ -23,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(
|
||||||
@ -33,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(
|
SolianThemeData(
|
||||||
AppTheme.build(
|
id: 'themeColorBlue',
|
||||||
Brightness.light,
|
seedColor: const Color.fromRGBO(103, 96, 193, 1),
|
||||||
seedColor: color,
|
),
|
||||||
),
|
SolianThemeData(
|
||||||
AppTheme.build(
|
id: 'themeColorMiku',
|
||||||
Brightness.dark,
|
seedColor: const Color.fromRGBO(56, 120, 126, 1),
|
||||||
seedColor: color,
|
),
|
||||||
),
|
SolianThemeData(
|
||||||
);
|
id: 'themeColorKagamine',
|
||||||
_prefs?.setInt('global_theme_color', color.value);
|
seedColor: const Color.fromRGBO(244, 183, 63, 1),
|
||||||
context.clearSnackbar();
|
),
|
||||||
context.showSnackbar('themeColorApplied'.tr);
|
SolianThemeData(
|
||||||
},
|
id: 'themeColorLuka',
|
||||||
);
|
seedColor: const Color.fromRGBO(243, 174, 218, 1),
|
||||||
}
|
),
|
||||||
|
|
||||||
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) {
|
||||||
@ -79,16 +86,98 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
return RootContainer(
|
return RootContainer(
|
||||||
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>(
|
||||||
.toList(),
|
isExpanded: true,
|
||||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
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(),
|
||||||
|
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(
|
||||||
message: 'settingsNotificationBgServiceDesc'.tr,
|
message: 'settingsNotificationBgServiceDesc'.tr,
|
||||||
|
@ -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,24 +27,26 @@ 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(
|
||||||
appBar: showAppBar
|
child: Scaffold(
|
||||||
? AppBar(
|
appBar: showAppBar
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
? AppBar(
|
||||||
title: AppBarTitle(
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
|
title: AppBarTitle(
|
||||||
),
|
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
|
||||||
centerTitle: isCenteredTitle,
|
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
|
||||||
actions: [
|
|
||||||
const BackgroundStateWidget(),
|
|
||||||
SizedBox(
|
|
||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
|
||||||
),
|
),
|
||||||
],
|
centerTitle: isCenteredTitle,
|
||||||
)
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
: null,
|
actions: [
|
||||||
body: child,
|
const BackgroundStateWidget(),
|
||||||
|
SizedBox(
|
||||||
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
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 {
|
||||||
@ -38,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,
|
||||||
),
|
),
|
||||||
@ -55,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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
|
|
||||||
class RootContainer extends StatelessWidget {
|
class RootContainer extends StatelessWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
@ -7,9 +11,38 @@ class RootContainer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return FutureBuilder(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
future: PlatformInfo.isWeb
|
||||||
child: child,
|
? 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.5,
|
||||||
|
image: FileImage(file),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user