LCOV - code coverage report
Current view:top level - file/lib/file/app_file.dart (source / functions)CoverageTotalHitMissed
Test:core.lcovLines:42.5 %1275473
Test Date:2026-05-09 12:09:36Functions:-0
Legend:Lines: hit not hit

           TLA  Line data    Source code
       1                 : // Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
       2                 : // 
       3                 : // This file is part of Flipdare's proprietary software and contains
       4                 : // confidential and copyrighted material. Unauthorised copying,
       5                 : // modification, distribution, or use of this file is strictly
       6                 : // prohibited without prior written permission from Flipdare Pty Ltd.
       7                 : // 
       8                 : // This software includes third-party components licensed under MIT,
       9                 : // BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
      10                 : //
      11                 : 
      12                 : import 'dart:io' show File;
      13                 : import 'dart:io';
      14                 : import 'dart:ui' show Size;
      15                 : 
      16                 : import 'package:core/admin/logging.dart';
      17                 : import 'package:core/helper/should_be_overridden.dart';
      18                 : import 'package:fpdart/fpdart.dart' show Either;
      19                 : import 'package:image_size_getter/file_input.dart';
      20                 : import 'package:image_size_getter/image_size_getter.dart' as size_getter;
      21                 : import 'package:core/admin/logging.dart' show LOG;
      22                 : import 'package:core/app_constants.dart';
      23                 : import 'package:core/constants/app_mime_type.dart';
      24                 : import 'package:core/extension/xfile_ext.dart';
      25                 : import 'package:core/file/file_helper.dart';
      26                 : import 'package:cross_file/cross_file.dart' show XFile;
      27                 : import 'package:flutter/material.dart' show Image;
      28                 : import 'package:video_player/video_player.dart';
      29                 : 
      30                 : abstract class AppFile {
      31                 :   const factory AppFile(XFile file) = AppXFile;
      32                 :   const factory AppFile.path(String path) = AppPathFile;
      33                 : 
      34 HIT           1 :   const AppFile._();
      35                 : 
      36                 :   @shouldBeOverridden
      37                 :   XFile get xfile;
      38                 : 
      39               3 :   String get path => xfile.path;
      40 MIS           0 :   String get name => FileHelper(xfile).name;
      41               0 :   File get asDartFile => xfile.asDartFile;
      42               0 :   Image get asImage => Image.file(xfile.asDartFile);
      43                 : 
      44               0 :   int? get maxUploadSizeBytes {
      45               0 :     if (isSupportedVideo) {
      46                 :       return kMaxVideoSizeBytes;
      47               0 :     } else if (isSupportedImage) {
      48                 :       return kMaxImageSizeBytes;
      49                 :     } else {
      50                 :       return null;
      51                 :     }
      52                 :   }
      53                 : 
      54                 :   // Note: this checks the string path, not file properties.
      55               0 :   bool get isValidPath => FileHelper(xfile).isValidPath;
      56 HIT           3 :   bool get isPathEmpty => path.isEmpty;
      57 MIS           0 :   bool get hasSuffix => (suffix != null);
      58                 : 
      59 HIT           4 :   String get nameWithoutExtension => FileHelper(xfile).nameWithoutExtension;
      60               4 :   String get fileName => FileHelper(xfile).name;
      61               4 :   String? get suffix => FileHelper(xfile).suffix;
      62                 : 
      63               1 :   AppMimeType? get mimeType {
      64               2 :     if (fileName.isEmpty) return null;
      65               2 :     final helper = FileHelper(xfile);
      66                 : 
      67               1 :     if (!helper.isValidPath) {
      68 MIS           0 :       LOG.e('File is not valid, cannot determine mimeType');
      69                 :       return null;
      70                 :     }
      71                 : 
      72 HIT           2 :     final fileExt = helper.suffix?.toLowerCase();
      73                 :     if (fileExt == null) {
      74               2 :       LOG.e('File has no suffix, cannot determine if it is supported');
      75                 :       return null;
      76                 :     }
      77                 : 
      78               1 :     return AppMimeType.fromString(fileExt);
      79                 :   }
      80                 : 
      81 MIS           0 :   Future<int> fileSize() async {
      82               0 :     if (isPathEmpty) {
      83               0 :       LOG.e('File path is empty, cannot determine size');
      84               0 :       return -1;
      85                 :     }
      86               0 :     return await xfile.length();
      87                 :   }
      88                 : 
      89               0 :   Future<bool> get isWithinUploadLimit async {
      90               0 :     final size = await fileSize();
      91               0 :     final maxSize = maxUploadSizeBytes;
      92                 :     if (maxSize == null) {
      93               0 :       LOG.e('File $fileName has unsupported type, cannot determine upload limit');
      94                 :       return false;
      95                 :     }
      96               0 :     if (size < 0) {
      97               0 :       LOG.e('File $fileName has invalid size $size, cannot determine upload limit');
      98                 :       return false;
      99                 :     }
     100               0 :     if (size > maxSize) {
     101               0 :       LOG.w('File $fileName size $size exceeds upload limit of $maxSize bytes');
     102                 :       return false;
     103                 :     }
     104                 :     return true;
     105                 :   }
     106                 : 
     107 HIT           1 :   bool get isSupported {
     108               2 :     final helper = FileHelper(xfile);
     109                 : 
     110               1 :     if (!helper.isValidPath) {
     111 MIS           0 :       LOG.e('File is not valid, cannot determine if it is supported: $fileName');
     112                 :       return false;
     113                 :     }
     114                 : 
     115 HIT           1 :     if (mimeType == null) {
     116               4 :       LOG.w('File mimeType is null, cannot be supported: $fileName');
     117                 :       return false;
     118                 :     }
     119                 : 
     120               2 :     if (isSupportedImage || isSupportedVideo) {
     121               4 :       LOG.d('File is supported: $fileName');
     122                 :       return true;
     123                 :     } else {
     124 MIS           0 :       LOG.w('File is not supported: $fileName');
     125                 :       return false;
     126                 :     }
     127                 :   }
     128                 : 
     129 HIT           1 :   bool get isSupportedImage {
     130               2 :     final helper = FileHelper(xfile);
     131                 : 
     132               1 :     if (!helper.isValidPath) {
     133 MIS           0 :       LOG.e('File is not valid, cannot determine if it is supported');
     134                 :       return false;
     135                 :     }
     136                 : 
     137 HIT           2 :     final fileExt = helper.suffix?.toLowerCase();
     138                 :     if (fileExt == null) {
     139 MIS           0 :       LOG.e('File has no suffix, cannot determine if it is supported');
     140                 :       return false;
     141                 :     }
     142                 : 
     143 HIT           1 :     final result = AppMimeType.fromString(fileExt);
     144                 :     if (result == null) {
     145 MIS           0 :       LOG.w('File extension is not a supported image format: $fileExt');
     146                 :       return false;
     147                 :     }
     148 HIT           1 :     return result.isSupportedImage;
     149                 :   }
     150                 : 
     151               1 :   bool get isSupportedVideo {
     152               2 :     final helper = FileHelper(xfile);
     153                 : 
     154               1 :     if (!helper.isValidPath) {
     155 MIS           0 :       LOG.e('File is not valid, cannot determine if it is supported');
     156                 :       return false;
     157                 :     }
     158                 : 
     159 HIT           2 :     final fileExt = helper.suffix?.toLowerCase();
     160                 :     if (fileExt == null) {
     161 MIS           0 :       LOG.e('File has no suffix, cannot determine if it is supported');
     162                 :       return false;
     163                 :     }
     164                 : 
     165 HIT           1 :     final result = AppMimeType.fromString(fileExt);
     166                 :     if (result == null) {
     167 MIS           0 :       LOG.w('File extension is not a supported video format: $fileExt');
     168                 :       return false;
     169                 :     }
     170 HIT           1 :     return result.isSupportedVideo;
     171                 :   }
     172                 : 
     173                 :   // coverage:ignore-start
     174                 :   String get debugStr {
     175                 :     return '[name=$nameWithoutExtension, '
     176                 :         'ext=$suffix, path=$path, mimeType=$mimeType]';
     177                 :   }
     178                 : }
     179                 : 
     180                 : // coverage:ignore-end
     181                 : 
     182                 : abstract class AppContentFile extends AppFile {
     183                 :   final Either<XFile, String> _file;
     184                 : 
     185 MIS           0 :   static AppContentFile content(Either<XFile, String> file) {
     186               0 :     final fileCheck = file.fold((xfile) => AppXFile(xfile), (path) => AppPathFile(path));
     187               0 :     if (fileCheck.isSupportedVideo) {
     188               0 :       return AppContentFile._video(file);
     189                 :     } else {
     190               0 :       return AppContentFile._image(file);
     191                 :     }
     192                 :   }
     193                 : 
     194               0 :   static AppContentFile? image(Either<XFile, String> file) {
     195               0 :     final contentFile = AppContentFile._image(file);
     196               0 :     if (!contentFile.isSupportedImage) {
     197               0 :       LOG.w('File is not a supported image: ${contentFile.debugStr}');
     198                 :       return null;
     199                 :     }
     200                 :     return contentFile;
     201                 :   }
     202                 : 
     203               0 :   static AppContentFile? video(Either<XFile, String> file) {
     204               0 :     final contentFile = AppContentFile._video(file);
     205               0 :     if (!contentFile.isSupportedVideo) {
     206               0 :       LOG.w('File is not a supported video: ${contentFile.debugStr}');
     207                 :       return null;
     208                 :     }
     209                 :     return contentFile;
     210                 :   }
     211                 : 
     212 HIT           2 :   const AppContentFile._(this._file) : super._();
     213                 :   const factory AppContentFile._image(Either<XFile, String> file) = AppImageFile;
     214                 :   const factory AppContentFile._video(Either<XFile, String> file) = AppVideoFile;
     215                 : 
     216               1 :   @override
     217               2 :   XFile get xfile => _file.fold(
     218 MIS           0 :     (file) => file,
     219 HIT           2 :     (path) => XFile(path),
     220                 :   );
     221                 : 
     222                 :   @shouldBeOverridden
     223                 :   bool get isImage;
     224                 : 
     225                 :   @shouldBeOverridden
     226                 :   bool get isVideo;
     227                 : 
     228                 :   @shouldBeOverridden
     229                 :   Size? get dimension;
     230                 : }
     231                 : 
     232                 : class AppXFile extends AppFile {
     233                 :   final XFile _file;
     234                 : 
     235               2 :   const AppXFile(this._file) : super._();
     236                 : 
     237               1 :   @override
     238               1 :   XFile get xfile => _file;
     239                 : }
     240                 : 
     241                 : class AppPathFile extends AppFile {
     242                 :   final String _path;
     243                 : 
     244               2 :   const AppPathFile(this._path) : super._();
     245                 : 
     246               1 :   @override
     247               2 :   XFile get xfile => XFile(_path);
     248                 : }
     249                 : 
     250                 : class AppImageFile extends AppContentFile {
     251                 :   final Size? _size;
     252                 : 
     253 MIS           0 :   const AppImageFile(super.file) : _size = null, super._();
     254                 : 
     255               0 :   const AppImageFile.withInfo(super.file, this._size) : super._();
     256                 : 
     257               0 :   @override
     258               0 :   bool get isImage => (mimeType != null) ? mimeType!.isSupportedImage : false;
     259                 : 
     260               0 :   @override
     261                 :   bool get isVideo => false;
     262                 : 
     263               0 :   @override
     264                 :   Size? get dimension {
     265               0 :     if (_size != null) {
     266               0 :       return _size;
     267                 :     }
     268                 : 
     269                 :     try {
     270               0 :       final result = size_getter.ImageSizeGetter.getSizeResult(
     271               0 :         FileInput(asDartFile),
     272                 :       );
     273               0 :       final size = result.size;
     274               0 :       final actualSize = Size(size.width.roundToDouble(), size.height.roundToDouble());
     275               0 :       LOG.d('Image size from metadata for $path is $actualSize');
     276                 :       return actualSize;
     277                 :     } catch (e) {
     278               0 :       LOG.e('Error getting image size from metadata for $path: $e');
     279                 :       return null;
     280                 :     }
     281                 :   }
     282                 : }
     283                 : 
     284                 : class AppVideoFile extends AppContentFile {
     285                 :   final Size? _size;
     286                 :   final Duration? _duration;
     287                 : 
     288               0 :   const AppVideoFile(super.file) : _size = null, _duration = null, super._();
     289                 : 
     290 HIT           2 :   const AppVideoFile.withInfo(super.file, this._size, this._duration) : super._();
     291                 : 
     292               1 :   @override
     293               3 :   bool get isVideo => (mimeType != null) ? mimeType!.isSupportedVideo : false;
     294 MIS           0 :   @override
     295                 :   bool get isImage => false;
     296                 : 
     297               0 :   @override
     298                 :   Size? get dimension {
     299               0 :     if (_size != null) {
     300               0 :       return _size;
     301                 :     }
     302                 : 
     303                 :     try {
     304               0 :       final controller = VideoPlayerController.file(xfile.asDartFile)..initialize();
     305               0 :       final videoInfo = controller.value;
     306               0 :       if (!videoInfo.isInitialized) {
     307                 :         return null;
     308                 :       }
     309                 : 
     310               0 :       final size = videoInfo.size;
     311               0 :       LOG.d('Image size from metadata for $path is $size');
     312                 :       return size;
     313                 :     } catch (e) {
     314               0 :       LOG.e('Error getting image size from metadata for $path: $e');
     315                 :       return null;
     316                 :     }
     317                 :   }
     318                 : 
     319 HIT           1 :   List<int> get timePositions {
     320               1 :     if (!isVideo) {
     321 MIS           0 :       return [];
     322                 :     }
     323                 : 
     324                 :     int videoDurationSec;
     325                 : 
     326 HIT           1 :     if (_duration != null) {
     327               2 :       videoDurationSec = _duration.inSeconds;
     328                 :     } else {
     329 MIS           0 :       videoDurationSec = VideoPlayerController.file(xfile.asDartFile).value.duration.inSeconds;
     330                 :     }
     331                 : 
     332 HIT           1 :     if (videoDurationSec <= 0) {
     333 MIS           0 :       LOG.e('Video duration is invalid for $path');
     334               0 :       return [];
     335                 :     }
     336                 : 
     337                 :     // Generate 10 evenly spaced time positions
     338                 :     final frameCount = 10;
     339 HIT           1 :     if (videoDurationSec <= frameCount) {
     340                 :       // If video is shorter than frame count, return each second
     341               2 :       return List<int>.generate(videoDurationSec, (index) => index);
     342                 :     }
     343                 : 
     344               2 :     final interval = videoDurationSec ~/ (frameCount + 1);
     345               4 :     final positions = List<int>.generate(frameCount, (index) => interval * (index + 1));
     346                 : 
     347                 :     return positions;
     348                 :   }
     349                 : }
        

Generated by: LCOV version 2.0-1