seenn_flutter 0.2.0 copy "seenn_flutter: ^0.2.0" to clipboard
seenn_flutter: ^0.2.0 copied to clipboard

Flutter SDK for Seenn - Real-time job progress tracking with SSE and Live Activity support.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:seenn_flutter/seenn_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Seenn SDK with development config
  await Seenn.init(
    appId: 'test_app',
    userToken: 'test_user',
    config: SeennConfig.development(
      apiUrl: 'http://localhost:3001',
      sseUrl: 'http://localhost:3000',
    ),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Seenn Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const JobListPage(),
    );
  }
}

class JobListPage extends StatelessWidget {
  const JobListPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Seenn Jobs'),
        actions: [
          // Connection indicator
          StreamBuilder<SeennConnectionState>(
            stream: Seenn.instance.connectionState$,
            builder: (context, snapshot) {
              final state = snapshot.data ?? SeennConnectionState.disconnected;
              return Padding(
                padding: const EdgeInsets.all(8.0),
                child: Icon(
                  state.isConnected ? Icons.cloud_done : Icons.cloud_off,
                  color: state.isConnected ? Colors.green : Colors.red,
                ),
              );
            },
          ),
        ],
      ),
      body: StreamBuilder<Map<String, SeennJob>>(
        stream: Seenn.instance.jobs.all$,
        builder: (context, snapshot) {
          final jobs = snapshot.data?.values.toList() ?? [];

          return Column(
            children: [
              // Live Activity Test Section
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: LiveActivityTestWidget(),
              ),
              const Divider(),
              // Jobs List
              Expanded(
                child: jobs.isEmpty
                    ? const Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.inbox, size: 64, color: Colors.grey),
                            SizedBox(height: 16),
                            Text('No jobs yet'),
                            SizedBox(height: 8),
                            Text(
                              'Start a job from your backend to see it here',
                              style: TextStyle(color: Colors.grey),
                            ),
                          ],
                        ),
                      )
                    : ListView.builder(
                        itemCount: jobs.length,
                        itemBuilder: (context, index) {
                          final job = jobs[index];
                          return JobCard(job: job);
                        },
                      ),
              ),
            ],
          );
        },
      ),
    );
  }
}

class JobCard extends StatelessWidget {
  final SeennJob job;

