[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(design-system): add feed tag fade

+88 -20
+66 -12
lib/src/core/design_system/components/molecules/feed_tag_list.dart
··· 30 30 this.onReorder, 31 31 this.onLongPress, 32 32 this.enableReordering = false, 33 + this.leadingSpacing = 0, 34 + this.enableRightFade = false, 35 + this.rightFadeWidth = 24, 33 36 }); 34 37 35 38 final List<FeedTagData> tags; ··· 38 41 final Function(int oldIndex, int newIndex)? onReorder; 39 42 final Function(FeedTagData tag)? onLongPress; 40 43 final bool enableReordering; 44 + final double leadingSpacing; 45 + final bool enableRightFade; 46 + final double rightFadeWidth; 41 47 42 48 @override 43 49 State<FeedTagList> createState() => _FeedTagListState(); ··· 148 154 } 149 155 150 156 // Non-reorderable version with long press support 151 - return SizedBox( 152 - height: 30, 153 - child: ListView.separated( 154 - scrollDirection: Axis.horizontal, 155 - separatorBuilder: (context, index) => const SizedBox(width: 30), 156 - itemCount: widget.tags.length, 157 - itemBuilder: (context, index) { 158 - final tag = widget.tags[index]; 159 - return GestureDetector( 157 + final hasLeadingSpacing = widget.leadingSpacing > 0; 158 + final itemCount = widget.tags.length + (hasLeadingSpacing ? 1 : 0); 159 + final listView = ListView.builder( 160 + scrollDirection: Axis.horizontal, 161 + itemCount: itemCount, 162 + itemBuilder: (context, index) { 163 + if (hasLeadingSpacing && index == 0) { 164 + return SizedBox(width: widget.leadingSpacing); 165 + } 166 + 167 + final tagIndex = hasLeadingSpacing ? index - 1 : index; 168 + final tag = widget.tags[tagIndex]; 169 + return Padding( 170 + padding: EdgeInsets.only( 171 + right: tagIndex < widget.tags.length - 1 ? 30 : 0, 172 + ), 173 + child: GestureDetector( 160 174 onLongPress: widget.onLongPress != null 161 175 ? () => _handleLongPress(tag) 162 176 : null, ··· 166 180 selected: _selectedTagId == tag.id, 167 181 onTap: () => _handleTagTap(tag.id), 168 182 ), 169 - ); 170 - }, 171 - ), 183 + ), 184 + ); 185 + }, 186 + ); 187 + 188 + final fadedList = widget.enableRightFade 189 + ? LayoutBuilder( 190 + builder: (context, constraints) { 191 + final width = constraints.maxWidth; 192 + final fadeWidth = widget.rightFadeWidth.clamp(0, width); 193 + return ShaderMask( 194 + blendMode: BlendMode.dstIn, 195 + shaderCallback: (bounds) { 196 + if (fadeWidth == 0 || width == 0) { 197 + return const LinearGradient( 198 + colors: [Colors.white, Colors.white], 199 + ).createShader(bounds); 200 + } 201 + 202 + final fadeStart = ((width - fadeWidth) / width).clamp( 203 + 0.0, 204 + 1.0, 205 + ); 206 + return LinearGradient( 207 + begin: Alignment.centerLeft, 208 + end: Alignment.centerRight, 209 + colors: const [ 210 + Colors.white, 211 + Colors.white, 212 + Colors.transparent, 213 + ], 214 + stops: [0.0, fadeStart, 1.0], 215 + ).createShader(bounds); 216 + }, 217 + child: listView, 218 + ); 219 + }, 220 + ) 221 + : listView; 222 + 223 + return SizedBox( 224 + height: 30, 225 + child: fadedList, 172 226 ); 173 227 } 174 228 }
+4 -2
lib/src/core/design_system/templates/feeds_bar_template.dart
··· 32 32 33 33 @override 34 34 Widget build(BuildContext context) { 35 + const double tagStartInset = 20; 35 36 return SizedBox( 36 37 height: 30 + kToolbarHeight, 37 38 child: Stack( ··· 50 51 SafeArea( 51 52 bottom: false, 52 53 child: Padding( 53 - padding: const EdgeInsets.symmetric(horizontal: 12), 54 + padding: const EdgeInsets.only(right: 12), 54 55 child: Row( 55 56 children: [ 56 - const SizedBox(width: 8), 57 57 Expanded( 58 58 child: FeedTagList( 59 59 tags: tags, ··· 62 62 onReorder: onReorder, 63 63 onLongPress: onLongPress, 64 64 enableReordering: enableReordering, 65 + leadingSpacing: tagStartInset, 66 + enableRightFade: true, 65 67 ), 66 68 ), 67 69 if (action != null) ...[
+12 -3
lib/src/core/design_system/templates/image_review_page_template.dart
··· 387 387 388 388 @override 389 389 Widget build(BuildContext context) { 390 + final theme = Theme.of(context); 391 + final colorScheme = theme.colorScheme; 392 + 393 + final tileColor = colorScheme.surfaceContainerHighest.withValues( 394 + alpha: 0.5, 395 + ); 396 + final borderColor = colorScheme.outline.withValues(alpha: 0.6); 397 + final titleColor = colorScheme.onSurface; 398 + 390 399 return Column( 391 400 children: [ 392 401 Container( 393 402 decoration: BoxDecoration( 394 - color: Colors.white.withAlpha(6), 403 + color: tileColor, 395 404 borderRadius: BorderRadius.circular(8), 396 - border: Border.all(color: Colors.white.withAlpha(24)), 405 + border: Border.all(color: borderColor), 397 406 ), 398 407 child: ListTile( 399 408 contentPadding: const EdgeInsets.symmetric( ··· 403 412 title: Text( 404 413 'Post to Bluesky', 405 414 style: AppTypography.textMediumBold.copyWith( 406 - color: AppColors.greyWhite, 415 + color: titleColor, 407 416 ), 408 417 ), 409 418 trailing: Switch(value: value, onChanged: onChanged),
+6 -3
lib/src/core/design_system/templates/video_review_page_template.dart
··· 236 236 237 237 @override 238 238 Widget build(BuildContext context) { 239 + final theme = Theme.of(context); 240 + final colorScheme = theme.colorScheme; 241 + 239 242 return Container( 240 243 decoration: BoxDecoration( 241 - color: Colors.white.withAlpha(6), 244 + color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), 242 245 borderRadius: BorderRadius.circular(8), 243 - border: Border.all(color: Colors.white.withAlpha(24)), 246 + border: Border.all(color: colorScheme.outline.withValues(alpha: 0.6)), 244 247 ), 245 248 child: ListTile( 246 249 contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), 247 250 title: Text( 248 251 'Post to Bluesky', 249 252 style: AppTypography.textMediumBold.copyWith( 250 - color: AppColors.greyWhite, 253 + color: colorScheme.onSurface, 251 254 ), 252 255 ), 253 256 trailing: Switch(value: value, onChanged: onChanged),