Skip to content

Commit 938b36e

Browse files
authored
feat! Flutter dashboard/stats overhaul — StatCard, GlassCard, sources sheet, nginx HLS tweak (#22)
* feat: Add Flutter client for Random Video Streamer - Introduced a new Flutter client application for controlling and monitoring the Random Video Clips Streaming Server. - Implemented API endpoints for admin context, stats, and audio file retrieval. - Enhanced the dashboard with navigation and improved UI for better user experience. - Added support for audio file management and chunk generation controls. - Included necessary configuration files and dependencies for Flutter development. * feat: Implement route-based navigation and enhance admin and stats screens - Added `go_router` dependency for improved routing capabilities. - Introduced separate pages for Dashboard, Admin, and Stats with a unified header for navigation. - Enhanced Admin screen to display live status and system usage metrics. - Updated Stats screen to include pagination for model and audio statistics. - Refactored Home screen to streamline data loading and improve UI consistency. * feat: Update dependencies and enhance UI components - Added `google_fonts` and `url_launcher` dependencies for improved typography and link handling. - Introduced `video_player_web_hls` for enhanced HLS streaming support on web platforms. - Refactored the theme and UI components for better consistency and user experience across screens. - Updated README with detailed instructions for enabling live streaming and platform-specific notes. * feat: Enhance stats screen with new `StatCard`, `GlassCard` widgets, and introduce model filtering, sorting, and pagination.
1 parent 0ea3c0a commit 938b36e

14 files changed

Lines changed: 3019 additions & 1108 deletions

flutter_client/lib/main.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
33

44
import 'src/screens/admin_screen.dart';
5+
import 'src/screens/app_shell.dart';
56
import 'src/screens/home_screen.dart';
67
import 'src/screens/stats_screen.dart';
78
import 'src/services/streaming_api.dart';
@@ -21,16 +22,31 @@ class RandomVideoStreamerApp extends StatelessWidget {
2122
Widget build(BuildContext context) {
2223
final router = GoRouter(
2324
routes: [
24-
GoRoute(path: '/', builder: (context, state) => HomeScreen(api: api)),
25-
GoRoute(path: '/admin', builder: (context, state) => AdminScreen(api: api)),
26-
GoRoute(path: '/stats', builder: (context, state) => StatsScreen(api: api)),
25+
ShellRoute(
26+
builder: (context, state, child) => AppShell(child: child),
27+
routes: [
28+
GoRoute(
29+
path: '/',
30+
builder: (context, state) => HomeScreen(api: api),
31+
),
32+
GoRoute(
33+
path: '/admin',
34+
builder: (context, state) => AdminScreen(api: api),
35+
),
36+
GoRoute(
37+
path: '/stats',
38+
builder: (context, state) => StatsScreen(api: api),
39+
),
40+
],
41+
),
2742
],
2843
);
2944

3045
return MaterialApp.router(
3146
routerConfig: router,
3247
title: 'Random Video Streamer',
3348
theme: AppTheme.dark(),
49+
debugShowCheckedModeBanner: false,
3450
);
3551
}
3652
}

flutter_client/lib/src/models/chunk.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,63 @@ class Chunk {
44
required this.createdAt,
55
required this.sizeMb,
66
this.daysToExpire,
7+
this.videoCodec,
8+
this.width,
9+
this.height,
10+
this.timestamp,
11+
this.sourceVideos,
712
});
813

914
final String name;
1015
final String createdAt;
1116
final num sizeMb;
1217
final int? daysToExpire;
18+
final String? videoCodec;
19+
final int? width;
20+
final int? height;
21+
final num? timestamp;
22+
final List<Map<String, dynamic>>? sourceVideos;
1323