  const JobCard({super.key, required this.job});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8.0),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header
            Row(
              children: [
                Expanded(
                  child: Text(
                    job.title,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                ),
                _buildStatusChip(job.status),
              ],
            ),
            const SizedBox(height: 8),

            // Progress bar
            if (!job.isTerminal) ...[
              LinearProgressIndicator(
                value: job.progress / 100,
                backgroundColor: Colors.grey[200],
              ),
              const SizedBox(height: 4),
              Text(
                '${job.progress}%',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],

            // Message
            if (job.message != null) ...[
              const SizedBox(height: 8),
              Text(
                job.message!,
                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                      color: Colors.grey[600],
                    ),
              ),
            ],

            // Stage info
            if (job.stage != null) ...[
              const SizedBox(height: 8),
              Text(
                'Stage ${job.stage!.index + 1}/${job.stage!.total}: ${job.stage!.label}',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],

            // ETA
            if (job.eta != null && !job.isTerminal) ...[
              const SizedBox(height: 4),
              Text(
                'ETA: ${_formatEta(job.eta!)}',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],

            // Result
            if (job.status == JobStatus.completed && job.resultUrl != null) ...[
              const SizedBox(height: 8),
              ElevatedButton.icon(
                onPressed: () {
                  // Open result URL
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Result: ${job.resultUrl}')),
                  );
                },
                icon: const Icon(Icons.download),
                label: const Text('Download'),
              ),
            ],

            // Error
            if (job.status == JobStatus.failed && job.errorMessage != null) ...[
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.red[50],
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Row(
                  children: [
                    const Icon(Icons.error, color: Colors.red, size: 16),
                    const SizedBox(width: 8),
                    Expanded(
                      child: Text(
                        job.errorMessage!,
                        style: const TextStyle(color: Colors.red),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildStatusChip(JobStatus status) {
    Color color;
    String label;

    switch (status) {
      case JobStatus.pending:
        color = Colors.grey;
        label = 'Pending';
        break;
      case JobStatus.queued:
        color = Colors.orange;
        label = 'Queued';
        break;
      case JobStatus.running:
        color = Colors.blue;
        label = 'Running';
        break;
      case JobStatus.completed:
        color = Colors.green;
        label = 'Completed';
        break;
      case JobStatus.failed:
        color = Colors.red;
        label = 'Failed';
        break;
    }

    return Chip(
      label: Text(
        label,
        style: const TextStyle(color: Colors.white, fontSize: 12),
      ),
      backgroundColor: color,
      padding: EdgeInsets.zero,
      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
    );
  }

  String _formatEta(int seconds) {
    if (seconds < 60) {
      return '${seconds}s';
    }
    final minutes = seconds ~/ 60;
    final remainingSeconds = seconds % 60;
    return '${minutes}m ${remainingSeconds}s';
  }
}

// Live Activity Test Widget
class LiveActivityTestWidget extends StatefulWidget {
  const LiveActivityTestWidget({super.key});

  @override
  State<LiveActivityTestWidget> createState() => _LiveActivityTestWidgetState();
}

class _LiveActivityTestWidgetState extends State<LiveActivityTestWidget> {
  bool _isSupported = false;
  bool _isEnabled = false;
  bool _isRunning = false;
  int _progress = 0;
  String _status = '';

  @override
  void initState() {
    super.initState();
    _checkSupport();
  }

  Future<void> _checkSupport() async {
    final supported = await Seenn.instance.liveActivity.isSupported();
    final enabled = await Seenn.instance.liveActivity.areActivitiesEnabled();
    setState(() {
      _isSupported = supported;
      _isEnabled = enabled;
      _status = supported
          ? (enabled ? 'Ready' : 'Disabled in Settings')
          : 'Not supported (iOS 16.2+ required)';
    });
  }

  Future<void> _startDemo() async {
    setState(() {
      _isRunning = true;
      _progress = 0;
      _status = 'Starting...';
    });

    // Start Live Activity
    final result = await Seenn.instance.liveActivity.startActivity(
      jobId: 'demo_job_${DateTime.now().millisecondsSinceEpoch}',
      title: 'AI Video Generation',
      jobType: 'video',
      initialProgress: 0,
      initialMessage: 'Initializing...',
    );

    if (!result.success) {
      setState(() {
        _status = 'Failed: ${result.error}';
        _isRunning = false;
      });
      return;
    }

    setState(() => _status = 'Running - check Dynamic Island!');

    // Simulate progress
    for (int i = 10; i <= 100; i += 10) {
      await Future.delayed(const Duration(seconds: 2));
      if (!_isRunning) break;

      setState(() => _progress = i);

      if (i < 100) {
        await Seenn.instance.liveActivity.updateActivity(
          jobId: result.jobId!,
          progress: i,
          status: 'running',
          message: _getMessageForProgress(i),
          stageName: _getStageForProgress(i),
          stageIndex: (i ~/ 25),
          stageTotal: 4,
          eta: ((100 - i) * 2),
        );
      } else {
        // Complete
        await Seenn.instance.liveActivity.endActivity(
          jobId: result.jobId!,
          finalProgress: 100,
          finalStatus: 'completed',
          message: 'Video ready!',
          resultUrl: 'https://example.com/video.mp4',
        );
      }
    }

    setState(() {
      _isRunning = false;
      _status = 'Completed!';
    });
  }

  String _getMessageForProgress(int progress) {
    if (progress < 25) return 'Analyzing prompt...';
    if (progress < 50) return 'Generating frames...';
    if (progress < 75) return 'Rendering video...';
    return 'Finalizing...';
  }

  String _getStageForProgress(int progress) {
    if (progress < 25) return 'Analysis';
    if (progress < 50) return 'Generation';
    if (progress < 75) return 'Rendering';
    return 'Finalization';
  }

  Future<void> _stopDemo() async {
    setState(() => _isRunning = false);
    await Seenn.instance.liveActivity.cancelAllActivities();
    setState(() => _status = 'Cancelled');
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.phone_iphone, color: Colors.blue),
                const SizedBox(width: 8),
                Text(
                  'Live Activity Demo',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              'Status: $_status',
              style: TextStyle(
                color: _isEnabled ? Colors.green : Colors.orange,
              ),
            ),
            if (_isRunning) ...[
              const SizedBox(height: 8),
              LinearProgressIndicator(value: _progress / 100),
              Text('$_progress%'),
            ],
            const SizedBox(height: 12),
            Row(
              children: [
                ElevatedButton.icon(
                  onPressed: (_isSupported && _isEnabled && !_isRunning)
                      ? _startDemo
                      : null,
                  icon: const Icon(Icons.play_arrow),
                  label: const Text('Start Demo'),
                ),
                const SizedBox(width: 8),
                if (_isRunning)
                  OutlinedButton.icon(
                    onPressed: _stopDemo,
                    icon: const Icon(Icons.stop),
                    label: const Text('Stop'),
                  ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
0
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for Seenn - Real-time job progress tracking with SSE and Live Activity support.

Homepage
Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

flutter, http, rxdart, shared_preferences

More

Packages that depend on seenn_flutter

Packages that implement seenn_flutter