Commit cb2e56adba0d67c718e495f89131f0b738fbc7f9

Authored by 权海
1 parent 74e2e02e

feat(ui):主题

... ... @@ -10,7 +10,7 @@ class WatchThemeController extends GetxController {
final ThemeApi _themeApi;
final selectedOfficialIndex = 0.obs;
final activeThemeId = RxnInt();
final hasCustomThemes = false.obs;
final isPremium = false.obs;
final isLoadingThemes = false.obs;
... ... @@ -30,9 +30,19 @@ class WatchThemeController extends GetxController {
Future<void> loadThemeList() async {
isLoadingThemes.value = true;
final result = await _themeApi.getThemeList(errorHandlingPolicy: null);
final themeListFuture = _themeApi.getThemeList(errorHandlingPolicy: null);
final activeThemeFuture =
_themeApi.getCurrentTheme(errorHandlingPolicy: null);
final result = await themeListFuture;
final activeResult = await activeThemeFuture;
isLoadingThemes.value = false;
if (activeResult case AppSuccess<WatchThemeItem>(data: final theme)) {
activeThemeId.value = theme.id;
} else {
activeThemeId.value = null;
}
if (result is! AppSuccess<WatchThemeResponse>) {
return;
}
... ... @@ -42,7 +52,6 @@ class WatchThemeController extends GetxController {
final custom = themes.where((theme) => theme.isCustomTheme).toList();
officialThemeItems.assignAll(official);
customThemeItems.assignAll(custom);
selectedOfficialIndex.value = 0;
hasCustomThemes.value = custom.isNotEmpty;
isPremium.value = isPremium.value || hasCustomThemes.value;
}
... ... @@ -51,20 +60,22 @@ class WatchThemeController extends GetxController {
Get.back();
}
void selectOfficialTheme(int index) {
selectedOfficialIndex.value = index;
Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: {
Future<void> selectOfficialTheme(int index) async {
await Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: {
'theme': officialThemeItems[index],
});
await loadThemeList();
}
void createCustomTheme() {
Get.toNamed(Routes.WATCH_THEME_CREATE);
Future<void> createCustomTheme() async {
await Get.toNamed(Routes.WATCH_THEME_CREATE);
await loadThemeList();
}
void previewCustomTheme(WatchThemeItem theme) {
Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: {
Future<void> previewCustomTheme(WatchThemeItem theme) async {
await Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: {
'theme': theme,
});
await loadThemeList();
}
}
... ...
... ... @@ -60,19 +60,19 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> {
Expanded(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(bottom: 28.dp),
padding: EdgeInsets.only(bottom: 28),
child: Column(
children: [
SizedBox(height: 12.dp),
SizedBox(height: 12),
WatchFacePreview(
faceAsset:
R.assetsImagesWatchThemeCustomFaceCreate,
),
SizedBox(height: 20.dp),
SizedBox(height: 20),
_EditorCard(controller: controller),
SizedBox(height: 26.dp),
SizedBox(height: 26),
_AgreementRow(controller: controller),
SizedBox(height: 18.dp),
SizedBox(height: 18),
_SaveButton(controller: controller),
],
),
... ... @@ -104,11 +104,11 @@ class _EditorCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16.dp),
padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 20.dp),
margin: EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.dp),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
... ... @@ -117,19 +117,19 @@ class _EditorCard extends StatelessWidget {
'自定义主题',
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 16.dp,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.dp),
SizedBox(height: 4),
Text(
'支持上传图片、视频、Live图、Gif、最长5秒',
style: TextStyle(
color: WatchThemeColors.textSecondary,
fontSize: 12.dp,
fontSize: 12,
),
),
SizedBox(height: 14.dp),
SizedBox(height: 14),
GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
... ... @@ -137,9 +137,9 @@ class _EditorCard extends StatelessWidget {
itemCount: 4,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 23.dp,
mainAxisSpacing: 14.dp,
mainAxisExtent: 170.dp,
crossAxisSpacing: 23,
mainAxisSpacing: 14,
mainAxisExtent: 170,
),
itemBuilder: (context, index) {
return Obx(
... ... @@ -154,22 +154,22 @@ class _EditorCard extends StatelessWidget {
);
},
),
SizedBox(height: 14.dp),
SizedBox(height: 14),
Text(
'主题名称',
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 16.dp,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 14.dp),
SizedBox(height: 14),
Container(
height: 52.dp,
padding: EdgeInsets.symmetric(horizontal: 26.dp),
height: 52,
padding: EdgeInsets.symmetric(horizontal: 26),
decoration: BoxDecoration(
color: WatchThemeColors.background,
borderRadius: BorderRadius.circular(16.dp),
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: TextField(
... ... @@ -180,14 +180,14 @@ class _EditorCard extends StatelessWidget {
hintText: '最多10个字符',
hintStyle: TextStyle(
color: WatchThemeColors.textTertiary,
fontSize: 14.dp,
fontSize: 14,
),
border: InputBorder.none,
isCollapsed: true,
),
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 14.dp,
fontSize: 14,
),
),
),
... ... @@ -222,19 +222,19 @@ class _UploadTile extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onTap: onPick,
child: Container(
width: 140.dp,
height: 140.dp,
width: 140,
height: 140,
decoration: BoxDecoration(
color: WatchThemeColors.background,
borderRadius: BorderRadius.circular(18.667.dp),
borderRadius: BorderRadius.circular(18.667),
),
clipBehavior: Clip.antiAlias,
child: path == null
? Icon(Icons.add, size: 34.dp, color: color)
? Icon(Icons.add, size: 34, color: color)
: Image.file(File(path!), fit: BoxFit.cover),
),
),
SizedBox(height: 10.dp),
SizedBox(height: 10),
GestureDetector(
onTap: onRename,
child:
... ... @@ -245,14 +245,14 @@ class _UploadTile extends StatelessWidget {
label,
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 12.dp,
fontSize: 12,
),
),
SizedBox(width: 4.dp),
SizedBox(width: 4),
Icon(
Icons.edit_outlined,
color: const Color(0xFFA084EF),
size: 14.dp,
size: 14,
),
],
),
... ... @@ -277,25 +277,25 @@ class _AgreementRow extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 16.dp,
height: 16.dp,
width: 16,
height: 16,
decoration: BoxDecoration(
color: controller.agreedToSubmission.value
? WatchThemeColors.brand
: Colors.transparent,
border: Border.all(color: WatchThemeColors.brand, width: 1.dp),
border: Border.all(color: WatchThemeColors.brand, width: 1),
shape: BoxShape.circle,
),
child: controller.agreedToSubmission.value
? Icon(Icons.check, color: Colors.white, size: 12.dp)
? Icon(Icons.check, color: Colors.white, size: 12)
: null,
),
SizedBox(width: 4.dp),
SizedBox(width: 4),
Text(
'我已阅读并同意用户投稿协议',
style: TextStyle(
color: WatchThemeColors.textSecondary,
fontSize: 12.dp,
fontSize: 12,
),
),
],
... ... @@ -320,18 +320,18 @@ class _SaveButton extends StatelessWidget {
child: GestureDetector(
onTap: enabled ? () => controller.saveCustomTheme() : null,
child: Container(
width: 280.dp,
height: 48.dp,
width: 280,
height: 48,
alignment: Alignment.center,
decoration: BoxDecoration(
color: WatchThemeColors.brand,
borderRadius: BorderRadius.circular(24.dp),
borderRadius: BorderRadius.circular(24),
),
child: Text(
controller.isSaving.value ? '保存中' : '保存主题',
style: TextStyle(
color: Colors.white,
fontSize: 16.dp,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
... ...
... ... @@ -63,8 +63,7 @@ class WatchThemeView extends GetView<WatchThemeController> {
const SizedBox(height: 26),
OfficialThemeGrid(
themes: controller.officialThemeItems,
selectedIndex:
controller.selectedOfficialIndex.value,
activeThemeId: controller.activeThemeId.value,
onThemeTap: controller.selectOfficialTheme,
),
const SizedBox(height: 12),
... ... @@ -72,6 +71,7 @@ class WatchThemeView extends GetView<WatchThemeController> {
isPremium: controller.isPremium.value,
hasCustomThemes: controller.hasCustomThemes.value,
customThemes: controller.customThemeItems,
activeThemeId: controller.activeThemeId.value,
onCreateTap: controller.createCustomTheme,
onThemeTap: controller.previewCustomTheme,
),
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import '../models/watch_theme_models.dart';
import 'watch_theme_character_avatar.dart';
import 'watch_theme_colors.dart';
import 'watch_theme_pro_badge.dart';
import 'watch_theme_selected_view.dart';
import 'watch_theme_section_card.dart';
class CustomThemeCard extends StatelessWidget {
... ... @@ -13,6 +13,7 @@ class CustomThemeCard extends StatelessWidget {
required this.isPremium,
required this.hasCustomThemes,
required this.customThemes,
required this.activeThemeId,
required this.onCreateTap,
this.onThemeTap,
});
... ... @@ -20,6 +21,7 @@ class CustomThemeCard extends StatelessWidget {
final bool isPremium;
final bool hasCustomThemes;
final List<WatchThemeItem> customThemes;
final int? activeThemeId;
final VoidCallback onCreateTap;
final ValueChanged<WatchThemeItem>? onThemeTap;
... ... @@ -33,32 +35,34 @@ class CustomThemeCard extends StatelessWidget {
'自定义主题',
trailing: isPremium ? null : const WatchThemeProBadge(),
),
SizedBox(height: 4.dp),
SizedBox(height: 4),
Text(
'用创意记录每一次情绪波动,打造懂你的专属表盘吧~',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: WatchThemeColors.textSecondary,
fontSize: 12.dp,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.25,
),
),
SizedBox(height: 14.dp),
SizedBox(height: 14),
_CreateThemeButton(onTap: onCreateTap),
if (hasCustomThemes) ...[
SizedBox(height: 12.dp),
SizedBox(height: 12),
Row(
children: [
for (var index = 0; index < customThemes.length; index++) ...[
_CustomThemeItem(
item: customThemes[index],
selected: customThemes[index].id != null &&
customThemes[index].id == activeThemeId,
onTap: onThemeTap == null
? null
: () => onThemeTap!(customThemes[index]),
),
if (index != customThemes.length - 1) SizedBox(width: 20.dp),
if (index != customThemes.length - 1) SizedBox(width: 20),
],
],
),
... ... @@ -80,10 +84,10 @@ class _CreateThemeButton extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 48.dp,
padding: EdgeInsets.symmetric(horizontal: 20.dp),
height: 48,
padding: EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.dp),
borderRadius: BorderRadius.circular(12),
gradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
... ... @@ -101,15 +105,15 @@ class _CreateThemeButton extends StatelessWidget {
Icon(
Icons.palette_outlined,
color: WatchThemeColors.overload,
size: 28.dp,
size: 28,
),
SizedBox(width: 8.dp),
SizedBox(width: 8),
Text(
'创作主题',
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 14.dp,
fontWeight: FontWeight.w500,
fontSize: 14,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
... ... @@ -117,7 +121,7 @@ class _CreateThemeButton extends StatelessWidget {
Icon(
Icons.chevron_right,
color: WatchThemeColors.textSecondary,
size: 20.dp,
size: 20,
),
],
),
... ... @@ -127,9 +131,14 @@ class _CreateThemeButton extends StatelessWidget {
}
class _CustomThemeItem extends StatelessWidget {
const _CustomThemeItem({required this.item, this.onTap});
const _CustomThemeItem({
required this.item,
required this.selected,
this.onTap,
});
final WatchThemeItem item;
final bool selected;
final VoidCallback? onTap;
@override
... ... @@ -138,23 +147,27 @@ class _CustomThemeItem extends StatelessWidget {
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
width: 88.dp,
width: 88,
child: Column(
children: [
WatchThemeCharacterAvatar(
WatchThemeSelectedView(
selected: selected,
size: 88,
assetPath: item.infoList.firstOrNull?.assetPath,
imageUrl: item.infoList.firstOrNull?.imgUrl,
empty: item.infoList.firstOrNull?.image == null,
child: WatchThemeCharacterAvatar(
size: 88,
assetPath: item.infoList.firstOrNull?.assetPath,
imageUrl: item.infoList.firstOrNull?.imgUrl,
empty: item.infoList.firstOrNull?.image == null,
),
),
SizedBox(height: 8.dp),
SizedBox(height: 8),
Text(
item.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 12.dp,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.25,
),
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import '../models/watch_theme_models.dart';
import 'watch_theme_character_avatar.dart';
import 'watch_theme_colors.dart';
import 'watch_theme_selected_view.dart';
import 'watch_theme_section_card.dart';
class OfficialThemeGrid extends StatelessWidget {
const OfficialThemeGrid({
super.key,
required this.themes,
required this.selectedIndex,
required this.activeThemeId,
required this.onThemeTap,
});
final List<WatchThemeItem> themes;
final int selectedIndex;
final int? activeThemeId;
final ValueChanged<int> onThemeTap;
@override
... ... @@ -25,7 +25,7 @@ class OfficialThemeGrid extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const WatchThemeSectionTitle('官方主题'),
SizedBox(height: 22.dp),
SizedBox(height: 22),
GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
... ... @@ -44,11 +44,14 @@ class OfficialThemeGrid extends StatelessWidget {
onTap: () => onThemeTap(index),
child: Column(
children: [
WatchThemeCharacterAvatar(
WatchThemeSelectedView(
selected: theme.id != null && theme.id == activeThemeId,
size: 88,
selected: index == selectedIndex,
assetPath: theme.infoList.firstOrNull?.assetPath,
imageUrl: theme.infoList.firstOrNull?.imgUrl,
child: WatchThemeCharacterAvatar(
size: 88,
assetPath: theme.infoList.firstOrNull?.assetPath,
imageUrl: theme.infoList.firstOrNull?.imgUrl,
),
),
SizedBox(height: 8),
Text(
... ... @@ -58,7 +61,7 @@ class OfficialThemeGrid extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 12.dp,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.25,
),
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'watch_theme_colors.dart';
class WatchThemeCharacterAvatar extends StatelessWidget {
const WatchThemeCharacterAvatar({
super.key,
required this.size,
this.assetPath,
this.imageUrl,
this.selected = false,
this.empty = false,
});
final double size;
final String? assetPath;
final String? imageUrl;
final bool selected;
final bool empty;
@override
... ... @@ -25,47 +20,23 @@ class WatchThemeCharacterAvatar extends StatelessWidget {
return SizedBox(
width: avatarSize,
height: avatarSize,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
width: avatarSize,
height: avatarSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: selected
? Border.all(color: WatchThemeColors.brand, width: 2.dp)
: null,
),
alignment: Alignment.center,
child: empty || (assetPath == null && imageUrl == null)
? const SizedBox.shrink()
: _buildImage(avatarSize)),
if (selected)
Positioned(
right: -1.dp,
bottom: 9.dp,
child: Container(
width: 16.dp,
height: 16.dp,
decoration: const BoxDecoration(
color: WatchThemeColors.brand,
shape: BoxShape.circle,
),
child: Icon(
Icons.check,
color: Colors.white,
size: 12.dp,
),
),
),
],
child: Container(
width: avatarSize,
height: avatarSize,
decoration: const BoxDecoration(
color: Color(0xFFF5F2FF),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: empty || (assetPath == null && imageUrl == null)
? const SizedBox.shrink()
: _buildImage(avatarSize),
),
);
}
Widget _buildImage(double avatarSize) {
final width = avatarSize * 0.82;
final width = avatarSize * 0.636;
if (imageUrl != null && imageUrl!.isNotEmpty) {
return Image.network(
imageUrl!,
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'watch_theme_colors.dart';
class WatchThemeProBadge extends StatelessWidget {
const WatchThemeProBadge({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 16.dp,
padding: EdgeInsets.only(left: 5.dp, right: 6.dp),
decoration: BoxDecoration(
color: WatchThemeColors.pro,
borderRadius: BorderRadius.circular(26.dp),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.workspace_premium,
color: WatchThemeColors.textPrimary,
size: 10.dp,
),
SizedBox(width: 1.dp),
Text(
'PRO',
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 10.dp,
fontWeight: FontWeight.w600,
height: 1.1,
),
),
],
),
return Image(
image: AssetImage('assets/images/common/ic_pro_badge.webp'),
width: 43,
height: 16,
);
}
}
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'watch_theme_colors.dart';
class WatchThemeSectionCard extends StatelessWidget {
... ... @@ -17,11 +15,11 @@ class WatchThemeSectionCard extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 16.dp),
padding: padding ?? EdgeInsets.all(20.dp),
margin: EdgeInsets.symmetric(horizontal: 16),
padding: padding ?? EdgeInsets.all(20),
decoration: BoxDecoration(
color: WatchThemeColors.card,
borderRadius: BorderRadius.circular(16.dp),
borderRadius: BorderRadius.circular(16),
),
child: child,
);
... ... @@ -43,13 +41,13 @@ class WatchThemeSectionTitle extends StatelessWidget {
title,
style: TextStyle(
color: WatchThemeColors.textPrimary,
fontSize: 16.dp,
fontWeight: FontWeight.w500,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
if (trailing != null) ...[
SizedBox(width: 4.dp),
SizedBox(width: 4),
trailing!,
],
],
... ...
import 'package:flutter/material.dart';
import 'watch_theme_colors.dart';
class WatchThemeSelectedView extends StatelessWidget {
const WatchThemeSelectedView({
super.key,
required this.selected,
required this.size,
required this.child,
});
final bool selected;
final double size;
final Widget child;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: Stack(
clipBehavior: Clip.none,
children: [
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: selected
? Border.all(color: WatchThemeColors.brand, width: 2)
: null,
),
child: child,
),
if (selected)
Positioned(
right: -1,
bottom: 9,
child: Container(
width: 16,
height: 16,
decoration: const BoxDecoration(
color: WatchThemeColors.brand,
shape: BoxShape.circle,
),
child: Icon(
Icons.check,
color: Colors.white,
size: 12,
),
),
),
],
),
);
}
}
... ...
... ... @@ -89,6 +89,11 @@ class AppleProductPaymentResult {
});
}
enum HResourceType {
image,
video,
}
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/platform_api.g.dart',
... ... @@ -121,8 +126,8 @@ abstract class PlatformHostApi {
/// 刷新会员信息
void refreshVip();
/// 刷新watch app 和 表盘的所有数据:
/// 刷新watch app 和 表盘的所有数据:
/// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
void refreshWatchAppAndWidgets();
... ... @@ -143,4 +148,9 @@ abstract class PlatformHostApi {
/// 恢复购买
@async
bool performRestore();
/// 上传文件到云端
/// 注意catch flutter error
@async
String? uploadFile(String filePath, HResourceType resourceType);
}
... ...