diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..797ac929 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @acvigue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26e5b102..72cecf43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,10 @@ jobs: runs-on: macos-latest steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + - name: Checkout uses: actions/checkout@v1 diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index e9a9c25f..5b62592f 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; + 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 628B95212670CABD0091AF3B /* WidgetKit.framework */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; 5321753E2671DE9C005491E6 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 5321753F2671DEA6005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; @@ -73,9 +74,38 @@ 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; 621C638226676728004216EA /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; + 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; + 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; + 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; + 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; + 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; + 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D92671138200A7371D /* ImageExtensions.swift */; }; + 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D92671138200A7371D /* ImageExtensions.swift */; }; + 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D92671138200A7371D /* ImageExtensions.swift */; }; + 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 628B95232670CABD0091AF3B /* SwiftUI.framework */; }; + 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628B95262670CABD0091AF3B /* NextUpWidget.swift */; }; + 628B95292670CABE0091AF3B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 628B95282670CABE0091AF3B /* Assets.xcassets */; }; + 628B952D2670CABE0091AF3B /* WidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 628B95202670CABD0091AF3B /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 628B95332670CAEA0091AF3B /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95322670CAEA0091AF3B /* NukeUI */; }; + 628B95352670CAEA0091AF3B /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95342670CAEA0091AF3B /* JellyfinAPI */; }; + 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628B95362670CB800091AF3B /* JellyfinWidget.swift */; }; + 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; + 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; }; + 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; + 62FA8A522671DE3C004BA2AB /* WidgetEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */; }; AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 628B952B2670CABE0091AF3B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5377CBE9263B596A003A4E83 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 628B951F2670CABD0091AF3B; + remoteInfo = WidgetExtensionExtension; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 5302F8322658B74800647A2E /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -108,6 +138,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + 628B95312670CABE0091AF3B /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 628B952D2670CABE0091AF3B /* WidgetExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -161,6 +202,17 @@ 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 621C638126676728004216EA /* NukeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; + 6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; + 6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = ""; }; + 628B95202670CABD0091AF3B /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 628B95212670CABD0091AF3B /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 628B95232670CABD0091AF3B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 628B95262670CABD0091AF3B /* NextUpWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpWidget.swift; sourceTree = ""; }; + 628B95282670CABE0091AF3B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = ""; }; + 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; + 62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEnvironment.swift; sourceTree = ""; }; AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; /* End PBXFileReference section */ @@ -189,6 +241,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 628B951D2670CABD0091AF3B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 628B95332670CAEA0091AF3B /* NukeUI in Frameworks */, + 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, + 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, + 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */, + 628B95352670CAEA0091AF3B /* JellyfinAPI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -245,6 +309,7 @@ isa = PBXGroup; children = ( 53D5E3DA264B460200BADDC8 /* Cartfile */, + 628B95252670CABD0091AF3B /* WidgetExtension */, 53D5E3DB264B47EE00BADDC8 /* Frameworks */, 5377CBF3263B596A003A4E83 /* JellyfinPlayer */, 535870612669D21600D05A09 /* JellyfinPlayer tvOS */, @@ -258,6 +323,7 @@ children = ( 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */, 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */, + 628B95202670CABD0091AF3B /* WidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -309,6 +375,8 @@ children = ( 5358709C2669D82900D05A09 /* TVVLCKit.framework */, 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */, + 628B95212670CABD0091AF3B /* WidgetKit.framework */, + 628B95232670CABD0091AF3B /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -324,10 +392,25 @@ 621C638126676728004216EA /* NukeExtensions.swift */, 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, 621338922660107500A81A2A /* StringExtensions.swift */, + 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, + 6267B3D92671138200A7371D /* ImageExtensions.swift */, ); path = Extensions; sourceTree = ""; }; + 628B95252670CABD0091AF3B /* WidgetExtension */ = { + isa = PBXGroup; + children = ( + 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */, + 628B95362670CB800091AF3B /* JellyfinWidget.swift */, + 628B95262670CABD0091AF3B /* NextUpWidget.swift */, + 628B95282670CABE0091AF3B /* Assets.xcassets */, + 628B952A2670CABE0091AF3B /* Info.plist */, + 62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */, + ); + path = WidgetExtension; + sourceTree = ""; + }; AE8C3157265D6F5E008AA076 /* Resources */ = { isa = PBXGroup; children = ( @@ -373,10 +456,12 @@ 5377CBEF263B596A003A4E83 /* Resources */, 53D5E3DF264B47EE00BADDC8 /* Embed Frameworks */, 5302F8322658B74800647A2E /* CopyFiles */, + 628B95312670CABE0091AF3B /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( + 628B952C2670CABE0091AF3B /* PBXTargetDependency */, ); name = "JellyfinPlayer iOS"; packageProductDependencies = ( @@ -389,6 +474,28 @@ productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; productType = "com.apple.product-type.application"; }; + 628B951F2670CABD0091AF3B /* WidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 628B952E2670CABE0091AF3B /* Build configuration list for PBXNativeTarget "WidgetExtension" */; + buildPhases = ( + 628B951C2670CABD0091AF3B /* Sources */, + 628B951D2670CABD0091AF3B /* Frameworks */, + 628B951E2670CABD0091AF3B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WidgetExtension; + packageProductDependencies = ( + 628B95322670CAEA0091AF3B /* NukeUI */, + 628B95342670CAEA0091AF3B /* JellyfinAPI */, + 628B95392670CE250091AF3B /* KeychainSwift */, + ); + productName = WidgetExtensionExtension; + productReference = 628B95202670CABD0091AF3B /* WidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -407,6 +514,9 @@ 5377CBF0263B596A003A4E83 = { CreatedOnToolsVersion = 12.5; }; + 628B951F2670CABD0091AF3B = { + CreatedOnToolsVersion = 12.5; + }; }; }; buildConfigurationList = 5377CBEC263B596A003A4E83 /* Build configuration list for PBXProject "JellyfinPlayer" */; @@ -430,6 +540,7 @@ targets = ( 5377CBF0263B596A003A4E83 /* JellyfinPlayer iOS */, 5358705F2669D21600D05A09 /* JellyfinPlayer tvOS */, + 628B951F2670CABD0091AF3B /* WidgetExtension */, ); }; /* End PBXProject section */ @@ -456,6 +567,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 628B951E2670CABD0091AF3B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 628B95292670CABE0091AF3B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -463,7 +582,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */, + 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, 535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */, @@ -504,12 +625,14 @@ 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, + 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, 53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */, + 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, @@ -521,8 +644,32 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 628B951C2670CABD0091AF3B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */, + 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */, + 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */, + 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, + 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */, + 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */, + 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, + 62FA8A522671DE3C004BA2AB /* WidgetEnvironment.swift in Sources */, + 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 628B952C2670CABE0091AF3B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 628B951F2670CABD0091AF3B /* WidgetExtension */; + targetProxy = 628B952B2670CABE0091AF3B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 535870722669D21700D05A09 /* Debug */ = { isa = XCBuildConfiguration; @@ -704,6 +851,7 @@ 5377CC1C263B596B003A4E83 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; @@ -735,6 +883,7 @@ 5377CC1D263B596B003A4E83 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; @@ -764,6 +913,56 @@ }; name = Release; }; + 628B952F2670CABE0091AF3B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = WidgetExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 36; + DEVELOPMENT_TEAM = 9R8RREG67J; + INFOPLIST_FILE = WidgetExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 628B95302670CABE0091AF3B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = WidgetExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 36; + DEVELOPMENT_TEAM = 9R8RREG67J; + INFOPLIST_FILE = WidgetExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0.0; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -794,6 +993,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 628B952E2670CABE0091AF3B /* Build configuration list for PBXNativeTarget "WidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 628B952F2670CABE0091AF3B /* Debug */, + 628B95302670CABE0091AF3B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -872,6 +1080,21 @@ package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; productName = NukeUI; }; + 628B95322670CAEA0091AF3B /* NukeUI */ = { + isa = XCSwiftPackageProductDependency; + package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */; + productName = NukeUI; + }; + 628B95342670CAEA0091AF3B /* JellyfinAPI */ = { + isa = XCSwiftPackageProductDependency; + package = 53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; + productName = JellyfinAPI; + }; + 628B95392670CE250091AF3B /* KeychainSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; + productName = KeychainSwift; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/WidgetExtension.xcscheme b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/WidgetExtension.xcscheme new file mode 100644 index 00000000..741157ab --- /dev/null +++ b/JellyfinPlayer.xcodeproj/xcshareddata/xcschemes/WidgetExtension.xcscheme @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JellyfinPlayer/ContentView.swift b/JellyfinPlayer/ContentView.swift index 5f6a16a5..5cd96842 100644 --- a/JellyfinPlayer/ContentView.swift +++ b/JellyfinPlayer/ContentView.swift @@ -11,6 +11,7 @@ import KeychainSwift import Nuke import Combine import JellyfinAPI +import WidgetKit struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @@ -63,6 +64,7 @@ struct ContentView: View { let savedUser = savedUsers[0] let keychain = KeychainSwift() + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" if keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil { globalData.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? "" globalData.server = servers[0] @@ -126,6 +128,7 @@ struct ContentView: View { defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") } } + WidgetCenter.shared.reloadAllTimelines() } var body: some View { diff --git a/JellyfinPlayer/JellyfinPlayer.entitlements b/JellyfinPlayer/JellyfinPlayer.entitlements index e7a5f472..b6b038ce 100644 --- a/JellyfinPlayer/JellyfinPlayer.entitlements +++ b/JellyfinPlayer/JellyfinPlayer.entitlements @@ -6,7 +6,15 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.me.vigue.jellyfin.mobileclient + com.apple.security.network.client + keychain-access-groups + + $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain + diff --git a/JellyfinPlayer/PersistenceController.swift b/JellyfinPlayer/PersistenceController.swift index 9edec083..ad84aad8 100644 --- a/JellyfinPlayer/PersistenceController.swift +++ b/JellyfinPlayer/PersistenceController.swift @@ -30,6 +30,9 @@ struct PersistenceController { init(inMemory: Bool = false) { container = NSPersistentCloudKitContainer(name: "Model") + container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.me.vigue.jellyfin.mobileclient")!.appendingPathComponent("\(container.name).sqlite"))] + if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } diff --git a/Shared/Extensions/CollectionExtensions.swift b/Shared/Extensions/CollectionExtensions.swift new file mode 100644 index 00000000..9bbd3c50 --- /dev/null +++ b/Shared/Extensions/CollectionExtensions.swift @@ -0,0 +1,23 @@ +/* SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation + + +public extension Collection { + + /// SwifterSwift: Safe protects the array from out of bounds by use of optional. + /// + /// let arr = [1, 2, 3, 4, 5] + /// arr[safe: 1] -> 2 + /// arr[safe: 10] -> nil + /// + /// - Parameter index: index of element to access element. + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Shared/Extensions/ImageExtensions.swift b/Shared/Extensions/ImageExtensions.swift new file mode 100644 index 00000000..15f7522e --- /dev/null +++ b/Shared/Extensions/ImageExtensions.swift @@ -0,0 +1,21 @@ +/* SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +extension Image { + func centerCropped() -> some View { + GeometryReader { geo in + self + .resizable() + .scaledToFill() + .frame(width: geo.size.width, height: geo.size.height) + .clipped() + } + } +} diff --git a/WidgetExtension.entitlements b/WidgetExtension.entitlements new file mode 100644 index 00000000..b164e1cb --- /dev/null +++ b/WidgetExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.me.vigue.jellyfin.mobileclient + + keychain-access-groups + + $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain + + + diff --git a/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json b/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..10fdb69d --- /dev/null +++ b/WidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,32 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.765", + "green" : "0.361", + "red" : "0.667" + } + }, + "idiom" : "iphone" + }, + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.765", + "green" : "0.361", + "red" : "0.667" + } + }, + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/Contents.json b/WidgetExtension/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/WidgetExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/1024.png b/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/1024.png new file mode 100644 index 00000000..39934f79 Binary files /dev/null and b/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/1024.png differ diff --git a/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json b/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json new file mode 100644 index 00000000..d7aaf31c --- /dev/null +++ b/WidgetExtension/Assets.xcassets/WidgetHeaderSymbol.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "1024.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WidgetExtension/Info.plist b/WidgetExtension/Info.plist new file mode 100644 index 00000000..a98e4afd --- /dev/null +++ b/WidgetExtension/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/WidgetExtension/JellyfinWidget.swift b/WidgetExtension/JellyfinWidget.swift new file mode 100644 index 00000000..74b885d5 --- /dev/null +++ b/WidgetExtension/JellyfinWidget.swift @@ -0,0 +1,18 @@ +/* SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import WidgetKit +import KeychainSwift + +@main +struct JellyfinWidgetBundle: WidgetBundle { + @WidgetBundleBuilder + var body: some Widget { + NextUpWidget() + } +} diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift new file mode 100644 index 00000000..76d84797 --- /dev/null +++ b/WidgetExtension/NextUpWidget.swift @@ -0,0 +1,484 @@ +/* SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Combine +import JellyfinAPI +import Nuke +import SwiftUI +import WidgetKit + +enum WidgetError: String, Error { + case unknown + case emptyServer + case emptyUser + case emptyHeader +} + +struct NextUpWidgetProvider: TimelineProvider { + func placeholder(in context: Context) -> NextUpEntry { + NextUpEntry(date: Date(), items: [], error: nil) + } + + func getSnapshot(in context: Context, completion: @escaping (NextUpEntry) -> Void) { + let currentDate = Date() + WidgetEnvironment.shared.update() + guard let server = WidgetEnvironment.shared.server else { return + DispatchQueue.main.async { + completion(NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyServer)) + } + } + guard let savedUser = WidgetEnvironment.shared.user else { return + DispatchQueue.main.async { + completion(NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyUser)) + } + } + guard let header = WidgetEnvironment.shared.header else { return + DispatchQueue.main.async { + completion(NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyHeader)) + } + } + var tempCancellables = Set() + JellyfinAPI.basePath = server.baseURI ?? "" + JellyfinAPI.customHeaders = ["X-Emby-Authorization": header] + TvShowsAPI.getNextUp(userId: savedUser.user_id, limit: 3, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .subscribe(on: DispatchQueue.global(qos: .background)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + break + case let .failure(error): + completion(NextUpEntry(date: currentDate, items: [], error: error)) + } + }, receiveValue: { response in + let dispatchGroup = DispatchGroup() + let items = response.items ?? [] + var downloadedItems = [(BaseItemDto, UIImage?)]() + items.enumerated().forEach { _, item in + dispatchGroup.enter() + ImagePipeline.shared.loadImage(with: item.getBackdropImage(baseURL: server.baseURI ?? "", maxWidth: 320)) { result in + guard case let .success(image) = result else { + dispatchGroup.leave() + return + } + downloadedItems.append((item, image.image)) + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + completion(NextUpEntry(date: currentDate, items: downloadedItems, error: nil)) + } + }) + .store(in: &tempCancellables) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let currentDate = Date() + let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)! + WidgetEnvironment.shared.update() + guard let server = WidgetEnvironment.shared.server else { return + DispatchQueue.main.async { + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyServer)], + policy: .after(entryDate))) + } + } + guard let savedUser = WidgetEnvironment.shared.user else { return + DispatchQueue.main.async { + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyUser)], + policy: .after(entryDate))) + } + } + guard let header = WidgetEnvironment.shared.header else { return + DispatchQueue.main.async { + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyHeader)], + policy: .after(entryDate))) + } + } + var tempCancellables = Set() + JellyfinAPI.basePath = server.baseURI ?? "" + JellyfinAPI.customHeaders = ["X-Emby-Authorization": header] + TvShowsAPI.getNextUp(userId: savedUser.user_id, limit: 3, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb]) + .subscribe(on: DispatchQueue.global(qos: .background)) + .sink(receiveCompletion: { result in + switch result { + case .finished: + break + case let .failure(error): + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: error)], policy: .after(entryDate))) + } + }, receiveValue: { response in + let dispatchGroup = DispatchGroup() + let items = response.items ?? [] + var downloadedItems = [(BaseItemDto, UIImage?)]() + items.enumerated().forEach { _, item in + dispatchGroup.enter() + ImagePipeline.shared.loadImage(with: item.getBackdropImage(baseURL: server.baseURI ?? "", maxWidth: 320)) { result in + guard case let .success(image) = result else { + dispatchGroup.leave() + return + } + downloadedItems.append((item, image.image)) + dispatchGroup.leave() + } + } + + dispatchGroup.notify(queue: .main) { + completion(Timeline(entries: [NextUpEntry(date: currentDate, items: downloadedItems, error: nil)], + policy: .after(entryDate))) + } + }) + .store(in: &tempCancellables) + } +} + +struct NextUpEntry: TimelineEntry { + let date: Date + let items: [(BaseItemDto, UIImage?)] + let error: Error? +} + +struct NextUpEntryView: View { + var entry: NextUpWidgetProvider.Entry + + @Environment(\.widgetFamily) + var family + + @ViewBuilder + var body: some View { + Group { + if let error = entry.error { + HStack { + Image(systemName: "exclamationmark.octagon") + Text((error as? WidgetError)?.rawValue ?? "") + } + .background(Color.blue) + } else if entry.items.isEmpty { + Text("Empty Next Up") + .font(.body) + .bold() + .foregroundColor(.primary) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + } else { + switch family { + case .systemSmall: + small(item: entry.items.first) + case .systemMedium: + medium(items: entry.items) + case .systemLarge: + large(items: entry.items) + @unknown default: + EmptyView() + } + } + } + .background(Color(.secondarySystemBackground)) + } +} + +extension NextUpEntryView { + var smallVideoPlaceholderView: some View { + VStack(alignment: .leading) { + Color(.systemGray) + .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) + .cornerRadius(8) + .shadow(radius: 8) + Color(.systemGray2) + .frame(width: 100, height: 10) + Color(.systemGray3) + .frame(width: 80, height: 10) + } + } + + var largeVideoPlaceholderView: some View { + HStack(spacing: 20) { + Color(.systemGray) + .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) + .cornerRadius(8) + .shadow(radius: 8) + VStack(alignment: .leading, spacing: 8) { + Color(.systemGray2) + .frame(width: 100, height: 10) + Color(.systemGray3) + .frame(width: 80, height: 10) + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } +} + +extension NextUpEntryView { + var headerSymbol: some View { + Image("WidgetHeaderSymbol") + .resizable() + .frame(width: 12, height: 12) + .cornerRadius(4) + .shadow(radius: 8) + } + + func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View { + VStack(alignment: .leading) { + if let image = item.1 { + Image(uiImage: image) + .resizable() + .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) + .clipped() + .cornerRadius(8) + .shadow(radius: 8) + } + Text(item.0.seriesName ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + + func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { + HStack(spacing: 20) { + if let image = item.1 { + Image(uiImage: image) + .resizable() + .aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill) + .clipped() + .cornerRadius(8) + .shadow(radius: 8) + } + VStack(alignment: .leading, spacing: 8) { + Text(item.0.seriesName ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } + } +} + +extension NextUpEntryView { + func small(item: (BaseItemDto, UIImage?)?) -> some View { + VStack(alignment: .trailing) { + headerSymbol + if let item = item { + smallVideoView(item: item) + } else { + smallVideoPlaceholderView + } + } + .padding(12) + } + + func medium(items: [(BaseItemDto, UIImage?)]) -> some View { + VStack(alignment: .trailing) { + headerSymbol + HStack(spacing: 16) { + if let firstItem = items[safe: 0] { + smallVideoView(item: firstItem) + } else { + smallVideoPlaceholderView + } + if let secondItem = items[safe: 1] { + smallVideoView(item: secondItem) + } else { + smallVideoPlaceholderView + } + } + } + .padding(12) + } + + func large(items: [(BaseItemDto, UIImage?)]) -> some View { + VStack(spacing: 0) { + if let firstItem = items[safe: 0] { + ZStack(alignment: .topTrailing) { + ZStack(alignment: .bottomLeading) { + if let image = firstItem.1 { + Image(uiImage: image) + .centerCropped() + .innerShadow(color: Color.black.opacity(0.5), radius: 0.5) + } + VStack(alignment: .leading, spacing: 8) { + Text(firstItem.0.seriesName ?? "") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + Text("\(firstItem.0.name ?? "") · S\(firstItem.0.parentIndexNumber ?? 0):E\(firstItem.0.indexNumber ?? 0)") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.gray) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + .shadow(radius: 8) + .padding(12) + } + headerSymbol + .padding(12) + } + .clipped() + .shadow(radius: 8) + } + VStack(spacing: 8) { + if let secondItem = items[safe: 1] { + largeVideoView(item: secondItem) + } else { + largeVideoPlaceholderView + } + Divider() + if let thirdItem = items[safe: 2] { + largeVideoView(item: thirdItem) + } else { + largeVideoPlaceholderView + } + } + .padding(12) + } + } +} + +struct NextUpWidget: Widget { + let kind: String = "NextUpWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, + provider: NextUpWidgetProvider()) { entry in + NextUpEntryView(entry: entry) + } + .configurationDisplayName("Next Up") + .description("Keep watching where you left off or see what's up next.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +struct NextUpWidget_Previews: PreviewProvider { + static var previews: some View { + Group { + NextUpEntryView(entry: .init(date: Date(), + items: [(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol"))], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + NextUpEntryView(entry: .init(date: Date(), + items: [ + (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), + UIImage(named: "WidgetHeaderSymbol")), + ], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + NextUpEntryView(entry: .init(date: Date(), + items: [ + (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), + UIImage(named: "WidgetHeaderSymbol")), + ], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemLarge)) + NextUpEntryView(entry: .init(date: Date(), + items: [(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol"))], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .preferredColorScheme(.dark) + NextUpEntryView(entry: .init(date: Date(), + items: [ + (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), + UIImage(named: "WidgetHeaderSymbol")), + ], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + .preferredColorScheme(.dark) + NextUpEntryView(entry: .init(date: Date(), + items: [ + (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"), + UIImage(named: "WidgetHeaderSymbol")), + ], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemLarge)) + .preferredColorScheme(.dark) + NextUpEntryView(entry: .init(date: Date(), + items: [], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + .preferredColorScheme(.dark) + NextUpEntryView(entry: .init(date: Date(), + items: [ + (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol")), + ], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + .preferredColorScheme(.dark) + NextUpEntryView(entry: .init(date: Date(), + items: [ + (.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"), + UIImage(named: "WidgetHeaderSymbol")), + (.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"), + UIImage(named: "WidgetHeaderSymbol")), + ], + error: nil)) + .previewContext(WidgetPreviewContext(family: .systemLarge)) + .preferredColorScheme(.dark) + } + } +} + +import SwiftUI + +private extension View { + func innerShadow(color: Color, radius: CGFloat = 0.1) -> some View { + modifier(InnerShadow(color: color, radius: min(max(0, radius), 1))) + } +} + +private struct InnerShadow: ViewModifier { + var color: Color = .gray + var radius: CGFloat = 0.1 + + private var colors: [Color] { + [color.opacity(0.75), color.opacity(0.0), .clear] + } + + func body(content: Content) -> some View { + GeometryReader { geo in + content + .overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .top, endPoint: .bottom) + .frame(height: self.radius * self.minSide(geo)), + alignment: .top) + .overlay(LinearGradient(gradient: Gradient(colors: self.colors), startPoint: .bottom, endPoint: .top) + .frame(height: self.radius * self.minSide(geo)), + alignment: .bottom) + } + } + + func minSide(_ geo: GeometryProxy) -> CGFloat { + CGFloat(3) * min(geo.size.width, geo.size.height) / 2 + } +} diff --git a/WidgetExtension/WidgetEnvironment.swift b/WidgetExtension/WidgetEnvironment.swift new file mode 100644 index 00000000..a9074a68 --- /dev/null +++ b/WidgetExtension/WidgetEnvironment.swift @@ -0,0 +1,56 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import CoreData +import KeychainSwift +import UIKit + +final class WidgetEnvironment { + static let shared = WidgetEnvironment() + + var server: Server! + var user: SignedInUser! + var header: String? + + init() { + update() + } + + func update() { + let serverRequest = NSFetchRequest(entityName: "Server") + let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server] + let savedUserRequest = NSFetchRequest(entityName: "SignedInUser") + let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) as? [SignedInUser] + + server = servers?.first + user = savedUsers?.first + + let keychain = KeychainSwift() + // need prefix + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + guard let authToken = keychain.get("AccessToken_\(user?.user_id ?? "")") else { + return + } + + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + var deviceName = UIDevice.current.name + deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current) + deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]") + + var header = "MediaBrowser " + header.append("Client=\"SwiftFin\", ") + header.append("Device=\"\(deviceName)\", ") + header.append("DeviceId=\"\(user?.device_uuid ?? "")\", ") + header.append("Version=\"\(appVersion ?? "0.0.1")\", ") + header.append("Token=\"\(authToken)\"") + + self.header = header + } +} diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtension/WidgetExtension.entitlements new file mode 100644 index 00000000..b164e1cb --- /dev/null +++ b/WidgetExtension/WidgetExtension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.me.vigue.jellyfin.mobileclient + + keychain-access-groups + + $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain + + +