Skip to content

Commit bc2be77

Browse files
authored
Merge pull request #463 from OpenPecha/chore/PlansBottomSheetSharableVersion
Chore/plans bottom sheet sharable version
2 parents 57084a6 + 763ed6c commit bc2be77

13 files changed

Lines changed: 424 additions & 118 deletions

File tree

lib/core/config/router/app_router.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
499499
},
500500
),
501501

502-
// plan text route - inline TEXT subtasks (sibling to /reader)
502+
// plan content route - inline TEXT/IMAGE subtasks (sibling to /reader)
503503
GoRoute(
504504
path: "/plan-text/:subtaskId",
505505
name: "plan-text",

lib/core/config/router/app_routes.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ class AppRoutes {
4848
static const String reader = '/reader';
4949

5050
// ========== PLAN TEXT ROUTES ==========
51-
/// Inline plan text screen — renders subtasks where `content_type == "TEXT"`.
51+
/// Inline plan content screen — renders subtasks where `content_type` is
52+
/// `TEXT` or `IMAGE`.
5253
/// Path param is the subtask id; the actual content travels in `extra`
53-
/// as a [NavigationContext] whose `currentItem` carries `inlineContent`.
54+
/// as a [NavigationContext] whose `currentItem` carries inline content.
5455
static const String planText = '/plan-text';
5556

5657
// ========== SEARCH ROUTES ==========
@@ -88,7 +89,7 @@ class AppRoutes {
8889
practicePlanPreview, // Allow guests to browse/preview plans
8990
reader,
9091
notifications, // Local-only — guests can configure routine notifications
91-
planText, // Guests can see inline TEXT subtasks
92+
planText, // Guests can see inline TEXT/IMAGE subtasks
9293
};
9394

9495
/// Base paths that require full authentication (prefix matching)

lib/features/plans/data/models/plan_days_model.dart

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class PlanDaysModel {
88
final List<PlanTasksModel>? tasks;
99
final String? audioUrl;
1010
final int? audioDurationMs;
11+
final String? thumbnailUrl;
12+
final String? shareableImageUrl;
1113
final List<PlanVideoModel> videos;
1214

1315
PlanDaysModel({
@@ -17,6 +19,8 @@ class PlanDaysModel {
1719
this.tasks,
1820
this.audioUrl,
1921
this.audioDurationMs,
22+
this.thumbnailUrl,
23+
this.shareableImageUrl,
2024
this.videos = const [],
2125
});
2226

@@ -27,20 +31,26 @@ class PlanDaysModel {
2731
id: json['id'] as String,
2832
dayNumber: json['day_number'] as int,
2933
title: json['title'] as String?,
30-
tasks: json['tasks'] != null
31-
? (json['tasks'] as List<dynamic>)
32-
.map((e) => PlanTasksModel.fromJson(e as Map<String, dynamic>))
33-
.toList()
34-
: null,
34+
tasks:
35+
json['tasks'] != null
36+
? (json['tasks'] as List<dynamic>)
37+
.map(
38+
(e) => PlanTasksModel.fromJson(e as Map<String, dynamic>),
39+
)
40+
.toList()
41+
: null,
3542
audioUrl: json['audio_url'] as String?,
3643
audioDurationMs: json['audio_duration_ms'] as int?,
37-
videos: json['videos'] != null
38-
? (json['videos'] as List<dynamic>)
39-
.map(
40-
(e) => PlanVideoModel.fromJson(e as Map<String, dynamic>),
41-
)
42-
.toList()
43-
: const [],
44+
thumbnailUrl: json['thumbnail_url'] as String?,
45+
shareableImageUrl: json['shareable_image_url'] as String?,
46+
videos:
47+
json['videos'] != null
48+
? (json['videos'] as List<dynamic>)
49+
.map(
50+
(e) => PlanVideoModel.fromJson(e as Map<String, dynamic>),
51+
)
52+
.toList()
53+
: const [],
4454
);
4555
}
4656

@@ -52,6 +62,8 @@ class PlanDaysModel {
5262
'tasks': tasks?.map((e) => e.toJson()).toList(),
5363
'audio_url': audioUrl,
5464
'audio_duration_ms': audioDurationMs,
65+
'thumbnail_url': thumbnailUrl,
66+
'shareable_image_url': shareableImageUrl,
5567
'videos': videos.map((e) => e.toJson()).toList(),
5668
};
5769
}
@@ -63,6 +75,8 @@ class PlanDaysModel {
6375
List<PlanTasksModel>? tasks,
6476
String? audioUrl,
6577
int? audioDurationMs,
78+
String? thumbnailUrl,
79+
String? shareableImageUrl,
6680
List<PlanVideoModel>? videos,
6781
}) {
6882
return PlanDaysModel(
@@ -72,6 +86,8 @@ class PlanDaysModel {
7286
tasks: tasks ?? this.tasks,
7387
audioUrl: audioUrl ?? this.audioUrl,
7488
audioDurationMs: audioDurationMs ?? this.audioDurationMs,
89+
thumbnailUrl: thumbnailUrl ?? this.thumbnailUrl,
90+
shareableImageUrl: shareableImageUrl ?? this.shareableImageUrl,
7591
videos: videos ?? this.videos,
7692
);
7793
}

lib/features/plans/data/models/response/user_plan_day_detail_response.dart

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class UserPlanDayDetailResponse {
88
final bool isCompleted;
99
final String? audioUrl;
1010
final int? audioDurationMs;
11+
final String? thumbnailUrl;
12+
final String? shareableImageUrl;
1113
final List<PlanVideoModel> videos;
1214

1315
UserPlanDayDetailResponse({
@@ -17,6 +19,8 @@ class UserPlanDayDetailResponse {
1719
required this.isCompleted,
1820
this.audioUrl,
1921
this.audioDurationMs,
22+
this.thumbnailUrl,
23+
this.shareableImageUrl,
2024
this.videos = const [],
2125
});
2226

@@ -33,13 +37,16 @@ class UserPlanDayDetailResponse {
3337
isCompleted: json['is_completed'] as bool,
3438
audioUrl: json['audio_url'] as String?,
3539
audioDurationMs: json['audio_duration_ms'] as int?,
36-
videos: json['videos'] != null
37-
? (json['videos'] as List<dynamic>)
38-
.map(
39-
(e) => PlanVideoModel.fromJson(e as Map<String, dynamic>),
40-
)
41-
.toList()
42-
: const [],
40+
thumbnailUrl: json['thumbnail_url'] as String?,
41+
shareableImageUrl: json['shareable_image_url'] as String?,
42+
videos:
43+
json['videos'] != null
44+
? (json['videos'] as List<dynamic>)
45+
.map(
46+
(e) => PlanVideoModel.fromJson(e as Map<String, dynamic>),
47+
)
48+
.toList()
49+
: const [],
4350
);
4451
}
4552

@@ -51,6 +58,8 @@ class UserPlanDayDetailResponse {
5158
'is_completed': isCompleted,
5259
'audio_url': audioUrl,
5360
'audio_duration_ms': audioDurationMs,
61+
'thumbnail_url': thumbnailUrl,
62+
'shareable_image_url': shareableImageUrl,
5463
'videos': videos.map((e) => e.toJson()).toList(),
5564
};
5665
}

lib/features/plans/domain/subtask_navigation.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter_pecha/features/reader/data/models/navigation_context.dar
1010
/// Validation rules (kept in one place):
1111
/// - SOURCE_REFERENCE → valid iff `sourceTextId` is non-null and non-empty.
1212
/// - TEXT → valid iff `content.trim()` is non-empty.
13+
/// - IMAGE → valid iff `content.trim()` is non-empty.
1314
/// - Anything else (unknown content type, missing fields) is silently dropped.
1415
///
1516
/// Both task models (`UserTasksDto` for enrolled users, `PlanTasksModel` for
@@ -52,7 +53,7 @@ class PlanSubtaskNavigation {
5253
}
5354

5455
/// True if the given task has at least one navigable subtask
55-
/// (SOURCE_REFERENCE or TEXT).
56+
/// (SOURCE_REFERENCE, TEXT, or IMAGE).
5657
static bool isUserTaskNavigable(UserTasksDto task) {
5758
return task.subTasks.any(_isUserSubtaskNavigable);
5859
}
@@ -115,6 +116,17 @@ class PlanSubtaskNavigation {
115116
startMs: subtask.startMs,
116117
endMs: subtask.endMs,
117118
);
119+
case PlanItemContentType.inlineImage:
120+
if (!_hasInlineContent(subtask.content)) return null;
121+
return PlanTextItem.inlineImage(
122+
imageUrl: subtask.content,
123+
title: title,
124+
subtaskId: subtask.id,
125+
isCompleted: subtask.isCompleted,
126+
audioUrl: subtask.audioUrl,
127+
startMs: subtask.startMs,
128+
endMs: subtask.endMs,
129+
);
118130
case null:
119131
return null;
120132
}
@@ -145,6 +157,15 @@ class PlanSubtaskNavigation {
145157
startMs: subtask.startMs,
146158
endMs: subtask.endMs,
147159
);
160+
case PlanItemContentType.inlineImage:
161+
if (!_hasInlineContent(subtask.content)) return null;
162+
return PlanTextItem.inlineImage(
163+
imageUrl: subtask.content!,
164+
title: title,
165+
audioUrl: subtask.audioUrl,
166+
startMs: subtask.startMs,
167+
endMs: subtask.endMs,
168+
);
148169
case null:
149170
return null;
150171
}

0 commit comments

Comments
 (0)