9.2 KiB
EnergyMedia Content Manager - AI Coding Agent Instructions
Project Overview
EnergyMedia Content Manager is a Flutter web application for managing multimedia content (videos, posters, categories) for the EnergyMedia platform. Single-organization focused system with video upload, categorization, playback, and analytics dashboard.
Tech Stack: Flutter 3.1.4+, Supabase (backend/auth/storage), Provider (state), GoRouter (navigation), PlutoGrid (tables), Video Players (appinio_video_player/video_player)
Architecture & Key Patterns
Dual Supabase Clients
supabase(default): Standard auth schema (public.users) for authentication ONLYsupabaseML: Custommedia_libraryschema for all media content management- Critical: Always use
supabaseMLfor media data,supabasefor auth only - Organization Filter: ALL queries to
media_filesMUST filter byorganization_fk = 17 - See lib/main.dart and lib/helpers/globals.dart
State Management (Provider)
All providers declared in lib/main.dart:
UserState: Auth state and current userVisualStateProvider: Theme/visual preferences (light/dark mode)VideosProvider: Media files CRUD, upload/download, metadata management- Pattern: Use
context.read<T>()for one-time actions,context.watch<T>()for reactive UI
Navigation Structure
/login → /dashboard (stats: reproducciones, videos, categorías)
└── Sidemenu:
├── Dashboard (default)
├── Gestor de Videos (PlutoGrid con CRUD)
└── Configuración (placeholder - work in progress)
- Simplified: No empresa/negocio selection - single organization (EnergyMedia)
- See lib/router/router.dart
Database Schema & Critical Rules
Media Library Schema (media_library)
Tables:
media_files: Main video records (file_name, title, file_url, storage_path, metadata_json, media_category_fk, organization_fk)media_categories: Video categories (category_name, category_description)media_posters: Poster/thumbnail associations (media_file_id, poster_file_id)- View:
vw_media_files_with_posters- Complete media info with category and poster
Organization Filter Rule
CRITICAL: ALL operations on media_files MUST include organization_fk = 17 filter:
// CORRECT - filtered by organization
final response = await supabaseML
.from('media_files')
.select()
.eq('organization_fk', 17);
// WRONG - missing organization filter
final response = await supabaseML
.from('media_files') // ❌ Returns all organizations!
.select();
Always: Insert/update operations must set organization_fk: 17
metadata_json Structure
Standard fields in media_files.metadata_json:
{
"uploaded_at": "2026-01-10T10:30:00Z",
"reproducciones": 150,
"categorias": ["tutorial", "energía"],
"original_file_name": "video_original.mp4",
"duration_seconds": 320,
"resolution": "1920x1080",
"last_viewed_at": "2026-01-10T12:00:00Z"
}
Supabase Storage
Bucket: energymedia
energymedia/videos/- Video filesenergymedia/imagenes/- Poster/thumbnail images
Responsive Design Standards
Breakpoints: Mobile ≤800px, Tablet 801-1200px, Desktop >1200px
Common pattern:
final isMobile = MediaQuery.of(context).size.width <= 800;
// Desktop: PlutoGrid tables with video thumbnails
// Mobile: Card-based lists with posters
Key files:
- Desktop layouts:
lib/pages/videos/*_page.dart - Mobile adaptations: Check for conditional rendering in same files
- Reference constant:
mobileSize = 800in lib/helpers/constants.dart
Critical Files & Workflows
Global State (lib/helpers/globals.dart)
currentUser: Authenticated user model (nullable)supabaseML: Media Library-specific Supabase client (schema:media_library)plutoGridScrollbarConfig(),plutoGridStyleConfig(): Consistent table styling- Always check
currentUser != nullbefore auth-dependent operations
Models (Domain-Driven)
- Media models:
lib/models/media/*.dart - Pattern:
fromMap()for Supabase JSON deserialization - Key models:
MediaFileModel,MediaCategoryModel,MediaPosterModel
Video Players
Libraries: appinio_video_player, video_player, chewie
Usage patterns:
VideoPlayerLive: Full-screen player with controls (chewie-based)VideoScreenNew: Embedded player (appinio-based)VideoScreenThumbnail: Generate thumbnails from video- See lib/pages/widgets/video_player_*.dart
Theme & Styling
Color Scheme (EnergyMedia):
- Primary Gradient: Cyan→Yellow (linear-gradient(135deg, #4EC9F5, #FFB733))
- Accents: Purple (#6B2F8A), Cyan (#4EC9F5), Yellow (#FFB733), Red (#FF2D2D), Orange (#FF7A3D)
- Dark Mode Backgrounds: #0B0B0D (main), #121214 (surface1), #1A1A1D (surface2)
- Light Mode Backgrounds: #FFFFFF (main), #F7F7F7 (surface1), #EFEFEF (surface2)
- Typography: Google Fonts Poppins (Regular 400, Bold 700)
- Use
AppTheme.of(context)for colors, not hardcoded values - See lib/theme/theme.dart
Development Commands
# Run (web dev)
flutter run -d chrome
# Build web (production)
flutter build web
# Dependencies
flutter pub get
# Clean build
flutter clean && flutter pub get
Common Tasks
Adding New Provider
- Create in
lib/providers/ - Extend
ChangeNotifier - Register in lib/main.dart
MultiProvider.providers - Export in
lib/providers/providers.dartif needed
Creating Responsive Pages
- Check screen width:
MediaQuery.of(context).size.width - Desktop: Use PlutoGrid for tables, full layouts
- Mobile: Use ListView with Cards, simplified forms
- Reference lib/pages/videos/gestor_videos_page.dart for pattern
Querying Media Data
// CORRECT - uses media_library schema + organization filter
final response = await supabaseML
.from('media_files')
.select()
.eq('organization_fk', 17)
.order('created_at_timestamp', ascending: false);
// WRONG - uses default schema
final response = await supabase
.from('media_files') // ❌ Table not found!
.select();
// WRONG - missing organization filter
final response = await supabaseML
.from('media_files')
.select(); // ❌ Returns data from ALL organizations!
Uploading Media Files
// 1. Upload video to storage
final videoPath = await supabaseML.storage
.from('energymedia')
.upload('videos/$fileName', videoBytes);
// 2. Insert record with organization filter
await supabaseML.from('media_files').insert({
'file_name': fileName,
'title': title,
'file_url': publicUrl,
'storage_path': 'videos/$fileName',
'organization_fk': 17, // ⚠️ REQUIRED!
'metadata_json': {
'uploaded_at': DateTime.now().toIso8601String(),
'reproducciones': 0,
'original_file_name': originalName,
}
});
Working with Video Thumbnails
// Generate thumbnail from video (VideoScreenThumbnail widget)
VideoScreenThumbnail(video: videoUrl)
// Display poster from media_posters
Image.network(posterUrl, fit: BoxFit.cover)
// Fallback: Show placeholder if no poster
if (posterUrl != null)
Image.network(posterUrl)
else
Icon(Icons.video_library, size: 48)
Project Conventions
Naming
- Files:
snake_case.dart(e.g.,videos_provider.dart) - Classes:
PascalCase(e.g.,VideosProvider) - Private members:
_underscorePrefixed(e.g.,_selectedVideo) - Models suffix:
*Model(e.g.,MediaFileModel)
File Organization
lib/
pages/ # Full pages
videos/ # Video management pages
gestor_videos_page.dart
dashboard_page.dart
widgets/ # Video-specific widgets
widgets/ # Shared widgets (video players, etc.)
providers/ # State management
videos_provider.dart
models/ # Data models
media/ # Media domain models
helpers/ # Utilities, globals, extensions
services/ # External service integrations
Import Style
- Absolute imports:
import 'package:energy_media/...' - Barrel exports: Use
lib/models/models.dart,lib/providers/providers.dart
Testing & Debugging
- No formal tests yet - manual testing in browser/device
- Dev mode: Hot reload enabled (Flutter devtools)
- Check browser console for Supabase RPC errors
- Useful: Flutter Inspector for widget tree debugging
Key Reference Documents
- assets/referencia/tablas_energymedia.txt: Database schema reference
- pubspec.yaml: All dependencies and versions
- lib/helpers/constants.dart: Environment config, API keys, constants
- GitHub repo: https://github.com/CB-Luna/energymedia_content_manager
Known Quirks
- PlutoGrid requires
dependency_overrideforintl: ^0.19.0compatibility - Always call
initGlobals()in main before app initialization - GoRouter
optionURLReflectsImperativeAPIsmust betruefor proper routing - Mobile forms use full-screen modals, not dialogs (better UX on small screens)
- Video thumbnails: Use
VideoScreenThumbnailwidget or fallback to category/poster image