1424
factory Chunk.fromJson(Map<String, dynamic> json) {
25+
final rawSources = json['source_videos'];
26+
List<Map<String, dynamic>>? sources;
27+
if (rawSources is List) {
28+
sources = rawSources
29+
.map((e) {
30+
if (e is Map<String, dynamic>) return e;
31+
if (e is String) return <String, dynamic>{'path': e};
32+
return null;
33+
})
34+
.whereType<Map<String, dynamic>>()
35+
.toList(growable: false);
36+
}
37+
1538
return Chunk(
1639
name: json['name'] as String? ?? '',
1740
createdAt: json['created_at'] as String? ?? '',
1841
sizeMb: (json['size_mb'] as num?) ?? 0,
1942
daysToExpire: json['days_to_expire'] as int?,
43+
videoCodec: json['video_codec'] as String?,
44+
width: json['width'] as int?,
45+
height: json['height'] as int?,
46+
timestamp: json['timestamp'] as num?,
47+
sourceVideos: sources,
2048
);
2149
}
50+
51+
bool get hasSources =>
52+
sourceVideos != null && sourceVideos!.isNotEmpty;
53+
54+
String get metaSummary {
55+
final parts = <String>[];
56+
if (videoCodec != null) {
57+
var v = 'Video: $videoCodec';
58+
if (width != null && height != null) v += ' ${width}x$height';
59+
parts.add(v);
60+
}
61+
parts.add('Size: $sizeMb MB');
62+
parts.add('Created: $createdAt');
63+
if (daysToExpire != null) parts.add('Expires: ~${daysToExpire}d');
64+
return parts.join(' · ');
65+
}
2266
}

flutter_client/lib/src/models/stream_status.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ class StreamStatus {
55
this.chunksPushed = 0,
66
this.chunksCreatedTotal = 0,
77
this.totalSecondsStreamed = 0,
8+
this.currentChunkStartedAt,
9+
this.currentChunkDuration,
10+
this.audioPositionSec,
11+
this.audioTrackDurationSec,
812
});
913

1014
final String? currentChunk;
1115
final String? currentAudio;
1216
final int chunksPushed;
1317
final int chunksCreatedTotal;
1418
final num totalSecondsStreamed;
19+
final num? currentChunkStartedAt;
20+
final num? currentChunkDuration;
21+
final num? audioPositionSec;
22+
final num? audioTrackDurationSec;
1523

1624
factory StreamStatus.fromJson(Map<String, dynamic> json) {
1725
return StreamStatus(
@@ -20,6 +28,10 @@ class StreamStatus {
2028
chunksPushed: json['chunks_pushed'] as int? ?? 0,
2129
chunksCreatedTotal: json['chunks_created_total'] as int? ?? 0,
2230
totalSecondsStreamed: json['total_seconds_streamed'] as num? ?? 0,
31+
currentChunkStartedAt: json['current_chunk_started_at'] as num?,
32+
currentChunkDuration: json['current_chunk_duration'] as num?,
33+
audioPositionSec: json['audio_position_sec'] as num?,
34+
audioTrackDurationSec: json['audio_track_duration_sec'] as num?,
2335
);
2436
}
2537
}

flutter_client/lib/src/models/system_usage.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,47 @@ class SystemUsage {
22
SystemUsage({
33
this.cpuPercent,
44
this.memPercent,
5+
this.memUsedMb,
6+
this.memTotalMb,
57
this.gpuPercent,
8+
this.gpuMemUsedMb,
9+
this.gpuMemTotalMb,
610
});
711

812
final num? cpuPercent;
913
final num? memPercent;
14+
final num? memUsedMb;
15+
final num? memTotalMb;
1016
final num? gpuPercent;
17+
final num? gpuMemUsedMb;
18+
final num? gpuMemTotalMb;
1119

1220
factory SystemUsage.fromJson(Map<String, dynamic> json) {
1321
return SystemUsage(
1422
cpuPercent: json['cpu_percent'] as num?,
1523
memPercent: json['mem_percent'] as num?,
24+
memUsedMb: json['mem_used_mb'] as num?,
25+
memTotalMb: json['mem_total_mb'] as num?,
1626
gpuPercent: json['gpu_percent'] as num?,
27+
gpuMemUsedMb: json['gpu_mem_used_mb'] as num?,
28+
gpuMemTotalMb: json['gpu_mem_total_mb'] as num?,
1729
);
1830
}
31+
32+
String formatMb(num? mb) {
33+
if (mb == null) return '—';
34+
return mb >= 1024
35+
? '${(mb / 1024).toStringAsFixed(1)} GB'
36+
: '${mb.round()} MB';
37+
}
38+
39+
String get memDisplay {
40+
if (memUsedMb == null || memTotalMb == null) return '—';
41+
return '${formatMb(memUsedMb)} / ${formatMb(memTotalMb)}';
42+
}
43+
44+
String get gpuMemDisplay {
45+
if (gpuMemUsedMb == null || gpuMemTotalMb == null) return '—';
46+
return '${formatMb(gpuMemUsedMb)} / ${formatMb(gpuMemTotalMb)}';
47+
}
1948
}

0 commit comments

Comments
 (0)