Add Next Up Widgets (#43)

* WIP

* add EntryView UI

apply AppGroups
apply KeychainGroups

* update widget layout

* update widget layout

* Refactoring

add WidgetEnvironment
add snapshot logic
add placeholder

* fix

* fix team id

* pass ci?

* update keychain group

* Update PersistenceController.swift

Co-authored-by: Aiden Vigue <acvigue@me.com>
This commit is contained in:
Kwangmin Bae 2021-06-10 17:05:52 +09:00 committed by GitHub
parent d6aa8c4a22
commit 7c5a4441c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1070 additions and 0 deletions

View File

@ -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 */; };
@ -72,9 +73,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;
@ -107,6 +137,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 */
@ -160,6 +201,17 @@
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
621C638126676728004216EA /* NukeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = "<group>"; };
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = "<group>"; };
6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = "<group>"; };
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 = "<group>"; };
628B95282670CABE0091AF3B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEnvironment.swift; sourceTree = "<group>"; };
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -188,6 +240,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 */
@ -244,6 +308,7 @@
isa = PBXGroup;
children = (
53D5E3DA264B460200BADDC8 /* Cartfile */,
628B95252670CABD0091AF3B /* WidgetExtension */,
53D5E3DB264B47EE00BADDC8 /* Frameworks */,
5377CBF3263B596A003A4E83 /* JellyfinPlayer */,
535870612669D21600D05A09 /* JellyfinPlayer tvOS */,
@ -257,6 +322,7 @@
children = (
5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */,
535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */,
628B95202670CABD0091AF3B /* WidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -308,6 +374,8 @@
children = (
5358709C2669D82900D05A09 /* TVVLCKit.framework */,
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */,
628B95212670CABD0091AF3B /* WidgetKit.framework */,
628B95232670CABD0091AF3B /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -323,10 +391,25 @@
621C638126676728004216EA /* NukeExtensions.swift */,
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
621338922660107500A81A2A /* StringExtensions.swift */,
6267B3D526710B8900A7371D /* CollectionExtensions.swift */,
6267B3D92671138200A7371D /* ImageExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
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 = "<group>";
};
AE8C3157265D6F5E008AA076 /* Resources */ = {
isa = PBXGroup;
children = (
@ -372,10 +455,12 @@
5377CBEF263B596A003A4E83 /* Resources */,
53D5E3DF264B47EE00BADDC8 /* Embed Frameworks */,
5302F8322658B74800647A2E /* CopyFiles */,
628B95312670CABE0091AF3B /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
628B952C2670CABE0091AF3B /* PBXTargetDependency */,
);
name = "JellyfinPlayer iOS";
packageProductDependencies = (
@ -388,6 +473,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 */
@ -406,6 +513,9 @@
5377CBF0263B596A003A4E83 = {
CreatedOnToolsVersion = 12.5;
};
628B951F2670CABD0091AF3B = {
CreatedOnToolsVersion = 12.5;
};
};
};
buildConfigurationList = 5377CBEC263B596A003A4E83 /* Build configuration list for PBXProject "JellyfinPlayer" */;
@ -429,6 +539,7 @@
targets = (
5377CBF0263B596A003A4E83 /* JellyfinPlayer iOS */,
5358705F2669D21600D05A09 /* JellyfinPlayer tvOS */,
628B951F2670CABD0091AF3B /* WidgetExtension */,
);
};
/* End PBXProject section */
@ -455,6 +566,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
628B951E2670CABD0091AF3B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
628B95292670CABE0091AF3B /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -462,7 +581,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 */,
@ -502,12 +623,14 @@
5321753B2671BCFC005491E6 /* SettingsViewModel.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 */,
@ -519,8 +642,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;
@ -702,6 +849,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;
@ -733,6 +881,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;
@ -762,6 +911,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 */
@ -792,6 +991,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 */
@ -870,6 +1078,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 */

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1250"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "628B951F2670CABD0091AF3B"
BuildableName = "WidgetExtension.appex"
BlueprintName = "WidgetExtension"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer iOS.app"
BlueprintName = "JellyfinPlayer iOS"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "2"
BundleIdentifier = "com.apple.springboard">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "628B951F2670CABD0091AF3B"
BuildableName = "WidgetExtension.appex"
BlueprintName = "WidgetExtension"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer iOS.app"
BlueprintName = "JellyfinPlayer iOS"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "snapshot"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "medium"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "5377CBF0263B596A003A4E83"
BuildableName = "JellyfinPlayer iOS.app"
BlueprintName = "JellyfinPlayer iOS"
ReferencedContainer = "container:JellyfinPlayer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -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 {

View File

@ -6,7 +6,15 @@
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.me.vigue.jellyfin.mobileclient</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain</string>
</array>
</dict>
</plist>

View File

@ -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")
}

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.me.vigue.jellyfin.mobileclient</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain</string>
</array>
</dict>
</plist>

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "1024.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -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()
}
}

View File

@ -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<AnyCancellable>()
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<Entry>) -> 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<AnyCancellable>()
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
}
}

View File

@ -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<NSFetchRequestResult>(entityName: "Server")
let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server]
let savedUserRequest = NSFetchRequest<NSFetchRequestResult>(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
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.me.vigue.jellyfin.mobileclient</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain</string>
</array>
</dict>
</plist>