397 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			397 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:flutter/material.dart';
 | 
						|
import 'package:gamer_updater/game.dart';
 | 
						|
import 'package:image_picker/image_picker.dart';
 | 
						|
 | 
						|
class GameCard extends StatefulWidget {
 | 
						|
  final Game game;
 | 
						|
  final Function(Game) onGameUpdated;
 | 
						|
  final Function()? onDelete;
 | 
						|
  final bool isNameEditable;
 | 
						|
 | 
						|
  const GameCard({
 | 
						|
    super.key,
 | 
						|
    required this.game,
 | 
						|
    required this.onGameUpdated,
 | 
						|
    this.onDelete,
 | 
						|
    this.isNameEditable = false,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  State<GameCard> createState() => _GameCardState();
 | 
						|
}
 | 
						|
 | 
						|
class _GameCardState extends State<GameCard>
 | 
						|
    with SingleTickerProviderStateMixin {
 | 
						|
  late final AnimationController _controller;
 | 
						|
  bool _isLoading = false;
 | 
						|
  int _deleteClickCount = 0;
 | 
						|
  late TextEditingController _nameController;
 | 
						|
  late TextEditingController _versionRegexController;
 | 
						|
  late TextEditingController _rssFeedUrlController;
 | 
						|
  late TextEditingController _lastPlayedController;
 | 
						|
  late FocusNode _nameFocus;
 | 
						|
  late FocusNode _versionRegexFocus;
 | 
						|
  late FocusNode _rssFeedUrlFocus;
 | 
						|
  late FocusNode _lastPlayedFocus;
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
    _controller = AnimationController(
 | 
						|
      duration: const Duration(seconds: 1),
 | 
						|
      vsync: this,
 | 
						|
    );
 | 
						|
    _nameController = TextEditingController(text: widget.game.name);
 | 
						|
    _versionRegexController = TextEditingController(
 | 
						|
      text: widget.game.versionRegex,
 | 
						|
    );
 | 
						|
    _rssFeedUrlController = TextEditingController(text: widget.game.rssFeedUrl);
 | 
						|
    _lastPlayedController = TextEditingController(text: widget.game.lastPlayed);
 | 
						|
 | 
						|
    _nameFocus = FocusNode();
 | 
						|
    _versionRegexFocus = FocusNode();
 | 
						|
    _rssFeedUrlFocus = FocusNode();
 | 
						|
    _lastPlayedFocus = FocusNode();
 | 
						|
 | 
						|
    _setupFocusListeners();
 | 
						|
  }
 | 
						|
 | 
						|
  void _setupFocusListeners() {
 | 
						|
    void updateGame() {
 | 
						|
      try {
 | 
						|
        var name =
 | 
						|
            widget.isNameEditable ? _nameController.text : widget.game.name;
 | 
						|
        if (name.isNotEmpty) {
 | 
						|
          widget.onGameUpdated(
 | 
						|
            Game(
 | 
						|
              name: name,
 | 
						|
              versionRegex: _versionRegexController.text,
 | 
						|
              lastPlayed: _lastPlayedController.text,
 | 
						|
              rssFeedUrl: _rssFeedUrlController.text,
 | 
						|
              actualVersion: widget.game.actualVersion,
 | 
						|
              lastUpdated: widget.game.lastUpdated,
 | 
						|
              imageData: widget.game.imageData,
 | 
						|
            ),
 | 
						|
          );
 | 
						|
        }
 | 
						|
      } catch (e) {
 | 
						|
        if (mounted) {
 | 
						|
          ScaffoldMessenger.of(
 | 
						|
            context,
 | 
						|
          ).showSnackBar(SnackBar(content: Text(e.toString())));
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    _nameFocus.addListener(() {
 | 
						|
      if (!_nameFocus.hasFocus) updateGame();
 | 
						|
    });
 | 
						|
    _versionRegexFocus.addListener(() {
 | 
						|
      if (!_versionRegexFocus.hasFocus) updateGame();
 | 
						|
    });
 | 
						|
    _rssFeedUrlFocus.addListener(() {
 | 
						|
      if (!_rssFeedUrlFocus.hasFocus) updateGame();
 | 
						|
    });
 | 
						|
    _lastPlayedFocus.addListener(() {
 | 
						|
      if (!_lastPlayedFocus.hasFocus) updateGame();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    _controller.dispose();
 | 
						|
    _nameController.dispose();
 | 
						|
    _versionRegexController.dispose();
 | 
						|
    _rssFeedUrlController.dispose();
 | 
						|
    _lastPlayedController.dispose();
 | 
						|
    _nameFocus.dispose();
 | 
						|
    _versionRegexFocus.dispose();
 | 
						|
    _rssFeedUrlFocus.dispose();
 | 
						|
    _lastPlayedFocus.dispose();
 | 
						|
    super.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _refreshVersion() async {
 | 
						|
    setState(() => _isLoading = true);
 | 
						|
    _controller.repeat();
 | 
						|
 | 
						|
    final updatedGame = widget.game;
 | 
						|
    try {
 | 
						|
      await updatedGame.updateActualVersion();
 | 
						|
    } catch (e) {
 | 
						|
      if (mounted) {
 | 
						|
        ScaffoldMessenger.of(
 | 
						|
          context,
 | 
						|
        ).showSnackBar(SnackBar(content: Text(e.toString())));
 | 
						|
      }
 | 
						|
    }
 | 
						|
    widget.onGameUpdated(updatedGame);
 | 
						|
 | 
						|
    _controller.stop();
 | 
						|
    _controller.reset();
 | 
						|
    setState(() => _isLoading = false);
 | 
						|
  }
 | 
						|
 | 
						|
  void _handleDeleteClick() {
 | 
						|
    setState(() {
 | 
						|
      _deleteClickCount++;
 | 
						|
      if (_deleteClickCount >= 2) {
 | 
						|
        widget.onDelete?.call();
 | 
						|
        _deleteClickCount = 0;
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    // Reset click count after a delay
 | 
						|
    Future.delayed(const Duration(seconds: 1), () {
 | 
						|
      if (mounted) {
 | 
						|
        setState(() => _deleteClickCount = 0);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _pickImage() async {
 | 
						|
    final picker = ImagePicker();
 | 
						|
    final image = await picker.pickImage(source: ImageSource.gallery);
 | 
						|
    if (image != null) {
 | 
						|
      final imageBytes = await image.readAsBytes();
 | 
						|
      final updatedGame = await GameRepository.updateImage(
 | 
						|
        Game(
 | 
						|
          name: widget.game.name,
 | 
						|
          versionRegex: _versionRegexController.text,
 | 
						|
          lastPlayed: _lastPlayedController.text,
 | 
						|
          rssFeedUrl: _rssFeedUrlController.text,
 | 
						|
          actualVersion: widget.game.actualVersion,
 | 
						|
          lastUpdated: widget.game.lastUpdated,
 | 
						|
          imageData: imageBytes,
 | 
						|
        ),
 | 
						|
      );
 | 
						|
      widget.onGameUpdated(updatedGame);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final isUpToDate = widget.game.actualVersion == widget.game.lastPlayed;
 | 
						|
    final hasImage = widget.game.imageData != null;
 | 
						|
 | 
						|
    return Card(
 | 
						|
      child: Stack(
 | 
						|
        children: [
 | 
						|
          if (hasImage)
 | 
						|
            Positioned.fill(
 | 
						|
              child: ClipRRect(
 | 
						|
                borderRadius: BorderRadius.circular(12),
 | 
						|
                child: Opacity(
 | 
						|
                  opacity: 0.4,
 | 
						|
                  child: Image.memory(widget.game.imageData!, fit: BoxFit.cover),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          if (hasImage)
 | 
						|
            Positioned.fill(
 | 
						|
              child: Container(
 | 
						|
                decoration: BoxDecoration(
 | 
						|
                  borderRadius: BorderRadius.circular(12),
 | 
						|
                  gradient: LinearGradient(
 | 
						|
                    begin: Alignment.topCenter,
 | 
						|
                    end: Alignment.bottomCenter,
 | 
						|
                    colors: [
 | 
						|
                      Colors.black.withAlpha(110),
 | 
						|
                      Colors.black.withAlpha(90),
 | 
						|
                      Colors.black.withAlpha(70),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          Padding(
 | 
						|
            padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
 | 
						|
            child: Column(
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
              children: [
 | 
						|
                SizedBox(
 | 
						|
                  height: 40,
 | 
						|
                  child: Row(
 | 
						|
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
                    children: [
 | 
						|
                      Expanded(
 | 
						|
                        child: TextField(
 | 
						|
                          controller: _nameController,
 | 
						|
                          focusNode: _nameFocus,
 | 
						|
                          style: Theme.of(context).textTheme.titleLarge
 | 
						|
                              ?.copyWith(color: hasImage ? Colors.white : null),
 | 
						|
                          enabled: widget.isNameEditable,
 | 
						|
                          decoration: const InputDecoration.collapsed(
 | 
						|
                            hintText: 'New Game',
 | 
						|
                          ),
 | 
						|
                          onSubmitted: (_) => _nameFocus.unfocus(),
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                      Row(
 | 
						|
                        mainAxisSize: MainAxisSize.min,
 | 
						|
                        children: [
 | 
						|
                          if (!widget.isNameEditable)
 | 
						|
                            IconButton(
 | 
						|
                              icon: const Icon(Icons.image),
 | 
						|
                              color: hasImage ? Colors.white : null,
 | 
						|
                              onPressed: _pickImage,
 | 
						|
                            ),
 | 
						|
                          if (widget.onDelete != null)
 | 
						|
                            IconButton(
 | 
						|
                              icon: Icon(
 | 
						|
                                Icons.delete,
 | 
						|
                                color:
 | 
						|
                                    _deleteClickCount > 0
 | 
						|
                                        ? Colors.red
 | 
						|
                                        : (hasImage ? Colors.white : null),
 | 
						|
                              ),
 | 
						|
                              onPressed: _handleDeleteClick,
 | 
						|
                            ),
 | 
						|
                          if (!widget.isNameEditable)
 | 
						|
                            RotationTransition(
 | 
						|
                              turns: _controller,
 | 
						|
                              child: IconButton(
 | 
						|
                                icon: Icon(
 | 
						|
                                  Icons.refresh,
 | 
						|
                                  color: hasImage ? Colors.white : null,
 | 
						|
                                ),
 | 
						|
                                onPressed: _isLoading ? null : _refreshVersion,
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                const SizedBox(height: 16),
 | 
						|
                Row(
 | 
						|
                  children: [
 | 
						|
                    SizedBox(
 | 
						|
                      width: 120,
 | 
						|
                      child: Text(
 | 
						|
                        'Version:',
 | 
						|
                        style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
						|
                          color: hasImage ? Colors.white : null,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    Text(
 | 
						|
                      widget.game.actualVersion.isEmpty
 | 
						|
                          ? 'Unknown'
 | 
						|
                          : widget.game.actualVersion,
 | 
						|
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
						|
                        color: isUpToDate ? Colors.green : Colors.red,
 | 
						|
                        fontWeight: FontWeight.bold,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                const SizedBox(height: 12),
 | 
						|
                Row(
 | 
						|
                  children: [
 | 
						|
                    SizedBox(
 | 
						|
                      width: 120,
 | 
						|
                      child: Text(
 | 
						|
                        'Last Updated:',
 | 
						|
                        style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
						|
                          color: hasImage ? Colors.white : null,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    Text(
 | 
						|
                      widget.game.lastUpdated.isEmpty
 | 
						|
                          ? 'Never'
 | 
						|
                          : DateTime.parse(widget.game.lastUpdated).toString(),
 | 
						|
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
						|
                        color: hasImage ? Colors.white : null,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                const SizedBox(height: 12),
 | 
						|
                Row(
 | 
						|
                  children: [
 | 
						|
                    SizedBox(
 | 
						|
                      width: 120,
 | 
						|
                      child: Text(
 | 
						|
                        'Last Played:',
 | 
						|
                        style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
						|
                          color: hasImage ? Colors.white : null,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    Text(
 | 
						|
                      widget.game.lastPlayed,
 | 
						|
                      style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
						|
                        color: isUpToDate ? Colors.green : Colors.red,
 | 
						|
                        fontWeight: FontWeight.bold,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                const SizedBox(height: 16),
 | 
						|
                const Divider(),
 | 
						|
                const SizedBox(height: 16),
 | 
						|
                TextField(
 | 
						|
                  controller: _versionRegexController,
 | 
						|
                  focusNode: _versionRegexFocus,
 | 
						|
                  style: TextStyle(color: hasImage ? Colors.white : null),
 | 
						|
                  decoration: InputDecoration(
 | 
						|
                    labelText: 'Version Regex',
 | 
						|
                    labelStyle: TextStyle(
 | 
						|
                      color: hasImage ? Colors.white70 : null,
 | 
						|
                    ),
 | 
						|
                    enabledBorder:
 | 
						|
                        hasImage
 | 
						|
                            ? const UnderlineInputBorder(
 | 
						|
                              borderSide: BorderSide(color: Colors.white70),
 | 
						|
                            )
 | 
						|
                            : null,
 | 
						|
                  ),
 | 
						|
                  onSubmitted: (_) => _versionRegexFocus.unfocus(),
 | 
						|
                ),
 | 
						|
                TextField(
 | 
						|
                  controller: _rssFeedUrlController,
 | 
						|
                  focusNode: _rssFeedUrlFocus,
 | 
						|
                  style: TextStyle(color: hasImage ? Colors.white : null),
 | 
						|
                  decoration: InputDecoration(
 | 
						|
                    labelText: 'RSS Feed URL',
 | 
						|
                    labelStyle: TextStyle(
 | 
						|
                      color: hasImage ? Colors.white70 : null,
 | 
						|
                    ),
 | 
						|
                    enabledBorder:
 | 
						|
                        hasImage
 | 
						|
                            ? const UnderlineInputBorder(
 | 
						|
                              borderSide: BorderSide(color: Colors.white70),
 | 
						|
                            )
 | 
						|
                            : null,
 | 
						|
                  ),
 | 
						|
                  onSubmitted: (_) => _rssFeedUrlFocus.unfocus(),
 | 
						|
                ),
 | 
						|
                TextField(
 | 
						|
                  controller: _lastPlayedController,
 | 
						|
                  focusNode: _lastPlayedFocus,
 | 
						|
                  style: TextStyle(color: hasImage ? Colors.white : null),
 | 
						|
                  decoration: InputDecoration(
 | 
						|
                    labelText: 'Last Played',
 | 
						|
                    labelStyle: TextStyle(
 | 
						|
                      color: hasImage ? Colors.white70 : null,
 | 
						|
                    ),
 | 
						|
                    enabledBorder:
 | 
						|
                        hasImage
 | 
						|
                            ? const UnderlineInputBorder(
 | 
						|
                              borderSide: BorderSide(color: Colors.white70),
 | 
						|
                            )
 | 
						|
                            : null,
 | 
						|
                  ),
 | 
						|
                  onSubmitted: (_) => _lastPlayedFocus.unfocus(),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |