Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ webdev serve --debug-extension

or you use devtools [Debugging Web Info](https://dart.dev/web/debugging)

## Update the livestreams page

After each stream, regenerate the episode list from the YouTube playlist and commit the result:

```sh
dart run tool/fetch_livestreams.dart
```

## Build and Deploy

```sh
Expand Down
1 change: 1 addition & 0 deletions lib/src/app/external_links.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum ExternalLink {
socialMastodon('https://fluttercommunity.social/@FlutterComm'),
socialGitHub('https://github.com/fluttercommunity/fc_ai_circle'),
socialMedium('https://medium.com/flutter-community'),
sessionizeCallForSpeakers('https://sessionize.com/fcaic'),
;

const ExternalLink(this.url);
Expand Down
2 changes: 2 additions & 0 deletions lib/src/app/website.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:fc_ai_circle/src/pages/builders_page.dart';
import 'package:fc_ai_circle/src/pages/livestreams_page.dart';
import 'package:fc_ai_circle/src/pages/home_page.dart';
import 'package:fc_ai_circle/src/pages/starters_page.dart';
import 'package:fc_ai_circle/src/pages/privacy_policy_page.dart';
Expand Down Expand Up @@ -30,6 +31,7 @@ class _SiteState extends State<Website> {
...HomePage.route(),
...StartersPage.route(),
...BuildersPage.route(),
...LivestreamsPage.route(),
...PrivacyPolicyPage.route(),
...CodeOfConductPage.route(),
],
Expand Down
2 changes: 2 additions & 0 deletions lib/src/components/footer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:fc_ai_circle/src/app/external_links.dart';
import 'package:jaspr/browser.dart';
import 'package:jaspr_router/jaspr_router.dart';
import 'package:fc_ai_circle/src/pages/builders_page.dart';
import 'package:fc_ai_circle/src/pages/livestreams_page.dart';
import 'package:fc_ai_circle/src/pages/starters_page.dart';
import 'package:fc_ai_circle/src/pages/privacy_policy_page.dart';
import 'package:fc_ai_circle/src/pages/code_of_conduct_page.dart';
Expand All @@ -25,6 +26,7 @@ class Footer extends StatelessComponent {
links: [
(path: StartersPage.path, label: 'Starters'),
(path: BuildersPage.path, label: 'Builders'),
(path: LivestreamsPage.path, label: 'Livestreams'),
],
),
div([]),
Expand Down
2 changes: 2 additions & 0 deletions lib/src/components/navbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:jaspr/browser.dart';
import 'package:jaspr_router/jaspr_router.dart' show Router;
import 'package:fc_ai_circle/src/pages/builders_page.dart';
import 'package:fc_ai_circle/src/pages/home_page.dart';
import 'package:fc_ai_circle/src/pages/livestreams_page.dart';
import 'package:fc_ai_circle/src/pages/starters_page.dart';
import 'package:web/web.dart' show window;

Expand Down Expand Up @@ -71,6 +72,7 @@ class _NavbarState extends State<Navbar> {
_NavItem(path: '/', label: 'Home'),
_NavItem(path: StartersPage.path, label: 'Starters'),
_NavItem(path: BuildersPage.path, label: 'Builders'),
_NavItem(path: LivestreamsPage.path, label: 'Livestreams'),
_NavItem(link: ExternalLink.youTubePlaylist, label: 'YouTube'),
_NavItem(link: ExternalLink.socialGitHub, label: 'GitHub'),
_NavItem(link: ExternalLink.forumCategory, label: 'Forum'),
Expand Down
46 changes: 46 additions & 0 deletions lib/src/data/livestream.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/// One episode of the FCAIC livestream on the Flutter Community YouTube channel.
class Livestream {
const Livestream({
required this.videoId,
required this.number,
required this.topic,
required this.length,
required this.date,
});

final String videoId;
final int number;
final String topic;

/// Duration as YouTube shows it, e.g. `1:02:13`.
final String length;

/// Stream start date in Europe/Berlin, ISO 8601 (`yyyy-MM-dd`).
final String date;

static const playlistId = 'PL4dBIh1xps-HIYvaEIbLWHZqt_WGBfpx3';

String get year => date.substring(0, 4);

String get watchUrl => 'https://www.youtube.com/watch?v=$videoId&list=$playlistId';

String thumbnailUrl({bool large = false}) =>
'https://i.ytimg.com/vi/$videoId/${large ? 'maxresdefault' : 'hqdefault'}.jpg';

int get lengthInSeconds {
const secondsPerUnit = 60;
return length
.split(':')
.fold(0, (total, part) => total * secondsPerUnit + (int.tryParse(part) ?? 0));
}

static const _months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', //
];

String get monthYear => '${_months[int.parse(date.substring(5, 7)) - 1]} $year';

String get prettyDate => '${int.parse(date.substring(8, 10))} $monthYear';

bool matches(String query) => '#$number $topic'.toLowerCase().contains(query);
}
209 changes: 209 additions & 0 deletions lib/src/data/livestreams.g.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Generated by tool/fetch_livestreams.dart. Do not edit by hand.

import 'package:fc_ai_circle/src/data/livestream.dart';

const livestreams = <Livestream>[
Livestream(
videoId: '7iB8U8nKyDI',
number: 29,
topic: 'Stakeholder-Ready Prototypes & Video Editor in Flutter',
length: '58:12',
date: '2026-09-22',
),
Livestream(
videoId: 'zaYVbkGpays',
number: 28,
topic: 'Bridging the SDK Version Gap with Agent Skills',
length: '1:29:40',
date: '2026-09-01',
),
Livestream(
videoId: 'NxK5lOzq9Mc',
number: 27,
topic: 'Adventure-GM & Randal鈥檚 Ticket Resolution Workflow',
length: '1:02:37',
date: '2026-08-11',
),
Livestream(
videoId: 'cWjoY-zBz2g',
number: 26,
topic: 'Building and Caching A2UI Grammar Lessons with Flutter & Firebase',
length: '1:49:19',
date: '2026-07-21',
),
Livestream(
videoId: 'xM7Q0_dnOs8',
number: 25,
topic: 'AI-Assisted E2E Testing in Flutter with Patrol MCP',
length: '1:18:10',
date: '2026-06-23',
),
Livestream(
videoId: 'NPz4lyP1WQI',
number: 24,
topic: 'On-device RAG Redefined: Production-Ready Mobile AI',
length: '1:01:33',
date: '2026-06-09',
),
Livestream(
videoId: 'dfo6FS814D0',
number: 23,
topic: 'Flutter Updates + Agentic Coding Toolkit w/ Andrea',
length: '1:18:58',
date: '2026-05-26',
),
Livestream(
videoId: 'yX_stItg0lI',
number: 22,
topic: 'Installing and Using Agent Skills for Dart & Flutter',
length: '1:03:23',
date: '2026-05-12',
),
Livestream(
videoId: 'wVi_EJPsB7Y',
number: 21,
topic: 'Flutter Updates from Google Cloud Next 2026',
length: '53:08',
date: '2026-04-28',
),
Livestream(
videoId: 'SU6d_r3o-gY',
number: 20,
topic: 'Build full-stack, agentic apps with Genkit Dart with Chris Gill',
length: '1:01:54',
date: '2026-04-14',
),
Livestream(
videoId: '8RHsCQIFx4M',
number: 19,
topic: 'Unlocking On-Device AI with llamadart',
length: '1:09:11',
date: '2026-03-31',
),
Livestream(
videoId: 'UcasNRoYbcU',
number: 18,
topic: 'The Architect鈥檚 Dilemma: Navigating Customization in Google Antigravity and Gemini CLI',
length: '1:12:00',
date: '2026-03-03',
),
Livestream(
videoId: '0QWOYTxjL8Y',
number: 17,
topic: 'QuAI: The AI Writing Assistant One Can Use Every Day',
length: '57:21',
date: '2026-02-17',
),
Livestream(
videoId: '6tWaOmzSodQ',
number: 16,
topic: 'A Flutter-first coding agent built on Claude Code',
length: '1:34:35',
date: '2026-02-03',
),
Livestream(
videoId: 'bAljWKk3ZfM',
number: 15,
topic: 'Building a pipeline using Flutter + Firebase + Gemini 3 Pro',
length: '1:27:46',
date: '2025-12-01',
),
Livestream(
videoId: 'c3OLJcSGrFw',
number: 14,
topic: 'First impressions of Gemini CLI, AI Studio, Antigravity, Stitch & Jules',
length: '1:24:06',
date: '2025-11-24',
),
Livestream(
videoId: 'gJW6FR_X5P0',
number: 13,
topic: 'Prompt, Code, Think: Gemini CLI Extensions 脳 Flutter with Jhin Lee',
length: '1:10:59',
date: '2025-11-03',
),
Livestream(
videoId: '6EWjQUhW9eE',
number: 12,
topic: 'Choose Your Own Adventure App Part 2 with Randal Schwartz',
length: '1:28:16',
date: '2025-10-20',
),
Livestream(
videoId: 'YlfXYIISlIY',
number: 11,
topic: 'Choose Your Own Adventure App with Randal Schwartz',
length: '1:59:01',
date: '2025-10-06',
),
Livestream(
videoId: 'K8R_0P_jI4I',
number: 10,
topic: 'Prompt, Code, Think: Flutter x Firebase AI Logic Agentic Apps with Khanh & Ander',
length: '1:55:31',
date: '2025-08-11',
),
Livestream(
videoId: 'xt5Ol3ZrdF4',
number: 9,
topic: 'Prompt, Code, Think: Agentic AI in Dart with dartantic_ai',
length: '1:49:30',
date: '2025-07-28',
),
Livestream(
videoId: 'WZVyDMZ97eY',
number: 8,
topic: 'Tool Talk: ClaudeCode, Prompt enhancer, & RooCode Enhanced Checklist Management',
length: '1:16:32',
date: '2025-07-21',
),
Livestream(
videoId: 'Mls_V7wjCCI',
number: 7,
topic: 'Building a Real-Time Flame Game MCP Server with AI Assistants (with @salihgueler)',
length: '1:44:59',
date: '2025-07-14',
),
Livestream(
videoId: 'QVCpoWWLYZo',
number: 6,
topic: 'flutter_gemma Package & Quick Look at Gemini CLI',
length: '2:07:15',
date: '2025-07-07',
),
Livestream(
videoId: 'km0ImsSNXSo',
number: 5,
topic: 'Firebase Studio, mcp_dart Package & Roo Code Updates',
length: '1:47:13',
date: '2025-06-23',
),
Livestream(
videoId: 'KPugx13h-bw',
number: 4,
topic: 'Agentic Apps Hands-On and RooCode',
length: '1:39:22',
date: '2025-06-09',
),
Livestream(
videoId: 'QRDBqSXKu7E',
number: 3,
topic: 'I/O AI Integration Updates',
length: '1:37:28',
date: '2025-06-02',
),
Livestream(
videoId: 'vJv9Fz61ieY',
number: 2,
topic: 'Prompting, Tools, Rules, and Dev Insights',
length: '2:21:22',
date: '2025-05-12',
),
Livestream(
videoId: 'Gub1DY8ScwU',
number: 1,
topic: 'Prompt, Code, Think: Welcome to AI in Flutter',
length: '2:27:40',
date: '2025-05-05',
),
];
Loading
Loading