diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 1dc6cf7..0000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 13.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 4b5a98e..0000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,616 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.xtremflow; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.xtremflow.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.xtremflow.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.xtremflow.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.xtremflow; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.xtremflow; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e3773d4..0000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift deleted file mode 100644 index 6266644..0000000 --- a/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d93..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b00..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe73094..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773c..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32a..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf1..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index e23eb60..0000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Xtremflow - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - xtremflow - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart deleted file mode 100644 index 76567ef..0000000 --- a/lib/core/router/app_router.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import '../../features/auth/providers/auth_provider.dart'; -import '../../features/auth/screens/login_screen.dart'; -import '../../features/iptv/screens/playlist_selection_screen.dart'; -import '../../features/iptv/screens/dashboard_screen.dart'; -import '../../features/admin/screens/admin_panel.dart'; -import '../models/playlist_config.dart'; -import '../../mobile/features/auth/screens/mobile_login_screen.dart'; -import '../../mobile/features/iptv/screens/mobile_playlist_selection_screen.dart'; -import '../../mobile/features/iptv/screens/mobile_dashboard_screen.dart'; -import '../widgets/themed_loading_screen.dart'; - -final routerProvider = Provider((ref) { - final authState = ref.watch(authProvider); - - return GoRouter( - initialLocation: '/login', - refreshListenable: RouterRefreshNotifier(ref), - redirect: (context, state) { - final isLoggedIn = authState.isAuthenticated; - final isLoginRoute = state.matchedLocation == '/login'; - - // Wait for initial auth check to complete - if (!authState.isInitialized) { - return null; - } - - // Redirect to login if not authenticated - if (!isLoggedIn && !isLoginRoute) { - return '/login'; - } - - // Redirect to playlists if already logged in and trying to access login - if (isLoggedIn && isLoginRoute) { - return '/playlists'; - } - - // Admin guard for /admin route - if (state.matchedLocation == '/admin' && !authState.isAdmin) { - return '/playlists'; - } - - return null; - }, - routes: [ - GoRoute( - path: '/login', - builder: (context, state) { - if (MediaQuery.of(context).size.width < 768) { - return const MobileLoginScreen(); - } - return const LoginScreen(); - }, - ), - GoRoute( - path: '/playlists', - builder: (context, state) { - if (MediaQuery.of(context).size.width < 768) { - return const MobilePlaylistSelectionScreen(); - } - return const PlaylistSelectionScreen(); - }, - ), - GoRoute( - path: '/dashboard', - builder: (context, state) { - final playlist = state.extra as PlaylistConfig?; - if (playlist == null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.go('/playlists'); - }); - return const Scaffold( - body: ThemedLoading(), - ); - } - - if (MediaQuery.of(context).size.width < 768) { - return MobileDashboardScreen(playlist: playlist); - } - return DashboardScreen(playlist: playlist); - }, - ), - GoRoute( - path: '/admin', - builder: (context, state) => const AdminPanel(), - ), - ], - ); -}); - -/// Notifier that triggers router refresh when auth state changes -class RouterRefreshNotifier extends ChangeNotifier { - RouterRefreshNotifier(this._ref) { - _ref.listen(authProvider, (_, __) { - notifyListeners(); - }); - } - - final Ref _ref; -} diff --git a/lib/core/shims/ui_web.dart b/lib/core/shims/ui_web.dart deleted file mode 100644 index 2962ab9..0000000 --- a/lib/core/shims/ui_web.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Conditional export -export 'ui_web_fake.dart' if (dart.library.html) 'ui_web_real.dart'; diff --git a/lib/core/shims/ui_web_fake.dart b/lib/core/shims/ui_web_fake.dart deleted file mode 100644 index 49ed900..0000000 --- a/lib/core/shims/ui_web_fake.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Fake implementation for non-web platforms -class PlatformViewRegistry { - /// Dummy implementation of registerViewFactory - void registerViewFactory(String viewId, dynamic cb) { - // No-op on non-web platforms - } -} - -/// Global instance to match dart:ui_web API -final platformViewRegistry = PlatformViewRegistry(); diff --git a/lib/core/shims/ui_web_real.dart b/lib/core/shims/ui_web_real.dart deleted file mode 100644 index 7744c83..0000000 --- a/lib/core/shims/ui_web_real.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Real implementation for web platforms -export 'dart:ui_web'; diff --git a/lib/core/utils/platform_utils_stub.dart b/lib/core/utils/platform_utils_stub.dart deleted file mode 100644 index af1486f..0000000 --- a/lib/core/utils/platform_utils_stub.dart +++ /dev/null @@ -1,15 +0,0 @@ -/// Platform-specific utilities for web -/// This file is used when compiling for web -library; - -/// Check if we're running on HTTPS -bool isHttps() { - // Implemented in platform_utils_web.dart - return true; -} - -/// Get the current window origin -String getWindowOrigin() { - // Implemented in platform_utils_web.dart - return ''; -} diff --git a/lib/core/utils/platform_utils_web.dart b/lib/core/utils/platform_utils_web.dart deleted file mode 100644 index 270c076..0000000 --- a/lib/core/utils/platform_utils_web.dart +++ /dev/null @@ -1,15 +0,0 @@ -/// Platform-specific utilities for web -/// This file is used when compiling for web -library; - -import 'dart:html' as html; - -/// Check if we're running on HTTPS -bool isHttps() { - return html.window.location.protocol == 'https:'; -} - -/// Get the current window origin -String getWindowOrigin() { - return html.window.location.origin; -} diff --git a/lib/features/admin/screens/admin_panel.dart b/lib/features/admin/screens/admin_panel.dart deleted file mode 100644 index da7418a..0000000 --- a/lib/features/admin/screens/admin_panel.dart +++ /dev/null @@ -1,842 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:go_router/go_router.dart'; -import '../../../core/services/playlist_api_service.dart'; -import '../../../core/services/user_api_service.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/models/app_user.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../../core/widgets/components/ui_components.dart'; -import '../../iptv/screens/playlist_selection_screen.dart'; - -/// Users provider for admin panel -final usersProvider = FutureProvider>((ref) async { - final service = UserApiService(); - return service.getAllUsers(); -}); - -class AdminPanel extends StatelessWidget { - const AdminPanel({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - extendBodyBehindAppBar: true, - body: Container( - decoration: const BoxDecoration( - gradient: RadialGradient( - center: Alignment.center, - radius: 1.5, - colors: [ - Color(0xFF2C2C2E), // Dark Grey (Apple TV Surface) - Color(0xFF000000), // Pure Black - ], - stops: [0.0, 1.0], - ), - ), - child: const AdminContent(), - ), - ); - } -} - -class AdminContent extends ConsumerStatefulWidget { - const AdminContent({super.key}); - - @override - ConsumerState createState() => _AdminContentState(); -} - -class _AdminContentState extends ConsumerState - with SingleTickerProviderStateMixin { - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - // Custom Glass Header - Container( - padding: const EdgeInsets.all(24), - child: Row( - children: [ - // Back button only if standalone (optional, keeping for now) - GlassCard( - borderRadius: 12, - padding: const EdgeInsets.all(8), - showBorder: false, - onTap: () => context.go('/playlists'), - child: const Icon(Icons.arrow_back, color: Colors.white70), - ), - const SizedBox(width: 16), - Text( - 'Administration', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - // Custom Tab Bar Indicator - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(50), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildTabButton(0, 'Playlists', Icons.playlist_play), - const SizedBox(width: 4), - _buildTabButton(1, 'Users', Icons.people), - ], - ), - ), - ], - ), - ), - - // Content - Expanded( - child: TabBarView( - controller: _tabController, - physics: - const NeverScrollableScrollPhysics(), // Custom tabs handle ref - children: const [ - _PlaylistsTab(), - _UsersTab(), - ], - ), - ), - ], - ), - ); - } - - Widget _buildTabButton(int index, String label, IconData icon) { - return AnimatedBuilder( - animation: _tabController.animation!, - builder: (context, child) { - final isSelected = _tabController.index == index; - return GestureDetector( - onTap: () => _tabController.animateTo(index), - child: AnimatedContainer( - duration: AppTheme.durationFast, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: isSelected ? AppColors.focusColor : Colors.transparent, - borderRadius: BorderRadius.circular(50), - ), - child: Row( - children: [ - Icon( - icon, - size: 18, - color: isSelected ? Colors.black : Colors.white70, - ), - const SizedBox(width: 8), - Text( - label, - style: GoogleFonts.inter( - fontWeight: FontWeight.w600, - fontSize: 14, - color: isSelected ? Colors.black : Colors.white70, - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -// ========== PLAYLISTS TAB ========== -class _PlaylistsTab extends ConsumerWidget { - const _PlaylistsTab(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final playlistsAsync = ref.watch(playlistsProvider); - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${playlistsAsync.asData?.value.length ?? 0} Playlists', - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: Colors.white70), - ), - GradientButton( - label: 'Add Playlist', - icon: Icons.add, - onPressed: () => _showPlaylistDialog(context, ref), - ), - ], - ), - ), - Expanded( - child: playlistsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center( - child: Text( - 'Error: $error', - style: const TextStyle(color: AppColors.error), - ), - ), - data: (playlists) { - if (playlists.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.playlist_remove, - size: 64, - color: AppColors.textSecondary.withOpacity(0.5), - ), - const SizedBox(height: 16), - Text( - 'No playlists configured', - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 8), - TextButton( - onPressed: () => _showPlaylistDialog(context, ref), - child: const Text('Add your first playlist'), - ), - ], - ), - ); - } - - return ListView.separated( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - itemCount: playlists.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (context, index) { - final playlist = playlists[index]; - return GlassCard( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.playlist_play, - color: AppColors.textPrimary, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - playlist.name, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: Colors.white), - ), - const SizedBox(height: 4), - Text( - playlist.dns, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Colors.white60), - ), - ], - ), - ), - Row( - children: [ - IconButton( - icon: const Icon(Icons.edit_outlined, size: 20), - color: Colors.white70, - onPressed: () => - _showPlaylistDialog(context, ref, playlist), - tooltip: 'Edit', - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 20), - color: AppColors.error, - onPressed: () => - _deletePlaylist(context, ref, playlist), - tooltip: 'Delete', - ), - ], - ), - ], - ), - ); - }, - ); - }, - ), - ), - ], - ); - } - - void _showPlaylistDialog( - BuildContext context, - WidgetRef ref, [ - PlaylistConfig? playlist, - ]) { - final nameController = TextEditingController(text: playlist?.name); - final dnsController = TextEditingController(text: playlist?.dns); - final usernameController = TextEditingController(text: playlist?.username); - final passwordController = TextEditingController(text: playlist?.password); - final service = PlaylistApiService(); - bool isLoading = false; - - showDialog( - context: context, - builder: (dialogContext) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text( - playlist == null ? 'Ajouter une playlist' : 'Modifier la playlist', - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - decoration: const InputDecoration(labelText: 'Nom'), - ), - const SizedBox(height: 12), - TextField( - controller: dnsController, - decoration: const InputDecoration( - labelText: 'URL du serveur', - hintText: 'http://server.com:8080', - ), - ), - const SizedBox(height: 12), - TextField( - controller: usernameController, - decoration: const InputDecoration(labelText: 'Utilisateur'), - ), - const SizedBox(height: 12), - TextField( - controller: passwordController, - decoration: const InputDecoration(labelText: 'Mot de passe'), - obscureText: true, - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: isLoading ? null : () => Navigator.pop(dialogContext), - child: const Text('Annuler'), - ), - FilledButton( - onPressed: isLoading - ? null - : () async { - if (nameController.text.isEmpty || - dnsController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Tous les champs sont requis'), - ), - ); - return; - } - setState(() => isLoading = true); - final dns = dnsController.text - .trim() - .replaceAll(RegExp(r'/$'), ''); - if (playlist == null) { - await service.createPlaylist( - name: nameController.text.trim(), - dns: dns, - username: usernameController.text.trim(), - password: passwordController.text, - ); - } else { - await service.updatePlaylist( - id: playlist.id, - name: nameController.text.trim(), - dns: dns, - username: usernameController.text.trim(), - password: passwordController.text, - ); - } - if (dialogContext.mounted) Navigator.pop(dialogContext); - ref.invalidate(playlistsProvider); - }, - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Sauvegarder'), - ), - ], - ), - ), - ); - } - - void _deletePlaylist( - BuildContext context, - WidgetRef ref, - PlaylistConfig playlist, - ) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Text('Supprimer la playlist'), - content: Text('Supprimer "${playlist.name}" ?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Annuler'), - ), - FilledButton( - onPressed: () async { - Navigator.pop(dialogContext); - await PlaylistApiService().deletePlaylist(playlist.id); - ref.invalidate(playlistsProvider); - }, - style: FilledButton.styleFrom(backgroundColor: AppColors.error), - child: const Text('Supprimer'), - ), - ], - ), - ); - } -} - -// ========== USERS TAB ========== -class _UsersTab extends ConsumerWidget { - const _UsersTab(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final usersAsync = ref.watch(usersProvider); - - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${usersAsync.asData?.value.length ?? 0} Users', - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: Colors.white70), - ), - GradientButton( - label: 'Create User', - icon: Icons.person_add, - onPressed: () => _showCreateUserDialog(context, ref), - ), - ], - ), - ), - Expanded( - child: usersAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center( - child: Text( - 'Error: $error', - style: const TextStyle(color: AppColors.error), - ), - ), - data: (users) { - if (users.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.people_outline, - size: 64, - color: AppColors.textSecondary.withOpacity(0.5), - ), - const SizedBox(height: 16), - Text( - 'No users found', - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - ); - } - - return ListView.separated( - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - itemCount: users.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (context, index) { - final user = users[index]; - return GlassCard( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: user.isAdmin - ? AppColors.focusColor - : AppColors.surface, - shape: BoxShape.circle, - ), - child: Icon( - user.isAdmin - ? Icons.admin_panel_settings - : Icons.person, - color: user.isAdmin - ? Colors.black - : AppColors.textPrimary, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - user.username, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: Colors.white), - ), - if (user.isAdmin) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: AppColors.focusColor - .withOpacity(0.2), - borderRadius: BorderRadius.circular(50), - ), - child: const Text( - 'ADMIN', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.focusColor, - ), - ), - ), - ], - ], - ), - const SizedBox(height: 4), - Text( - 'ID: ${user.id}', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Colors.white60), - ), - ], - ), - ), - PopupMenuButton( - icon: const Icon( - Icons.more_vert, - color: Colors.white70, - ), - color: AppColors.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - onSelected: (value) { - switch (value) { - case 'password': - _showChangePasswordDialog(context, ref, user); - break; - case 'delete': - _deleteUser(context, ref, user); - break; - } - }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'password', - child: Row( - children: [ - Icon(Icons.lock_reset, size: 18), - SizedBox(width: 8), - Text('Change Password'), - ], - ), - ), - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon( - Icons.delete, - color: AppColors.error, - size: 18, - ), - SizedBox(width: 8), - Text( - 'Delete', - style: TextStyle(color: AppColors.error), - ), - ], - ), - ), - ], - ), - ], - ), - ); - }, - ); - }, - ), - ), - ], - ); - } - - void _showCreateUserDialog(BuildContext context, WidgetRef ref) { - final usernameController = TextEditingController(); - final passwordController = TextEditingController(); - bool isAdmin = false; - bool isLoading = false; - - showDialog( - context: context, - builder: (dialogContext) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: const Text('Créer un utilisateur'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: usernameController, - decoration: const InputDecoration( - labelText: 'Nom d\'utilisateur', - prefixIcon: Icon(Icons.person_outline), - ), - ), - const SizedBox(height: 12), - TextField( - controller: passwordController, - decoration: const InputDecoration( - labelText: 'Mot de passe', - prefixIcon: Icon(Icons.lock_outline), - ), - obscureText: true, - ), - const SizedBox(height: 16), - SwitchListTile( - title: const Text('Administrateur'), - subtitle: const Text('Accès au panneau admin'), - value: isAdmin, - onChanged: (v) => setState(() => isAdmin = v), - ), - ], - ), - actions: [ - TextButton( - onPressed: isLoading ? null : () => Navigator.pop(dialogContext), - child: const Text('Annuler'), - ), - FilledButton( - onPressed: isLoading - ? null - : () async { - if (usernameController.text.isEmpty || - passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Tous les champs sont requis'), - ), - ); - return; - } - setState(() => isLoading = true); - final result = await UserApiService().createUser( - username: usernameController.text.trim(), - password: passwordController.text, - isAdmin: isAdmin, - ); - if (dialogContext.mounted) Navigator.pop(dialogContext); - ref.invalidate(usersProvider); - if (!result.success && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(result.error ?? 'Erreur'), - backgroundColor: AppColors.error, - ), - ); - } - }, - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Créer'), - ), - ], - ), - ), - ); - } - - void _showChangePasswordDialog( - BuildContext context, - WidgetRef ref, - AppUser user, - ) { - final passwordController = TextEditingController(); - bool isLoading = false; - - showDialog( - context: context, - builder: (dialogContext) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text('Changer le mot de passe de ${user.username}'), - content: TextField( - controller: passwordController, - decoration: const InputDecoration( - labelText: 'Nouveau mot de passe', - prefixIcon: Icon(Icons.lock_outline), - ), - obscureText: true, - ), - actions: [ - TextButton( - onPressed: isLoading ? null : () => Navigator.pop(dialogContext), - child: const Text('Annuler'), - ), - FilledButton( - onPressed: isLoading - ? null - : () async { - if (passwordController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Mot de passe requis'), - ), - ); - return; - } - setState(() => isLoading = true); - final result = await UserApiService().updatePassword( - userId: user.id, - newPassword: passwordController.text, - ); - if (dialogContext.mounted) Navigator.pop(dialogContext); - if (result.success && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Mot de passe modifié'), - backgroundColor: AppColors.success, - ), - ); - } else if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(result.error ?? 'Erreur'), - backgroundColor: AppColors.error, - ), - ); - } - }, - child: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Changer'), - ), - ], - ), - ), - ); - } - - void _deleteUser(BuildContext context, WidgetRef ref, AppUser user) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Text('Supprimer l\'utilisateur'), - content: Text( - 'Supprimer "${user.username}" ? Cette action est irréversible.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text('Annuler'), - ), - FilledButton( - onPressed: () async { - Navigator.pop(dialogContext); - final result = await UserApiService().deleteUser(user.id); - ref.invalidate(usersProvider); - if (!result.success && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(result.error ?? 'Erreur'), - backgroundColor: AppColors.error, - ), - ); - } - }, - style: FilledButton.styleFrom(backgroundColor: AppColors.error), - child: const Text('Supprimer'), - ), - ], - ), - ); - } -} diff --git a/lib/features/auth/providers/auth_provider.dart b/lib/features/auth/providers/auth_provider.dart deleted file mode 100644 index 4933163..0000000 --- a/lib/features/auth/providers/auth_provider.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/services/auth_api_service.dart'; -import '../../../core/models/app_user.dart'; - -/// Auth state -class AuthState { - final AppUser? currentUser; - final bool isLoading; - final String? errorMessage; - final bool isInitialized; - - const AuthState({ - this.currentUser, - this.isLoading = false, - this.errorMessage, - this.isInitialized = false, - }); - - bool get isAuthenticated => currentUser != null; - bool get isAdmin => currentUser?.isAdmin ?? false; - - AuthState copyWith({ - AppUser? currentUser, - bool? isLoading, - String? errorMessage, - bool? isInitialized, - bool clearUser = false, - }) { - return AuthState( - currentUser: clearUser ? null : (currentUser ?? this.currentUser), - isLoading: isLoading ?? this.isLoading, - errorMessage: errorMessage, - isInitialized: isInitialized ?? this.isInitialized, - ); - } -} - -/// Auth notifier using API -class AuthNotifier extends StateNotifier { - final AuthApiService _authService = AuthApiService(); - - AuthNotifier() : super(const AuthState()) { - // Auto-check for existing session - checkSession(); - } - - /// Check if there's an existing valid session - Future checkSession() async { - state = state.copyWith(isLoading: true); - - try { - final user = await _authService.getCurrentUser(); - if (user != null) { - state = AuthState( - currentUser: user, - isLoading: false, - isInitialized: true, - ); - } else { - state = const AuthState(isInitialized: true); - } - } catch (e) { - state = const AuthState(isInitialized: true); - } - } - - /// Login with username and password - Future login(String username, String password) async { - state = state.copyWith(isLoading: true, errorMessage: null); - - try { - final result = await _authService.login(username, password); - - if (result.success && result.user != null) { - state = AuthState( - currentUser: result.user, - isLoading: false, - isInitialized: true, - ); - return true; - } else { - state = AuthState( - isLoading: false, - isInitialized: true, - errorMessage: result.error ?? 'Login failed', - ); - return false; - } - } catch (e) { - state = AuthState( - isLoading: false, - isInitialized: true, - errorMessage: 'Network error: $e', - ); - return false; - } - } - - /// Logout current user - Future logout() async { - await _authService.logout(); - state = const AuthState(isInitialized: true); - } - - /// Get current user - AppUser? get currentUser => state.currentUser; -} - -/// Auth provider -final authProvider = StateNotifierProvider((ref) { - return AuthNotifier(); -}); diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart deleted file mode 100644 index 56ce28a..0000000 --- a/lib/features/auth/screens/login_screen.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import '../providers/auth_provider.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/widgets/components/ui_components.dart'; - -class LoginScreen extends ConsumerStatefulWidget { - const LoginScreen({super.key}); - - @override - ConsumerState createState() => _LoginScreenState(); -} - -class _LoginScreenState extends ConsumerState { - final _formKey = GlobalKey(); - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _obscurePassword = true; - - @override - void dispose() { - _usernameController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - if (!_formKey.currentState!.validate()) return; - - final auth = ref.read(authProvider.notifier); - final success = await auth.login( - _usernameController.text.trim(), - _passwordController.text, - ); - - if (success && mounted) { - context.go('/playlists'); - } - } - - @override - Widget build(BuildContext context) { - final authState = ref.watch(authProvider); - - return Scaffold( - backgroundColor: AppColors.background, - body: Stack( - children: [ - // Ambient background glow (Subtle) - Positioned( - top: -100, - left: -100, - child: Container( - width: 500, - height: 500, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: RadialGradient( - colors: [ - AppColors.primary.withOpacity(0.15), - Colors.transparent, - ], - radius: 0.6, - ), - ), - ), - ), - - // Main content - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Logo and title - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColors.border), - boxShadow: [ - BoxShadow( - color: AppColors.primary.withOpacity(0.2), - blurRadius: 30, - offset: const Offset(0, 10), - ), - ], - ), - child: const Icon( - Icons.live_tv, - color: AppColors.primary, - size: 40, - ), - ), - const SizedBox(height: 24), - const Text( - 'XtremFlow', - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - letterSpacing: -0.5, - ), - ), - const SizedBox(height: 8), - const Text( - 'Cinematic Experience', - style: TextStyle( - fontSize: 14, - color: AppColors.textSecondary, - letterSpacing: 2, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 48), - // Login form - GlassCard( - padding: const EdgeInsets.all(32), - borderRadius: 16, - showBorder: true, - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Welcome Back', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - TextFormField( - controller: _usernameController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - labelText: 'Username', - labelStyle: - const TextStyle(color: Colors.white70), - prefixIcon: const Icon( - Icons.person_outline, - color: Colors.white70, - ), - filled: true, - fillColor: Colors.white.withOpacity(0.05), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Colors.white.withOpacity(0.1), - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Colors.white.withOpacity(0.1), - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: AppColors.primary, - ), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Username required'; - } - return null; - }, - autofocus: true, - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 16), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - labelText: 'Password', - labelStyle: - const TextStyle(color: Colors.white70), - prefixIcon: const Icon( - Icons.lock_outline, - color: Colors.white70, - ), - filled: true, - fillColor: Colors.white.withOpacity(0.05), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Colors.white.withOpacity(0.1), - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Colors.white.withOpacity(0.1), - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide( - color: AppColors.primary, - ), - ), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - color: Colors.white70, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Password required'; - } - return null; - }, - textInputAction: TextInputAction.done, - onFieldSubmitted: (_) => _handleLogin(), - ), - if (authState.errorMessage != null) ...[ - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.error.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppColors.error.withOpacity(0.3), - ), - ), - child: Row( - children: [ - const Icon( - Icons.error_outline, - color: AppColors.error, - size: 20, - ), - const SizedBox(height: 8), - Expanded( - child: Text( - authState.errorMessage!, - style: const TextStyle( - color: AppColors.error, - fontSize: 13, - ), - ), - ), - ], - ), - ), - ], - const SizedBox(height: 32), - GradientButton( - label: 'Sign In', - icon: Icons.arrow_forward, - isLoading: authState.isLoading, - isExpanded: true, - onPressed: _handleLogin, - ), - ], - ), - ), - ), - const SizedBox(height: 24), - const Text( - 'Default: admin / admin', - style: TextStyle( - fontSize: 12, - color: AppColors.textTertiary, - fontStyle: FontStyle.italic, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/features/iptv/models/xtream_models.dart b/lib/features/iptv/models/xtream_models.dart deleted file mode 100644 index aa5b9ac..0000000 --- a/lib/features/iptv/models/xtream_models.dart +++ /dev/null @@ -1,344 +0,0 @@ -import 'dart:convert'; - -/// Xtream API Data Models (lightweight for memory optimization) - -class LiveChannel { - final String streamId; - final String name; - final String? streamIcon; - final String? categoryId; - final String streamType; - final String? epgChannelId; - - const LiveChannel({ - required this.streamId, - required this.name, - this.streamIcon, - this.categoryId, - required this.streamType, - this.epgChannelId, - }); - - factory LiveChannel.fromJson(Map json) { - return LiveChannel( - streamId: json['stream_id']?.toString() ?? '', - name: json['name']?.toString() ?? 'Unknown', - streamIcon: json['stream_icon']?.toString(), - categoryId: json['category_id']?.toString(), - streamType: json['stream_type']?.toString() ?? 'live', - epgChannelId: json['epg_channel_id']?.toString(), - ); - } - - String getStreamUrl(String dns, String username, String password) { - return '$dns/live/$username/$password/$streamId.m3u8'; - } -} - -class Movie { - final String streamId; - final String name; - final String? streamIcon; - final String? categoryId; - final String categoryName; - final String? containerExtension; - final String? rating; - - const Movie({ - required this.streamId, - required this.name, - this.streamIcon, - this.categoryId, - this.categoryName = '', - this.containerExtension, - this.rating, - }); - - factory Movie.fromJson(Map json) { - // Essayer plusieurs champs possibles pour l'image (différentes API Xtream utilisent différents noms) - final imageUrl = json['stream_icon']?.toString() ?? - json['cover']?.toString() ?? - json['movie_image']?.toString() ?? - json['cover_big']?.toString(); - - return Movie( - streamId: json['stream_id']?.toString() ?? '', - name: json['name']?.toString() ?? 'Unknown', - streamIcon: imageUrl, - categoryId: json['category_id']?.toString(), - categoryName: json['category_name']?.toString() ?? '', - containerExtension: json['container_extension']?.toString() ?? 'mp4', - rating: json['rating']?.toString(), - ); - } - - String getStreamUrl(String dns, String username, String password) { - return '$dns/movie/$username/$password/$streamId.$containerExtension'; - } -} - -class Series { - final String seriesId; - final String name; - final String? cover; - final String? categoryId; - final String categoryName; - final String? rating; - - const Series({ - required this.seriesId, - required this.name, - this.cover, - this.categoryId, - this.categoryName = '', - this.rating, - }); - - factory Series.fromJson(Map json) { - return Series( - seriesId: json['series_id']?.toString() ?? '', - name: json['name']?.toString() ?? 'Unknown', - cover: json['cover']?.toString(), - categoryId: json['category_id']?.toString(), - categoryName: json['category_name']?.toString() ?? '', - rating: json['rating']?.toString(), - ); - } -} - -/// Season model for series -class Season { - final int seasonNumber; - final String name; - final int episodeCount; - final String? cover; - - const Season({ - required this.seasonNumber, - required this.name, - required this.episodeCount, - this.cover, - }); - - factory Season.fromJson(Map json) { - return Season( - seasonNumber: int.tryParse(json['season_number']?.toString() ?? '1') ?? 1, - name: json['name']?.toString() ?? 'Season ${json['season_number'] ?? 1}', - episodeCount: int.tryParse(json['episode_count']?.toString() ?? '0') ?? 0, - cover: json['cover']?.toString(), - ); - } -} - -/// Episode model for series -class Episode { - final String id; - final int episodeNum; - final int seasonNum; // Added field - final String title; - final String? containerExtension; - final String? info; - final String? cover; - final int? durationSecs; - - const Episode({ - required this.id, - required this.episodeNum, - this.seasonNum = 0, // Default to 0 if unknown - required this.title, - this.containerExtension, - this.info, - this.cover, - this.durationSecs, - }); - - factory Episode.fromJson(Map json, {int? season}) { // Accept season param - return Episode( - id: json['id']?.toString() ?? '', - episodeNum: int.tryParse(json['episode_num']?.toString() ?? '1') ?? 1, - seasonNum: season ?? int.tryParse(json['season']?.toString() ?? '0') ?? 0, - title: json['title']?.toString() ?? 'Episode ${json['episode_num'] ?? 1}', - containerExtension: json['container_extension']?.toString() ?? 'mkv', - info: json['info']?.toString(), - cover: json['custom_cover']?.toString() ?? json['cover']?.toString(), - durationSecs: int.tryParse(json['duration_secs']?.toString() ?? '0'), - ); - } - - // Getter for compatibility - String get streamId => id; - - String getStreamUrl(String dns, String username, String password) { - return '$dns/series/$username/$password/$id.${containerExtension ?? 'mkv'}'; - } -} - -/// Series info with seasons and episodes -class SeriesInfo { - final String seriesId; - final String name; - final String? cover; - final String? plot; - final String? cast; - final String? director; - final String? genre; - final String? releaseDate; - final String? rating; - final List seasons; - final Map> episodes; - - const SeriesInfo({ - required this.seriesId, - required this.name, - this.cover, - this.plot, - this.cast, - this.director, - this.genre, - this.releaseDate, - this.rating, - required this.seasons, - required this.episodes, - }); - - factory SeriesInfo.fromJson(Map json) { - final seasonsData = json['seasons'] as List? ?? []; - final seasons = seasonsData.map((s) => Season.fromJson(s as Map)).toList(); - - final episodesData = json['episodes'] as Map? ?? {}; - final episodes = >{}; - - episodesData.forEach((seasonNum, episodesList) { - final seasonInt = int.tryParse(seasonNum) ?? 1; - final episodeList = (episodesList as List?) - ?.map((e) => Episode.fromJson(e as Map, season: seasonInt)) - .toList() ?? []; - episodes[seasonInt] = episodeList; - }); - - final info = json['info'] as Map? ?? {}; - - return SeriesInfo( - seriesId: json['series_id']?.toString() ?? info['series_id']?.toString() ?? '', - name: info['name']?.toString() ?? json['name']?.toString() ?? 'Unknown', - cover: info['cover']?.toString() ?? json['cover']?.toString(), - plot: info['plot']?.toString(), - cast: info['cast']?.toString(), - director: info['director']?.toString(), - genre: info['genre']?.toString(), - releaseDate: info['releaseDate']?.toString(), - rating: info['rating']?.toString(), - seasons: seasons, - episodes: episodes, - ); - } -} - -class Category { - final String categoryId; - final String categoryName; - - const Category({ - required this.categoryId, - required this.categoryName, - }); - - factory Category.fromJson(Map json) { - return Category( - categoryId: json['category_id']?.toString() ?? '', - categoryName: json['category_name']?.toString() ?? 'Unknown', - ); - } -} - -class ShortEPG { - final String? nowPlaying; - final String? nextPlaying; - final double? progress; // 0.0 to 1.0 - - const ShortEPG({ - this.nowPlaying, - this.nextPlaying, - this.progress, - }); - - factory ShortEPG.fromJson(Map json) { - final epgListings = json['epg_listings'] as List?; - - if (epgListings == null || epgListings.isEmpty) { - return const ShortEPG(); - } - - String? nowTitle; - String? nextTitle; - double? currentProgress; - - final now = DateTime.now(); - - // Helper to decode Base64 encoded title with proper UTF-8 support - String decodeTitle(String? encodedTitle) { - if (encodedTitle == null || encodedTitle.isEmpty) return ''; - try { - // Try Base64 decode with UTF-8 encoding - final bytes = base64Decode(encodedTitle); - return utf8.decode(bytes, allowMalformed: true); - } catch (_) { - // Not Base64, return as-is - return encodedTitle; - } - } - - // Helper to parse timestamp (Unix epoch or ISO format) - DateTime? parseTimestamp(dynamic value) { - if (value == null) return null; - - final strValue = value.toString(); - - // Try parsing as Unix timestamp (seconds) - final intValue = int.tryParse(strValue); - if (intValue != null) { - return DateTime.fromMillisecondsSinceEpoch(intValue * 1000); - } - - // Try parsing as ISO date string - return DateTime.tryParse(strValue); - } - - for (var i = 0; i < epgListings.length; i++) { - final listing = epgListings[i] as Map; - - // Parse start/stop timestamps (can be 'start', 'start_timestamp', or Unix epoch) - final start = parseTimestamp(listing['start']) ?? - parseTimestamp(listing['start_timestamp']); - final stop = parseTimestamp(listing['stop']) ?? - parseTimestamp(listing['end']) ?? - parseTimestamp(listing['stop_timestamp']); - - if (start != null && stop != null) { - if (now.isAfter(start) && now.isBefore(stop)) { - // Decode title (may be Base64 encoded) - nowTitle = decodeTitle(listing['title']?.toString()); - - final duration = stop.difference(start).inSeconds; - final elapsed = now.difference(start).inSeconds; - if (duration > 0) { - currentProgress = elapsed / duration; - } - - // Get next item - if (i + 1 < epgListings.length) { - final nextListing = epgListings[i + 1] as Map; - nextTitle = decodeTitle(nextListing['title']?.toString()); - } - break; - } - } - } - - return ShortEPG( - nowPlaying: nowTitle, - nextPlaying: nextTitle, - progress: currentProgress, - ); - } -} diff --git a/lib/features/iptv/providers/favorites_provider.dart b/lib/features/iptv/providers/favorites_provider.dart deleted file mode 100644 index 04b35c0..0000000 --- a/lib/features/iptv/providers/favorites_provider.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// Manages the list of favorite channel IDs -class FavoritesNotifier extends StateNotifier> { - FavoritesNotifier() : super([]) { - _loadFavorites(); - } - - static const String _key = 'xtremflow_favorites'; - - Future _loadFavorites() async { - final prefs = await SharedPreferences.getInstance(); - final favorites = prefs.getStringList(_key) ?? []; - state = favorites; - } - - Future toggleFavorite(String streamId) async { - final prefs = await SharedPreferences.getInstance(); - if (state.contains(streamId)) { - state = state.where((id) => id != streamId).toList(); - } else { - state = [...state, streamId]; - } - await prefs.setStringList(_key, state); - } - - bool isFavorite(String streamId) { - return state.contains(streamId); - } -} - -final favoritesProvider = StateNotifierProvider>((ref) { - return FavoritesNotifier(); -}); diff --git a/lib/features/iptv/providers/playback_positions_provider.dart b/lib/features/iptv/providers/playback_positions_provider.dart deleted file mode 100644 index f71214f..0000000 --- a/lib/features/iptv/providers/playback_positions_provider.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:convert'; - -/// Playback position state - tracks resume positions for movies and episodes -class PlaybackPositions { - /// Map of content ID to position in seconds (for movies: streamId, for episodes: seriesId_seasonNum_episodeNum) - final Map positions; - - const PlaybackPositions({ - this.positions = const {}, - }); - - PlaybackPositions copyWith({ - Map? positions, - }) { - return PlaybackPositions( - positions: positions ?? this.positions, - ); - } - - /// Get saved position for a content (returns 0 if not found) - double getPosition(String contentId) => positions[contentId] ?? 0; - - /// Check if content has a saved position (beyond 30 seconds) - bool hasPosition(String contentId) => (positions[contentId] ?? 0) > 30; -} - -/// Playback positions notifier with persistence -class PlaybackPositionsNotifier extends StateNotifier { - static const String _storageKey = 'playback_positions'; - - SharedPreferences? _prefs; - bool _initialized = false; - - PlaybackPositionsNotifier() : super(const PlaybackPositions()) { - _loadPositions(); - } - - Future _loadPositions() async { - if (_initialized) return; - - try { - _prefs = await SharedPreferences.getInstance(); - - final positionsJson = _prefs?.getString(_storageKey); - - if (positionsJson != null) { - final decoded = jsonDecode(positionsJson) as Map; - final positions = decoded.map((key, value) => - MapEntry(key, (value as num).toDouble()), - ); - - state = PlaybackPositions(positions: positions); - print('Playback positions loaded: ${positions.length} entries'); - } - - _initialized = true; - } catch (e) { - print('Error loading playback positions: $e'); - } - } - - Future _savePositions() async { - try { - _prefs ??= await SharedPreferences.getInstance(); - await _prefs?.setString(_storageKey, jsonEncode(state.positions)); - } catch (e) { - print('Error saving playback positions: $e'); - } - } - - /// Save position for a content - void savePosition(String contentId, double positionSeconds, double durationSeconds) { - // Don't save if less than 30 seconds in or more than 95% complete - if (positionSeconds < 30 || (durationSeconds > 0 && positionSeconds / durationSeconds > 0.95)) { - // If 95% complete, remove any saved position (mark as fully watched) - if (durationSeconds > 0 && positionSeconds / durationSeconds > 0.95) { - clearPosition(contentId); - } - return; - } - - final newPositions = Map.from(state.positions); - newPositions[contentId] = positionSeconds; - state = state.copyWith(positions: newPositions); - _savePositions(); - } - - /// Clear position for a content - void clearPosition(String contentId) { - final newPositions = Map.from(state.positions); - newPositions.remove(contentId); - state = state.copyWith(positions: newPositions); - _savePositions(); - } - - /// Clear all positions - void clearAll() { - state = const PlaybackPositions(); - _savePositions(); - } - - /// Get formatted resume time - static String formatTime(double seconds) { - final mins = (seconds / 60).floor(); - final secs = (seconds % 60).floor(); - if (mins >= 60) { - final hours = (mins / 60).floor(); - final remainingMins = mins % 60; - return '${hours}h ${remainingMins}m'; - } - return '${mins}m ${secs}s'; - } -} - -/// Provider for playback positions -final playbackPositionsProvider = - StateNotifierProvider((ref) { - return PlaybackPositionsNotifier(); -}); diff --git a/lib/features/iptv/providers/settings_provider.dart b/lib/features/iptv/providers/settings_provider.dart deleted file mode 100644 index 3fc1d0b..0000000 --- a/lib/features/iptv/providers/settings_provider.dart +++ /dev/null @@ -1,415 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/services/settings_api_service.dart'; -import '../../auth/providers/auth_provider.dart'; - -/// Streaming quality presets for Live TV -enum StreamQuality { - low, // 1.5 Mbps, CRF 26 - medium, // 3 Mbps, CRF 23 - high, // 5 Mbps, CRF 20 -} - -/// Buffer size presets for Live TV -enum BufferSize { - low, // 2s segments, 4MB buffer - medium, // 4s segments, 8MB buffer - high, // 6s segments, 12MB buffer -} - -/// Connection timeout presets -enum ConnectionTimeout { - short, // 15 seconds - medium, // 30 seconds - long, // 60 seconds -} - -/// EPG cache duration presets -enum EpgCacheDuration { - short, // 5 minutes - medium, // 15 minutes - long, // 60 minutes -} - -/// Transcoding mode for Live TV -enum TranscodingMode { - auto, // Auto-detect best mode - forced, // Always transcode - disabled, // Direct stream (no transcoding) -} - -/// Keys for API JSON storage -class _SettingsKeys { - // Filters - static const String liveTvFilter = 'filter_live_tv'; - static const String moviesFilter = 'filter_movies'; - static const String seriesFilter = 'filter_series'; - - // Streaming settings - static const String streamQuality = 'stream_quality'; - static const String bufferSize = 'buffer_size'; - static const String connectionTimeout = 'connection_timeout'; - static const String autoReconnect = 'auto_reconnect'; - static const String epgCacheDuration = 'epg_cache_duration'; - static const String transcodingMode = 'transcoding_mode'; - static const String preferDirectPlay = 'prefer_direct_play'; - // Player Display Settings - static const String showClock = 'show_clock'; - static const String preferredAspectRatio = 'aspect_ratio'; -} - -/// Settings state for IPTV preferences with persistence -class IptvSettings { - // Category filters - final String liveTvCategoryFilter; - final String moviesCategoryFilter; - final String seriesCategoryFilter; - - // Streaming settings (Live TV only) - final StreamQuality streamQuality; - final BufferSize bufferSize; - final ConnectionTimeout connectionTimeout; - final bool autoReconnect; - final EpgCacheDuration epgCacheDuration; - final TranscodingMode transcodingMode; - final bool preferDirectPlay; - - // Player Display Settings - final bool showClock; - final String preferredAspectRatio; - - const IptvSettings({ - // Filters - this.liveTvCategoryFilter = '', - this.moviesCategoryFilter = '', - this.seriesCategoryFilter = '', - // Streaming defaults - this.streamQuality = StreamQuality.medium, - this.bufferSize = BufferSize.medium, - this.connectionTimeout = ConnectionTimeout.medium, - this.autoReconnect = true, - this.epgCacheDuration = EpgCacheDuration.medium, - this.transcodingMode = TranscodingMode.auto, - this.preferDirectPlay = false, - // Player defaults - this.showClock = false, - this.preferredAspectRatio = 'contain', - }); - - IptvSettings copyWith({ - String? liveTvCategoryFilter, - String? moviesCategoryFilter, - String? seriesCategoryFilter, - StreamQuality? streamQuality, - BufferSize? bufferSize, - ConnectionTimeout? connectionTimeout, - bool? autoReconnect, - EpgCacheDuration? epgCacheDuration, - TranscodingMode? transcodingMode, - bool? preferDirectPlay, - bool? showClock, - String? preferredAspectRatio, - }) { - return IptvSettings( - liveTvCategoryFilter: liveTvCategoryFilter ?? this.liveTvCategoryFilter, - moviesCategoryFilter: moviesCategoryFilter ?? this.moviesCategoryFilter, - seriesCategoryFilter: seriesCategoryFilter ?? this.seriesCategoryFilter, - streamQuality: streamQuality ?? this.streamQuality, - bufferSize: bufferSize ?? this.bufferSize, - connectionTimeout: connectionTimeout ?? this.connectionTimeout, - autoReconnect: autoReconnect ?? this.autoReconnect, - epgCacheDuration: epgCacheDuration ?? this.epgCacheDuration, - transcodingMode: transcodingMode ?? this.transcodingMode, - preferDirectPlay: preferDirectPlay ?? this.preferDirectPlay, - showClock: showClock ?? this.showClock, - preferredAspectRatio: preferredAspectRatio ?? this.preferredAspectRatio, - ); - } - - // ===== Streaming Value Getters ===== - - /// Get bitrate in kbps based on quality setting - int get bitrateKbps { - switch (streamQuality) { - case StreamQuality.low: return 1500; - case StreamQuality.medium: return 3000; - case StreamQuality.high: return 5000; - } - } - - /// Get CRF value based on quality setting - int get crfValue { - switch (streamQuality) { - case StreamQuality.low: return 26; - case StreamQuality.medium: return 23; - case StreamQuality.high: return 20; - } - } - - /// Get HLS segment duration in seconds - int get hlsSegmentDuration { - switch (bufferSize) { - case BufferSize.low: return 2; - case BufferSize.medium: return 4; - case BufferSize.high: return 6; - } - } - - /// Get buffer size in KB - int get bufferSizeKb { - switch (bufferSize) { - case BufferSize.low: return 4000; - case BufferSize.medium: return 8000; - case BufferSize.high: return 12000; - } - } - - /// Get connection timeout in seconds - int get timeoutSeconds { - switch (connectionTimeout) { - case ConnectionTimeout.short: return 15; - case ConnectionTimeout.medium: return 30; - case ConnectionTimeout.long: return 60; - } - } - - /// Get EPG cache duration in minutes - int get epgCacheMinutes { - switch (epgCacheDuration) { - case EpgCacheDuration.short: return 5; - case EpgCacheDuration.medium: return 15; - case EpgCacheDuration.long: return 60; - } - } - - /// Get transcoding mode string for FFmpeg URL parameter - String get modeString { - switch (transcodingMode) { - case TranscodingMode.disabled: return 'direct'; // Passthrough, 0% CPU - case TranscodingMode.forced: return 'transcode'; - case TranscodingMode.auto: return 'auto'; - } - } - - // ===== Filter Methods ===== - - static List _parseKeywords(String filter) { - if (filter.isEmpty) return []; - return filter - .split(',') - .map((s) => s.trim().toUpperCase()) - .where((s) => s.isNotEmpty) - .toList(); - } - - List get liveTvKeywords => _parseKeywords(liveTvCategoryFilter); - List get moviesKeywords => _parseKeywords(moviesCategoryFilter); - List get seriesKeywords => _parseKeywords(seriesCategoryFilter); - - bool matchesLiveTvFilter(String categoryName) { - return _matchesFilter(categoryName, liveTvKeywords); - } - - bool matchesMoviesFilter(String categoryName) { - return _matchesFilter(categoryName, moviesKeywords); - } - - bool matchesSeriesFilter(String categoryName) { - return _matchesFilter(categoryName, seriesKeywords); - } - - bool _matchesFilter(String categoryName, List keywords) { - if (keywords.isEmpty) return true; - final upperName = categoryName.toUpperCase(); - return keywords.any((keyword) => upperName.contains(keyword)); - } - - bool matchesFilter(String categoryName) => matchesLiveTvFilter(categoryName); -} - -/// Settings notifier with API persistence only (No Local Cache) -class IptvSettingsNotifier extends StateNotifier { - final SettingsApiService _apiService = SettingsApiService(); - final Ref _ref; - // ignore: unused_field - bool _initialized = false; - - IptvSettingsNotifier(this._ref) : super(const IptvSettings()) { - _init(); - _setupAuthListener(); - } - - void _init() { - final authState = _ref.read(authProvider); - if (authState.isAuthenticated) { - _fetchFromApi(); - } - } - - void _setupAuthListener() { - _ref.listen(authProvider, (previous, next) { - // Sync on login - if (previous?.isAuthenticated == false && next.isAuthenticated) { - _fetchFromApi(); - } - // Reset on logout - if (previous?.isAuthenticated == true && !next.isAuthenticated) { - state = const IptvSettings(); - _initialized = false; - } - }); - } - - Future _fetchFromApi() async { - try { - final remoteSettings = await _apiService.getSettings(); - if (remoteSettings != null && remoteSettings.isNotEmpty) { - // Helper to safely cast dynamic types from JSON - T? getValue(String key) => remoteSettings[key] as T?; - - state = state.copyWith( - liveTvCategoryFilter: getValue(_SettingsKeys.liveTvFilter), - moviesCategoryFilter: getValue(_SettingsKeys.moviesFilter), - seriesCategoryFilter: getValue(_SettingsKeys.seriesFilter), - - streamQuality: remoteSettings[_SettingsKeys.streamQuality] != null - ? StreamQuality.values[remoteSettings[_SettingsKeys.streamQuality] as int] : null, - bufferSize: remoteSettings[_SettingsKeys.bufferSize] != null - ? BufferSize.values[remoteSettings[_SettingsKeys.bufferSize] as int] : null, - connectionTimeout: remoteSettings[_SettingsKeys.connectionTimeout] != null - ? ConnectionTimeout.values[remoteSettings[_SettingsKeys.connectionTimeout] as int] : null, - autoReconnect: getValue(_SettingsKeys.autoReconnect), - epgCacheDuration: remoteSettings[_SettingsKeys.epgCacheDuration] != null - ? EpgCacheDuration.values[remoteSettings[_SettingsKeys.epgCacheDuration] as int] : null, - transcodingMode: remoteSettings[_SettingsKeys.transcodingMode] != null - ? TranscodingMode.values[remoteSettings[_SettingsKeys.transcodingMode] as int] : null, - preferDirectPlay: getValue(_SettingsKeys.preferDirectPlay), - showClock: getValue(_SettingsKeys.showClock), - preferredAspectRatio: getValue(_SettingsKeys.preferredAspectRatio), - ); - } - _initialized = true; - } catch (e) { - print('Error syncing settings from API: $e'); - } - } - - Future _saveToApi() async { - final authState = _ref.read(authProvider); - if (!authState.isAuthenticated) { - print('[SettingsProvider] _saveToApi: Not authenticated, skipping save'); - return; - } - - // Create map from state - final settingsMap = { - _SettingsKeys.liveTvFilter: state.liveTvCategoryFilter, - _SettingsKeys.moviesFilter: state.moviesCategoryFilter, - _SettingsKeys.seriesFilter: state.seriesCategoryFilter, - _SettingsKeys.streamQuality: state.streamQuality.index, - _SettingsKeys.bufferSize: state.bufferSize.index, - _SettingsKeys.connectionTimeout: state.connectionTimeout.index, - _SettingsKeys.autoReconnect: state.autoReconnect, - _SettingsKeys.epgCacheDuration: state.epgCacheDuration.index, - _SettingsKeys.transcodingMode: state.transcodingMode.index, - _SettingsKeys.preferDirectPlay: state.preferDirectPlay, - _SettingsKeys.showClock: state.showClock, - _SettingsKeys.preferredAspectRatio: state.preferredAspectRatio, - }; - - print('[SettingsProvider] _saveToApi: Saving settings...'); - final success = await _apiService.saveSettings(settingsMap); - print('[SettingsProvider] _saveToApi: Save result: $success'); - } - - // ===== Setters with Auto-Save ===== - - void setLiveTvFilter(String filter) { - state = state.copyWith(liveTvCategoryFilter: filter); - _saveToApi(); - } - - void setMoviesFilter(String filter) { - state = state.copyWith(moviesCategoryFilter: filter); - _saveToApi(); - } - - void setSeriesFilter(String filter) { - state = state.copyWith(seriesCategoryFilter: filter); - _saveToApi(); - } - - void clearLiveTvFilter() => setLiveTvFilter(''); - void clearMoviesFilter() => setMoviesFilter(''); - void clearSeriesFilter() => setSeriesFilter(''); - - void clearAllFilters() { - state = state.copyWith( - liveTvCategoryFilter: '', - moviesCategoryFilter: '', - seriesCategoryFilter: '', - ); - _saveToApi(); - } - - // ===== Streaming Setters ===== - - void setStreamQuality(StreamQuality quality) { - state = state.copyWith(streamQuality: quality); - _saveToApi(); - } - - void setBufferSize(BufferSize buffer) { - state = state.copyWith(bufferSize: buffer); - _saveToApi(); - } - - void setConnectionTimeout(ConnectionTimeout timeout) { - state = state.copyWith(connectionTimeout: timeout); - _saveToApi(); - } - - void setAutoReconnect(bool value) { - state = state.copyWith(autoReconnect: value); - _saveToApi(); - } - - void setEpgCacheDuration(EpgCacheDuration duration) { - state = state.copyWith(epgCacheDuration: duration); - _saveToApi(); - } - - void setTranscodingMode(TranscodingMode mode) { - state = state.copyWith(transcodingMode: mode); - _saveToApi(); - } - - void setPreferDirectPlay(bool value) { - state = state.copyWith(preferDirectPlay: value); - _saveToApi(); - } - - // ===== Player Display Setters ===== - - void setShowClock(bool value) { - state = state.copyWith(showClock: value); - _saveToApi(); - } - - void setPreferredAspectRatio(String value) { - state = state.copyWith(preferredAspectRatio: value); - _saveToApi(); - } - - // Legacy methods - void setCategoryFilter(String filter) => setLiveTvFilter(filter); - void clearCategoryFilter() => clearLiveTvFilter(); -} - -/// Provider for IPTV settings -final iptvSettingsProvider = - StateNotifierProvider((ref) { - // Watch auth provider to re-fetch settings on login/logout - // We just watch it to ensure the provider tree updates, but the listener above handles logic - ref.watch(authProvider); - return IptvSettingsNotifier(ref); -}); diff --git a/lib/features/iptv/providers/watch_history_provider.dart b/lib/features/iptv/providers/watch_history_provider.dart deleted file mode 100644 index d09ff7f..0000000 --- a/lib/features/iptv/providers/watch_history_provider.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:convert'; - -/// Watch history state - tracks which content has been watched -class WatchHistory { - /// Set of watched movie IDs - final Set watchedMovies; - - /// Set of watched episode IDs (format: seriesId_seasonNum_episodeNum) - final Set watchedEpisodes; - - const WatchHistory({ - this.watchedMovies = const {}, - this.watchedEpisodes = const {}, - }); - - WatchHistory copyWith({ - Set? watchedMovies, - Set? watchedEpisodes, - }) { - return WatchHistory( - watchedMovies: watchedMovies ?? this.watchedMovies, - watchedEpisodes: watchedEpisodes ?? this.watchedEpisodes, - ); - } - - /// Check if a movie has been watched - bool isMovieWatched(String movieId) => watchedMovies.contains(movieId); - - /// Check if an episode has been watched - bool isEpisodeWatched(String episodeId) => watchedEpisodes.contains(episodeId); - - /// Generate episode key - static String episodeKey(String seriesId, int seasonNum, int episodeNum) { - return '${seriesId}_${seasonNum}_$episodeNum'; - } -} - -/// Watch history notifier with persistence -class WatchHistoryNotifier extends StateNotifier { - static const String _moviesKey = 'watch_history_movies'; - static const String _episodesKey = 'watch_history_episodes'; - - SharedPreferences? _prefs; - bool _initialized = false; - - WatchHistoryNotifier() : super(const WatchHistory()) { - _loadHistory(); - } - - Future _loadHistory() async { - if (_initialized) return; - - try { - _prefs = await SharedPreferences.getInstance(); - - final moviesJson = _prefs?.getString(_moviesKey); - final episodesJson = _prefs?.getString(_episodesKey); - - final movies = moviesJson != null - ? Set.from(jsonDecode(moviesJson) as List) - : {}; - - final episodes = episodesJson != null - ? Set.from(jsonDecode(episodesJson) as List) - : {}; - - state = WatchHistory( - watchedMovies: movies, - watchedEpisodes: episodes, - ); - - _initialized = true; - print('Watch history loaded: ${movies.length} movies, ${episodes.length} episodes'); - } catch (e) { - print('Error loading watch history: $e'); - } - } - - Future _saveMovies() async { - try { - _prefs ??= await SharedPreferences.getInstance(); - await _prefs?.setString(_moviesKey, jsonEncode(state.watchedMovies.toList())); - } catch (e) { - print('Error saving movie watch history: $e'); - } - } - - Future _saveEpisodes() async { - try { - _prefs ??= await SharedPreferences.getInstance(); - await _prefs?.setString(_episodesKey, jsonEncode(state.watchedEpisodes.toList())); - } catch (e) { - print('Error saving episode watch history: $e'); - } - } - - /// Mark a movie as watched - void markMovieWatched(String movieId) { - final newSet = Set.from(state.watchedMovies)..add(movieId); - state = state.copyWith(watchedMovies: newSet); - _saveMovies(); - } - - /// Mark a movie as unwatched - void markMovieUnwatched(String movieId) { - final newSet = Set.from(state.watchedMovies)..remove(movieId); - state = state.copyWith(watchedMovies: newSet); - _saveMovies(); - } - - /// Toggle movie watched status - void toggleMovieWatched(String movieId) { - if (state.isMovieWatched(movieId)) { - markMovieUnwatched(movieId); - } else { - markMovieWatched(movieId); - } - } - - /// Mark an episode as watched - void markEpisodeWatched(String episodeKey) { - final newSet = Set.from(state.watchedEpisodes)..add(episodeKey); - state = state.copyWith(watchedEpisodes: newSet); - _saveEpisodes(); - } - - /// Mark an episode as unwatched - void markEpisodeUnwatched(String episodeKey) { - final newSet = Set.from(state.watchedEpisodes)..remove(episodeKey); - state = state.copyWith(watchedEpisodes: newSet); - _saveEpisodes(); - } - - /// Toggle episode watched status - void toggleEpisodeWatched(String episodeKey) { - if (state.isEpisodeWatched(episodeKey)) { - markEpisodeUnwatched(episodeKey); - } else { - markEpisodeWatched(episodeKey); - } - } - - /// Clear all watch history - void clearAll() { - state = const WatchHistory(); - _saveMovies(); - _saveEpisodes(); - } -} - -/// Provider for watch history -final watchHistoryProvider = - StateNotifierProvider((ref) { - return WatchHistoryNotifier(); -}); diff --git a/lib/features/iptv/providers/xtream_provider.dart b/lib/features/iptv/providers/xtream_provider.dart deleted file mode 100644 index 632f38f..0000000 --- a/lib/features/iptv/providers/xtream_provider.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/models/playlist_config.dart'; -import '../services/xtream_service.dart'; - -/// Family provider for Xtream service - one instance per playlist -final xtreamServiceProvider = Provider.family((ref, playlist) { - final service = XtreamService(); - service.setPlaylist(playlist); - ref.onDispose(() => service.dispose()); - return service; -}); - -/// Singleton Xtream service provider (legacy, for activeXtreamServiceProvider) -final _singletonXtreamServiceProvider = Provider((ref) { - final service = XtreamService(); - ref.onDispose(() => service.dispose()); - return service; -}); - -/// Current selected playlist provider -final selectedPlaylistProvider = StateProvider((ref) => null); - -/// Provider that watches for playlist changes and updates Xtream service -final activeXtreamServiceProvider = Provider((ref) { - final service = ref.watch(_singletonXtreamServiceProvider); - final playlist = ref.watch(selectedPlaylistProvider); - - if (playlist != null) { - service.setPlaylist(playlist); - } - - return service; -}); - -/// Provider for live TV channels grouped by category -final liveChannelsProvider = FutureProvider.autoDispose((ref) async { - final service = ref.watch(activeXtreamServiceProvider); - return await service.getLiveChannels(); -}); - -/// Provider for live TV channels by playlist (family provider) -final liveChannelsByPlaylistProvider = FutureProvider.family.autoDispose((ref, PlaylistConfig playlist) async { - final service = ref.watch(xtreamServiceProvider(playlist)); - return await service.getLiveChannels(); -}); - -/// Provider for VOD items (movies) grouped by category -final vodItemsProvider = FutureProvider.autoDispose((ref) async { - final service = ref.watch(activeXtreamServiceProvider); - return await service.getVodItems(); -}); - -/// Provider for series grouped by category -final seriesProvider = FutureProvider.autoDispose((ref) async { - final service = ref.watch(activeXtreamServiceProvider); - return await service.getSeries(); -}); - -/// Provider for EPG of a specific stream -final epgProvider = FutureProvider.family.autoDispose((ref, String streamId) async { - final service = ref.watch(activeXtreamServiceProvider); - return await service.getShortEpg(streamId); -}); - -/// EPG request key for family provider (combines playlist and streamId) -class EpgRequestKey { - final PlaylistConfig playlist; - final String streamId; - - const EpgRequestKey({required this.playlist, required this.streamId}); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is EpgRequestKey && - runtimeType == other.runtimeType && - playlist == other.playlist && - streamId == other.streamId; - - @override - int get hashCode => playlist.hashCode ^ streamId.hashCode; -} - -/// Provider for EPG with playlist context -final epgByPlaylistProvider = FutureProvider.family.autoDispose((ref, EpgRequestKey key) async { - final service = ref.watch(xtreamServiceProvider(key.playlist)); - return await service.getShortEpg(key.streamId); -}); diff --git a/lib/features/iptv/screens/dashboard_screen.dart b/lib/features/iptv/screens/dashboard_screen.dart deleted file mode 100644 index 23b10fe..0000000 --- a/lib/features/iptv/screens/dashboard_screen.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/widgets/digital_clock.dart'; -import '../widgets/live_tv_tab.dart'; -import '../widgets/movies_tab.dart'; -import '../widgets/series_tab.dart'; -import '../widgets/settings_tab.dart'; - -/// Apple TV Style Dashboard -/// -/// Uses a floating Top Bar for navigation instead of a sidebar. -/// Content flows underneath the top bar. -class DashboardScreen extends ConsumerStatefulWidget { - final PlaylistConfig playlist; - - const DashboardScreen({ - super.key, - required this.playlist, - }); - - @override - ConsumerState createState() => _DashboardScreenState(); -} - -class _DashboardScreenState extends ConsumerState - with SingleTickerProviderStateMixin { - late TabController _tabController; - - final List _tabs = ['Live TV', 'Films', 'Séries', 'Réglages']; - final List _icons = [ - Icons.live_tv, - Icons.movie, - Icons.tv, - Icons.settings, - ]; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 4, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // Glassmorphism Top Bar - return Scaffold( - extendBodyBehindAppBar: true, // Content behind app bar - backgroundColor: Colors.transparent, // Transparent to show gradient - appBar: PreferredSize( - preferredSize: const Size.fromHeight(80), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.6), // Subtler top shadow - Colors.transparent, - ], - ), - ), - child: Row( - children: [ - // 1. Logo Area (Fixed Width for centering balance) - SizedBox( - width: 200, - child: Align( - alignment: Alignment.centerLeft, - child: _buildLogo(), - ), - ), - - // 2. Centered Navigation Tabs (Expanded to fill space, then Centered) - Expanded( - child: Center( - child: Container( - height: 50, - constraints: const BoxConstraints( - maxWidth: 600, - ), // Prevent taking too much space - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(30), - border: Border.all(color: Colors.white.withOpacity(0.05)), - ), - child: TabBar( - controller: _tabController, - isScrollable: true, - dividerColor: Colors.transparent, - indicator: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(25), - ), - indicatorSize: TabBarIndicatorSize.tab, - indicatorPadding: const EdgeInsets.all(4), - labelColor: Colors.black, - unselectedLabelColor: - Colors.white70, // Fixed: Improved readability - labelStyle: GoogleFonts.inter( - fontWeight: FontWeight.w700, - fontSize: 13, - ), - overlayColor: WidgetStateProperty.all(Colors.transparent), - tabs: List.generate(_tabs.length, (index) { - return Tab( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Icon(_icons[index], size: 18), - const SizedBox(width: 8), - Text(_tabs[index]), - ], - ), - ), - ); - }), - ), - ), - ), - ), - - // 3. Search / Profile (Fixed Width for centering balance) - SizedBox( - width: 200, - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.search, color: Colors.white), - onPressed: () { - // Global search trigger - }, - ), - const SizedBox(width: 16), - const DigitalClock(), - const SizedBox(width: 16), - const CircleAvatar( - radius: 16, - backgroundColor: Colors.white24, - child: - Icon(Icons.person, size: 18, color: Colors.white), - ), - ], - ), - ), - ), - ], - ), - ), - ), - body: Stack( - children: [ - // Background Gradient (Apple TV Depth Effect) - Container( - decoration: const BoxDecoration( - gradient: RadialGradient( - center: Alignment(0, -0.2), // Slightly above center - radius: 1.5, - colors: [ - Color(0xFF2C2C2E), // Dark Grey center (Light Source) - Color(0xFF000000), // Pure Black edges - ], - stops: [0.0, 1.0], - ), - ), - ), - - // Content - TabBarView( - controller: _tabController, - physics: - const NeverScrollableScrollPhysics(), // Disable swipe to avoid gesture conflicts - children: [ - // Padding top to account for the transparent AppBar - Padding( - padding: const EdgeInsets.only(top: 80), - child: LiveTVTab(playlist: widget.playlist), - ), - Padding( - padding: const EdgeInsets.only(top: 80), - child: MoviesTab(playlist: widget.playlist), - ), - Padding( - padding: const EdgeInsets.only(top: 80), - child: SeriesTab(playlist: widget.playlist), - ), - const Padding( - padding: EdgeInsets.only(top: 80), - child: SettingsTab(), - ), - ], - ), - ], - ), - ); - } - - Widget _buildLogo() { - return Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - Color(0xFF43cea2), - Color(0xFF185a9d), - ], - ), // Green to Blue - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.play_arrow_rounded, color: Colors.white), - ), - const SizedBox(width: 12), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Xtrem', - style: GoogleFonts.inter( - fontWeight: FontWeight.w800, - fontSize: 16, - color: Colors.white, - ), - ), - Text( - 'Flow', - style: GoogleFonts.inter( - fontWeight: FontWeight.w300, - fontSize: 16, - color: Colors.white.withOpacity(0.8), - height: 0.8, - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart deleted file mode 100644 index 7ac38dd..0000000 --- a/lib/features/iptv/screens/player_screen.dart +++ /dev/null @@ -1,1200 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:universal_html/html.dart' as html; -import '../../../core/shims/ui_web.dart' as ui_web; -import 'dart:async'; -import '../providers/xtream_provider.dart'; -import '../providers/playback_positions_provider.dart'; -import '../providers/settings_provider.dart'; -import '../widgets/epg_overlay.dart'; -import '../widgets/clock_widget.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/models/iptv_models.dart'; - -import '../../../core/widgets/themed_loading_screen.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'dart:ui' as ui; // Essential for BackdropFilter -import '../../../core/theme/app_colors.dart'; - -/// Stream type enum for player -enum StreamType { live, vod, series } - -/// Video player screen using FFmpeg transcoding for live streams -class PlayerScreen extends ConsumerStatefulWidget { - final String streamId; - final String title; - final StreamType streamType; - final String containerExtension; - final PlaylistConfig playlist; - final List? channels; - final int initialIndex; - - const PlayerScreen({ - super.key, - required this.streamId, - required this.title, - required this.playlist, - this.streamType = StreamType.live, - this.containerExtension = 'mp4', - this.channels, - this.initialIndex = 0, - }); - - @override - ConsumerState createState() => _PlayerScreenState(); -} - -class _PlayerScreenState extends ConsumerState { - String? _hlsUrl; - String? _errorMessage; - String? _statusMessage; - late String _viewId; - late String _contentId; - late int _currentIndex; - bool _isInitialized = false; - bool _isLoading = true; - bool _showControls = false; - bool _isPlaying = true; - double _currentPosition = 0; - - double _totalDuration = 1; - double _volume = 1.0; - double _previousVolume = 1.0; - bool _isFullscreen = false; - Timer? _controlsTimer; - StreamSubscription? _messageSubscription; - - String _formatDuration(Duration duration) { - String twoDigits(int n) => n.toString().padLeft(2, '0'); - String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60)); - String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60)); - return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds"; - } - - // Premium Features State - List> _audioTracks = []; - String _aspectRatio = 'contain'; - - void _setupMessageListener() { - // Listen for postMessage from player.html iframe - _messageSubscription = html.window.onMessage.listen((event) { - try { - final data = event.data; - if (data is Map) { - final type = data['type']; - if (type == 'playback_position' && _contentId.isNotEmpty) { - final currentTime = (data['currentTime'] as num).toDouble(); - final rawDuration = (data['duration'] as num).toDouble(); - final duration = rawDuration.isFinite ? rawDuration : 0.0; - setState(() { - _currentPosition = currentTime; - _totalDuration = duration > 0 ? duration : 1; - }); - ref.read(playbackPositionsProvider.notifier).savePosition( - _contentId, - currentTime, - duration, - ); - } else if (type == 'playback_status') { - final status = data['status']; - setState(() { - _isPlaying = status == 'playing'; - }); - } else if (type == 'playback_ended' && _contentId.isNotEmpty) { - } else if (type == 'user_activity') { - _onHover(); - } else if (type == 'audio_tracks') { - final tracks = List>.from(data['tracks']); - setState(() { - _audioTracks = tracks; - }); - } - } - } catch (e) { - debugPrint('Error handling postMessage: $e'); - } - }); - } - - void _sendMessage(Map message) { - // Helper to send message to iframe - debugPrint('Sending message to iframe ($_viewId): $message'); - final iframe = html.document.getElementById(_viewId) as html.IFrameElement?; - if (iframe == null) { - debugPrint('ERROR: Iframe with ID $_viewId not found!'); - } else { - iframe.contentWindow?.postMessage(message, '*'); - } - } - - void _setAspectRatio(String mode) { - setState(() { - _aspectRatio = mode; - }); - _sendMessage({'type': 'set_aspect_ratio', 'value': mode}); - } - - void _setAudioTrack(int index) { - _sendMessage({'type': 'set_audio_track', 'index': index}); - Navigator.pop(context); // Close settings - } - - @override - void dispose() { - _messageSubscription?.cancel(); - _controlsTimer?.cancel(); - super.dispose(); - } - - void _hideControls() { - if (!mounted) return; - setState(() { - _showControls = false; - }); - } - - void _onHover() { - if (!_showControls) { - setState(() { - _showControls = true; - }); - } - _controlsTimer?.cancel(); - _controlsTimer = Timer(const Duration(seconds: 4), _hideControls); - } - - void _togglePlayPause() { - setState(() { - _isPlaying = !_isPlaying; - }); - _sendMessage({'type': _isPlaying ? 'play' : 'pause'}); - _onHover(); // Keep controls visible - } - - void _seekTo(double value) { - setState(() { - _currentPosition = value; - }); - - // Check if we are using server-side transcoding for VOD/Series - final settings = ref.read(iptvSettingsProvider); - // Only reload for seeking if we are rigorously Transcoding (FFmpeg pipe) - // If we are in Direct Proxy mode (default), we can seek natively via JS. - final bool usingTranscoding = - settings.transcodingMode == TranscodingMode.forced; - - if (usingTranscoding) { - debugPrint( - 'Seeking in transcoded VOD - reloading player at ${value.round()}s', - ); - ref - .read(playbackPositionsProvider.notifier) - .savePosition(_contentId, value, _totalDuration); - _initializePlayer(startTimeOverride: value); - } else { - // Direct play (Proxy) or Live - standard seeking happens in the browser via Range headers - _sendMessage({'type': 'seek', 'value': value}); - } - - _onHover(); - } - - void _setVolume(double value) { - setState(() { - _volume = value; - if (value > 0) { - _previousVolume = value; - } - }); - _sendMessage({'type': 'set_volume', 'value': value}); - _onHover(); - } - - void _toggleMute() { - if (_volume > 0) { - // Mute - setState(() { - _previousVolume = _volume; - _volume = 0; - }); - _sendMessage({'type': 'set_volume', 'value': 0.0}); - } else { - // Unmute (restore previous volume) - setState(() { - _volume = _previousVolume > 0 ? _previousVolume : 0.5; - }); - _sendMessage({'type': 'set_volume', 'value': _volume}); - } - _onHover(); - } - - void _playNext() { - if (widget.channels == null || widget.channels!.isEmpty) return; - final nextIndex = (_currentIndex + 1) % widget.channels!.length; - _switchChannel(nextIndex); - } - - void _playPrevious() { - if (widget.channels == null || widget.channels!.isEmpty) return; - final prevIndex = - (_currentIndex - 1 + widget.channels!.length) % widget.channels!.length; - _switchChannel(prevIndex); - } - - void _switchChannel(int index) { - setState(() { - _currentIndex = index; - _isLoading = true; - _isInitialized = false; - _errorMessage = null; - }); - // Give UI a moment to clear before re-initializing - Future.microtask(() => _initializePlayer()); - } - - void _toggleFullscreen() { - final document = html.document; - final docElement = document.documentElement; - - if (_isFullscreen) { - document.exitFullscreen(); - setState(() { - _isFullscreen = false; - }); - } else { - docElement?.requestFullscreen(); - setState(() { - _isFullscreen = true; - }); - } - _onHover(); - } - - Future _initializePlayer({double? startTimeOverride}) async { - try { - setState(() { - _isLoading = true; - _statusMessage = 'Validating playlist...'; - }); - - // Validate playlist before proceeding - if (widget.playlist.dns.isEmpty || - widget.playlist.username.isEmpty || - widget.playlist.password.isEmpty) { - throw Exception('Invalid playlist configuration: missing credentials'); - } - - final xtreamService = ref.read(xtreamServiceProvider(widget.playlist)); - xtreamService.setPlaylist(widget.playlist); - - // Determine effective Stream ID (from navigation or initial) - final currentStreamId = - widget.channels != null && widget.channels!.isNotEmpty - ? widget.channels![_currentIndex].streamId - : widget.streamId; - - // Set content ID for resume features - _contentId = widget.streamType == StreamType.live - ? '' - : '${widget.streamType.name}_$currentStreamId'; - - String hlsUrl; - - if (widget.streamType == StreamType.live) { - // For live streams, use FFmpeg transcoding - setState(() { - _statusMessage = 'Starting stream transcoding...'; - }); - - // Build the FFmpeg endpoint URL with streaming settings - final settings = ref.read(iptvSettingsProvider); - final baseUrl = html.window.location.origin; - final iptvUrl = - '${widget.playlist.dns}/live/${widget.playlist.username}/${widget.playlist.password}/$currentStreamId.ts'; - final encodedUrl = Uri.encodeComponent(iptvUrl); - - // Map enum values to string params using if-else (avoids switch exhaustivity issues) - String qualityParam = 'medium'; - if (settings.streamQuality == StreamQuality.low) { - qualityParam = 'low'; - } else if (settings.streamQuality == StreamQuality.high) { - qualityParam = 'high'; - } - - String bufferParam = 'medium'; - if (settings.bufferSize == BufferSize.low) { - bufferParam = 'low'; - } else if (settings.bufferSize == BufferSize.high) { - bufferParam = 'high'; - } - - String timeoutParam = 'medium'; - if (settings.connectionTimeout == ConnectionTimeout.short) { - timeoutParam = 'short'; - } else if (settings.connectionTimeout == ConnectionTimeout.long) { - timeoutParam = 'long'; - } - final modeParam = settings.modeString; // direct, transcode, or auto - - // Construct the stream URL directly using MPEG-TS format - final streamEndpoint = - '$baseUrl/api/stream/$currentStreamId?url=$encodedUrl&quality=$qualityParam&buffer=$bufferParam&timeout=$timeoutParam&mode=$modeParam&ext=.ts'; - - debugPrint('PlayerScreen: Starting Direct Stream: $streamEndpoint'); - hlsUrl = streamEndpoint; - } else { - // For VOD and Series - - // For VOD and Series - - // Determine play mode: Direct (Proxy) vs Transcode (FFmpeg) - // Default to Direct/Proxy because it preserves duration metadata and seeking capability (via Range headers) - // Only use Transcode if explicitly FORCED (e.g. for audio compatibility issues like AC3/DTS on some browsers) - final settings = ref.read(iptvSettingsProvider); - final baseUrl = html.window.location.origin; - bool useTranscode = settings.transcodingMode == TranscodingMode.forced; - - setState(() { - _statusMessage = - useTranscode ? 'Preparing stream...' : 'Loading video...'; - }); - - if (useTranscode) { - // Transcoding Mode (FFmpeg pipe) - final encodedUrl = Uri.encodeComponent( - widget.streamType == StreamType.vod - ? xtreamService.getVodStreamUrl( - currentStreamId, - widget.containerExtension, - ) - : xtreamService.getSeriesStreamUrl( - currentStreamId, - widget.containerExtension, - ), - ); - - String qualityParam = 'medium'; - if (settings.streamQuality == StreamQuality.low) { - qualityParam = 'low'; - } else if (settings.streamQuality == StreamQuality.high) - qualityParam = 'high'; - - // Calculate start time for seeking support - double startSeconds = startTimeOverride ?? 0; - if (startTimeOverride == null && _contentId.isNotEmpty) { - final positions = ref.read(playbackPositionsProvider); - final saved = positions.getPosition(_contentId); - if (saved > 0) startSeconds = saved; - } - - // We force extension to .ts for mpegts.js compatibility - hlsUrl = - '$baseUrl/api/stream/$currentStreamId?url=$encodedUrl&quality=$qualityParam&start=${startSeconds.round()}&ext=.ts'; - debugPrint('PlayerScreen: Streaming VOD (Transcoded): $hlsUrl'); - } else { - // Direct/Proxy Mode (Preserves Duration) - final rawUrl = widget.streamType == StreamType.vod - ? '${widget.playlist.dns}/movie/${widget.playlist.username}/${widget.playlist.password}/$currentStreamId.${widget.containerExtension}' - : '${widget.playlist.dns}/series/${widget.playlist.username}/${widget.playlist.password}/$currentStreamId.${widget.containerExtension}'; - - // Force use of local proxy /api/xtream/ to handle CORS and Range headers correctly - final proxyPath = '/api/xtream/$rawUrl'; - hlsUrl = '$baseUrl$proxyPath'; - - // Append start time for direct play? - // HTML5 video handles seeking naturally via headers, but if we want to resume: - // We have to seek AFTER metadata is loaded via JS (_seekTo called in onMessage logic). - - debugPrint('PlayerScreen: Streaming VOD (Direct Proxy): $hlsUrl'); - } - } - - setState(() { - _statusMessage = 'Loading player...'; - }); - - // Get resume position for VOD/Series - double startTime = startTimeOverride ?? 0; - if (startTime <= 0 && - widget.streamType != StreamType.live && - _contentId.isNotEmpty) { - final positions = ref.read(playbackPositionsProvider); - startTime = positions.getPosition(_contentId); - if (startTime > 0) { - debugPrint( - 'PlayerScreen: Resuming from ${startTime.toStringAsFixed(0)}s', - ); - } - } - - // If we are Transcoding VOD, the start time is handled by the server (burned into stream) - // So we should NOT pass startTime to player.html, otherwise it might try to seek - // on a stream that already starts at X. - // EXCEPTION: If player.html receives an MPEG-TS stream that has timestamps starting at X, - // setting currentTime might be redundant or needed depending on player impl. - // Safest: If Transcoding, don't pass startTime to player.html (server handles it). - // If Direct Play, pass startTime. - - final settings = ref.read(iptvSettingsProvider); - // We force transcoding/proxy usage for all VOD/Series now - final bool usingTranscoding = - settings.transcodingMode == TranscodingMode.forced; - - final encodedHlsUrl = Uri.encodeComponent(hlsUrl); - // Pass startTime to player.html if we are NOT using server-side transcoding (Direct/Proxy mode) - // For Transcoding, the server handles start time via the 'start' query param in hlsUrl. - final playerUrl = (!usingTranscoding && startTime > 0) - ? 'player.html?url=$encodedHlsUrl&startTime=${startTime.toStringAsFixed(0)}' - : 'player.html?url=$encodedHlsUrl'; - - debugPrint('PlayerScreen: Player URL: $playerUrl'); - - // Generate a new view ID for every load to force iframe recreation - _viewId = - 'iptv-player-${widget.streamId}-${DateTime.now().millisecondsSinceEpoch}'; - - // Register platform view factory - // ignore: undefined_prefixed_name - ui_web.platformViewRegistry.registerViewFactory( - _viewId, - (int viewId) { - final iframe = html.IFrameElement() - ..id = _viewId // CRITICAL: Set ID for getElementById lookup - ..src = playerUrl - ..style.border = 'none' - ..style.width = '100%' - ..style.height = '100%' - ..allow = 'autoplay; fullscreen; picture-in-picture'; - return iframe; - }, - ); - - setState(() { - _hlsUrl = hlsUrl; - _isInitialized = true; - _isLoading = false; - }); - } catch (e) { - debugPrint('PlayerScreen error: $e'); - setState(() { - _isLoading = false; - _errorMessage = e.toString(); - }); - } - } - - @override - void initState() { - super.initState(); - _currentIndex = widget.initialIndex; - - // Load preferred aspect ratio from settings - final settings = ref.read(iptvSettingsProvider); - _aspectRatio = settings.preferredAspectRatio; - - _initializePlayer(); - _setupMessageListener(); - } - - void _showSettingsDialog() { - // Read current settings - final settingsNotifier = ref.read(iptvSettingsProvider.notifier); - // We use a local state variable for the switch in the dialog, - // initialized from the provider. - bool showClock = ref.read(iptvSettingsProvider).showClock; - - showDialog( - context: context, - useRootNavigator: true, - builder: (context) => PointerInterceptor( - child: Dialog( - backgroundColor: Colors.transparent, - insetPadding: const EdgeInsets.all(24), - child: StatefulBuilder( - builder: (context, setDialogState) { - return TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - builder: (context, value, child) => Transform.scale( - scale: 0.9 + (0.1 * value), - child: Opacity( - opacity: value, - child: child, - ), - ), - child: Container( - width: 400, - decoration: BoxDecoration( - color: AppColors.surface.withOpacity(0.8), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withOpacity(0.1)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.5), - blurRadius: 30, - offset: const Offset(0, 10), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Réglages', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: -0.5, - ), - ), - IconButton( - icon: const Icon( - Icons.close, - color: Colors.white54, - ), - onPressed: () => Navigator.pop(context), - tooltip: 'Fermer', - ), - ], - ), - const SizedBox(height: 24), - const Text( - 'FORMAT D\'IMAGE', - style: TextStyle( - color: AppColors.textSecondary, - fontSize: 12, - fontWeight: FontWeight.bold, - letterSpacing: 1, - ), - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - _buildRatioOption( - 'Original', - 'contain', - Icons.crop_original, - setDialogState, - ), - _buildRatioOption( - 'Remplir', - 'cover', - Icons.crop_free, - setDialogState, - ), - _buildRatioOption( - 'Étirer', - 'fill', - Icons.aspect_ratio, - setDialogState, - ), - ], - ), - ), - if (_audioTracks.isNotEmpty) ...[ - const SizedBox(height: 32), - const Text( - 'AUDIO', - style: TextStyle( - color: AppColors.textSecondary, - fontSize: 12, - fontWeight: FontWeight.bold, - letterSpacing: 1, - ), - ), - const SizedBox(height: 12), - Container( - constraints: - const BoxConstraints(maxHeight: 150), - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.white.withOpacity(0.05), - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: SingleChildScrollView( - child: Column( - children: _audioTracks.map((track) { - return Material( - color: Colors.transparent, - child: ListTile( - title: Text( - track['label'] ?? - 'Piste ${track['id']}', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - subtitle: Text( - track['lang'] ?? '', - style: const TextStyle( - color: Colors.white54, - fontSize: 12, - ), - ), - onTap: () { - _setAudioTrack(track['id']); - setDialogState(() {}); - }, - dense: true, - leading: const Icon( - Icons.audiotrack, - size: 18, - color: Colors.white54, - ), - hoverColor: - Colors.white.withOpacity(0.1), - ), - ); - }).toList(), - ), - ), - ), - ), - ], - const SizedBox(height: 32), - const Text( - 'AFFICHAGE', - style: TextStyle( - color: AppColors.textSecondary, - fontSize: 12, - fontWeight: FontWeight.bold, - letterSpacing: 1, - ), - ), - const SizedBox(height: 12), - SwitchListTile( - value: showClock, - onChanged: (val) { - setDialogState(() { - showClock = val; - }); - settingsNotifier.setShowClock(val); - }, - title: const Text( - 'Afficher l\'heure', - style: TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - secondary: const Icon( - Icons.access_time, - color: Colors.white54, - ), - activeThumbColor: AppColors.primary, - contentPadding: EdgeInsets.zero, - ), - ], - ), - ), - ), - ), - ), - ); - }, - ), - ), - ), - ); - } - - Widget _buildRatioOption( - String label, - String value, - IconData icon, - StateSetter setDialogState, - ) { - final isSelected = _aspectRatio == value; - return Expanded( - child: GestureDetector( - onTap: () { - debugPrint('Changing Aspect Ratio to: $value'); - _setAspectRatio(value); - // Save preference - ref - .read(iptvSettingsProvider.notifier) - .setPreferredAspectRatio(value); - setDialogState(() {}); // Force rebuild of the dialog content - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected ? AppColors.surfaceVariant : Colors.transparent, - borderRadius: BorderRadius.circular(10), - border: isSelected ? Border.all(color: Colors.white12) : null, - boxShadow: isSelected - ? [ - const BoxShadow( - color: Colors.black12, - blurRadius: 4, - offset: Offset(0, 2), - ), - ] - : null, - ), - child: Column( - children: [ - Icon( - icon, - color: isSelected ? Colors.white : Colors.white54, - size: 20, - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - color: isSelected ? Colors.white : Colors.white54, - fontSize: 11, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ], - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final settings = ref.watch(iptvSettingsProvider); - return Scaffold( - appBar: _showControls - ? AppBar( - title: Text( - widget.channels != null && widget.channels!.isNotEmpty - ? widget.channels![_currentIndex].name - : widget.title, - ), - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - elevation: 0, - actions: [ - PointerInterceptor( - child: IconButton( - icon: const Icon(Icons.settings), - onPressed: _showSettingsDialog, - tooltip: 'Réglages', - ), - ), - const SizedBox(width: 16), - ], - ) - : null, - extendBodyBehindAppBar: true, - backgroundColor: Colors.black, - body: ClipRect( - child: MouseRegion( - onHover: (_) => _onHover(), - child: _errorMessage != null - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - size: 64, - color: Colors.red, - ), - const SizedBox(height: 16), - const Text( - 'Failed to load stream', - style: TextStyle(color: Colors.white, fontSize: 18), - ), - const SizedBox(height: 8), - Text( - _errorMessage!, - style: const TextStyle(color: Colors.white70), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () { - setState(() { - _errorMessage = null; - _isLoading = true; - }); - _initializePlayer(); - }, - child: const Text('Retry'), - ), - ], - ), - ) - : _isLoading - ? const ThemedLoading() - : !_isInitialized - ? const ThemedLoading() - : Stack( - children: [ - // HTML5 video player via iframe - HtmlElementView(viewType: _viewId), - - // GLOBAL INTERACTION LAYER (Fixes Click/Hover issues) - // This transparent layer sits ABOVE the iframe but BELOW the controls. - // It intercepts all pointer events, enabling Flutter to handle them. - Positioned.fill( - child: PointerInterceptor( - child: MouseRegion( - onHover: (_) => _onHover(), - child: GestureDetector( - // Toggle controls on tap - onTap: () { - if (_showControls) { - _hideControls(); - } else { - _onHover(); - } - }, - behavior: HitTestBehavior.translucent, - child: Container(color: Colors.transparent), - ), - ), - ), - ), - - // Hover Controls Layer - if (_showControls) ...[ - // EPG Overlay (Moved behind controls for Z-ordering) - if (widget.streamType == StreamType.live) - Positioned( - bottom: - 15, // Moves EPG down to avoid blocking controls - left: 0, - right: 0, - child: PointerInterceptor( - // Wrap with IgnorePointer so EPG text doesn't steal clicks from controls - child: IgnorePointer( - child: EpgOverlay( - streamId: widget.channels != null - ? widget.channels![_currentIndex] - .streamId - : widget.streamId, - playlist: widget.playlist, - ), - ), - ), - ), - - // Volume Slider (Left Side) - Positioned( - left: 24, - top: 0, - bottom: 0, - child: Center( - child: Container( - height: 200, - width: 40, - decoration: BoxDecoration( - color: Colors.black45, - borderRadius: BorderRadius.circular(20), - ), - padding: const EdgeInsets.symmetric( - vertical: 12, - ), - child: Column( - children: [ - PointerInterceptor( - child: IconButton( - icon: Icon( - _volume == 0 - ? Icons.volume_off - : _volume < 0.5 - ? Icons.volume_down - : Icons.volume_up, - color: Colors.white, - size: 20, - ), - onPressed: _toggleMute, - tooltip: _volume == 0 - ? 'Unmute' - : 'Mute', - ), - ), - const SizedBox(height: 8), - Expanded( - child: RotatedBox( - quarterTurns: 3, - child: SliderTheme( - data: SliderThemeData( - trackHeight: 4, - thumbShape: - const RoundSliderThumbShape( - enabledThumbRadius: 6, - ), - overlayShape: - const RoundSliderOverlayShape( - overlayRadius: 12, - ), - activeTrackColor: Colors.white, - inactiveTrackColor: - Colors.white24, - thumbColor: AppColors.primary, - overlayColor: AppColors.primary - .withOpacity(0.2), - ), - child: Slider( - value: _volume, - onChanged: _setVolume, - ), - ), - ), - ), - ], - ), - ), - ), - ), - - // Playback Controls (Centered Row: Previous - Play/Pause - Next - Fullscreen) - Positioned( - top: null, - bottom: 150, - left: 0, - right: 0, - child: Center( - child: PointerInterceptor( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - // Previous Channel Button - if (widget.channels != null && - widget.channels!.isNotEmpty) - InkWell( - onTap: _playPrevious, - borderRadius: - BorderRadius.circular(50), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black - .withOpacity(0.5), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.skip_previous, - color: Colors.white, - size: 32, - ), - ), - ), - if (widget.channels != null && - widget.channels!.isNotEmpty) - const SizedBox(width: 24), - - // Play/Pause Button - InkWell( - onTap: _togglePlayPause, - borderRadius: - BorderRadius.circular(50), - child: Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: - Colors.black.withOpacity(0.5), - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 2, - ), - ), - child: Icon( - _isPlaying - ? Icons.pause - : Icons.play_arrow, - color: Colors.white, - size: 40, - ), - ), - ), - - // Next Channel Button - if (widget.channels != null && - widget.channels!.isNotEmpty) - const SizedBox(width: 24), - if (widget.channels != null && - widget.channels!.isNotEmpty) - InkWell( - onTap: _playNext, - borderRadius: - BorderRadius.circular(50), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.black - .withOpacity(0.5), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.skip_next, - color: Colors.white, - size: 32, - ), - ), - ), - - // Fullscreen Button - const SizedBox(width: 32), - InkWell( - onTap: _toggleFullscreen, - borderRadius: - BorderRadius.circular(50), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: - Colors.black.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: Icon( - _isFullscreen - ? Icons.fullscreen_exit - : Icons.fullscreen, - color: Colors.white, - size: 28, - ), - ), - ), - ], - ), - ), - ), - ), - - // Progress Bar (Bottom) - Only for VOD/Series - if (widget.streamType != StreamType.live) - Positioned( - bottom: 80, - left: 24, - right: 24, - child: PointerInterceptor( - child: Column( - children: [ - SliderTheme( - data: SliderThemeData( - thumbShape: - const RoundSliderThumbShape( - enabledThumbRadius: 8, - ), - overlayShape: - const RoundSliderOverlayShape( - overlayRadius: 16, - ), - activeTrackColor: AppColors.primary, - inactiveTrackColor: Colors.white24, - thumbColor: AppColors.primary, - overlayColor: AppColors.primary - .withOpacity(0.2), - ), - child: Slider( - value: _currentPosition, - max: _totalDuration, - onChanged: (value) { - _seekTo(value); - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - _formatDuration( - Duration( - seconds: _currentPosition - .isFinite - ? _currentPosition - .toInt() - : 0, - ), - ), - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - Text( - _formatDuration( - Duration( - seconds: _totalDuration - .isFinite - ? _totalDuration.toInt() - : 0, - ), - ), - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ], - - // Clock Widget (Always visible if enabled, or only with controls? User asked "met l'heure dans les options") - // Positioned in top right - if (settings.showClock) - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - top: 24, // Fixed top - right: _showControls - ? 80 - : 24, // Shift left when settings visible - child: const IgnorePointer( - child: ClockWidget( - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - shadows: [ - Shadow( - color: Colors.black, - blurRadius: 8, - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/iptv/screens/playlist_selection_screen.dart b/lib/features/iptv/screens/playlist_selection_screen.dart deleted file mode 100644 index 608d76d..0000000 --- a/lib/features/iptv/screens/playlist_selection_screen.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; -import '../../../core/services/playlist_api_service.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../auth/providers/auth_provider.dart'; - -/// Provider for fetching playlists from API -final playlistsProvider = FutureProvider>((ref) async { - final service = PlaylistApiService(); - return service.getPlaylists(); -}); - -class PlaylistSelectionScreen extends ConsumerWidget { - const PlaylistSelectionScreen({super.key}); - - @override - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentUser = ref.watch(authProvider).currentUser; - final playlistsAsync = ref.watch(playlistsProvider); - - return Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - title: Text( - 'Select Playlist', - style: GoogleFonts.roboto( - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - backgroundColor: Colors.transparent, - elevation: 0, - actions: [ - if (currentUser?.isAdmin ?? false) - IconButton( - icon: - const Icon(Icons.admin_panel_settings, color: Colors.white70), - onPressed: () => context.go('/admin'), - tooltip: 'Admin Panel', - ), - IconButton( - icon: const Icon(Icons.logout, color: Colors.white70), - onPressed: () async { - await ref.read(authProvider.notifier).logout(); - if (context.mounted) { - context.go('/login'); - } - }, - tooltip: 'Logout', - ), - ], - ), - body: Container( - decoration: const BoxDecoration( - gradient: RadialGradient( - center: Alignment.center, - radius: 1.5, - colors: [ - Color(0xFF2C2C2E), // Dark Grey (Apple TV Surface) - Color(0xFF000000), // Pure Black - ], - stops: [0.0, 1.0], - ), - ), - child: playlistsAsync.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error_outline, size: 64, color: Colors.red.shade400), - const SizedBox(height: 16), - Text( - 'Error loading playlists', - style: GoogleFonts.roboto( - fontSize: 18, - color: Colors.white70, - ), - ), - const SizedBox(height: 8), - ElevatedButton( - onPressed: () => ref.refresh(playlistsProvider), - child: const Text('Retry'), - ), - ], - ), - ), - data: (playlists) { - if (playlists.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.playlist_remove, - size: 64, - color: Colors.white24, - ), - const SizedBox(height: 16), - Text( - 'No playlists available', - style: GoogleFonts.roboto( - fontSize: 18, - color: Colors.white60, - ), - ), - const SizedBox(height: 8), - Text( - currentUser?.isAdmin ?? false - ? 'Add playlists in Admin Panel' - : 'Contact administrator', - style: GoogleFonts.roboto( - fontSize: 14, - color: Colors.white38, - ), - ), - ], - ), - ); - } - - return GridView.builder( - padding: const EdgeInsets.fromLTRB( - 24, - 100, - 24, - 24, - ), // Top padding for transparent AppBar - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 24, - mainAxisSpacing: 24, - childAspectRatio: 1.5, - ), - itemCount: playlists.length, - itemBuilder: (context, index) { - final playlist = playlists[index]; - return _PlaylistCard( - playlist: playlist, - onTap: () { - context.go('/dashboard', extra: playlist); - }, - ); - }, - ); - }, - ), - ), - ); - } -} - -class _PlaylistCard extends StatefulWidget { - final PlaylistConfig playlist; - final VoidCallback onTap; - - const _PlaylistCard({ - required this.playlist, - required this.onTap, - }); - - @override - State<_PlaylistCard> createState() => _PlaylistCardState(); -} - -class _PlaylistCardState extends State<_PlaylistCard> { - bool _isHovered = false; - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setState(() => _isHovered = true), - onExit: (_) => setState(() => _isHovered = false), - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: widget.onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: BoxDecoration( - color: _isHovered - ? Colors.white.withOpacity(0.15) - : Colors.white.withOpacity(0.05), // Glass effect - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: _isHovered - ? Colors.white.withOpacity(0.5) - : Colors.white.withOpacity(0.1), - width: 1.5, - ), - boxShadow: _isHovered - ? [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ] - : [], - ), - padding: const EdgeInsets.all(24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.2), // Accent color tint - shape: BoxShape.circle, - ), - child: const Icon( - Icons.playlist_play, - size: 32, - color: Colors.white, // White icon - ), - ), - const SizedBox(height: 16), - Text( - widget.playlist.name, - style: GoogleFonts.outfit( - // Using Outfit for modern look - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 6), - Text( - Uri.tryParse(widget.playlist.dns)?.host ?? widget.playlist.dns, - style: GoogleFonts.inter( - fontSize: 13, - color: Colors.white54, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/iptv/screens/series_detail_screen.dart b/lib/features/iptv/screens/series_detail_screen.dart deleted file mode 100644 index 2a1aeb9..0000000 --- a/lib/features/iptv/screens/series_detail_screen.dart +++ /dev/null @@ -1,367 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:google_fonts/google_fonts.dart'; -import '../../../core/models/playlist_config.dart'; -import '../models/xtream_models.dart'; -import '../providers/xtream_provider.dart'; -import '../providers/watch_history_provider.dart'; -import 'player_screen.dart'; - -/// Series detail screen showing seasons and episodes -class SeriesDetailScreen extends ConsumerStatefulWidget { - final Series series; - final PlaylistConfig playlist; - - const SeriesDetailScreen({ - super.key, - required this.series, - required this.playlist, - }); - - @override - ConsumerState createState() => _SeriesDetailScreenState(); -} - -class _SeriesDetailScreenState extends ConsumerState { - SeriesInfo? _seriesInfo; - bool _isLoading = true; - String? _error; - int _selectedSeason = 1; - - @override - void initState() { - super.initState(); - _loadSeriesInfo(); - } - - Future _loadSeriesInfo() async { - try { - setState(() { - _isLoading = true; - _error = null; - }); - - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final info = await service.getSeriesInfo(widget.series.seriesId); - - setState(() { - _seriesInfo = info; - _isLoading = false; - // Select first available season - if (info.episodes.isNotEmpty) { - _selectedSeason = info.episodes.keys.first; - } - }); - } catch (e) { - setState(() { - _error = e.toString(); - _isLoading = false; - }); - } - } - - String? _formatRating(String? rating) { - if (rating == null || rating.isEmpty) return null; - final value = double.tryParse(rating); - if (value != null) { - return value.toStringAsFixed(1); - } - return rating; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.series.name), - backgroundColor: Colors.black87, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), - ), - ), - backgroundColor: Colors.grey.shade900, - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _error != null - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, size: 48, color: Colors.red), - const SizedBox(height: 16), - Text('Error: $_error', style: const TextStyle(color: Colors.white70)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadSeriesInfo, - child: const Text('Retry'), - ), - ], - ), - ) - : _buildContent(), - ); - } - - Widget _buildContent() { - if (_seriesInfo == null) return const SizedBox.shrink(); - - final currentEpisodes = _seriesInfo!.episodes[_selectedSeason] ?? []; - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Left panel: Series info and season selector - SizedBox( - width: 280, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Cover image - if (_seriesInfo!.cover != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: _seriesInfo!.cover!, - width: 248, - height: 350, - fit: BoxFit.cover, - errorWidget: (_, __, ___) => Container( - width: 248, - height: 350, - color: Colors.grey.shade800, - child: const Icon(Icons.tv, size: 64, color: Colors.white54), - ), - ), - ), - const SizedBox(height: 16), - - // Title - Text( - _seriesInfo!.name, - style: GoogleFonts.roboto( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - - // Rating - if (_seriesInfo!.rating != null) - Row( - children: [ - Icon(Icons.star, size: 16, color: Colors.amber.shade700), - const SizedBox(width: 4), - Text( - _formatRating(_seriesInfo!.rating!)!, - style: GoogleFonts.roboto(fontSize: 13, color: Colors.white70), - ), - ], - ), - const SizedBox(height: 12), - - // Plot - if (_seriesInfo!.plot != null && _seriesInfo!.plot!.isNotEmpty) - Text( - _seriesInfo!.plot!, - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white60), - maxLines: 5, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 16), - - // Season selector - Text( - 'Seasons', - style: GoogleFonts.roboto( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: _seriesInfo!.episodes.keys.map((seasonNum) { - final isSelected = seasonNum == _selectedSeason; - return ChoiceChip( - label: Text('S$seasonNum'), - selected: isSelected, - onSelected: (selected) { - if (selected) { - setState(() => _selectedSeason = seasonNum); - } - }, - selectedColor: Colors.blue.shade700, - backgroundColor: Colors.grey.shade800, - labelStyle: GoogleFonts.roboto( - color: isSelected ? Colors.white : Colors.white70, - fontSize: 12, - ), - ); - }).toList(), - ), - ], - ), - ), - ), - - // Divider - Container(width: 1, color: Colors.grey.shade700), - - // Right panel: Episodes list - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'Season $_selectedSeason - ${currentEpisodes.length} Episodes', - style: GoogleFonts.roboto( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - Expanded( - child: currentEpisodes.isEmpty - ? Center( - child: Text( - 'No episodes found', - style: GoogleFonts.roboto(color: Colors.white54), - ), - ) - : ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: currentEpisodes.length, - itemBuilder: (context, index) { - final episode = currentEpisodes[index]; - return _buildEpisodeTile(episode); - }, - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildEpisodeTile(Episode episode) { - final watchHistory = ref.watch(watchHistoryProvider); - final episodeKey = WatchHistory.episodeKey( - widget.series.seriesId, - _selectedSeason, - episode.episodeNum, - ); - final isWatched = watchHistory.isEpisodeWatched(episodeKey); - - return Card( - color: Colors.grey.shade800, - margin: const EdgeInsets.only(bottom: 8), - child: InkWell( - onTap: () { - // Mark as watched when playing - ref.read(watchHistoryProvider.notifier).markEpisodeWatched(episodeKey); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PlayerScreen( - streamId: episode.id, - title: '${widget.series.name} - ${episode.title}', - playlist: widget.playlist, - streamType: StreamType.series, - containerExtension: episode.containerExtension ?? 'mkv', - ), - ), - ); - }, - onLongPress: () { - // Toggle watched on long press - ref.read(watchHistoryProvider.notifier).toggleEpisodeWatched(episodeKey); - }, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - // Episode number badge - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isWatched ? Colors.green : Colors.blue.shade700, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: isWatched - ? const Icon(Icons.check, color: Colors.white, size: 20) - : Text( - '${episode.episodeNum}', - style: GoogleFonts.roboto( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(width: 12), - - // Episode info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - episode.title, - style: GoogleFonts.roboto( - fontSize: 14, - fontWeight: FontWeight.w500, - color: isWatched ? Colors.white54 : Colors.white, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (episode.durationSecs != null && episode.durationSecs! > 0) - Text( - _formatDuration(episode.durationSecs!), - style: GoogleFonts.roboto( - fontSize: 12, - color: Colors.white54, - ), - ), - ], - ), - ), - - // Play icon - Icon( - isWatched ? Icons.replay : Icons.play_circle_outline, - color: Colors.white70, - size: 28, - ), - ], - ), - ), - ), - ); - } - - String _formatDuration(int seconds) { - final hours = seconds ~/ 3600; - final minutes = (seconds % 3600) ~/ 60; - if (hours > 0) { - return '${hours}h ${minutes}m'; - } - return '${minutes}m'; - } -} diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart deleted file mode 100644 index 953ff59..0000000 --- a/lib/features/iptv/services/xtream_service.dart +++ /dev/null @@ -1,595 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/models/iptv_models.dart'; -import '../models/xtream_models.dart' as xm; -import '../../../core/utils/platform_utils.dart' as platform; - -/// Xtream Codes API Service -/// -/// Handles all communication with Xtream API servers -class XtreamService { - late final Dio _dio; - late final CacheOptions _cacheOptions; - - PlaylistConfig? _currentPlaylist; - - XtreamService() { - _dio = Dio( - BaseOptions( - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), - ), - ); - - // Setup caching for API responses - _cacheOptions = CacheOptions( - store: HiveCacheStore('./cache'), - policy: CachePolicy.forceCache, - maxStale: const Duration(hours: 1), - priority: CachePriority.high, - ); - - _dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions)); - } - - /// Check if we're running on HTTPS (production) - bool get _isHttps { - return platform.isHttps(); - } - - /// Wrap URL with proxy if needed (for HTTPS to HTTP bridge) - String _wrapWithProxy(String url) { - // If we're on HTTPS and the target URL is HTTP, use the proxy - if (_isHttps && url.startsWith('http://')) { - final baseUrl = platform.getWindowOrigin(); - return '$baseUrl/api/xtream/$url'; - } - return url; - } - - /// Initialize connection with a playlist - void setPlaylist(PlaylistConfig playlist) { - _currentPlaylist = playlist; - } - - /// Generate stream URL for live TV via FFmpeg transcoding - /// - /// Returns the start URL for FFmpeg transcoding endpoint. - /// The endpoint will start FFmpeg and return the local HLS URL. - String getLiveStreamUrl(String streamId) { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - // Use .ts format for direct MPEG-TS streaming (FFmpeg handles this better) - final iptvUrl = - '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; - - // Use FFmpeg transcoding endpoint - final baseUrl = platform.getWindowOrigin(); - final encodedUrl = Uri.encodeComponent(iptvUrl); - return '$baseUrl/api/stream/$streamId?url=$encodedUrl'; - } - - /// Generate stream URL for VOD (movies) - String getVodStreamUrl(String streamId, String containerExtension) { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - final url = - '${_currentPlaylist!.dns}/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; - return _wrapWithProxy(url); - } - - /// Generate stream URL for series episodes - String getSeriesStreamUrl(String streamId, String containerExtension) { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - final url = - '${_currentPlaylist!.dns}/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; - return _wrapWithProxy(url); - } - - /// Authenticate and get server info - Future> authenticate() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - return response.data as Map; - } catch (e) { - throw Exception('Authentication failed: $e'); - } - } - - /// Load categories mapping (category_id -> category_name) - Future> _getLiveCategories() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_live_categories', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List categories = response.data as List; - final Map categoryMap = {}; - - for (final cat in categories) { - final catData = cat as Map; - final id = catData['category_id']?.toString() ?? ''; - final name = catData['category_name']?.toString() ?? 'Unknown'; - if (id.isNotEmpty) { - categoryMap[id] = name; - } - } - - return categoryMap; - } catch (e) { - return {}; // Return empty map on error, channels will be "Uncategorized" - } - } - - /// Get all live TV channels grouped by category - /// - /// Returns a Map where key is category name and value is list of channels - Future>> getLiveChannels() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - // First, load categories to get the mapping - final categoryMap = await _getLiveCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_live_streams', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List streams = response.data as List; - final Map> groupedChannels = {}; - - for (final streamData in streams) { - final data = streamData as Map; - // Get category name from our mapping using category_id - final categoryId = data['category_id']?.toString() ?? ''; - final categoryName = categoryMap[categoryId] ?? 'Uncategorized'; - - // Inject category_name into data before parsing - data['category_name'] = categoryName; - - final channel = Channel.fromJson(data); - - if (!groupedChannels.containsKey(categoryName)) { - groupedChannels[categoryName] = []; - } - groupedChannels[categoryName]!.add(channel); - } - - return groupedChannels; - } catch (e) { - throw Exception('Failed to fetch live channels: $e'); - } - } - - /// Load VOD categories mapping - Future> _getVodCategories() async { - if (_currentPlaylist == null) return {}; - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_categories', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List categories = response.data as List; - final Map categoryMap = {}; - - for (final cat in categories) { - final catData = cat as Map; - final id = catData['category_id']?.toString() ?? ''; - final name = catData['category_name']?.toString() ?? 'Unknown'; - if (id.isNotEmpty) categoryMap[id] = name; - } - - return categoryMap; - } catch (e) { - return {}; - } - } - - /// Get all VOD items (movies) grouped by category - Future>> getVodItems() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final categoryMap = await _getVodCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_streams', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List vods = response.data as List; - final Map> groupedVods = {}; - - for (final vodData in vods) { - final data = vodData as Map; - final categoryId = data['category_id']?.toString() ?? ''; - final categoryName = categoryMap[categoryId] ?? 'Uncategorized'; - data['category_name'] = categoryName; - - final vod = VodItem.fromJson(data); - - if (!groupedVods.containsKey(categoryName)) { - groupedVods[categoryName] = []; - } - groupedVods[categoryName]!.add(vod); - } - - return groupedVods; - } catch (e) { - throw Exception('Failed to fetch VOD items: $e'); - } - } - - /// Load Series categories mapping - Future> _getSeriesCategories() async { - if (_currentPlaylist == null) return {}; - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series_categories', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List categories = response.data as List; - final Map categoryMap = {}; - - for (final cat in categories) { - final catData = cat as Map; - final id = catData['category_id']?.toString() ?? ''; - final name = catData['category_name']?.toString() ?? 'Unknown'; - if (id.isNotEmpty) categoryMap[id] = name; - } - - return categoryMap; - } catch (e) { - return {}; - } - } - - /// Get all series grouped by category - Future>> getSeries() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final categoryMap = await _getSeriesCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List seriesList = response.data as List; - final Map> groupedSeries = {}; - - for (final seriesData in seriesList) { - final data = seriesData as Map; - final categoryId = data['category_id']?.toString() ?? ''; - final categoryName = categoryMap[categoryId] ?? 'Uncategorized'; - data['category_name'] = categoryName; - - final series = Series.fromJson(data); - - if (!groupedSeries.containsKey(categoryName)) { - groupedSeries[categoryName] = []; - } - groupedSeries[categoryName]!.add(series); - } - - return groupedSeries; - } catch (e) { - throw Exception('Failed to fetch series: $e'); - } - } - - /// Get movies with pagination support - Future> getMoviesPaginated({ - int offset = 0, - int limit = 100, - }) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - // Load categories for mapping - final categoryMap = await _getVodCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_streams', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List allMovies = response.data as List; - - // Apply pagination - final endIndex = (offset + limit) > allMovies.length - ? allMovies.length - : offset + limit; - if (offset >= allMovies.length) return []; - - final paginatedMovies = allMovies.sublist(offset, endIndex); - - return paginatedMovies.map((movieData) { - final data = movieData as Map; - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - return xm.Movie.fromJson(data); - }).toList(); - } catch (e) { - throw Exception('Failed to fetch movies: $e'); - } - } - - /// Search movies in the entire catalogue - Future> searchMovies(String query) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - if (query.isEmpty) return []; - - try { - final categoryMap = await _getVodCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_streams', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List allMovies = response.data as List; - final queryLower = query.toLowerCase(); - - // Filter by search query - return allMovies - .where((m) { - final name = (m['name']?.toString() ?? '').toLowerCase(); - return name.contains(queryLower); - }) - .take(100) // Limit results - .map((movieData) { - final data = movieData as Map; - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - return xm.Movie.fromJson(data); - }) - .toList(); - } catch (e) { - return []; - } - } - - /// Get series with pagination support (returns flat list) - Future> getSeriesPaginated({ - int offset = 0, - int limit = 100, - }) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - // Load categories for mapping - final categoryMap = await _getSeriesCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List allSeries = response.data as List; - - // Apply pagination - final endIndex = (offset + limit) > allSeries.length - ? allSeries.length - : offset + limit; - if (offset >= allSeries.length) return []; - - final paginatedSeries = allSeries.sublist(offset, endIndex); - - return paginatedSeries.map((seriesData) { - final data = seriesData as Map; - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - return xm.Series.fromJson(data); - }).toList(); - } catch (e) { - throw Exception('Failed to fetch series: $e'); - } - } - - /// Search series in the entire catalogue - Future> searchSeries(String query) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - if (query.isEmpty) return []; - - try { - final categoryMap = await _getSeriesCategories(); - - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series', - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - final List allSeries = response.data as List; - final queryLower = query.toLowerCase(); - - // Filter by search query - return allSeries - .where((s) { - final name = (s['name']?.toString() ?? '').toLowerCase(); - return name.contains(queryLower); - }) - .take(100) // Limit results - .map((seriesData) { - final data = seriesData as Map; - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - return xm.Series.fromJson(data); - }) - .toList(); - } catch (e) { - return []; - } - } - - /// Get series info with seasons and episodes - Future getSeriesInfo(String seriesId) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series_info', - 'series_id': seriesId, - }, - options: Options(extra: _cacheOptions.toExtra()), - ); - - return xm.SeriesInfo.fromJson(response.data as Map); - } catch (e) { - throw Exception('Failed to fetch series info: $e'); - } - } - - /// Get short EPG for a specific stream - /// - /// Returns "Now" and "Next" program info - Future> getShortEpg(String streamId) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_short_epg', - 'stream_id': streamId, - }, - options: Options( - extra: CacheOptions( - store: _cacheOptions.store, - policy: CachePolicy.request, - maxStale: const Duration(minutes: 5), // EPG changes frequently - ).toExtra(), - ), - ); - - if (response.data == null || response.data['epg_listings'] == null) { - return []; - } - - final List epgData = - response.data['epg_listings'] as List; - return epgData - .map((entry) => EpgEntry.fromJson(entry as Map)) - .toList(); - } catch (e) { - // EPG is optional, don't throw on failure - return []; - } - } - - /// Get short EPG as ShortEPG object (for EPGWidget) - Future getShortEPG(String streamId) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _wrapWithProxy(_currentPlaylist!.apiBaseUrl), - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_short_epg', - 'stream_id': streamId, - }, - options: Options( - extra: CacheOptions( - store: _cacheOptions.store, - policy: CachePolicy.request, - maxStale: const Duration(minutes: 5), - ).toExtra(), - ), - ); - - if (response.data == null) { - return const xm.ShortEPG(); - } - - return xm.ShortEPG.fromJson(response.data as Map); - } catch (e) { - // EPG is optional, don't throw on failure - return const xm.ShortEPG(); - } - } - - /// Dispose resources - void dispose() { - _dio.close(); - } -} diff --git a/lib/features/iptv/services/xtream_service_mobile.dart b/lib/features/iptv/services/xtream_service_mobile.dart deleted file mode 100644 index 6f47d4c..0000000 --- a/lib/features/iptv/services/xtream_service_mobile.dart +++ /dev/null @@ -1,723 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:dio/dio.dart'; -import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; -import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/models/iptv_models.dart'; -import '../../../core/api/dns_resolver.dart'; -import '../models/xtream_models.dart' as xm; - -/// Xtream Codes API Service for Mobile (no dart:html dependency) -/// -/// Handles all communication with Xtream API servers -class XtreamServiceMobile { - late final Dio _dio; - late final CacheOptions _cacheOptions; - - PlaylistConfig? _currentPlaylist; - String? _resolvedIp; // Cached resolved IP for the playlist server - String? _originalHost; // Original hostname for Host header - - // In-memory cache for pagination performance - List? _cachedMoviesRaw; - List? _cachedSeriesRaw; - Map? _cachedVodCategories; - Map? _cachedSeriesCategories; - - late final HiveCacheStore _cacheStore; - - XtreamServiceMobile(String cachePath) { - _cacheStore = HiveCacheStore(cachePath); - _dio = Dio( - BaseOptions( - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), - ), - ); - - // Setup caching for API responses - 24h cache for VOD content - _cacheOptions = CacheOptions( - store: _cacheStore, - policy: CachePolicy.forceCache, - maxStale: const Duration(hours: 24), - priority: CachePriority.high, - keyBuilder: (request) { - // Use the Original Host header if available to generate a stable cache key - // regardless of the resolved IP address used for the connection - if (request.headers.containsKey('Host')) { - final host = request.headers['Host'] as String; - final uri = request.uri; - // Reconstruct URI with the original hostname - final stableUri = uri.replace(host: host); - return stableUri.toString(); - } - return request.uri.toString(); - }, - ); - - _dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions)); - } - - /// Initialize connection with a playlist - now async for DNS resolution - Future setPlaylistAsync(PlaylistConfig playlist) async { - _currentPlaylist = playlist; - - // Extract hostname from DNS URL - final uri = Uri.tryParse(playlist.dns); - if (uri != null && uri.host.isNotEmpty) { - _originalHost = uri.host; - - // Proactively resolve DNS - print('XtreamServiceMobile: Pre-resolving ${uri.host}'); - _resolvedIp = await DnsResolver.resolve(uri.host); - - if (_resolvedIp != null) { - print('XtreamServiceMobile: Will use IP $_resolvedIp for ${uri.host}'); - } else { - print( - 'XtreamServiceMobile: DNS resolution failed, will use hostname directly', - ); - } - } - } - - /// Legacy sync method - calls async internally - void setPlaylist(PlaylistConfig playlist) { - _currentPlaylist = playlist; - // Trigger async resolution in background - final uri = Uri.tryParse(playlist.dns); - if (uri != null && uri.host.isNotEmpty) { - _originalHost = uri.host; - DnsResolver.resolve(uri.host).then((ip) { - _resolvedIp = ip; - }); - } - } - - /// Get the API base URL, using resolved IP if available - String get _effectiveApiBaseUrl { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - if (_resolvedIp != null && _originalHost != null) { - // Replace hostname with IP in the URL - final originalUrl = _currentPlaylist!.apiBaseUrl; - return originalUrl.replaceFirst(_originalHost!, _resolvedIp!); - } - return _currentPlaylist!.apiBaseUrl; - } - - /// Get effective DNS base, using resolved IP if available - String get _effectiveDnsBase { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - if (_resolvedIp != null && _originalHost != null) { - return _currentPlaylist!.dns.replaceFirst(_originalHost!, _resolvedIp!); - } - return _currentPlaylist!.dns; - } - - /// Get request options with Host header if using IP - Options _getOptions() { - final opts = Options(extra: _cacheOptions.toExtra()); - if (_resolvedIp != null && _originalHost != null) { - opts.headers = {'Host': _originalHost!}; - } - return opts; - } - - PlaylistConfig? get currentPlaylist => _currentPlaylist; - - /// Generate direct stream URL for live TV (MPEG-TS format for maximum compatibility) - String getLiveStreamUrl(String streamId) { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - // Use .ts format (MPEG-TS) for better compatibility with more providers - return '$_effectiveDnsBase/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; - } - - /// Generate stream URL for VOD (movies) - String getVodStreamUrl(String streamId, String containerExtension) { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - return '$_effectiveDnsBase/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; - } - - /// Generate stream URL for series episodes - String getSeriesStreamUrl(String streamId, String containerExtension) { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - return '$_effectiveDnsBase/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; - } - - /// Authenticate and get server info - Future> authenticate() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - }, - options: _getOptions(), - ); - - if (response.data is! Map) { - if (response.data is String) { - throw Exception('Auth Error: ${response.data}'); - } - throw Exception('Invalid auth response format'); - } - return response.data as Map; - } catch (e) { - throw Exception('Authentication failed: $e'); - } - } - - /// Load categories mapping (category_id -> category_name) - Future> _getLiveCategories() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_live_categories', - }, - options: _getOptions(), - ); - - if (response.data is! List) { - if (response.data is String) { - throw Exception('Category Error: ${response.data}'); - } - return {}; - } - final List categories = response.data as List; - final Map categoryMap = {}; - - for (final cat in categories) { - final catData = cat as Map; - final id = catData['category_id']?.toString() ?? ''; - final name = catData['category_name']?.toString() ?? 'Unknown'; - if (id.isNotEmpty) { - categoryMap[id] = name; - } - } - - return categoryMap; - } catch (e) { - return {}; // Return empty map on error, channels will be "Uncategorized" - } - } - - /// Get all live TV channels grouped by category - Future>> getLiveChannels() async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - // First, load categories to get the mapping - final categoryMap = await _getLiveCategories(); - - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_live_streams', - }, - options: _getOptions(), - ); - - if (response.data is! List) { - if (response.data is String) { - throw Exception('Live TV Error: ${response.data}'); - } - return {}; - } - final List streams = response.data as List; - final Map> groupedChannels = {}; - - for (final streamData in streams) { - try { - final data = streamData as Map; - // Get category name from our mapping using category_id - final categoryId = data['category_id']?.toString() ?? ''; - final categoryName = categoryMap[categoryId] ?? 'Uncategorized'; - - // Inject category_name into data before parsing - data['category_name'] = categoryName; - - final channel = Channel.fromJson(data); - - if (!groupedChannels.containsKey(categoryName)) { - groupedChannels[categoryName] = []; - } - groupedChannels[categoryName]!.add(channel); - } catch (e) { - // Skip malformed channel - continue; - } - } - - return groupedChannels; - } catch (e) { - throw Exception('Failed to fetch live channels: $e'); - } - } - - /// Load VOD categories mapping - Future> getVodCategories() async { - if (_currentPlaylist == null) return {}; - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_categories', - }, - options: _getOptions(), - ); - - if (response.data is! List) return {}; - final List categories = response.data as List; - final Map categoryMap = {}; - - for (final cat in categories) { - final catData = cat as Map; - final id = catData['category_id']?.toString() ?? ''; - final name = catData['category_name']?.toString() ?? 'Unknown'; - if (id.isNotEmpty) categoryMap[id] = name; - } - - return categoryMap; - } catch (e) { - return {}; - } - } - - /// Load Series categories mapping - Future> _getSeriesCategories() async { - if (_currentPlaylist == null) return {}; - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series_categories', - }, - options: _getOptions(), - ); - - if (response.data is! List) return {}; - final List categories = response.data as List; - final Map categoryMap = {}; - - for (final cat in categories) { - final catData = cat as Map; - final id = catData['category_id']?.toString() ?? ''; - final name = catData['category_name']?.toString() ?? 'Unknown'; - if (id.isNotEmpty) categoryMap[id] = name; - } - - return categoryMap; - } catch (e) { - return {}; - } - } - - /// Get movies with pagination support (uses in-memory cache for performance) - Future> getMoviesPaginated({ - int offset = 0, - int limit = 100, - }) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - // Load from cache or fetch from API - if (_cachedMoviesRaw == null) { - // First load: fetch from API and cache - _cachedVodCategories ??= await getVodCategories(); - - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_streams', - }, - options: _getOptions(), - ); - - if (response.data is! List) { - if (response.data is String) { - throw Exception('VOD Error: ${response.data}'); - } - _cachedMoviesRaw = []; - } else { - _cachedMoviesRaw = response.data as List; - } - } - - final allMovies = _cachedMoviesRaw!; - final categoryMap = _cachedVodCategories!; - - // Apply pagination - final endIndex = (offset + limit) > allMovies.length - ? allMovies.length - : offset + limit; - if (offset >= allMovies.length) return []; - - final paginatedMovies = allMovies.sublist(offset, endIndex); - - final List movies = []; - for (final movieData in paginatedMovies) { - try { - final data = - Map.from(movieData as Map); - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - movies.add(xm.Movie.fromJson(data)); - } catch (e) { - continue; - } - } - return movies; - } catch (e) { - throw Exception('Failed to fetch movies: $e'); - } - } - - /// Get movies by category (Fast, optimized for large libraries) - Future> getMoviesByCategory(String categoryId) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final categoryMap = await getVodCategories(); - final categoryName = categoryMap[categoryId] ?? 'Unknown'; - - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_vod_streams', - 'category_id': categoryId, - }, - options: _getOptions(), - ); - - if (response.data is! List) return []; - final List moviesRaw = response.data as List; - - final List movies = []; - for (final movieData in moviesRaw) { - try { - final data = - Map.from(movieData as Map); - data['category_name'] = categoryName; - movies.add(xm.Movie.fromJson(data)); - } catch (e) { - continue; - } - } - return movies; - } catch (e) { - // Return empty list instead of throwing to prevent UI crash - return []; - } - } - - /// Search movies in the entire catalogue - Future> searchMovies(String query) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - if (query.isEmpty) return []; - - try { - // Ensure data is loaded in cache - if (_cachedMoviesRaw == null) { - await getMoviesPaginated(limit: 1); - } - - final allMovies = _cachedMoviesRaw ?? []; - final categoryMap = _cachedVodCategories ?? await getVodCategories(); - - final queryLower = query.toLowerCase(); - - // Filter by search query (Local Cache) - final List results = []; - - // Efficient filtering: check name matches, limited to 100 results - final matches = allMovies.where((m) { - final name = (m['name']?.toString() ?? '').toLowerCase(); - return name.contains(queryLower); - }).take(100); - - for (final movieData in matches) { - try { - final data = - Map.from(movieData as Map); - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - results.add(xm.Movie.fromJson(data)); - } catch (e) { - continue; - } - } - return results; - } catch (e) { - debugPrint('Search error: $e'); - return []; - } - } - - /// Get series with pagination support (uses in-memory cache for performance) - Future> getSeriesPaginated({ - int offset = 0, - int limit = 100, - }) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - // Load from cache or fetch from API - if (_cachedSeriesRaw == null) { - // First load: fetch from API and cache - _cachedSeriesCategories ??= await _getSeriesCategories(); - - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series', - }, - options: _getOptions(), - ); - - if (response.data is! List) { - if (response.data is String) { - throw Exception('Series Error: ${response.data}'); - } - _cachedSeriesRaw = []; - } else { - _cachedSeriesRaw = response.data as List; - } - } - - final allSeries = _cachedSeriesRaw!; - final categoryMap = _cachedSeriesCategories!; - - // Apply pagination - final endIndex = (offset + limit) > allSeries.length - ? allSeries.length - : offset + limit; - if (offset >= allSeries.length) return []; - - final paginatedSeries = allSeries.sublist(offset, endIndex); - - final List seriesList = []; - for (final seriesData in paginatedSeries) { - try { - final data = - Map.from(seriesData as Map); - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - seriesList.add(xm.Series.fromJson(data)); - } catch (e) { - continue; - } - } - return seriesList; - } catch (e) { - throw Exception('Failed to fetch series: $e'); - } - } - - /// Search series in the entire catalogue - Future> searchSeries(String query) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - if (query.isEmpty) return []; - - try { - // Ensure data is loaded in cache - if (_cachedSeriesRaw == null) { - await getSeriesPaginated(limit: 1); - } - - final allSeries = _cachedSeriesRaw ?? []; - final categoryMap = - _cachedSeriesCategories ?? await _getSeriesCategories(); - - final queryLower = query.toLowerCase(); - - // Filter by search query (Local Cache) - final List results = []; - - final matches = allSeries.where((s) { - final name = (s['name']?.toString() ?? '').toLowerCase(); - return name.contains(queryLower); - }).take(100); - - for (final seriesData in matches) { - try { - final data = - Map.from(seriesData as Map); - final categoryId = data['category_id']?.toString() ?? ''; - data['category_name'] = categoryMap[categoryId] ?? 'Uncategorized'; - results.add(xm.Series.fromJson(data)); - } catch (e) { - continue; - } - } - return results; - } catch (e) { - debugPrint('Search series error: $e'); - return []; - } - } - - /// Get series info with seasons and episodes - Future getSeriesInfo(String seriesId) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_series_info', - 'series_id': seriesId, - }, - options: _getOptions(), - ); - - if (response.data is! Map) { - throw Exception('Invalid response format for Series Info'); - } - return xm.SeriesInfo.fromJson(response.data as Map); - } catch (e) { - throw Exception('Failed to fetch series info: $e'); - } - } - - /// Get short EPG for a specific stream - Future> getShortEpg(String streamId) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_short_epg', - 'stream_id': streamId, - }, - options: Options( - headers: _resolvedIp != null && _originalHost != null - ? {'Host': _originalHost!} - : null, - extra: CacheOptions( - store: _cacheOptions.store, - policy: CachePolicy.request, - maxStale: const Duration(minutes: 5), // EPG changes frequently - ).toExtra(), - ), - ); - - if (response.data == null || response.data['epg_listings'] == null) { - return []; - } - - if (response.data['epg_listings'] is! List) return []; - final List epgData = - response.data['epg_listings'] as List; - return epgData - .map((entry) => EpgEntry.fromJson(entry as Map)) - .toList(); - } catch (e) { - // EPG is optional, don't throw on failure - return []; - } - } - - /// Get short EPG as ShortEPG object (for EPGWidget) - /// Caches results for 12 hours by default - Future getShortEPG( - String streamId, { - bool forceRefresh = false, - }) async { - if (_currentPlaylist == null) throw Exception('No playlist configured'); - - try { - final response = await _dio.get( - _effectiveApiBaseUrl, - queryParameters: { - 'username': _currentPlaylist!.username, - 'password': _currentPlaylist!.password, - 'action': 'get_short_epg', - 'stream_id': streamId, - }, - options: Options( - headers: _resolvedIp != null && _originalHost != null - ? {'Host': _originalHost!} - : null, - extra: CacheOptions( - store: _cacheOptions.store, - policy: forceRefresh ? CachePolicy.refresh : CachePolicy.request, - maxStale: const Duration(hours: 12), // Cache for 12h as requested - ).toExtra(), - ), - ); - - if (response.data == null) { - return const xm.ShortEPG(); - } - - if (response.data is! Map) { - return const xm.ShortEPG(); - } - return xm.ShortEPG.fromJson(response.data as Map); - } catch (e) { - // EPG is optional, don't throw on failure - return const xm.ShortEPG(); - } - } - - /// Clear in-memory cache (call on refresh) - void clearCache() { - _cachedMoviesRaw = null; - _cachedSeriesRaw = null; - _cachedVodCategories = null; - _cachedSeriesCategories = null; - } - - /// Force refresh all cached data (clears both memory and Hive HTTP cache) - /// This will cause the next API calls to fetch fresh data from the server - Future forceRefreshCache() async { - // Clear in-memory cache - clearCache(); - - // Clear HTTP cache (Hive store) - try { - await _cacheStore.clean(); - print('XtreamServiceMobile: Cache cleared successfully'); - } catch (e) { - print('XtreamServiceMobile: Error clearing cache: $e'); - } - } - - /// Dispose resources - void dispose() { - clearCache(); - _dio.close(); - } -} diff --git a/lib/features/iptv/widgets/category_carousel.dart b/lib/features/iptv/widgets/category_carousel.dart deleted file mode 100644 index fd159c2..0000000 --- a/lib/features/iptv/widgets/category_carousel.dart +++ /dev/null @@ -1,217 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../core/theme/app_colors.dart'; - -/// Horizontal carousel for displaying channels by category -/// -/// Netflix-style horizontal scrolling list with: -/// - Category title with count -/// - Smooth horizontal scrolling -/// - Lazy loading support -/// - Fade edges for visual polish -class CategoryCarousel extends StatelessWidget { - final String title; - final int? itemCount; - final List items; - final Widget Function(BuildContext context, T item, int index) itemBuilder; - final double itemWidth; - final double itemHeight; - final double spacing; - final EdgeInsets? padding; - final VoidCallback? onSeeAllTap; - - const CategoryCarousel({ - super.key, - required this.title, - required this.items, - required this.itemBuilder, - this.itemCount, - this.itemWidth = 180, - this.itemHeight = 120, - this.spacing = 12, - this.padding, - this.onSeeAllTap, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - if (items.isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Category header - Padding( - padding: padding ?? const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, - ), - ), - if (itemCount != null) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: colorScheme.primary.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '$itemCount', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - ), - ], - ], - ), - if (onSeeAllTap != null) - TextButton( - onPressed: onSeeAllTap, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Voir tout', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - const SizedBox(width: 4), - Icon( - Icons.arrow_forward_ios_rounded, - size: 12, - color: colorScheme.primary, - ), - ], - ), - ), - ], - ), - ), - - const SizedBox(height: 12), - - // Horizontal scrolling list with fade edges - SizedBox( - height: itemHeight, - child: ShaderMask( - shaderCallback: (Rect bounds) { - return const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.transparent, - Colors.white, - Colors.white, - Colors.transparent, - ], - stops: [0.0, 0.02, 0.98, 1.0], - ).createShader(bounds); - }, - blendMode: BlendMode.dstIn, - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: EdgeInsets.symmetric( - horizontal: (padding?.left ?? 16), - ), - physics: const BouncingScrollPhysics(), - itemCount: items.length, - separatorBuilder: (_, __) => SizedBox(width: spacing), - itemBuilder: (context, index) { - return SizedBox( - width: itemWidth, - height: itemHeight, - child: itemBuilder(context, items[index], index), - ); - }, - ), - ), - ), - ], - ); - } -} - -/// Simple category chip for filtering -class CategoryChipButton extends StatelessWidget { - final String label; - final bool isSelected; - final VoidCallback? onTap; - final IconData? icon; - - const CategoryChipButton({ - super.key, - required this.label, - this.isSelected = false, - this.onTap, - this.icon, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return GestureDetector( - onTap: onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - gradient: isSelected ? AppColors.primaryGradient : null, - color: isSelected ? null : colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: isSelected ? Colors.transparent : colorScheme.outlineVariant, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) ...[ - Icon( - icon, - size: 16, - color: isSelected - ? colorScheme.onPrimary - : colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 6), - ], - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: - isSelected ? colorScheme.onPrimary : colorScheme.onSurface, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/iptv/widgets/channel_card.dart b/lib/features/iptv/widgets/channel_card.dart deleted file mode 100644 index b0d4d8b..0000000 --- a/lib/features/iptv/widgets/channel_card.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_theme.dart'; -import '../providers/favorites_provider.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../iptv/providers/xtream_provider.dart'; - -class ChannelCard extends ConsumerStatefulWidget { - final String streamId; - final String name; - final String? iconUrl; - final String? currentProgram; - final bool isLive; - final VoidCallback? onTap; - final double? width; - final double? height; - final PlaylistConfig playlist; - - const ChannelCard({ - super.key, - required this.streamId, - required this.name, - required this.playlist, - this.iconUrl, - this.currentProgram, - this.isLive = true, - this.onTap, - this.width, - this.height, - }); - - @override - ConsumerState createState() => _ChannelCardState(); -} - -class _ChannelCardState extends ConsumerState - with SingleTickerProviderStateMixin { - bool _isHovered = false; - late AnimationController _pulseController; - - // EPG State - String? _epgNow; - bool _epgLoaded = false; - - - @override - void initState() { - super.initState(); - _pulseController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1000), - )..repeat(reverse: true); - - // Fetch EPG immediately when card is built - WidgetsBinding.instance.addPostFrameCallback((_) { - _fetchEpg(); - }); - } - - @override - void dispose() { - _pulseController.dispose(); - super.dispose(); - } - - Future _fetchEpg() async { - if (_epgLoaded) return; - - try { - // Don't modify state if unmounted - if (!mounted) return; - - final service = ref.read(xtreamServiceProvider(widget.playlist)); - // We use the short EPG endpoint for quick access - final entries = await service.getShortEpg(widget.streamId); - - if (mounted && entries.isNotEmpty) { - final now = DateTime.now(); - // Find current program - final current = entries.firstWhere( - (e) { - try { - final start = DateTime.parse(e.start); - final end = DateTime.parse(e.end); - return now.isAfter(start) && now.isBefore(end); - } catch (_) { - return false; - } - }, - orElse: () => entries.first, - ); - - setState(() { - _epgNow = current.title; - _epgLoaded = true; - }); - } - } catch (_) { - // Fail silently - if (mounted) setState(() => _epgLoaded = true); - } - } - - @override - Widget build(BuildContext context) { - final favorites = ref.watch(favoritesProvider); - final isFavorite = favorites.contains(widget.streamId); - - // Dynamic sizing based on hover - return MouseRegion( - onEnter: (_) { - setState(() => _isHovered = true); - }, - onExit: (_) => setState(() => _isHovered = false), - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: widget.onTap, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // The Image Card (Scales) - AnimatedScale( - scale: _isHovered ? 1.05 : 1.0, - duration: AppTheme.durationFast, - curve: AppTheme.curveDefault, - child: AnimatedContainer( - duration: AppTheme.durationFast, - width: widget.width ?? 180, - height: widget.height ?? 110, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppTheme.radiusMd), - color: AppColors.surface, // Fallback color - // Thick white border on focus (tvOS style) - border: Border.all( - color: _isHovered ? AppColors.focusColor : Colors.transparent, - width: _isHovered ? 3 : 0, - ), - // Deep shadow on focus - boxShadow: _isHovered - ? [ - BoxShadow( - color: Colors.black.withOpacity(0.5), - blurRadius: 30, - offset: const Offset(0, 15), - spreadRadius: 2, - ), - ] - : [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(AppTheme.radiusMd - (_isHovered ? 2 : 0)), // Adjust inner radius - child: Stack( - fit: StackFit.expand, - children: [ - // 1. Background Image / Logo - EDGE TO EDGE - _buildChannelImage(), - - // 2. Gradient Overlay (Subtle) - if (_isHovered) - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Colors.transparent, Colors.black.withOpacity(0.4)], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - ), - ), - - // 3. Live Badge (Top Right) - if (widget.isLive) - Positioned( - top: 8, - right: 8, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: AppColors.live, - borderRadius: BorderRadius.circular(4), - ), - child: const Text('LIVE', style: TextStyle(color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold)), - ), - ), - - // 4. Favorite Icon (Top Left) - if (_isHovered || isFavorite) - Positioned( - top: 5, - left: 5, - child: GestureDetector( - onTap: () => ref.read(favoritesProvider.notifier).toggleFavorite(widget.streamId), - child: Icon( - isFavorite ? Icons.favorite : Icons.favorite_border, - color: isFavorite ? AppColors.live : Colors.white.withOpacity(0.7), - size: 20, - ), - ), - ), - ], - ), - ), - ), - ), - - const SizedBox(height: 8), - - // Text separates from card (Classic tvOS look) - SizedBox( - width: widget.width ?? 180, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - widget.name, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _isHovered ? AppColors.focusColor : AppColors.textSecondary, - fontWeight: _isHovered ? FontWeight.bold : FontWeight.w500, - fontSize: 13, - ), - ), - if ((widget.currentProgram != null || _epgNow != null)) ...[ - const SizedBox(height: 2), - Text( - _epgNow ?? widget.currentProgram ?? '', - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.textTertiary, - fontSize: 11, - ), - ), - ], - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildChannelImage() { - if (widget.iconUrl != null && widget.iconUrl!.isNotEmpty) { - return Container( - color: Colors.white, // Logos usually look best on white/light grey - child: CachedNetworkImage( - imageUrl: widget.iconUrl!, - fit: BoxFit.contain, - width: double.infinity, - height: double.infinity, - placeholder: (context, url) => _buildPlaceholder(), - errorWidget: (context, url, error) => _buildPlaceholder(), - ), - ); - } - return _buildPlaceholder(); - } - - Widget _buildPlaceholder() { - return Container( - color: AppColors.surface, - child: const Center( - child: Icon(Icons.tv, color: AppColors.textTertiary), - ), - ); - } -} diff --git a/lib/features/iptv/widgets/clock_widget.dart b/lib/features/iptv/widgets/clock_widget.dart deleted file mode 100644 index e6fb9ed..0000000 --- a/lib/features/iptv/widgets/clock_widget.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -class ClockWidget extends StatefulWidget { - final TextStyle? style; - - const ClockWidget({super.key, this.style}); - - @override - State createState() => _ClockWidgetState(); -} - -class _ClockWidgetState extends State { - late DateTime _now; - Timer? _timer; - - @override - void initState() { - super.initState(); - _now = DateTime.now(); - _startTimer(); - } - - void _startTimer() { - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (mounted) { - setState(() { - _now = DateTime.now(); - }); - } - }); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // Format: HH:mm - final timeString = DateFormat('HH:mm').format(_now); - return Text( - timeString, - style: widget.style ?? const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - shadows: [Shadow(color: Colors.black, blurRadius: 4)], - ), - ); - } -} diff --git a/lib/features/iptv/widgets/epg_overlay.dart b/lib/features/iptv/widgets/epg_overlay.dart deleted file mode 100644 index 1c555c1..0000000 --- a/lib/features/iptv/widgets/epg_overlay.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/models/playlist_config.dart'; -import '../providers/xtream_provider.dart'; - -/// Minimal EPG overlay - shows program title and info -class EpgOverlay extends ConsumerWidget { - final String streamId; - final PlaylistConfig playlist; - - const EpgOverlay({ - super.key, - required this.streamId, - required this.playlist, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final epgAsync = ref.watch( - epgByPlaylistProvider( - EpgRequestKey(playlist: playlist, streamId: streamId), - ), - ); - - return epgAsync.when( - data: (epgEntries) { - if (epgEntries.isEmpty) { - return const SizedBox.shrink(); - } - - // Get "Now" program - final now = DateTime.now(); - final currentProgram = epgEntries.firstWhere( - (entry) { - final start = DateTime.parse(entry.start); - final end = DateTime.parse(entry.end); - return now.isAfter(start) && now.isBefore(end); - }, - orElse: () => epgEntries.first, - ); - - // Enhanced info display - return Align( - alignment: Alignment.bottomLeft, // Align to bottom left near controls - child: Container( - margin: const EdgeInsets.fromLTRB(24, 0, 24, 24), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.black - .withOpacity(0.5), // Semi-transparent backing for readability - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: const Text( - 'LIVE', - style: TextStyle( - color: Colors.white, - fontSize: 12, // Larger - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 12), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), // Wider - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - currentProgram.title, - style: const TextStyle( - color: Colors.white, - fontSize: 18, // Much Larger - fontWeight: FontWeight.bold, - shadows: [Shadow(color: Colors.black, blurRadius: 4)], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (currentProgram.description.isNotEmpty) - Text( - currentProgram.description, - style: const TextStyle( - color: Colors.white70, - fontSize: 13, // Larger desc - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ), - ); - }, - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - ); - } -} diff --git a/lib/features/iptv/widgets/epg_widget.dart b/lib/features/iptv/widgets/epg_widget.dart deleted file mode 100644 index 7c0071f..0000000 --- a/lib/features/iptv/widgets/epg_widget.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import '../../../core/models/playlist_config.dart'; -import '../providers/xtream_provider.dart'; -import '../models/xtream_models.dart'; - -class EPGWidget extends ConsumerStatefulWidget { - final String channelId; - final PlaylistConfig playlist; - - const EPGWidget({ - super.key, - required this.channelId, - required this.playlist, - }); - - @override - ConsumerState createState() => _EPGWidgetState(); -} - -class _EPGWidgetState extends ConsumerState { - ShortEPG? _epg; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _loadEPG(); - } - - Future _loadEPG() async { - try { - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final epg = await service.getShortEPG(widget.channelId); - - if (mounted) { - setState(() { - _epg = epg; - _isLoading = false; - }); - } - } catch (e) { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return Container( - padding: const EdgeInsets.all(8), - child: const Row( - children: [ - SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator(strokeWidth: 1.5), - ), - SizedBox(width: 8), - Text('Loading EPG...', style: TextStyle(fontSize: 10)), - ], - ), - ); - } - - if (_epg == null || - (_epg!.nowPlaying == null && _epg!.nextPlaying == null)) { - return const SizedBox.shrink(); - } - - return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.6), - borderRadius: BorderRadius.circular(4), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Now Playing - if (_epg!.nowPlaying != null) ...[ - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(3), - ), - child: Text( - 'LIVE', - style: GoogleFonts.roboto( - fontSize: 9, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - const SizedBox(width: 6), - Expanded( - child: Text( - _epg!.nowPlaying!, - style: GoogleFonts.roboto( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - - // Progress bar - if (_epg!.progress != null) ...[ - const SizedBox(height: 4), - ClipRRect( - borderRadius: BorderRadius.circular(2), - child: LinearProgressIndicator( - value: _epg!.progress!.clamp(0.0, 1.0), - backgroundColor: Colors.white24, - valueColor: const AlwaysStoppedAnimation(Colors.red), - minHeight: 3, - ), - ), - ], - ], - - // Next Playing - if (_epg!.nextPlaying != null) ...[ - const SizedBox(height: 6), - Row( - children: [ - Icon( - Icons.upcoming, - size: 12, - color: Colors.grey.shade400, - ), - const SizedBox(width: 6), - Expanded( - child: Text( - 'Next: ${_epg!.nextPlaying}', - style: GoogleFonts.roboto( - fontSize: 10, - color: Colors.grey.shade400, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ], - ), - ); - } -} diff --git a/lib/features/iptv/widgets/live_tv_tab.dart b/lib/features/iptv/widgets/live_tv_tab.dart deleted file mode 100644 index 24d82a6..0000000 --- a/lib/features/iptv/widgets/live_tv_tab.dart +++ /dev/null @@ -1,436 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../providers/xtream_provider.dart'; -import '../providers/settings_provider.dart'; -import '../providers/favorites_provider.dart'; -import '../screens/player_screen.dart'; -import '../../../core/models/iptv_models.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../../core/utils/responsive_layout.dart'; -import '../../../core/widgets/themed_loading_screen.dart'; -import 'channel_card.dart'; - -/// Live TV tab - Apple TV Style with Horizontal Category Filter -class LiveTVTab extends ConsumerStatefulWidget { - final PlaylistConfig playlist; - - const LiveTVTab({super.key, required this.playlist}); - - @override - ConsumerState createState() => _LiveTVTabState(); -} - -class _LiveTVTabState extends ConsumerState - with AutomaticKeepAliveClientMixin { - // State - String? _selectedCategory; - - // UI State - bool _isGridView = true; - final TextEditingController _searchController = TextEditingController(); - bool _showFavoritesOnly = false; - String _searchQuery = ''; - - // Scroll Controllers - final ScrollController _mainScrollController = ScrollController(); - - @override - void initState() { - super.initState(); - _searchController.addListener(() { - setState(() { - _searchQuery = _searchController.text.toLowerCase(); - }); - }); - } - - @override - void dispose() { - _searchController.dispose(); - _mainScrollController.dispose(); - super.dispose(); - } - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - - final channelsAsync = - ref.watch(liveChannelsByPlaylistProvider(widget.playlist)); - final favorites = ref.watch(favoritesProvider); - final settings = ref.watch(iptvSettingsProvider); - - return Scaffold( - backgroundColor: Colors.transparent, - body: channelsAsync.when( - loading: () => const ThemedLoading(), - error: (e, s) => Center(child: Text('Error: $e')), - data: (groupedChannels) { - // Groups - var categories = groupedChannels.keys.toList(); - if (settings.liveTvKeywords.isNotEmpty) { - categories = categories - .where((cat) => settings.matchesLiveTvFilter(cat)) - .toList(); - } - categories.sort(); - - // Filter Logic - List displayedChannels = []; - bool showingCategoryGrid = false; - - if (_searchQuery.isNotEmpty) { - // Global Search - displayedChannels = groupedChannels.values - .expand((l) => l) - .where((c) => c.name.toLowerCase().contains(_searchQuery)) - .toList(); - } else if (_showFavoritesOnly) { - // Favorites - displayedChannels = groupedChannels.values - .expand((l) => l) - .where((c) => favorites.contains(c.streamId)) - .toList(); - } else if (_selectedCategory != null) { - // Selected Category View - displayedChannels = groupedChannels[_selectedCategory] ?? []; - } else { - // Category Grid View (Default) - showingCategoryGrid = true; - } - - return Column( - children: [ - // Header - Container( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), - child: Row( - children: [ - // Back Button (if in category view and not searching) - if (_selectedCategory != null && - _searchQuery.isEmpty && - !_showFavoritesOnly) ...[ - IconButton( - icon: const Icon( - Icons.arrow_back, - color: AppColors.textPrimary, - ), - onPressed: () { - setState(() { - _selectedCategory = null; - _mainScrollController.jumpTo(0); - }); - }, - tooltip: 'Back to Categories', - ), - const SizedBox(width: 8), - ], - - Text( - _searchQuery.isNotEmpty - ? 'Search Results' - : _showFavoritesOnly - ? 'Favorites' - : _selectedCategory ?? 'Live TV', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - - // Search Pill - Container( - width: 300, - height: 40, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: - BorderRadius.circular(AppTheme.radiusFull), - border: Border.all(color: AppColors.border), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - const Icon( - Icons.search, - size: 20, - color: AppColors.textSecondary, - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: _searchController, - style: const TextStyle( - fontSize: 14, - color: Colors.white, - ), - decoration: const InputDecoration( - hintText: 'Rechercher...', - hintStyle: TextStyle(color: Colors.white54), - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.only(bottom: 11), - ), - ), - ), - if (_searchQuery.isNotEmpty) - GestureDetector( - onTap: () => _searchController.clear(), - child: const Icon( - Icons.close, - size: 16, - color: AppColors.textSecondary, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - - // Favorites Toggle - IconButton( - icon: Icon( - _showFavoritesOnly - ? Icons.favorite - : Icons.favorite_border, - color: _showFavoritesOnly - ? AppColors.error - : AppColors.textSecondary, - ), - onPressed: () { - setState(() { - _showFavoritesOnly = !_showFavoritesOnly; - if (_showFavoritesOnly) _searchController.clear(); - _selectedCategory = - null; // Reset selection when toggling favorites - }); - }, - tooltip: 'Favorites Only', - ), - - // View Toggle (Only show when not in category grid) - if (!showingCategoryGrid) - IconButton( - icon: Icon( - _isGridView ? Icons.view_list : Icons.grid_view, - color: AppColors.textSecondary, - ), - onPressed: () => - setState(() => _isGridView = !_isGridView), - tooltip: 'Toggle View', - ), - ], - ), - ), - - // Content - Expanded( - child: showingCategoryGrid - ? _buildCategoryGrid(categories, groupedChannels) - : displayedChannels.isEmpty - ? Center( - child: Text( - _searchQuery.isNotEmpty - ? 'No channels found' - : _showFavoritesOnly - ? 'No favorites' - : 'No channels in this group', - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: AppColors.textSecondary), - ), - ) - : _isGridView - ? _buildChannelGrid(displayedChannels) - : _buildChannelList(displayedChannels), - ), - ], - ); - }, - ), - ); - } - - Widget _buildCategoryGrid( - List categories, - Map> groupedChannels, - ) { - final columns = ResponsiveLayout.value( - context, - mobile: 2, - tablet: 3, - desktop: 5, - ); - - return GridView.builder( - controller: _mainScrollController, - padding: const EdgeInsets.all(24), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 1.5, - ), - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - final count = groupedChannels[category]?.length ?? 0; - - return Card( - clipBehavior: Clip.antiAlias, - elevation: 2, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: InkWell( - onTap: () { - setState(() { - _selectedCategory = category; - _mainScrollController.jumpTo(0); - }); - }, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - AppColors.surfaceVariant, - AppColors.surfaceVariant.withOpacity(0.8), - ], - ), - border: Border.all(color: Colors.white.withOpacity(0.2)), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColors.primary.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.folder_open, - color: AppColors.primary, - size: 32, - ), - ), - const SizedBox(height: 12), - Text( - category, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppColors.textPrimary, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - '$count Channels', - style: const TextStyle( - fontSize: 12, - color: AppColors.textSecondary, - ), - ), - ], - ), - ), - ), - ); - }, - ); - } - - Widget _buildChannelGrid(List channels) { - final columns = ResponsiveLayout.value( - context, - mobile: 2, - tablet: 4, - desktop: 6, - ); - - return GridView.builder( - controller: _mainScrollController, - padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 1.3, - ), - itemCount: channels.length, - itemBuilder: (context, index) { - final channel = channels[index]; - return ChannelCard( - streamId: channel.streamId, - name: channel.name, - iconUrl: _getProxiedIconUrl(channel.streamIcon), - currentProgram: null, - playlist: widget.playlist, - onTap: () => _playChannel(channel, channels), - ); - }, - ); - } - - Widget _buildChannelList(List channels) { - return ListView.separated( - controller: _mainScrollController, - padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), - itemCount: channels.length, - separatorBuilder: (_, __) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final channel = channels[index]; - return SizedBox( - height: 70, - child: ChannelCard( - streamId: channel.streamId, - name: channel.name, - iconUrl: _getProxiedIconUrl(channel.streamIcon), - height: 70, - width: double.infinity, - playlist: widget.playlist, - onTap: () => _playChannel(channel, channels), - ), - ); - }, - ); - } - - String? _getProxiedIconUrl(String originalUrl) { - if (originalUrl.isEmpty) return null; - if (originalUrl.startsWith('http://')) { - return '/api/xtream/$originalUrl'; - } - return originalUrl; - } - - void _playChannel(Channel channel, List contextList) { - final index = contextList.indexOf(channel); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => PlayerScreen( - streamId: channel.streamId, - title: channel.name, - playlist: widget.playlist, - streamType: StreamType.live, - channels: contextList, - initialIndex: index, - ), - ), - ); - } -} diff --git a/lib/features/iptv/widgets/movies_tab.dart b/lib/features/iptv/widgets/movies_tab.dart deleted file mode 100644 index 50b27f4..0000000 --- a/lib/features/iptv/widgets/movies_tab.dart +++ /dev/null @@ -1,355 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../../core/utils/responsive_layout.dart'; -import '../../../core/widgets/components/hero_carousel.dart'; -import '../../../core/widgets/components/ui_components.dart'; -import '../../../core/widgets/themed_loading_screen.dart'; -import '../providers/watch_history_provider.dart'; -import '../models/xtream_models.dart'; -import '../providers/xtream_provider.dart'; -import '../providers/settings_provider.dart'; -import '../screens/player_screen.dart'; - -class MoviesTab extends ConsumerStatefulWidget { - final PlaylistConfig playlist; - - const MoviesTab({super.key, required this.playlist}); - - @override - ConsumerState createState() => _MoviesTabState(); -} - -class _MoviesTabState extends ConsumerState { - final ScrollController _scrollController = ScrollController(); - final TextEditingController _searchController = TextEditingController(); - final List _movies = []; - List? _searchResults; - String _searchQuery = ''; - bool _isLoading = false; - bool _isSearching = false; - bool _hasMore = true; - int _currentOffset = 0; - static const int _pageSize = 100; - Timer? _searchDebounce; - - @override - void initState() { - super.initState(); - _loadMoreMovies(); - _scrollController.addListener(_onScroll); - } - - @override - void dispose() { - _scrollController.dispose(); - _searchController.dispose(); - _searchDebounce?.cancel(); - super.dispose(); - } - - void _onScroll() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent * 0.8) { - _loadMoreMovies(); - } - } - - void _onSearchChanged(String query) { - _searchDebounce?.cancel(); - - setState(() { - _searchQuery = query; - if (query.isEmpty) { - _searchResults = null; - _isSearching = false; - } - }); - - if (query.length >= 2) { - _searchDebounce = Timer(const Duration(milliseconds: 500), () { - _performSearch(query); - }); - } - } - - Future _performSearch(String query) async { - if (query.isEmpty) return; - - setState(() => _isSearching = true); - - try { - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final results = await service.searchMovies(query); - - if (mounted && _searchQuery == query) { - setState(() { - _searchResults = results; - _isSearching = false; - }); - } - } catch (e) { - if (mounted) { - setState(() => _isSearching = false); - } - } - } - - Future _loadMoreMovies() async { - if (_isLoading || !_hasMore) return; - - setState(() { - _isLoading = true; - }); - - try { - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final newMovies = await service.getMoviesPaginated( - offset: _currentOffset, - limit: _pageSize, - ); - - setState(() { - _movies.addAll(newMovies); - _currentOffset += _pageSize; - _hasMore = newMovies.length == _pageSize; - _isLoading = false; - }); - } catch (e) { - setState(() { - _isLoading = false; - }); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load movies: $e')), - ); - } - } - } - - String? _formatRating(String? rating) { - if (rating == null || rating.isEmpty) return null; - final value = double.tryParse(rating); - if (value != null) { - return value.toStringAsFixed(1); - } - return rating; - } - - /// Proxy HTTP images through backend to avoid CORS/mixed-content issues - String _getProxiedImageUrl(String? originalUrl) { - if (originalUrl == null || originalUrl.isEmpty) return ''; - if (originalUrl.startsWith('http://')) { - return '/api/xtream/$originalUrl'; - } - return originalUrl; - } - - void _playMovie(Movie movie) { - ref.read(watchHistoryProvider.notifier).markMovieWatched(movie.streamId); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PlayerScreen( - streamId: movie.streamId, - title: movie.name, - playlist: widget.playlist, - streamType: StreamType.vod, - containerExtension: movie.containerExtension ?? 'mp4', - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final settings = ref.watch(iptvSettingsProvider); - final watchHistory = ref.watch(watchHistoryProvider); - - List displayMovies; - if (_searchQuery.isNotEmpty && _searchResults != null) { - displayMovies = _searchResults!; - } else { - displayMovies = settings.moviesKeywords.isEmpty - ? _movies - : _movies - .where((m) => settings.matchesMoviesFilter(m.categoryName)) - .toList(); - } - - // Hero Items (Take 5 random or first 5 from filtered list) - final heroItems = displayMovies - .take(5) - .map( - (m) => HeroItem( - id: m.streamId, - title: m.name, - imageUrl: _getProxiedImageUrl(m.streamIcon), - subtitle: m.rating != null ? '${_formatRating(m.rating)} ★' : null, - onMoreInfo: () { - _playMovie(m); - }, - ), - ) - .toList(); - - if (_movies.isEmpty && !_isLoading) { - return const Center(child: Text('No movies available')); - } - - const double gridItemRatio = 0.65; - final int crossAxisCount = ResponsiveLayout.value( - context, - mobile: 3, - tablet: 5, - desktop: 7, - ); - - return CustomScrollView( - controller: _scrollController, - slivers: [ - // Header - SliverToBoxAdapter( - child: Container( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), - child: Row( - children: [ - Text( - 'Movies', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - Container( - width: 300, - height: 40, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(AppTheme.radiusFull), - border: Border.all(color: AppColors.border), - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - const Icon( - Icons.search, - size: 20, - color: AppColors.textSecondary, - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: _searchController, - style: const TextStyle( - fontSize: 14, - color: Colors.white, - ), - decoration: const InputDecoration( - hintText: 'Rechercher...', - hintStyle: TextStyle(color: Colors.white54), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 12), - isDense: true, - ), - onChanged: _onSearchChanged, - ), - ), - if (_isSearching) - const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator(strokeWidth: 2), - ), - if (_searchQuery.isNotEmpty) - GestureDetector( - onTap: () { - _searchController.clear(); - _onSearchChanged(''); - }, - child: const Icon( - Icons.close, - size: 16, - color: AppColors.textSecondary, - ), - ), - ], - ), - ), - ], - ), - ), - ), - - // Hero Carousel - if (_searchQuery.isEmpty && heroItems.isNotEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 24), - child: HeroCarousel( - items: heroItems, - onTap: (item) { - final movie = _movies - .firstWhere((element) => element.streamId == item.id); - _playMovie(movie); - }, - ), - ), - ), - - // Grid - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - crossAxisSpacing: 24, - mainAxisSpacing: 32, - childAspectRatio: gridItemRatio, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index >= displayMovies.length) return null; - final movie = displayMovies[index]; - final isWatched = watchHistory.isMovieWatched(movie.streamId); - - return MediaCard( - title: movie.name, - imageUrl: _getProxiedImageUrl(movie.streamIcon), - subtitle: movie.rating != null - ? '${_formatRating(movie.rating)} ★' - : null, - rating: _formatRating(movie.rating), - isWatched: isWatched, - onTap: () => _playMovie(movie), - onLongPress: () { - ref - .read(watchHistoryProvider.notifier) - .toggleMovieWatched(movie.streamId); - }, - ); - }, - childCount: displayMovies.length, - ), - ), - ), - - // Loader - if (_isLoading) - const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(24), - child: ThemedLoading(), - ), - ), - ], - ); - } -} diff --git a/lib/features/iptv/widgets/series_tab.dart b/lib/features/iptv/widgets/series_tab.dart deleted file mode 100644 index 8ec7485..0000000 --- a/lib/features/iptv/widgets/series_tab.dart +++ /dev/null @@ -1,341 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/models/playlist_config.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../../core/utils/responsive_layout.dart'; -import '../../../core/widgets/components/hero_carousel.dart'; -import '../../../core/widgets/components/ui_components.dart'; -import '../../../core/widgets/themed_loading_screen.dart'; - -import '../models/xtream_models.dart'; -import '../providers/xtream_provider.dart'; -import '../providers/settings_provider.dart'; -import '../screens/series_detail_screen.dart'; - -class SeriesTab extends ConsumerStatefulWidget { - final PlaylistConfig playlist; - - const SeriesTab({super.key, required this.playlist}); - - @override - ConsumerState createState() => _SeriesTabState(); -} - -class _SeriesTabState extends ConsumerState { - final ScrollController _scrollController = ScrollController(); - final TextEditingController _searchController = TextEditingController(); - final List _series = []; - List? _searchResults; - String _searchQuery = ''; - bool _isLoading = false; - bool _isSearching = false; - bool _hasMore = true; - int _currentOffset = 0; - static const int _pageSize = 100; - Timer? _searchDebounce; - - @override - void initState() { - super.initState(); - _loadMoreSeries(); - _scrollController.addListener(_onScroll); - } - - @override - void dispose() { - _scrollController.dispose(); - _searchController.dispose(); - _searchDebounce?.cancel(); - super.dispose(); - } - - void _onScroll() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent * 0.8) { - _loadMoreSeries(); - } - } - - void _onSearchChanged(String query) { - _searchDebounce?.cancel(); - - setState(() { - _searchQuery = query; - if (query.isEmpty) { - _searchResults = null; - _isSearching = false; - } - }); - - if (query.length >= 2) { - _searchDebounce = Timer(const Duration(milliseconds: 500), () { - _performSearch(query); - }); - } - } - - Future _performSearch(String query) async { - if (query.isEmpty) return; - - setState(() => _isSearching = true); - - try { - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final results = await service.searchSeries(query); - - if (mounted && _searchQuery == query) { - setState(() { - _searchResults = results; - _isSearching = false; - }); - } - } catch (e) { - if (mounted) { - setState(() => _isSearching = false); - } - } - } - - Future _loadMoreSeries() async { - if (_isLoading || !_hasMore) return; - - setState(() { - _isLoading = true; - }); - - try { - final service = ref.read(xtreamServiceProvider(widget.playlist)); - final newSeries = await service.getSeriesPaginated( - offset: _currentOffset, - limit: _pageSize, - ); - - setState(() { - _series.addAll(newSeries); - _currentOffset += _pageSize; - _hasMore = newSeries.length == _pageSize; - _isLoading = false; - }); - } catch (e) { - setState(() { - _isLoading = false; - }); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to load series: $e')), - ); - } - } - } - - String? _formatRating(String? rating) { - if (rating == null || rating.isEmpty) return null; - final value = double.tryParse(rating); - if (value != null) { - return value.toStringAsFixed(1); - } - return rating; - } - - /// Proxy HTTP images through backend to avoid CORS/mixed-content issues - String _getProxiedImageUrl(String? originalUrl) { - if (originalUrl == null || originalUrl.isEmpty) return ''; - if (originalUrl.startsWith('http://')) { - return '/api/xtream/$originalUrl'; - } - return originalUrl; - } - - void _openSeries(Series series) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SeriesDetailScreen( - series: series, - playlist: widget.playlist, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final settings = ref.watch(iptvSettingsProvider); - - List displaySeries; - if (_searchQuery.isNotEmpty && _searchResults != null) { - displaySeries = _searchResults!; - } else { - displaySeries = settings.seriesKeywords.isEmpty - ? _series - : _series - .where((s) => settings.matchesSeriesFilter(s.categoryName)) - .toList(); - } - - if (_series.isEmpty && !_isLoading) { - return const Center(child: Text('No series available')); - } - - // Hero Items - final heroItems = displaySeries - .take(5) - .map( - (s) => HeroItem( - id: s.seriesId.toString(), // ID logic might differ for Series - title: s.name, - imageUrl: _getProxiedImageUrl(s.cover), - subtitle: s.rating != null ? '${_formatRating(s.rating)} ★' : null, - onMoreInfo: () => _openSeries(s), - ), - ) - .toList(); - - const double gridItemRatio = 0.65; - final int crossAxisCount = ResponsiveLayout.value( - context, - mobile: 3, - tablet: 5, - desktop: 7, - ); - - return CustomScrollView( - controller: _scrollController, - slivers: [ - // Header - SliverToBoxAdapter( - child: Container( - padding: const EdgeInsets.fromLTRB(24, 16, 24, 8), - child: Row( - children: [ - Text( - 'TV Series', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - Container( - width: 300, - height: 40, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(AppTheme.radiusFull), - border: Border.all(color: AppColors.border), - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - const Icon( - Icons.search, - size: 20, - color: AppColors.textSecondary, - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: _searchController, - style: const TextStyle( - fontSize: 14, - color: Colors.white, - ), - decoration: const InputDecoration( - hintText: 'Rechercher...', - hintStyle: TextStyle(color: Colors.white54), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 12), - isDense: true, - ), - onChanged: _onSearchChanged, - ), - ), - if (_isSearching) - const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator(strokeWidth: 2), - ), - if (_searchQuery.isNotEmpty) - GestureDetector( - onTap: () { - _searchController.clear(); - _onSearchChanged(''); - }, - child: const Icon( - Icons.close, - size: 16, - color: AppColors.textSecondary, - ), - ), - ], - ), - ), - ], - ), - ), - ), - - // Hero Carousel - if (_searchQuery.isEmpty && heroItems.isNotEmpty) - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 24), - child: HeroCarousel( - items: heroItems, - onTap: (item) { - final series = _series.firstWhere( - (s) => s.name == item.title, - ); // Fallback by title if ID mismatch - _openSeries(series); - }, - ), - ), - ), - - // Grid - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - crossAxisSpacing: 24, - mainAxisSpacing: 32, - childAspectRatio: gridItemRatio, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index >= displaySeries.length) return null; - final serie = displaySeries[index]; - return MediaCard( - title: serie.name, - imageUrl: _getProxiedImageUrl(serie.cover), - subtitle: serie.rating != null - ? '${_formatRating(serie.rating)} ★' - : null, - rating: _formatRating(serie.rating), - placeholderIcon: Icons.tv, - onTap: () => _openSeries(serie), - ); - }, - childCount: displaySeries.length, - ), - ), - ), - - // Loader - if (_isLoading) - const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.all(24), - child: ThemedLoading(), - ), - ), - ], - ); - } -} diff --git a/lib/features/iptv/widgets/settings_tab.dart b/lib/features/iptv/widgets/settings_tab.dart deleted file mode 100644 index 6cf6e2d..0000000 --- a/lib/features/iptv/widgets/settings_tab.dart +++ /dev/null @@ -1,646 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:universal_html/html.dart' as html; // For reloading -import '../../auth/providers/auth_provider.dart'; -import '../providers/settings_provider.dart'; -import 'streaming_settings_tab.dart'; -import '../../../core/providers/theme_provider.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../admin/screens/admin_panel.dart'; - -/// Main Settings tab with sub-tabs for Filters, Streaming, and Appearance -class SettingsTab extends ConsumerStatefulWidget { - const SettingsTab({super.key}); - - @override - ConsumerState createState() => _SettingsTabState(); -} - -class _SettingsTabState extends ConsumerState - with SingleTickerProviderStateMixin { - late TabController _tabController; - late TextEditingController _liveTvController; - late TextEditingController _moviesController; - late TextEditingController _seriesController; - bool _initialized = false; - - @override - void initState() { - super.initState(); - final isAdmin = ref.read(authProvider).currentUser?.isAdmin ?? false; - _tabController = TabController(length: isAdmin ? 4 : 3, vsync: this); - _liveTvController = TextEditingController(); - _moviesController = TextEditingController(); - _seriesController = TextEditingController(); - } - - @override - void dispose() { - _tabController.dispose(); - _liveTvController.dispose(); - _moviesController.dispose(); - _seriesController.dispose(); - super.dispose(); - } - - void _syncControllers(IptvSettings settings) { - if (!_initialized || - _liveTvController.text != settings.liveTvCategoryFilter) { - _liveTvController.text = settings.liveTvCategoryFilter; - } - if (!_initialized || - _moviesController.text != settings.moviesCategoryFilter) { - _moviesController.text = settings.moviesCategoryFilter; - } - if (!_initialized || - _seriesController.text != settings.seriesCategoryFilter) { - _seriesController.text = settings.seriesCategoryFilter; - } - _initialized = true; - } - - @override - Widget build(BuildContext context) { - final currentUser = ref.watch(authProvider).currentUser; - final settings = ref.watch(iptvSettingsProvider); - - // Sync controllers with persisted state (only on first load) - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_initialized) _syncControllers(settings); - }); - - return Column( - children: [ - // TabBar - Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(50), - ), - child: TabBar( - controller: _tabController, - indicator: BoxDecoration( - color: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(50), - ), - labelColor: Colors.white, - unselectedLabelColor: Colors.white54, - dividerColor: Colors.transparent, - overlayColor: WidgetStateProperty.all(Colors.transparent), - tabs: [ - const Tab(icon: Icon(Icons.filter_list), text: 'Filtres'), - const Tab(icon: Icon(Icons.stream), text: 'Streaming'), - const Tab(icon: Icon(Icons.palette), text: 'Apparence'), - if (currentUser?.isAdmin ?? false) - const Tab( - icon: Icon(Icons.admin_panel_settings), - text: 'Administration', - ), - ], - ), - ), - - // TabBarView - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - // Filters Tab - _buildFiltersTab(context, currentUser, settings), - - // Streaming Tab - const StreamingSettingsTab(), - - // Appearance Tab - _buildAppearanceTab(context), - - // Admin Tab - if (currentUser?.isAdmin ?? false) const AdminContent(), - ], - ), - ), - ], - ); - } - - /// Build Appearance tab with theme toggle - Widget _buildAppearanceTab(BuildContext context) { - final themeState = ref.watch(themeProvider); - final themeNotifier = ref.read(themeProvider.notifier); - final isDark = Theme.of(context).brightness == Brightness.dark; - - return ListView( - padding: const EdgeInsets.all(16), - children: [ - // Theme selection card - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.dark_mode, - color: Colors.white, - ), - const SizedBox(width: 12), - Text( - 'Thème', - style: GoogleFonts.inter( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Theme options - _buildThemeOption( - context: context, - title: 'Système', - subtitle: 'Suivre les paramètres du système', - icon: Icons.settings_suggest, - isSelected: themeState.appThemeMode == AppThemeMode.system, - onTap: () => themeNotifier.setThemeMode(AppThemeMode.system), - ), - const SizedBox(height: 8), - _buildThemeOption( - context: context, - title: 'Sombre', - subtitle: 'Interface sombre premium', - icon: Icons.dark_mode, - isSelected: themeState.appThemeMode == AppThemeMode.dark, - onTap: () => themeNotifier.setThemeMode(AppThemeMode.dark), - ), - const SizedBox(height: 8), - _buildThemeOption( - context: context, - title: 'Clair', - subtitle: 'Interface claire et lumineuse', - icon: Icons.light_mode, - isSelected: themeState.appThemeMode == AppThemeMode.light, - onTap: () => themeNotifier.setThemeMode(AppThemeMode.light), - ), - ], - ), - ), - - const SizedBox(height: 16), - - // Quick toggle card - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - child: ListTile( - leading: Icon( - isDark ? Icons.dark_mode : Icons.light_mode, - color: Colors.white, - ), - title: Text( - 'Mode actuel', - style: GoogleFonts.inter( - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - subtitle: Text( - isDark ? 'Thème sombre actif' : 'Thème clair actif', - style: const TextStyle(color: Colors.white70), - ), - trailing: Switch( - value: isDark, - onChanged: (_) => themeNotifier.toggleTheme(), - activeThumbColor: AppColors.primary, - trackColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.selected) - ? AppColors.primary.withOpacity(0.5) - : Colors.grey.shade800, - ), - ), - ), - ), - ], - ); - } - - Widget _buildThemeOption({ - required BuildContext context, - required String title, - required String subtitle, - required IconData icon, - required bool isSelected, - required VoidCallback onTap, - }) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isSelected - ? colorScheme.primary.withOpacity(0.1) - : colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected ? colorScheme.primary : Colors.transparent, - width: 2, - ), - ), - child: Row( - children: [ - Icon( - icon, - color: isSelected - ? colorScheme.primary - : colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: GoogleFonts.inter( - fontWeight: FontWeight.w600, - color: isSelected - ? colorScheme.primary - : colorScheme.onSurface, - ), - ), - Text( - subtitle, - style: GoogleFonts.inter( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - if (isSelected) - Icon( - Icons.check_circle, - color: colorScheme.primary, - ), - ], - ), - ), - ); - } - - Widget _buildFiltersTab( - BuildContext context, - dynamic currentUser, - IptvSettings settings, - ) { - return ListView( - padding: const EdgeInsets.all(16), - children: [ - // User info card - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - padding: const EdgeInsets.all(4), - child: ListTile( - leading: const Icon(Icons.person, color: Colors.white), - title: Text( - 'Connecté en tant que', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white70), - ), - subtitle: Text( - currentUser?.username ?? 'Unknown', - style: GoogleFonts.roboto( - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - trailing: currentUser?.isAdmin ?? false - ? Chip( - label: Text( - 'Admin', - style: GoogleFonts.roboto( - fontSize: 11, - color: Colors.black, - ), - ), - backgroundColor: Colors.white, - ) - : null, - ), - ), - const SizedBox(height: 16), - - // Category filters section - Text( - 'Filtres de Catégories', - style: GoogleFonts.roboto( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Colors.white, - ), - ), - const SizedBox(height: 4), - Text( - 'Seules les catégories contenant un de ces mots-clés seront affichées. Séparez par des virgules.', - style: GoogleFonts.roboto( - fontSize: 12, - color: Colors.white54, - ), - ), - const SizedBox(height: 12), - - // Live TV Filter - _buildFilterCard( - title: 'Filtre TV Live', - icon: Icons.live_tv, - controller: _liveTvController, - keywords: settings.liveTvKeywords, - onChanged: (value) { - ref.read(iptvSettingsProvider.notifier).setLiveTvFilter(value); - }, - onClear: () { - _liveTvController.clear(); - ref.read(iptvSettingsProvider.notifier).clearLiveTvFilter(); - }, - ), - const SizedBox(height: 8), - - // Movies Filter - _buildFilterCard( - title: 'Filtre Films', - icon: Icons.movie, - controller: _moviesController, - keywords: settings.moviesKeywords, - onChanged: (value) { - ref.read(iptvSettingsProvider.notifier).setMoviesFilter(value); - }, - onClear: () { - _moviesController.clear(); - ref.read(iptvSettingsProvider.notifier).clearMoviesFilter(); - }, - ), - const SizedBox(height: 8), - - // Series Filter - _buildFilterCard( - title: 'Filtre Séries', - icon: Icons.tv, - controller: _seriesController, - keywords: settings.seriesKeywords, - onChanged: (value) { - ref.read(iptvSettingsProvider.notifier).setSeriesFilter(value); - }, - onClear: () { - _seriesController.clear(); - ref.read(iptvSettingsProvider.notifier).clearSeriesFilter(); - }, - ), - const SizedBox(height: 16), - - // Change playlist - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - child: ListTile( - leading: const Icon(Icons.playlist_play, color: Colors.white), - title: Text( - 'Changer de Playlist', - style: GoogleFonts.roboto(color: Colors.white), - ), - trailing: const Icon(Icons.chevron_right, color: Colors.white54), - onTap: () => context.go('/playlists'), - ), - ), - const SizedBox(height: 8), - - // About - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - child: ListTile( - leading: const Icon(Icons.info_outline, color: Colors.white), - title: Text( - 'À propos', - style: GoogleFonts.roboto(color: Colors.white), - ), - subtitle: Text( - 'XtremFlow IPTV v1.0.0', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), - ), - ), - ), - const SizedBox(height: 16), - - // Maintenance Section - Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.orange.withOpacity(0.3)), - ), - child: ListTile( - leading: const Icon(Icons.cleaning_services, color: Colors.orange), - title: Text( - 'Maintenance', - style: GoogleFonts.roboto(color: Colors.white), - ), - subtitle: Text( - 'En cas de problèmes d\'affichage ou de mise à jour', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), - ), - trailing: FilledButton.icon( - onPressed: () { - // Show confirmation dialog - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: const Color(0xFF1E1E1E), - title: const Text( - 'Vider le cache ?', - style: TextStyle(color: Colors.white), - ), - content: const Text( - 'Cette action va recharger l\'application et effacer les données temporaires.\n' - 'Vos réglages et favoris sont sauvegardés sur le serveur.', - style: TextStyle(color: Colors.white70), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Annuler'), - ), - FilledButton( - style: FilledButton.styleFrom( - backgroundColor: Colors.orange, - ), - onPressed: () { - Navigator.pop(context); - - // Clear Flutter Image Cache - PaintingBinding.instance.imageCache.clear(); - PaintingBinding.instance.imageCache.clearLiveImages(); - - // Clear Browser Local Storage (SharedPreferences backend for Web) - html.window.localStorage.clear(); - html.window.sessionStorage.clear(); - - // Reload application - html.window.location.reload(); - }, - child: const Text('Vider et Recharger'), - ), - ], - ), - ); - }, - icon: const Icon(Icons.refresh, size: 16), - label: const Text('Vider le Cache'), - style: FilledButton.styleFrom( - backgroundColor: Colors.orange.withOpacity(0.2), - foregroundColor: Colors.orange, - ), - ), - ), - ), - const SizedBox(height: 24), - - // Logout button - FilledButton.icon( - onPressed: () { - ref.read(authProvider.notifier).logout(); - context.go('/login'); - }, - icon: const Icon(Icons.logout), - label: const Text('Déconnexion'), - style: FilledButton.styleFrom( - backgroundColor: Colors.red.shade700, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - ), - ], - ); - } - - Widget _buildFilterCard({ - required String title, - required IconData icon, - required TextEditingController controller, - required List keywords, - required ValueChanged onChanged, - required VoidCallback onClear, - }) { - return Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(icon, size: 18, color: Colors.blueAccent), - const SizedBox(width: 8), - Text( - title, - style: GoogleFonts.roboto( - fontWeight: FontWeight.w600, - fontSize: 13, - color: Colors.white, - ), - ), - ], - ), - const SizedBox(height: 12), - TextField( - controller: controller, - decoration: InputDecoration( - hintText: 'Ex: FR,FRANCE,HD,SPORT', - hintStyle: - GoogleFonts.roboto(fontSize: 12, color: Colors.white30), - filled: true, - fillColor: Colors.black26, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - suffixIcon: controller.text.isNotEmpty - ? IconButton( - icon: const Icon( - Icons.clear, - size: 16, - color: Colors.white54, - ), - onPressed: onClear, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ) - : null, - ), - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white), - onChanged: onChanged, - ), - if (keywords.isNotEmpty) ...[ - const SizedBox(height: 12), - Wrap( - spacing: 6, - runSpacing: 6, - children: keywords.map((keyword) { - return Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: Colors.blueAccent.withOpacity(0.2), - borderRadius: BorderRadius.circular(50), - border: - Border.all(color: Colors.blueAccent.withOpacity(0.5)), - ), - child: Text( - keyword, - style: - GoogleFonts.roboto(fontSize: 11, color: Colors.white), - ), - ); - }).toList(), - ), - ], - ], - ), - ); - } -} diff --git a/lib/features/iptv/widgets/streaming_settings_tab.dart b/lib/features/iptv/widgets/streaming_settings_tab.dart deleted file mode 100644 index 3fb5a96..0000000 --- a/lib/features/iptv/widgets/streaming_settings_tab.dart +++ /dev/null @@ -1,278 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import '../providers/settings_provider.dart'; - -/// Streaming settings tab for Live TV configuration -class StreamingSettingsTab extends ConsumerWidget { - const StreamingSettingsTab({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final settings = ref.watch(iptvSettingsProvider); - final notifier = ref.read(iptvSettingsProvider.notifier); - - return ListView( - padding: const EdgeInsets.all(16), - children: [ - // Header - Text( - 'Paramètres Streaming Live TV', - style: GoogleFonts.roboto( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(height: 8), - Text( - 'Ces paramètres s\'appliquent uniquement aux flux TV en direct.', - style: GoogleFonts.roboto( - fontSize: 12, - color: Colors.white54, - ), - ), - const SizedBox(height: 24), - - // Quality Section - _buildSectionCard( - context: context, - title: 'Qualité Vidéo', - icon: Icons.high_quality, - children: [ - _buildDropdownTile( - title: 'Qualité du flux', - subtitle: 'Bitrate: ${settings.bitrateKbps} kbps', - value: settings.streamQuality, - items: StreamQuality.values, - onChanged: (v) => notifier.setStreamQuality(v!), - labelBuilder: (v) => switch (v) { - StreamQuality.low => 'Faible (1.5 Mbps)', - StreamQuality.medium => 'Moyen (3 Mbps)', - StreamQuality.high => 'Élevé (5 Mbps)', - }, - ), - ], - ), - const SizedBox(height: 16), - - // Buffer Section - _buildSectionCard( - context: context, - title: 'Buffer & Latence', - icon: Icons.speed, - children: [ - _buildDropdownTile( - title: 'Taille du buffer', - subtitle: 'Segments HLS: ${settings.hlsSegmentDuration}s', - value: settings.bufferSize, - items: BufferSize.values, - onChanged: (v) => notifier.setBufferSize(v!), - labelBuilder: (v) => switch (v) { - BufferSize.low => 'Faible (latence min)', - BufferSize.medium => 'Moyen (équilibré)', - BufferSize.high => 'Élevé (stabilité max)', - }, - ), - ], - ), - const SizedBox(height: 16), - - // Connection Section - _buildSectionCard( - context: context, - title: 'Connexion', - icon: Icons.wifi, - children: [ - _buildDropdownTile( - title: 'Timeout de connexion', - subtitle: '${settings.timeoutSeconds} secondes', - value: settings.connectionTimeout, - items: ConnectionTimeout.values, - onChanged: (v) => notifier.setConnectionTimeout(v!), - labelBuilder: (v) => switch (v) { - ConnectionTimeout.short => 'Court (15s)', - ConnectionTimeout.medium => 'Moyen (30s)', - ConnectionTimeout.long => 'Long (60s)', - }, - ), - Divider(height: 1, color: Colors.white.withOpacity(0.1)), - SwitchListTile( - title: Text( - 'Reconnexion automatique', - style: GoogleFonts.roboto(fontSize: 14, color: Colors.white), - ), - subtitle: Text( - 'Se reconnecter en cas de coupure', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), - ), - value: settings.autoReconnect, - activeThumbColor: Colors.blueAccent, - onChanged: (v) => notifier.setAutoReconnect(v), - ), - ], - ), - const SizedBox(height: 16), - - // Transcoding Section - _buildSectionCard( - context: context, - title: 'Transcodage', - icon: Icons.transform, - children: [ - _buildDropdownTile( - title: 'Mode de transcodage', - subtitle: 'Comment le flux est traité', - value: settings.transcodingMode, - items: TranscodingMode.values, - onChanged: (v) => notifier.setTranscodingMode(v!), - labelBuilder: (v) => switch (v) { - TranscodingMode.auto => 'Auto (recommandé)', - TranscodingMode.forced => 'Forcé (toujours transcoder)', - TranscodingMode.disabled => 'Désactivé (direct)', - }, - ), - Divider(height: 1, color: Colors.white.withOpacity(0.1)), - SwitchListTile( - title: Text( - 'Préférer lecture directe', - style: GoogleFonts.roboto(fontSize: 14, color: Colors.white), - ), - subtitle: Text( - 'Utiliser le flux original si compatible', - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), - ), - value: settings.preferDirectPlay, - activeThumbColor: Colors.blueAccent, - onChanged: (v) => notifier.setPreferDirectPlay(v), - ), - ], - ), - const SizedBox(height: 16), - - // Cache Section - _buildSectionCard( - context: context, - title: 'Cache EPG', - icon: Icons.schedule, - children: [ - _buildDropdownTile( - title: 'Durée du cache EPG', - subtitle: '${settings.epgCacheMinutes} minutes', - value: settings.epgCacheDuration, - items: EpgCacheDuration.values, - onChanged: (v) => notifier.setEpgCacheDuration(v!), - labelBuilder: (v) => switch (v) { - EpgCacheDuration.short => 'Court (5 min)', - EpgCacheDuration.medium => 'Moyen (15 min)', - EpgCacheDuration.long => 'Long (1 heure)', - }, - ), - ], - ), - const SizedBox(height: 32), - - // Info card - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue.withOpacity(0.3)), - ), - child: Row( - children: [ - const Icon(Icons.info_outline, color: Colors.blue), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Les changements prennent effet immédiatement pour les nouveaux flux.', - style: GoogleFonts.roboto( - fontSize: 12, - color: Colors.blue.shade700, - ), - ), - ), - ], - ), - ), - ], - ); - } - - Widget _buildSectionCard({ - required BuildContext context, - required String title, - required IconData icon, - required List children, - }) { - return Container( - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white.withOpacity(0.1)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Icon(icon, size: 20, color: Colors.blueAccent), - const SizedBox(width: 8), - Text( - title, - style: GoogleFonts.roboto( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ], - ), - ), - Divider(height: 1, color: Colors.white.withOpacity(0.1)), - ...children, - ], - ), - ); - } - - Widget _buildDropdownTile({ - required String title, - required String subtitle, - required T value, - required List items, - required ValueChanged onChanged, - required String Function(T) labelBuilder, - }) { - return ListTile( - title: Text( - title, - style: GoogleFonts.roboto(fontSize: 14, color: Colors.white), - ), - subtitle: Text( - subtitle, - style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54), - ), - trailing: DropdownButton( - value: value, - underline: const SizedBox(), - dropdownColor: const Color(0xFF1C1C1E), // Dark background for dropdown - items: items.map((item) { - return DropdownMenuItem( - value: item, - child: Text( - labelBuilder(item), - style: GoogleFonts.roboto(fontSize: 13, color: Colors.white), - ), - ); - }).toList(), - onChanged: onChanged, - style: GoogleFonts.roboto(color: Colors.white), - iconEnabledColor: Colors.white70, - ), - ); - } -} diff --git a/lib/main_mobile.dart b/lib/main_mobile.dart deleted file mode 100644 index 7cd7db6..0000000 --- a/lib/main_mobile.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:media_kit/media_kit.dart'; -import 'core/theme/app_theme.dart'; -import 'core/providers/theme_provider.dart'; -import 'mobile/features/iptv/screens/mobile_playlist_screen.dart'; -import 'mobile/features/iptv/screens/mobile_dashboard_screen.dart'; -import 'core/models/playlist_config.dart'; -import 'core/database/hive_service.dart'; - -/// Mobile-optimized entry point for XtremFlow IPTV -/// No authentication - direct access to playlist selection -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - // Initialize MediaKit (required for video playback) - MediaKit.ensureInitialized(); - - // Initialize Hive (Centralized) - await HiveService.init(); - - // CLEAR CACHE ON STARTUP (Requested by User) - try { - // Clear API/EPG Cache (using default box name 'dio_cache') - await Hive.deleteBoxFromDisk('dio_cache'); - debugPrint('XtremFlow: API Cache cleared'); - } catch (e) { - debugPrint('XtremFlow: Failed to clear cache: $e'); - } - - runApp(const ProviderScope(child: MobileApp())); -} - -class MobileApp extends ConsumerWidget { - const MobileApp({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final themeState = ref.watch(themeProvider); - - return MaterialApp( - title: 'XtremFlow IPTV', - debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: themeState.themeMode, - // Start directly at playlist selection (no auth) - home: const MobilePlaylistScreen(), - onGenerateRoute: (settings) { - // Handle navigation to dashboard with playlist - if (settings.name == '/dashboard') { - final playlist = settings.arguments as PlaylistConfig?; - if (playlist != null) { - return MaterialPageRoute( - builder: (context) => MobileDashboardScreen(playlist: playlist), - ); - } - } - return null; - }, - ); - } -} diff --git a/lib/mobile/features/auth/screens/mobile_login_screen.dart b/lib/mobile/features/auth/screens/mobile_login_screen.dart deleted file mode 100644 index 1580733..0000000 --- a/lib/mobile/features/auth/screens/mobile_login_screen.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import '../../../../features/auth/providers/auth_provider.dart'; -import '../../../../core/theme/app_colors.dart'; -import '../../../theme/mobile_theme.dart'; - -class MobileLoginScreen extends ConsumerStatefulWidget { - const MobileLoginScreen({super.key}); - - @override - ConsumerState createState() => _MobileLoginScreenState(); -} - -class _MobileLoginScreenState extends ConsumerState { - final _formKey = GlobalKey(); - final _usernameController = TextEditingController(); - final _passwordController = TextEditingController(); - bool _obscurePassword = true; - - @override - void dispose() { - _usernameController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - if (!_formKey.currentState!.validate()) return; - - final auth = ref.read(authProvider.notifier); - final success = await auth.login( - _usernameController.text.trim(), - _passwordController.text, - ); - - if (success && mounted) { - context.go('/playlists'); - } - } - - @override - Widget build(BuildContext context) { - final authState = ref.watch(authProvider); - - return Theme( - data: MobileTheme.darkTheme, - child: Scaffold( - backgroundColor: AppColors.background, - body: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Logo - Container( - width: 72, - height: 72, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.border), - ), - child: const Icon( - Icons.live_tv, - color: AppColors.primary, - size: 36, - ), - ), - const SizedBox(height: 16), - const Text( - 'XtremFlow', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - color: AppColors.textPrimary, - letterSpacing: -0.5, - ), - ), - const SizedBox(height: 4), - const Text( - 'Mobile Edition', - style: TextStyle( - fontSize: 12, - color: AppColors.textSecondary, - letterSpacing: 1.5, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 48), - - // Form - Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - controller: _usernameController, - style: const TextStyle(color: AppColors.textPrimary), - decoration: const InputDecoration( - labelText: 'Username', - prefixIcon: Icon(Icons.person_outline), - ), - validator: (value) => - (value == null || value.trim().isEmpty) - ? 'Required' - : null, - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 16), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - style: const TextStyle(color: AppColors.textPrimary), - decoration: InputDecoration( - labelText: 'Password', - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - onPressed: () => setState( - () => _obscurePassword = !_obscurePassword, - ), - ), - ), - validator: (value) => (value == null || value.isEmpty) - ? 'Required' - : null, - onFieldSubmitted: (_) => _handleLogin(), - ), - if (authState.errorMessage != null) ...[ - const SizedBox(height: 16), - Text( - authState.errorMessage!, - style: const TextStyle( - color: AppColors.error, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: authState.isLoading ? null : _handleLogin, - child: authState.isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, - ), - ) - : const Text('Sign In'), - ), - ), - ], - ), - ), - const SizedBox(height: 24), - const Text( - 'Default: admin / admin', - style: TextStyle( - fontSize: 12, - color: AppColors.textTertiary, - fontStyle: FontStyle.italic, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/mobile/features/iptv/screens/mobile_player_screen.dart b/lib/mobile/features/iptv/screens/mobile_player_screen.dart deleted file mode 100644 index 71c53e4..0000000 --- a/lib/mobile/features/iptv/screens/mobile_player_screen.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../../features/iptv/screens/player_screen.dart'; -export '../../../../features/iptv/screens/player_screen.dart' show StreamType; -import '../../../../core/models/playlist_config.dart'; - -/// Mobile wrapper for the PlayerScreen. -/// Currently reuses the desktop PlayerScreen as it adapts well to mobile (video player), -/// but allows for future mobile-specific overlays or gesture controls. -class MobilePlayerScreen extends StatelessWidget { - final String streamId; - final String title; - final StreamType streamType; - final String containerExtension; - final PlaylistConfig playlist; - - const MobilePlayerScreen({ - super.key, - required this.streamId, - required this.title, - required this.playlist, - this.streamType = StreamType.live, - this.containerExtension = 'mp4', - }); - - @override - Widget build(BuildContext context) { - // For now, we reuse the existing PlayerScreen which is responsive. - // In the future, we can add Mobile-specific gesture detectors here. - return PlayerScreen( - streamId: streamId, - title: title, - playlist: playlist, - streamType: streamType, - containerExtension: containerExtension, - ); - } -} diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index 7be658c..0000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100644 index 7be658c..0000000 Binary files a/web/icons/Icon-192.png and /dev/null differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100644 index 7be658c..0000000 Binary files a/web/icons/Icon-512.png and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 17b1719..0000000 --- a/web/index.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - XtremFlow IPTV - - - - - - - - - - -
- -
-
Loading...
-
- - - - - - \ No newline at end of file diff --git a/web/manifest.json b/web/manifest.json deleted file mode 100644 index 71c2580..0000000 --- a/web/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "XtremFlow IPTV", - "short_name": "XtremFlow", - "start_url": ".", - "display": "standalone", - "background_color": "#000000", - "theme_color": "#1976D2", - "description": "High-performance IPTV Web Application", - "orientation": "landscape-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - } - ] -} diff --git a/web/player.html b/web/player.html deleted file mode 100644 index b2a1bc5..0000000 --- a/web/player.html +++ /dev/null @@ -1,688 +0,0 @@ - - - - - - IPTV Player - - - - - - - -
- - -
-
-
Loading stream...
-
-
-
⚠
-
Failed to load stream
-
-
-
- - - - - - - - \ No newline at end of file