Commit cb2e56adba0d67c718e495f89131f0b738fbc7f9

Authored by 权海
1 parent 74e2e02e

feat(ui):主题

@@ -10,7 +10,7 @@ class WatchThemeController extends GetxController { @@ -10,7 +10,7 @@ class WatchThemeController extends GetxController {
10 10
11 final ThemeApi _themeApi; 11 final ThemeApi _themeApi;
12 12
13 - final selectedOfficialIndex = 0.obs; 13 + final activeThemeId = RxnInt();
14 final hasCustomThemes = false.obs; 14 final hasCustomThemes = false.obs;
15 final isPremium = false.obs; 15 final isPremium = false.obs;
16 final isLoadingThemes = false.obs; 16 final isLoadingThemes = false.obs;
@@ -30,9 +30,19 @@ class WatchThemeController extends GetxController { @@ -30,9 +30,19 @@ class WatchThemeController extends GetxController {
30 30
31 Future<void> loadThemeList() async { 31 Future<void> loadThemeList() async {
32 isLoadingThemes.value = true; 32 isLoadingThemes.value = true;
33 - final result = await _themeApi.getThemeList(errorHandlingPolicy: null); 33 + final themeListFuture = _themeApi.getThemeList(errorHandlingPolicy: null);
  34 + final activeThemeFuture =
  35 + _themeApi.getCurrentTheme(errorHandlingPolicy: null);
  36 + final result = await themeListFuture;
  37 + final activeResult = await activeThemeFuture;
34 isLoadingThemes.value = false; 38 isLoadingThemes.value = false;
35 39
  40 + if (activeResult case AppSuccess<WatchThemeItem>(data: final theme)) {
  41 + activeThemeId.value = theme.id;
  42 + } else {
  43 + activeThemeId.value = null;
  44 + }
  45 +
36 if (result is! AppSuccess<WatchThemeResponse>) { 46 if (result is! AppSuccess<WatchThemeResponse>) {
37 return; 47 return;
38 } 48 }
@@ -42,7 +52,6 @@ class WatchThemeController extends GetxController { @@ -42,7 +52,6 @@ class WatchThemeController extends GetxController {
42 final custom = themes.where((theme) => theme.isCustomTheme).toList(); 52 final custom = themes.where((theme) => theme.isCustomTheme).toList();
43 officialThemeItems.assignAll(official); 53 officialThemeItems.assignAll(official);
44 customThemeItems.assignAll(custom); 54 customThemeItems.assignAll(custom);
45 - selectedOfficialIndex.value = 0;  
46 hasCustomThemes.value = custom.isNotEmpty; 55 hasCustomThemes.value = custom.isNotEmpty;
47 isPremium.value = isPremium.value || hasCustomThemes.value; 56 isPremium.value = isPremium.value || hasCustomThemes.value;
48 } 57 }
@@ -51,20 +60,22 @@ class WatchThemeController extends GetxController { @@ -51,20 +60,22 @@ class WatchThemeController extends GetxController {
51 Get.back(); 60 Get.back();
52 } 61 }
53 62
54 - void selectOfficialTheme(int index) {  
55 - selectedOfficialIndex.value = index;  
56 - Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: { 63 + Future<void> selectOfficialTheme(int index) async {
  64 + await Get.toNamed(Routes.WATCH_THEME_PREVIEW, arguments: {
57 'theme': officialThemeItems[index], 65 'theme': officialThemeItems[index],
58 }); 66 });
  67 + await loadThemeList();
59 } 68 }
60 69
61 - void createCustomTheme() {  
62 - Get.toNamed(Routes.WATCH_THEME_CREATE); 70 + Future<void> createCustomTheme() async {
  71 + await Get.toNamed(Routes.WATCH_THEME_CREATE);
  72 + await loadThemeList();
63 } 73 }
64 74
65 - void previewCustomTheme(WatchThemeItem theme) {  
66 - Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: { 75 + Future<void> previewCustomTheme(WatchThemeItem theme) async {
  76 + await Get.toNamed(Routes.WATCH_THEME_CUSTOM_PREVIEW, arguments: {
67 'theme': theme, 77 'theme': theme,
68 }); 78 });
  79 + await loadThemeList();
69 } 80 }
70 } 81 }
@@ -60,19 +60,19 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> { @@ -60,19 +60,19 @@ class CreateWatchThemeView extends GetView<CreateWatchThemeController> {
60 Expanded( 60 Expanded(
61 child: SingleChildScrollView( 61 child: SingleChildScrollView(
62 physics: const AlwaysScrollableScrollPhysics(), 62 physics: const AlwaysScrollableScrollPhysics(),
63 - padding: EdgeInsets.only(bottom: 28.dp), 63 + padding: EdgeInsets.only(bottom: 28),
64 child: Column( 64 child: Column(
65 children: [ 65 children: [
66 - SizedBox(height: 12.dp), 66 + SizedBox(height: 12),
67 WatchFacePreview( 67 WatchFacePreview(
68 faceAsset: 68 faceAsset:
69 R.assetsImagesWatchThemeCustomFaceCreate, 69 R.assetsImagesWatchThemeCustomFaceCreate,
70 ), 70 ),
71 - SizedBox(height: 20.dp), 71 + SizedBox(height: 20),
72 _EditorCard(controller: controller), 72 _EditorCard(controller: controller),
73 - SizedBox(height: 26.dp), 73 + SizedBox(height: 26),
74 _AgreementRow(controller: controller), 74 _AgreementRow(controller: controller),
75 - SizedBox(height: 18.dp), 75 + SizedBox(height: 18),
76 _SaveButton(controller: controller), 76 _SaveButton(controller: controller),
77 ], 77 ],
78 ), 78 ),
@@ -104,11 +104,11 @@ class _EditorCard extends StatelessWidget { @@ -104,11 +104,11 @@ class _EditorCard extends StatelessWidget {
104 @override 104 @override
105 Widget build(BuildContext context) { 105 Widget build(BuildContext context) {
106 return Container( 106 return Container(
107 - margin: EdgeInsets.symmetric(horizontal: 16.dp),  
108 - padding: EdgeInsets.fromLTRB(20.dp, 20.dp, 20.dp, 20.dp), 107 + margin: EdgeInsets.symmetric(horizontal: 16),
  108 + padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
109 decoration: BoxDecoration( 109 decoration: BoxDecoration(
110 color: Colors.white, 110 color: Colors.white,
111 - borderRadius: BorderRadius.circular(16.dp), 111 + borderRadius: BorderRadius.circular(16),
112 ), 112 ),
113 child: Column( 113 child: Column(
114 crossAxisAlignment: CrossAxisAlignment.start, 114 crossAxisAlignment: CrossAxisAlignment.start,
@@ -117,19 +117,19 @@ class _EditorCard extends StatelessWidget { @@ -117,19 +117,19 @@ class _EditorCard extends StatelessWidget {
117 '自定义主题', 117 '自定义主题',
118 style: TextStyle( 118 style: TextStyle(
119 color: WatchThemeColors.textPrimary, 119 color: WatchThemeColors.textPrimary,
120 - fontSize: 16.dp, 120 + fontSize: 16,
121 fontWeight: FontWeight.w500, 121 fontWeight: FontWeight.w500,
122 ), 122 ),
123 ), 123 ),
124 - SizedBox(height: 4.dp), 124 + SizedBox(height: 4),
125 Text( 125 Text(
126 '支持上传图片、视频、Live图、Gif、最长5秒', 126 '支持上传图片、视频、Live图、Gif、最长5秒',
127 style: TextStyle( 127 style: TextStyle(
128 color: WatchThemeColors.textSecondary, 128 color: WatchThemeColors.textSecondary,
129 - fontSize: 12.dp, 129 + fontSize: 12,
130 ), 130 ),
131 ), 131 ),
132 - SizedBox(height: 14.dp), 132 + SizedBox(height: 14),
133 GridView.builder( 133 GridView.builder(
134 shrinkWrap: true, 134 shrinkWrap: true,
135 padding: EdgeInsets.zero, 135 padding: EdgeInsets.zero,
@@ -137,9 +137,9 @@ class _EditorCard extends StatelessWidget { @@ -137,9 +137,9 @@ class _EditorCard extends StatelessWidget {
137 itemCount: 4, 137 itemCount: 4,
138 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 138 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
139 crossAxisCount: 2, 139 crossAxisCount: 2,
140 - crossAxisSpacing: 23.dp,  
141 - mainAxisSpacing: 14.dp,  
142 - mainAxisExtent: 170.dp, 140 + crossAxisSpacing: 23,
  141 + mainAxisSpacing: 14,
  142 + mainAxisExtent: 170,
143 ), 143 ),
144 itemBuilder: (context, index) { 144 itemBuilder: (context, index) {
145 return Obx( 145 return Obx(
@@ -154,22 +154,22 @@ class _EditorCard extends StatelessWidget { @@ -154,22 +154,22 @@ class _EditorCard extends StatelessWidget {
154 ); 154 );
155 }, 155 },
156 ), 156 ),
157 - SizedBox(height: 14.dp), 157 + SizedBox(height: 14),
158 Text( 158 Text(
159 '主题名称', 159 '主题名称',
160 style: TextStyle( 160 style: TextStyle(
161 color: WatchThemeColors.textPrimary, 161 color: WatchThemeColors.textPrimary,
162 - fontSize: 16.dp, 162 + fontSize: 16,
163 fontWeight: FontWeight.w500, 163 fontWeight: FontWeight.w500,
164 ), 164 ),
165 ), 165 ),
166 - SizedBox(height: 14.dp), 166 + SizedBox(height: 14),
167 Container( 167 Container(
168 - height: 52.dp,  
169 - padding: EdgeInsets.symmetric(horizontal: 26.dp), 168 + height: 52,
  169 + padding: EdgeInsets.symmetric(horizontal: 26),
170 decoration: BoxDecoration( 170 decoration: BoxDecoration(
171 color: WatchThemeColors.background, 171 color: WatchThemeColors.background,
172 - borderRadius: BorderRadius.circular(16.dp), 172 + borderRadius: BorderRadius.circular(16),
173 ), 173 ),
174 alignment: Alignment.center, 174 alignment: Alignment.center,
175 child: TextField( 175 child: TextField(
@@ -180,14 +180,14 @@ class _EditorCard extends StatelessWidget { @@ -180,14 +180,14 @@ class _EditorCard extends StatelessWidget {
180 hintText: '最多10个字符', 180 hintText: '最多10个字符',
181 hintStyle: TextStyle( 181 hintStyle: TextStyle(
182 color: WatchThemeColors.textTertiary, 182 color: WatchThemeColors.textTertiary,
183 - fontSize: 14.dp, 183 + fontSize: 14,
184 ), 184 ),
185 border: InputBorder.none, 185 border: InputBorder.none,
186 isCollapsed: true, 186 isCollapsed: true,
187 ), 187 ),
188 style: TextStyle( 188 style: TextStyle(
189 color: WatchThemeColors.textPrimary, 189 color: WatchThemeColors.textPrimary,
190 - fontSize: 14.dp, 190 + fontSize: 14,
191 ), 191 ),
192 ), 192 ),
193 ), 193 ),
@@ -222,19 +222,19 @@ class _UploadTile extends StatelessWidget { @@ -222,19 +222,19 @@ class _UploadTile extends StatelessWidget {
222 behavior: HitTestBehavior.opaque, 222 behavior: HitTestBehavior.opaque,
223 onTap: onPick, 223 onTap: onPick,
224 child: Container( 224 child: Container(
225 - width: 140.dp,  
226 - height: 140.dp, 225 + width: 140,
  226 + height: 140,
227 decoration: BoxDecoration( 227 decoration: BoxDecoration(
228 color: WatchThemeColors.background, 228 color: WatchThemeColors.background,
229 - borderRadius: BorderRadius.circular(18.667.dp), 229 + borderRadius: BorderRadius.circular(18.667),
230 ), 230 ),
231 clipBehavior: Clip.antiAlias, 231 clipBehavior: Clip.antiAlias,
232 child: path == null 232 child: path == null
233 - ? Icon(Icons.add, size: 34.dp, color: color) 233 + ? Icon(Icons.add, size: 34, color: color)
234 : Image.file(File(path!), fit: BoxFit.cover), 234 : Image.file(File(path!), fit: BoxFit.cover),
235 ), 235 ),
236 ), 236 ),
237 - SizedBox(height: 10.dp), 237 + SizedBox(height: 10),
238 GestureDetector( 238 GestureDetector(
239 onTap: onRename, 239 onTap: onRename,
240 child: 240 child:
@@ -245,14 +245,14 @@ class _UploadTile extends StatelessWidget { @@ -245,14 +245,14 @@ class _UploadTile extends StatelessWidget {
245 label, 245 label,
246 style: TextStyle( 246 style: TextStyle(
247 color: WatchThemeColors.textPrimary, 247 color: WatchThemeColors.textPrimary,
248 - fontSize: 12.dp, 248 + fontSize: 12,
249 ), 249 ),
250 ), 250 ),
251 - SizedBox(width: 4.dp), 251 + SizedBox(width: 4),
252 Icon( 252 Icon(
253 Icons.edit_outlined, 253 Icons.edit_outlined,
254 color: const Color(0xFFA084EF), 254 color: const Color(0xFFA084EF),
255 - size: 14.dp, 255 + size: 14,
256 ), 256 ),
257 ], 257 ],
258 ), 258 ),
@@ -277,25 +277,25 @@ class _AgreementRow extends StatelessWidget { @@ -277,25 +277,25 @@ class _AgreementRow extends StatelessWidget {
277 mainAxisAlignment: MainAxisAlignment.center, 277 mainAxisAlignment: MainAxisAlignment.center,
278 children: [ 278 children: [
279 Container( 279 Container(
280 - width: 16.dp,  
281 - height: 16.dp, 280 + width: 16,
  281 + height: 16,
282 decoration: BoxDecoration( 282 decoration: BoxDecoration(
283 color: controller.agreedToSubmission.value 283 color: controller.agreedToSubmission.value
284 ? WatchThemeColors.brand 284 ? WatchThemeColors.brand
285 : Colors.transparent, 285 : Colors.transparent,
286 - border: Border.all(color: WatchThemeColors.brand, width: 1.dp), 286 + border: Border.all(color: WatchThemeColors.brand, width: 1),
287 shape: BoxShape.circle, 287 shape: BoxShape.circle,
288 ), 288 ),
289 child: controller.agreedToSubmission.value 289 child: controller.agreedToSubmission.value
290 - ? Icon(Icons.check, color: Colors.white, size: 12.dp) 290 + ? Icon(Icons.check, color: Colors.white, size: 12)
291 : null, 291 : null,
292 ), 292 ),
293 - SizedBox(width: 4.dp), 293 + SizedBox(width: 4),
294 Text( 294 Text(
295 '我已阅读并同意用户投稿协议', 295 '我已阅读并同意用户投稿协议',
296 style: TextStyle( 296 style: TextStyle(
297 color: WatchThemeColors.textSecondary, 297 color: WatchThemeColors.textSecondary,
298 - fontSize: 12.dp, 298 + fontSize: 12,
299 ), 299 ),
300 ), 300 ),
301 ], 301 ],
@@ -320,18 +320,18 @@ class _SaveButton extends StatelessWidget { @@ -320,18 +320,18 @@ class _SaveButton extends StatelessWidget {
320 child: GestureDetector( 320 child: GestureDetector(
321 onTap: enabled ? () => controller.saveCustomTheme() : null, 321 onTap: enabled ? () => controller.saveCustomTheme() : null,
322 child: Container( 322 child: Container(
323 - width: 280.dp,  
324 - height: 48.dp, 323 + width: 280,
  324 + height: 48,
325 alignment: Alignment.center, 325 alignment: Alignment.center,
326 decoration: BoxDecoration( 326 decoration: BoxDecoration(
327 color: WatchThemeColors.brand, 327 color: WatchThemeColors.brand,
328 - borderRadius: BorderRadius.circular(24.dp), 328 + borderRadius: BorderRadius.circular(24),
329 ), 329 ),
330 child: Text( 330 child: Text(
331 controller.isSaving.value ? '保存中' : '保存主题', 331 controller.isSaving.value ? '保存中' : '保存主题',
332 style: TextStyle( 332 style: TextStyle(
333 color: Colors.white, 333 color: Colors.white,
334 - fontSize: 16.dp, 334 + fontSize: 16,
335 fontWeight: FontWeight.w600, 335 fontWeight: FontWeight.w600,
336 ), 336 ),
337 ), 337 ),
@@ -63,8 +63,7 @@ class WatchThemeView extends GetView<WatchThemeController> { @@ -63,8 +63,7 @@ class WatchThemeView extends GetView<WatchThemeController> {
63 const SizedBox(height: 26), 63 const SizedBox(height: 26),
64 OfficialThemeGrid( 64 OfficialThemeGrid(
65 themes: controller.officialThemeItems, 65 themes: controller.officialThemeItems,
66 - selectedIndex:  
67 - controller.selectedOfficialIndex.value, 66 + activeThemeId: controller.activeThemeId.value,
68 onThemeTap: controller.selectOfficialTheme, 67 onThemeTap: controller.selectOfficialTheme,
69 ), 68 ),
70 const SizedBox(height: 12), 69 const SizedBox(height: 12),
@@ -72,6 +71,7 @@ class WatchThemeView extends GetView<WatchThemeController> { @@ -72,6 +71,7 @@ class WatchThemeView extends GetView<WatchThemeController> {
72 isPremium: controller.isPremium.value, 71 isPremium: controller.isPremium.value,
73 hasCustomThemes: controller.hasCustomThemes.value, 72 hasCustomThemes: controller.hasCustomThemes.value,
74 customThemes: controller.customThemeItems, 73 customThemes: controller.customThemeItems,
  74 + activeThemeId: controller.activeThemeId.value,
75 onCreateTap: controller.createCustomTheme, 75 onCreateTap: controller.createCustomTheme,
76 onThemeTap: controller.previewCustomTheme, 76 onThemeTap: controller.previewCustomTheme,
77 ), 77 ),
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
3 2
4 import '../models/watch_theme_models.dart'; 3 import '../models/watch_theme_models.dart';
5 import 'watch_theme_character_avatar.dart'; 4 import 'watch_theme_character_avatar.dart';
6 import 'watch_theme_colors.dart'; 5 import 'watch_theme_colors.dart';
7 import 'watch_theme_pro_badge.dart'; 6 import 'watch_theme_pro_badge.dart';
  7 +import 'watch_theme_selected_view.dart';
8 import 'watch_theme_section_card.dart'; 8 import 'watch_theme_section_card.dart';
9 9
10 class CustomThemeCard extends StatelessWidget { 10 class CustomThemeCard extends StatelessWidget {
@@ -13,6 +13,7 @@ class CustomThemeCard extends StatelessWidget { @@ -13,6 +13,7 @@ class CustomThemeCard extends StatelessWidget {
13 required this.isPremium, 13 required this.isPremium,
14 required this.hasCustomThemes, 14 required this.hasCustomThemes,
15 required this.customThemes, 15 required this.customThemes,
  16 + required this.activeThemeId,
16 required this.onCreateTap, 17 required this.onCreateTap,
17 this.onThemeTap, 18 this.onThemeTap,
18 }); 19 });
@@ -20,6 +21,7 @@ class CustomThemeCard extends StatelessWidget { @@ -20,6 +21,7 @@ class CustomThemeCard extends StatelessWidget {
20 final bool isPremium; 21 final bool isPremium;
21 final bool hasCustomThemes; 22 final bool hasCustomThemes;
22 final List<WatchThemeItem> customThemes; 23 final List<WatchThemeItem> customThemes;
  24 + final int? activeThemeId;
23 final VoidCallback onCreateTap; 25 final VoidCallback onCreateTap;
24 final ValueChanged<WatchThemeItem>? onThemeTap; 26 final ValueChanged<WatchThemeItem>? onThemeTap;
25 27
@@ -33,32 +35,34 @@ class CustomThemeCard extends StatelessWidget { @@ -33,32 +35,34 @@ class CustomThemeCard extends StatelessWidget {
33 '自定义主题', 35 '自定义主题',
34 trailing: isPremium ? null : const WatchThemeProBadge(), 36 trailing: isPremium ? null : const WatchThemeProBadge(),
35 ), 37 ),
36 - SizedBox(height: 4.dp), 38 + SizedBox(height: 4),
37 Text( 39 Text(
38 '用创意记录每一次情绪波动,打造懂你的专属表盘吧~', 40 '用创意记录每一次情绪波动,打造懂你的专属表盘吧~',
39 maxLines: 1, 41 maxLines: 1,
40 overflow: TextOverflow.ellipsis, 42 overflow: TextOverflow.ellipsis,
41 style: TextStyle( 43 style: TextStyle(
42 color: WatchThemeColors.textSecondary, 44 color: WatchThemeColors.textSecondary,
43 - fontSize: 12.dp, 45 + fontSize: 12,
44 fontWeight: FontWeight.w400, 46 fontWeight: FontWeight.w400,
45 height: 1.25, 47 height: 1.25,
46 ), 48 ),
47 ), 49 ),
48 - SizedBox(height: 14.dp), 50 + SizedBox(height: 14),
49 _CreateThemeButton(onTap: onCreateTap), 51 _CreateThemeButton(onTap: onCreateTap),
50 if (hasCustomThemes) ...[ 52 if (hasCustomThemes) ...[
51 - SizedBox(height: 12.dp), 53 + SizedBox(height: 12),
52 Row( 54 Row(
53 children: [ 55 children: [
54 for (var index = 0; index < customThemes.length; index++) ...[ 56 for (var index = 0; index < customThemes.length; index++) ...[
55 _CustomThemeItem( 57 _CustomThemeItem(
56 item: customThemes[index], 58 item: customThemes[index],
  59 + selected: customThemes[index].id != null &&
  60 + customThemes[index].id == activeThemeId,
57 onTap: onThemeTap == null 61 onTap: onThemeTap == null
58 ? null 62 ? null
59 : () => onThemeTap!(customThemes[index]), 63 : () => onThemeTap!(customThemes[index]),
60 ), 64 ),
61 - if (index != customThemes.length - 1) SizedBox(width: 20.dp), 65 + if (index != customThemes.length - 1) SizedBox(width: 20),
62 ], 66 ],
63 ], 67 ],
64 ), 68 ),
@@ -80,10 +84,10 @@ class _CreateThemeButton extends StatelessWidget { @@ -80,10 +84,10 @@ class _CreateThemeButton extends StatelessWidget {
80 behavior: HitTestBehavior.opaque, 84 behavior: HitTestBehavior.opaque,
81 onTap: onTap, 85 onTap: onTap,
82 child: Container( 86 child: Container(
83 - height: 48.dp,  
84 - padding: EdgeInsets.symmetric(horizontal: 20.dp), 87 + height: 48,
  88 + padding: EdgeInsets.symmetric(horizontal: 20),
85 decoration: BoxDecoration( 89 decoration: BoxDecoration(
86 - borderRadius: BorderRadius.circular(12.dp), 90 + borderRadius: BorderRadius.circular(12),
87 gradient: const LinearGradient( 91 gradient: const LinearGradient(
88 begin: Alignment.centerLeft, 92 begin: Alignment.centerLeft,
89 end: Alignment.centerRight, 93 end: Alignment.centerRight,
@@ -101,15 +105,15 @@ class _CreateThemeButton extends StatelessWidget { @@ -101,15 +105,15 @@ class _CreateThemeButton extends StatelessWidget {
101 Icon( 105 Icon(
102 Icons.palette_outlined, 106 Icons.palette_outlined,
103 color: WatchThemeColors.overload, 107 color: WatchThemeColors.overload,
104 - size: 28.dp, 108 + size: 28,
105 ), 109 ),
106 - SizedBox(width: 8.dp), 110 + SizedBox(width: 8),
107 Text( 111 Text(
108 '创作主题', 112 '创作主题',
109 style: TextStyle( 113 style: TextStyle(
110 color: WatchThemeColors.textPrimary, 114 color: WatchThemeColors.textPrimary,
111 - fontSize: 14.dp,  
112 - fontWeight: FontWeight.w500, 115 + fontSize: 14,
  116 + fontWeight: FontWeight.w600,
113 height: 1.25, 117 height: 1.25,
114 ), 118 ),
115 ), 119 ),
@@ -117,7 +121,7 @@ class _CreateThemeButton extends StatelessWidget { @@ -117,7 +121,7 @@ class _CreateThemeButton extends StatelessWidget {
117 Icon( 121 Icon(
118 Icons.chevron_right, 122 Icons.chevron_right,
119 color: WatchThemeColors.textSecondary, 123 color: WatchThemeColors.textSecondary,
120 - size: 20.dp, 124 + size: 20,
121 ), 125 ),
122 ], 126 ],
123 ), 127 ),
@@ -127,9 +131,14 @@ class _CreateThemeButton extends StatelessWidget { @@ -127,9 +131,14 @@ class _CreateThemeButton extends StatelessWidget {
127 } 131 }
128 132
129 class _CustomThemeItem extends StatelessWidget { 133 class _CustomThemeItem extends StatelessWidget {
130 - const _CustomThemeItem({required this.item, this.onTap}); 134 + const _CustomThemeItem({
  135 + required this.item,
  136 + required this.selected,
  137 + this.onTap,
  138 + });
131 139
132 final WatchThemeItem item; 140 final WatchThemeItem item;
  141 + final bool selected;
133 final VoidCallback? onTap; 142 final VoidCallback? onTap;
134 143
135 @override 144 @override
@@ -138,23 +147,27 @@ class _CustomThemeItem extends StatelessWidget { @@ -138,23 +147,27 @@ class _CustomThemeItem extends StatelessWidget {
138 behavior: HitTestBehavior.opaque, 147 behavior: HitTestBehavior.opaque,
139 onTap: onTap, 148 onTap: onTap,
140 child: SizedBox( 149 child: SizedBox(
141 - width: 88.dp, 150 + width: 88,
142 child: Column( 151 child: Column(
143 children: [ 152 children: [
144 - WatchThemeCharacterAvatar( 153 + WatchThemeSelectedView(
  154 + selected: selected,
145 size: 88, 155 size: 88,
146 - assetPath: item.infoList.firstOrNull?.assetPath,  
147 - imageUrl: item.infoList.firstOrNull?.imgUrl,  
148 - empty: item.infoList.firstOrNull?.image == null, 156 + child: WatchThemeCharacterAvatar(
  157 + size: 88,
  158 + assetPath: item.infoList.firstOrNull?.assetPath,
  159 + imageUrl: item.infoList.firstOrNull?.imgUrl,
  160 + empty: item.infoList.firstOrNull?.image == null,
  161 + ),
149 ), 162 ),
150 - SizedBox(height: 8.dp), 163 + SizedBox(height: 8),
151 Text( 164 Text(
152 item.title, 165 item.title,
153 maxLines: 1, 166 maxLines: 1,
154 overflow: TextOverflow.ellipsis, 167 overflow: TextOverflow.ellipsis,
155 style: TextStyle( 168 style: TextStyle(
156 color: WatchThemeColors.textPrimary, 169 color: WatchThemeColors.textPrimary,
157 - fontSize: 12.dp, 170 + fontSize: 12,
158 fontWeight: FontWeight.w400, 171 fontWeight: FontWeight.w400,
159 height: 1.25, 172 height: 1.25,
160 ), 173 ),
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
3 2
4 import '../models/watch_theme_models.dart'; 3 import '../models/watch_theme_models.dart';
5 import 'watch_theme_character_avatar.dart'; 4 import 'watch_theme_character_avatar.dart';
6 import 'watch_theme_colors.dart'; 5 import 'watch_theme_colors.dart';
  6 +import 'watch_theme_selected_view.dart';
7 import 'watch_theme_section_card.dart'; 7 import 'watch_theme_section_card.dart';
8 8
9 class OfficialThemeGrid extends StatelessWidget { 9 class OfficialThemeGrid extends StatelessWidget {
10 const OfficialThemeGrid({ 10 const OfficialThemeGrid({
11 super.key, 11 super.key,
12 required this.themes, 12 required this.themes,
13 - required this.selectedIndex, 13 + required this.activeThemeId,
14 required this.onThemeTap, 14 required this.onThemeTap,
15 }); 15 });
16 16
17 final List<WatchThemeItem> themes; 17 final List<WatchThemeItem> themes;
18 - final int selectedIndex; 18 + final int? activeThemeId;
19 final ValueChanged<int> onThemeTap; 19 final ValueChanged<int> onThemeTap;
20 20
21 @override 21 @override
@@ -25,7 +25,7 @@ class OfficialThemeGrid extends StatelessWidget { @@ -25,7 +25,7 @@ class OfficialThemeGrid extends StatelessWidget {
25 crossAxisAlignment: CrossAxisAlignment.start, 25 crossAxisAlignment: CrossAxisAlignment.start,
26 children: [ 26 children: [
27 const WatchThemeSectionTitle('官方主题'), 27 const WatchThemeSectionTitle('官方主题'),
28 - SizedBox(height: 22.dp), 28 + SizedBox(height: 22),
29 GridView.builder( 29 GridView.builder(
30 shrinkWrap: true, 30 shrinkWrap: true,
31 padding: EdgeInsets.zero, 31 padding: EdgeInsets.zero,
@@ -44,11 +44,14 @@ class OfficialThemeGrid extends StatelessWidget { @@ -44,11 +44,14 @@ class OfficialThemeGrid extends StatelessWidget {
44 onTap: () => onThemeTap(index), 44 onTap: () => onThemeTap(index),
45 child: Column( 45 child: Column(
46 children: [ 46 children: [
47 - WatchThemeCharacterAvatar( 47 + WatchThemeSelectedView(
  48 + selected: theme.id != null && theme.id == activeThemeId,
48 size: 88, 49 size: 88,
49 - selected: index == selectedIndex,  
50 - assetPath: theme.infoList.firstOrNull?.assetPath,  
51 - imageUrl: theme.infoList.firstOrNull?.imgUrl, 50 + child: WatchThemeCharacterAvatar(
  51 + size: 88,
  52 + assetPath: theme.infoList.firstOrNull?.assetPath,
  53 + imageUrl: theme.infoList.firstOrNull?.imgUrl,
  54 + ),
52 ), 55 ),
53 SizedBox(height: 8), 56 SizedBox(height: 8),
54 Text( 57 Text(
@@ -58,7 +61,7 @@ class OfficialThemeGrid extends StatelessWidget { @@ -58,7 +61,7 @@ class OfficialThemeGrid extends StatelessWidget {
58 overflow: TextOverflow.ellipsis, 61 overflow: TextOverflow.ellipsis,
59 style: TextStyle( 62 style: TextStyle(
60 color: WatchThemeColors.textPrimary, 63 color: WatchThemeColors.textPrimary,
61 - fontSize: 12.dp, 64 + fontSize: 12,
62 fontWeight: FontWeight.w400, 65 fontWeight: FontWeight.w400,
63 height: 1.25, 66 height: 1.25,
64 ), 67 ),
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
3 2
4 -import 'watch_theme_colors.dart';  
5 -  
6 class WatchThemeCharacterAvatar extends StatelessWidget { 3 class WatchThemeCharacterAvatar extends StatelessWidget {
7 const WatchThemeCharacterAvatar({ 4 const WatchThemeCharacterAvatar({
8 super.key, 5 super.key,
9 required this.size, 6 required this.size,
10 this.assetPath, 7 this.assetPath,
11 this.imageUrl, 8 this.imageUrl,
12 - this.selected = false,  
13 this.empty = false, 9 this.empty = false,
14 }); 10 });
15 11
16 final double size; 12 final double size;
17 final String? assetPath; 13 final String? assetPath;
18 final String? imageUrl; 14 final String? imageUrl;
19 - final bool selected;  
20 final bool empty; 15 final bool empty;
21 16
22 @override 17 @override
@@ -25,47 +20,23 @@ class WatchThemeCharacterAvatar extends StatelessWidget { @@ -25,47 +20,23 @@ class WatchThemeCharacterAvatar extends StatelessWidget {
25 return SizedBox( 20 return SizedBox(
26 width: avatarSize, 21 width: avatarSize,
27 height: avatarSize, 22 height: avatarSize,
28 - child: Stack(  
29 - clipBehavior: Clip.none,  
30 - children: [  
31 - Container(  
32 - width: avatarSize,  
33 - height: avatarSize,  
34 - decoration: BoxDecoration(  
35 - shape: BoxShape.circle,  
36 - border: selected  
37 - ? Border.all(color: WatchThemeColors.brand, width: 2.dp)  
38 - : null,  
39 - ),  
40 - alignment: Alignment.center,  
41 - child: empty || (assetPath == null && imageUrl == null)  
42 - ? const SizedBox.shrink()  
43 - : _buildImage(avatarSize)),  
44 - if (selected)  
45 - Positioned(  
46 - right: -1.dp,  
47 - bottom: 9.dp,  
48 - child: Container(  
49 - width: 16.dp,  
50 - height: 16.dp,  
51 - decoration: const BoxDecoration(  
52 - color: WatchThemeColors.brand,  
53 - shape: BoxShape.circle,  
54 - ),  
55 - child: Icon(  
56 - Icons.check,  
57 - color: Colors.white,  
58 - size: 12.dp,  
59 - ),  
60 - ),  
61 - ),  
62 - ], 23 + child: Container(
  24 + width: avatarSize,
  25 + height: avatarSize,
  26 + decoration: const BoxDecoration(
  27 + color: Color(0xFFF5F2FF),
  28 + shape: BoxShape.circle,
  29 + ),
  30 + alignment: Alignment.center,
  31 + child: empty || (assetPath == null && imageUrl == null)
  32 + ? const SizedBox.shrink()
  33 + : _buildImage(avatarSize),
63 ), 34 ),
64 ); 35 );
65 } 36 }
66 37
67 Widget _buildImage(double avatarSize) { 38 Widget _buildImage(double avatarSize) {
68 - final width = avatarSize * 0.82; 39 + final width = avatarSize * 0.636;
69 if (imageUrl != null && imageUrl!.isNotEmpty) { 40 if (imageUrl != null && imageUrl!.isNotEmpty) {
70 return Image.network( 41 return Image.network(
71 imageUrl!, 42 imageUrl!,
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
3 2
4 -import 'watch_theme_colors.dart';  
5 -  
6 class WatchThemeProBadge extends StatelessWidget { 3 class WatchThemeProBadge extends StatelessWidget {
7 const WatchThemeProBadge({super.key}); 4 const WatchThemeProBadge({super.key});
8 5
9 @override 6 @override
10 Widget build(BuildContext context) { 7 Widget build(BuildContext context) {
11 - return Container(  
12 - height: 16.dp,  
13 - padding: EdgeInsets.only(left: 5.dp, right: 6.dp),  
14 - decoration: BoxDecoration(  
15 - color: WatchThemeColors.pro,  
16 - borderRadius: BorderRadius.circular(26.dp),  
17 - ),  
18 - child: Row(  
19 - mainAxisSize: MainAxisSize.min,  
20 - children: [  
21 - Icon(  
22 - Icons.workspace_premium,  
23 - color: WatchThemeColors.textPrimary,  
24 - size: 10.dp,  
25 - ),  
26 - SizedBox(width: 1.dp),  
27 - Text(  
28 - 'PRO',  
29 - style: TextStyle(  
30 - color: WatchThemeColors.textPrimary,  
31 - fontSize: 10.dp,  
32 - fontWeight: FontWeight.w600,  
33 - height: 1.1,  
34 - ),  
35 - ),  
36 - ],  
37 - ), 8 + return Image(
  9 + image: AssetImage('assets/images/common/ic_pro_badge.webp'),
  10 + width: 43,
  11 + height: 16,
38 ); 12 );
39 } 13 }
40 } 14 }
1 -import 'package:doublefeel_flutter/core/util/size_extensions.dart';  
2 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
3 -  
4 import 'watch_theme_colors.dart'; 2 import 'watch_theme_colors.dart';
5 3
6 class WatchThemeSectionCard extends StatelessWidget { 4 class WatchThemeSectionCard extends StatelessWidget {
@@ -17,11 +15,11 @@ class WatchThemeSectionCard extends StatelessWidget { @@ -17,11 +15,11 @@ class WatchThemeSectionCard extends StatelessWidget {
17 Widget build(BuildContext context) { 15 Widget build(BuildContext context) {
18 return Container( 16 return Container(
19 width: double.infinity, 17 width: double.infinity,
20 - margin: EdgeInsets.symmetric(horizontal: 16.dp),  
21 - padding: padding ?? EdgeInsets.all(20.dp), 18 + margin: EdgeInsets.symmetric(horizontal: 16),
  19 + padding: padding ?? EdgeInsets.all(20),
22 decoration: BoxDecoration( 20 decoration: BoxDecoration(
23 color: WatchThemeColors.card, 21 color: WatchThemeColors.card,
24 - borderRadius: BorderRadius.circular(16.dp), 22 + borderRadius: BorderRadius.circular(16),
25 ), 23 ),
26 child: child, 24 child: child,
27 ); 25 );
@@ -43,13 +41,13 @@ class WatchThemeSectionTitle extends StatelessWidget { @@ -43,13 +41,13 @@ class WatchThemeSectionTitle extends StatelessWidget {
43 title, 41 title,
44 style: TextStyle( 42 style: TextStyle(
45 color: WatchThemeColors.textPrimary, 43 color: WatchThemeColors.textPrimary,
46 - fontSize: 16.dp,  
47 - fontWeight: FontWeight.w500, 44 + fontSize: 16,
  45 + fontWeight: FontWeight.w600,
48 height: 1.25, 46 height: 1.25,
49 ), 47 ),
50 ), 48 ),
51 if (trailing != null) ...[ 49 if (trailing != null) ...[
52 - SizedBox(width: 4.dp), 50 + SizedBox(width: 4),
53 trailing!, 51 trailing!,
54 ], 52 ],
55 ], 53 ],
  1 +import 'package:flutter/material.dart';
  2 +
  3 +import 'watch_theme_colors.dart';
  4 +
  5 +class WatchThemeSelectedView extends StatelessWidget {
  6 + const WatchThemeSelectedView({
  7 + super.key,
  8 + required this.selected,
  9 + required this.size,
  10 + required this.child,
  11 + });
  12 +
  13 + final bool selected;
  14 + final double size;
  15 + final Widget child;
  16 +
  17 + @override
  18 + Widget build(BuildContext context) {
  19 + return SizedBox(
  20 + width: size,
  21 + height: size,
  22 + child: Stack(
  23 + clipBehavior: Clip.none,
  24 + children: [
  25 + Container(
  26 + width: size,
  27 + height: size,
  28 + decoration: BoxDecoration(
  29 + shape: BoxShape.circle,
  30 + border: selected
  31 + ? Border.all(color: WatchThemeColors.brand, width: 2)
  32 + : null,
  33 + ),
  34 + child: child,
  35 + ),
  36 + if (selected)
  37 + Positioned(
  38 + right: -1,
  39 + bottom: 9,
  40 + child: Container(
  41 + width: 16,
  42 + height: 16,
  43 + decoration: const BoxDecoration(
  44 + color: WatchThemeColors.brand,
  45 + shape: BoxShape.circle,
  46 + ),
  47 + child: Icon(
  48 + Icons.check,
  49 + color: Colors.white,
  50 + size: 12,
  51 + ),
  52 + ),
  53 + ),
  54 + ],
  55 + ),
  56 + );
  57 + }
  58 +}
@@ -89,6 +89,11 @@ class AppleProductPaymentResult { @@ -89,6 +89,11 @@ class AppleProductPaymentResult {
89 }); 89 });
90 } 90 }
91 91
  92 +enum HResourceType {
  93 + image,
  94 + video,
  95 +}
  96 +
92 @ConfigurePigeon( 97 @ConfigurePigeon(
93 PigeonOptions( 98 PigeonOptions(
94 dartOut: 'lib/pigeon/platform_api.g.dart', 99 dartOut: 'lib/pigeon/platform_api.g.dart',
@@ -121,8 +126,8 @@ abstract class PlatformHostApi { @@ -121,8 +126,8 @@ abstract class PlatformHostApi {
121 126
122 /// 刷新会员信息 127 /// 刷新会员信息
123 void refreshVip(); 128 void refreshVip();
124 -  
125 - /// 刷新watch app 和 表盘的所有数据: 129 +
  130 + /// 刷新watch app 和 表盘的所有数据:
126 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等) 131 /// 任何可能导致展示watch内容变化时调用(好友变化、某些设置导致变化等)
127 void refreshWatchAppAndWidgets(); 132 void refreshWatchAppAndWidgets();
128 133
@@ -143,4 +148,9 @@ abstract class PlatformHostApi { @@ -143,4 +148,9 @@ abstract class PlatformHostApi {
143 /// 恢复购买 148 /// 恢复购买
144 @async 149 @async
145 bool performRestore(); 150 bool performRestore();
  151 +
  152 + /// 上传文件到云端
  153 + /// 注意catch flutter error
  154 + @async
  155 + String? uploadFile(String filePath, HResourceType resourceType);
146 } 156 }