diff --git a/Persistence/Sources/Persistence/LocalData.swift b/Persistence/Sources/Persistence/LocalData.swift
index 7899214..c746652 100644
--- a/Persistence/Sources/Persistence/LocalData.swift
+++ b/Persistence/Sources/Persistence/LocalData.swift
@@ -59,6 +59,12 @@ public struct LocalData {
public let clientSecret: String
public let token: Token
+ /// A filename-safe string for this account
+ public var persistenceKey: String {
+ // slashes the base64 string turn into subdirectories which we don't want
+ id.base64EncodedString().replacingOccurrences(of: "/", with: "_")
+ }
+
public init(instanceURL: URL, clientID: String, clientSecret: String, token: Token) {
// we use a hash of instance host and account id rather than random ids so that
// user activites can uniquely identify accounts across devices
diff --git a/Persistence/Sources/Persistence/PersistentContainer.swift b/Persistence/Sources/Persistence/PersistentContainer.swift
index fc0bf3c..06db8d8 100644
--- a/Persistence/Sources/Persistence/PersistentContainer.swift
+++ b/Persistence/Sources/Persistence/PersistentContainer.swift
@@ -27,9 +27,7 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
public init(account: LocalData.Account) {
- // slashes the base64 string turn into subdirectories which we don't want
- let name = account.id.base64EncodedString().replacingOccurrences(of: "/", with: "_")
- super.init(name: name, managedObjectModel: PersistentContainer.managedObjectModel)
+ super.init(name: account.persistenceKey, managedObjectModel: PersistentContainer.managedObjectModel)
let groupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.net.shadowfacts.Reader")!
let containerAppSupportURL = groupContainerURL
diff --git a/Persistence/Sources/Persistence/Reader.xcdatamodeld/Reader.xcdatamodel/contents b/Persistence/Sources/Persistence/Reader.xcdatamodeld/Reader.xcdatamodel/contents
index eecde3a..19f6ec5 100644
--- a/Persistence/Sources/Persistence/Reader.xcdatamodeld/Reader.xcdatamodel/contents
+++ b/Persistence/Sources/Persistence/Reader.xcdatamodeld/Reader.xcdatamodel/contents
@@ -29,6 +29,7 @@
+
diff --git a/Reader.xcodeproj/project.pbxproj b/Reader.xcodeproj/project.pbxproj
index 265e25d..4ce96fb 100644
--- a/Reader.xcodeproj/project.pbxproj
+++ b/Reader.xcodeproj/project.pbxproj
@@ -27,6 +27,9 @@
D6C68806272CD27700874C10 /* ReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68805272CD27700874C10 /* ReaderTests.swift */; };
D6C68810272CD27700874C10 /* ReaderUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6880F272CD27700874C10 /* ReaderUITests.swift */; };
D6C68812272CD27700874C10 /* ReaderUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68811272CD27700874C10 /* ReaderUITestsLaunchTests.swift */; };
+ D6D5FA24285FE9DB00BBF188 /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D5FA23285FE9DB00BBF188 /* WidgetHelper.swift */; };
+ D6D5FA27285FEA6900BBF188 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D5FA26285FEA6900BBF188 /* WidgetData.swift */; };
+ D6D5FA28285FEA8A00BBF188 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D5FA26285FEA6900BBF188 /* WidgetData.swift */; };
D6E2434C278B456A0005E546 /* ItemsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2434B278B456A0005E546 /* ItemsViewController.swift */; };
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24361278BA1410005E546 /* ItemCollectionViewCell.swift */; };
D6E24369278BABB40005E546 /* UIColor+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24368278BABB40005E546 /* UIColor+App.swift */; };
@@ -39,6 +42,15 @@
D6EEDE87285FA75D009F854E /* Fervor in Frameworks */ = {isa = PBXBuildFile; productRef = D6EEDE86285FA75D009F854E /* Fervor */; };
D6EEDE89285FA75F009F854E /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = D6EEDE88285FA75F009F854E /* Persistence */; };
D6EEDE8B285FA7FD009F854E /* LocalData+Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE8A285FA7FD009F854E /* LocalData+Migration.swift */; };
+ D6EEDE92285FA915009F854E /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6EEDE91285FA915009F854E /* WidgetKit.framework */; };
+ D6EEDE94285FA915009F854E /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6EEDE93285FA915009F854E /* SwiftUI.framework */; };
+ D6EEDE97285FA915009F854E /* Widgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE96285FA915009F854E /* Widgets.swift */; };
+ D6EEDE9A285FA915009F854E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6EEDE99285FA915009F854E /* Assets.xcassets */; };
+ D6EEDE9C285FA915009F854E /* Widgets.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE98285FA915009F854E /* Widgets.intentdefinition */; };
+ D6EEDE9D285FA915009F854E /* Widgets.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE98285FA915009F854E /* Widgets.intentdefinition */; };
+ D6EEDEA0285FA915009F854E /* WidgetsExtension.appex in Embed PlugIns */ = {isa = PBXBuildFile; fileRef = D6EEDE90285FA915009F854E /* WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ D6EEDEA7285FAE4D009F854E /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDEA5285FAD24009F854E /* Recents.swift */; };
+ D6EEDEA9285FAE60009F854E /* Persistence in Frameworks */ = {isa = PBXBuildFile; productRef = D6EEDEA8285FAE60009F854E /* Persistence */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -63,6 +75,13 @@
remoteGlobalIDString = D6C687E7272CD27600874C10;
remoteInfo = Reader;
};
+ D6EEDE9E285FA915009F854E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = D6C687E0272CD27600874C10 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = D6EEDE8F285FA915009F854E;
+ remoteInfo = WidgetsExtension;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -72,6 +91,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
+ D6EEDEA0285FA915009F854E /* WidgetsExtension.appex in Embed PlugIns */,
D6840914279487DC00E327D2 /* ReaderMac.bundle in Embed PlugIns */,
);
name = "Embed PlugIns";
@@ -119,6 +139,8 @@
D6C6880B272CD27700874C10 /* ReaderUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReaderUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D6C6880F272CD27700874C10 /* ReaderUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderUITests.swift; sourceTree = ""; };
D6C68811272CD27700874C10 /* ReaderUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderUITestsLaunchTests.swift; sourceTree = ""; };
+ D6D5FA23285FE9DB00BBF188 /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = ""; };
+ D6D5FA26285FEA6900BBF188 /* WidgetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetData.swift; sourceTree = ""; };
D6E2434B278B456A0005E546 /* ItemsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsViewController.swift; sourceTree = ""; };
D6E24361278BA1410005E546 /* ItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCollectionViewCell.swift; sourceTree = ""; };
D6E24368278BABB40005E546 /* UIColor+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+App.swift"; sourceTree = ""; };
@@ -128,6 +150,15 @@
D6EB531C278C89C300AD2E61 /* AppNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationController.swift; sourceTree = ""; };
D6EB531E278E4A7500AD2E61 /* StretchyMenuInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StretchyMenuInteraction.swift; sourceTree = ""; };
D6EEDE8A285FA7FD009F854E /* LocalData+Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocalData+Migration.swift"; sourceTree = ""; };
+ D6EEDE90285FA915009F854E /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ D6EEDE91285FA915009F854E /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
+ D6EEDE93285FA915009F854E /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+ D6EEDE96285FA915009F854E /* Widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Widgets.swift; sourceTree = ""; };
+ D6EEDE98285FA915009F854E /* Widgets.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Widgets.intentdefinition; sourceTree = ""; };
+ D6EEDE99285FA915009F854E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ D6EEDE9B285FA915009F854E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ D6EEDEA1285FA915009F854E /* WidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtension.entitlements; sourceTree = ""; };
+ D6EEDEA5285FAD24009F854E /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -162,6 +193,16 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D6EEDE8D285FA915009F854E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D6EEDEA9285FAE60009F854E /* Persistence in Frameworks */,
+ D6EEDE94285FA915009F854E /* SwiftUI.framework in Frameworks */,
+ D6EEDE92285FA915009F854E /* WidgetKit.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -216,6 +257,8 @@
isa = PBXGroup;
children = (
D68B3037279099FD00E8B3FA /* liblolhtml.a */,
+ D6EEDE91285FA915009F854E /* WidgetKit.framework */,
+ D6EEDE93285FA915009F854E /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -223,12 +266,14 @@
D6C687DF272CD27600874C10 = {
isa = PBXGroup;
children = (
+ D6EEDEA1285FA915009F854E /* WidgetsExtension.entitlements */,
D6AB5E9B285F706F00157F2F /* Persistence */,
D6AB5E9A285F6FE100157F2F /* Fervor */,
D6C687EA272CD27600874C10 /* Reader */,
D6C68804272CD27700874C10 /* ReaderTests */,
D6C6880E272CD27700874C10 /* ReaderUITests */,
D6840911279486C400E327D2 /* ReaderMac */,
+ D6EEDE95285FA915009F854E /* Widgets */,
D6C687E9272CD27600874C10 /* Products */,
D68B302E278FDCE200E8B3FA /* Frameworks */,
);
@@ -241,6 +286,7 @@
D6C68801272CD27700874C10 /* ReaderTests.xctest */,
D6C6880B272CD27700874C10 /* ReaderUITests.xctest */,
D684090D279486BF00E327D2 /* ReaderMac.bundle */,
+ D6EEDE90285FA915009F854E /* WidgetsExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -261,6 +307,7 @@
D68408EE2794808E00E327D2 /* Preferences.swift */,
D608238C27DE729E00D7D5F9 /* ItemListType.swift */,
D6EEDE8A285FA7FD009F854E /* LocalData+Migration.swift */,
+ D6D5FA25285FEA5B00BBF188 /* Widgets */,
D65B18AF2750468B004A9448 /* Screens */,
D6C687F7272CD27700874C10 /* Assets.xcassets */,
D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */,
@@ -288,6 +335,15 @@
path = ReaderUITests;
sourceTree = "";
};
+ D6D5FA25285FEA5B00BBF188 /* Widgets */ = {
+ isa = PBXGroup;
+ children = (
+ D6D5FA23285FE9DB00BBF188 /* WidgetHelper.swift */,
+ D6D5FA26285FEA6900BBF188 /* WidgetData.swift */,
+ );
+ path = Widgets;
+ sourceTree = "";
+ };
D6E2434A278B455C0005E546 /* Items */ = {
isa = PBXGroup;
children = (
@@ -305,6 +361,18 @@
path = Read;
sourceTree = "";
};
+ D6EEDE95285FA915009F854E /* Widgets */ = {
+ isa = PBXGroup;
+ children = (
+ D6EEDE96285FA915009F854E /* Widgets.swift */,
+ D6EEDEA5285FAD24009F854E /* Recents.swift */,
+ D6EEDE98285FA915009F854E /* Widgets.intentdefinition */,
+ D6EEDE99285FA915009F854E /* Assets.xcassets */,
+ D6EEDE9B285FA915009F854E /* Info.plist */,
+ );
+ path = Widgets;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -340,6 +408,7 @@
);
dependencies = (
D6840916279487DC00E327D2 /* PBXTargetDependency */,
+ D6EEDE9F285FA915009F854E /* PBXTargetDependency */,
);
name = Reader;
packageProductDependencies = (
@@ -387,6 +456,26 @@
productReference = D6C6880B272CD27700874C10 /* ReaderUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
+ D6EEDE8F285FA915009F854E /* WidgetsExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = D6EEDEA2285FA915009F854E /* Build configuration list for PBXNativeTarget "WidgetsExtension" */;
+ buildPhases = (
+ D6EEDE8C285FA915009F854E /* Sources */,
+ D6EEDE8D285FA915009F854E /* Frameworks */,
+ D6EEDE8E285FA915009F854E /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = WidgetsExtension;
+ packageProductDependencies = (
+ D6EEDEA8285FAE60009F854E /* Persistence */,
+ );
+ productName = WidgetsExtension;
+ productReference = D6EEDE90285FA915009F854E /* WidgetsExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -394,7 +483,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1320;
+ LastSwiftUpdateCheck = 1400;
LastUpgradeCheck = 1400;
TargetAttributes = {
D684090C279486BF00E327D2 = {
@@ -412,6 +501,9 @@
CreatedOnToolsVersion = 13.2;
TestTargetID = D6C687E7272CD27600874C10;
};
+ D6EEDE8F285FA915009F854E = {
+ CreatedOnToolsVersion = 14.0;
+ };
};
};
buildConfigurationList = D6C687E3272CD27600874C10 /* Build configuration list for PBXProject "Reader" */;
@@ -434,6 +526,7 @@
D6C68800272CD27700874C10 /* ReaderTests */,
D6C6880A272CD27700874C10 /* ReaderUITests */,
D684090C279486BF00E327D2 /* ReaderMac */,
+ D6EEDE8F285FA915009F854E /* WidgetsExtension */,
);
};
/* End PBXProject section */
@@ -471,6 +564,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D6EEDE8E285FA915009F854E /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D6EEDE9A285FA915009F854E /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -510,6 +611,7 @@
files = (
D608238D27DE729E00D7D5F9 /* ItemListType.swift in Sources */,
D65B18B627504920004A9448 /* FervorController.swift in Sources */,
+ D6D5FA27285FEA6900BBF188 /* WidgetData.swift in Sources */,
D68B304227932ED500E8B3FA /* UserActivities.swift in Sources */,
D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */,
D6E2436B278BB1880005E546 /* HomeCollectionViewCell.swift in Sources */,
@@ -521,9 +623,11 @@
D6EB531F278E4A7500AD2E61 /* StretchyMenuInteraction.swift in Sources */,
D68B30402792729A00E8B3FA /* AppSplitViewController.swift in Sources */,
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
+ D6EEDE9D285FA915009F854E /* Widgets.intentdefinition in Sources */,
D68408ED2794803D00E327D2 /* PrefsView.swift in Sources */,
D68408EF2794808E00E327D2 /* Preferences.swift in Sources */,
D68B303627907D9200E8B3FA /* ExcerptGenerator.swift in Sources */,
+ D6D5FA24285FE9DB00BBF188 /* WidgetHelper.swift in Sources */,
D6E24363278BA1410005E546 /* ItemCollectionViewCell.swift in Sources */,
D6E2436E278BD8160005E546 /* ReadViewController.swift in Sources */,
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
@@ -548,6 +652,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ D6EEDE8C285FA915009F854E /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D6EEDE9C285FA915009F854E /* Widgets.intentdefinition in Sources */,
+ D6D5FA28285FEA8A00BBF188 /* WidgetData.swift in Sources */,
+ D6EEDEA7285FAE4D009F854E /* Recents.swift in Sources */,
+ D6EEDE97285FA915009F854E /* Widgets.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -567,6 +682,11 @@
target = D6C687E7272CD27600874C10 /* Reader */;
targetProxy = D6C6880C272CD27700874C10 /* PBXContainerItemProxy */;
};
+ D6EEDE9F285FA915009F854E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = D6EEDE8F285FA915009F854E /* WidgetsExtension */;
+ targetProxy = D6EEDE9E285FA915009F854E /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -922,6 +1042,66 @@
};
name = Release;
};
+ D6EEDEA3285FA915009F854E /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CODE_SIGN_ENTITLEMENTS = WidgetsExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = V4WK9KR9U2;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Widgets/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = Widgets;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Reader.Widgets;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ D6EEDEA4285FA915009F854E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CODE_SIGN_ENTITLEMENTS = WidgetsExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = V4WK9KR9U2;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Widgets/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = Widgets;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.Reader.Widgets;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SUPPORTS_MACCATALYST = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -970,6 +1150,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ D6EEDEA2285FA915009F854E /* Build configuration list for PBXNativeTarget "WidgetsExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ D6EEDEA3285FA915009F854E /* Debug */,
+ D6EEDEA4285FA915009F854E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -997,6 +1186,10 @@
isa = XCSwiftPackageProductDependency;
productName = Persistence;
};
+ D6EEDEA8285FAE60009F854E /* Persistence */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = Persistence;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = D6C687E0272CD27600874C10 /* Project object */;
diff --git a/Reader/FervorController.swift b/Reader/FervorController.swift
index a1e6d16..dda93c3 100644
--- a/Reader/FervorController.swift
+++ b/Reader/FervorController.swift
@@ -89,6 +89,8 @@ actor FervorController {
setSyncState(.excerpts)
await ExcerptGenerator.generateAll(self)
+
+ await WidgetHelper.updateWidgetData(fervorController: self)
}
@MainActor
diff --git a/Reader/Info.plist b/Reader/Info.plist
index 1542fc1..a497217 100644
--- a/Reader/Info.plist
+++ b/Reader/Info.plist
@@ -4,14 +4,15 @@
NSUserActivityTypes
- $(PRODUCT_BUNDLE_IDENTIFIER).activity.read-unread
+ $(PRODUCT_BUNDLE_IDENTIFIER).activity.activate-account
+ $(PRODUCT_BUNDLE_IDENTIFIER).activity.add-account
+ $(PRODUCT_BUNDLE_IDENTIFIER).activity.preferences
$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-all
$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-feed
$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-group
$(PRODUCT_BUNDLE_IDENTIFIER).activity.read-item
- $(PRODUCT_BUNDLE_IDENTIFIER).activity.preferences
- $(PRODUCT_BUNDLE_IDENTIFIER).activity.add-account
- $(PRODUCT_BUNDLE_IDENTIFIER).activity.activate-account
+ $(PRODUCT_BUNDLE_IDENTIFIER).activity.read-unread
+ ConfigurationIntent
UIApplicationSceneManifest
@@ -28,10 +29,10 @@
$(PRODUCT_MODULE_NAME).SceneDelegate
- UISceneDelegateClassName
- $(PRODUCT_MODULE_NAME).PrefsSceneDelegate
UISceneConfigurationName
prefs
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).PrefsSceneDelegate
diff --git a/Reader/SceneDelegate.swift b/Reader/SceneDelegate.swift
index 8701db9..c53134d 100644
--- a/Reader/SceneDelegate.swift
+++ b/Reader/SceneDelegate.swift
@@ -95,6 +95,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
Task(priority: .userInitiated) {
await fervorController.syncReadToServer()
}
+ Task(priority: .userInitiated) {
+ await WidgetHelper.updateWidgetData(fervorController: fervorController)
+ }
}
}
@@ -119,6 +122,36 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
+ func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
+ let url = URLContexts.first!.url
+ guard url.scheme == "reader",
+ url.host == "item",
+ let itemID = url.pathComponents.last else {
+ logger.error("Launched with unknown URL: \(url.absoluteString, privacy: .public)")
+ return
+ }
+ guard let split = window?.rootViewController as? AppSplitViewController else {
+ logger.error("Could not handle URL: missing split VC")
+ return
+ }
+ let req = Item.fetchRequest()
+ req.predicate = NSPredicate(format: "id = %@", itemID)
+ if let items = try? fervorController.persistentContainer.viewContext.fetch(req),
+ let item = items.first {
+ split.showItem(item)
+ Task {
+ await fervorController.markItem(item, read: true)
+ }
+ } else {
+ Task {
+ if let item = try? await fervorController.fetchItem(id: itemID) {
+ split.showItem(item)
+ await fervorController.markItem(item, read: true)
+ }
+ }
+ }
+ }
+
private func setupUI(from activity: NSUserActivity) async {
guard let split = window?.rootViewController as? AppSplitViewController else {
logger.error("Failed to setup UI for user activity: missing split VC")
diff --git a/Reader/Screens/AppSplitViewController.swift b/Reader/Screens/AppSplitViewController.swift
index eb3bb9d..1df944c 100644
--- a/Reader/Screens/AppSplitViewController.swift
+++ b/Reader/Screens/AppSplitViewController.swift
@@ -87,6 +87,7 @@ class AppSplitViewController: UISplitViewController {
home.selectItem(type)
}
+ @discardableResult
func showItem(_ item: Item) -> ReadViewController {
if traitCollection.horizontalSizeClass == .compact {
let nav = viewController(for: .compact) as! UINavigationController
diff --git a/Reader/Widgets/WidgetData.swift b/Reader/Widgets/WidgetData.swift
new file mode 100644
index 0000000..80cdb32
--- /dev/null
+++ b/Reader/Widgets/WidgetData.swift
@@ -0,0 +1,53 @@
+//
+// WidgetData.swift
+// Reader
+//
+// Created by Shadowfacts on 6/19/22.
+//
+
+import Foundation
+import Persistence
+
+struct WidgetData: Codable {
+ let recentItems: [Item]
+
+ private static func url(for account: LocalData.Account) -> URL {
+ let groupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.net.shadowfacts.Reader")!
+ let appSupport = groupContainerURL
+ .appendingPathComponent("Library", isDirectory: true)
+ .appendingPathComponent("Application Support", isDirectory: true)
+ let widgetData = appSupport.appendingPathComponent("Widget Data", isDirectory: true)
+ return widgetData
+ .appendingPathComponent(account.persistenceKey)
+ .appendingPathExtension("plist")
+ }
+
+ static func load(account: LocalData.Account) -> WidgetData {
+ let url = url(for: account)
+ let decoder = PropertyListDecoder()
+ guard let data = try? Data(contentsOf: url),
+ let decoded = try? decoder.decode(WidgetData.self, from: data) else {
+ return WidgetData(recentItems: [])
+ }
+ return decoded
+ }
+
+ func save(account: LocalData.Account) {
+ let encoder = PropertyListEncoder()
+ let data = try! encoder.encode(self)
+ let url = WidgetData.url(for: account)
+ try! FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try! data.write(to: url, options: .noFileProtection)
+ }
+
+ struct Item: Codable, Identifiable {
+ let id: String
+ let feedTitle: String?
+ let title: String?
+ let published: Date?
+
+ var launchURL: URL {
+ return URL(string: "reader://item/\(id)")!
+ }
+ }
+}
diff --git a/Reader/Widgets/WidgetHelper.swift b/Reader/Widgets/WidgetHelper.swift
new file mode 100644
index 0000000..b4d6901
--- /dev/null
+++ b/Reader/Widgets/WidgetHelper.swift
@@ -0,0 +1,54 @@
+//
+// WidgetHelper.swift
+// Reader
+//
+// Created by Shadowfacts on 6/19/22.
+//
+
+import Foundation
+import WidgetKit
+import Persistence
+
+struct WidgetHelper {
+ private init() {}
+
+ private static let maxDisplayableItems = 8
+
+ static func updateWidgetData(fervorController: FervorController) async {
+ // Accessing CoreData from the widget extension puts us over the memory limit, so we pre-generate all the data it needs and save it to disk
+
+ let prioritizedItems: [WidgetData.Item] = await fervorController.persistentContainer.performBackgroundTask { ctx in
+ let req = Item.fetchRequest()
+ req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
+ req.fetchLimit = 32
+ req.predicate = NSPredicate(format: "read = NO")
+ var items = (try? ctx.fetch(req)) ?? []
+
+ var prioritizedItems: [Item] = []
+
+ while prioritizedItems.count < maxDisplayableItems {
+ let firstFromUnseenFeedIdx = items.firstIndex { item in
+ prioritizedItems.allSatisfy { existing in
+ existing.feed != item.feed
+ }
+ }
+ if let firstFromUnseenFeedIdx {
+ prioritizedItems.append(items.remove(at: firstFromUnseenFeedIdx))
+ } else if let item = items.first {
+ prioritizedItems.append(item)
+ items.removeFirst()
+ } else {
+ break
+ }
+ }
+
+ return prioritizedItems.map {
+ WidgetData.Item(id: $0.id!, feedTitle: $0.feed?.title, title: $0.title, published: $0.published)
+ }
+ }
+
+ WidgetData(recentItems: prioritizedItems).save(account: fervorController.account!)
+
+ WidgetCenter.shared.reloadAllTimelines()
+ }
+}
diff --git a/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json b/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json b/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..13613e3
--- /dev/null
+++ b/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Assets.xcassets/Contents.json b/Widgets/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Widgets/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Widgets/Info.plist b/Widgets/Info.plist
new file mode 100644
index 0000000..0f118fb
--- /dev/null
+++ b/Widgets/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/Widgets/Recents.swift b/Widgets/Recents.swift
new file mode 100644
index 0000000..970e598
--- /dev/null
+++ b/Widgets/Recents.swift
@@ -0,0 +1,196 @@
+//
+// Recents.swift
+// Reader
+//
+// Created by Shadowfacts on 6/19/22.
+//
+
+import WidgetKit
+import SwiftUI
+import Persistence
+
+struct RecentsProvider: IntentTimelineProvider {
+ func placeholder(in context: Context) -> RecentsEntry {
+ RecentsEntry(items: [], configuration: ConfigurationIntent())
+ }
+
+ func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (RecentsEntry) -> ()) {
+ let entry = RecentsEntry(items: getItems(), configuration: configuration)
+ completion(entry)
+ }
+
+ func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
+ // TODO: get account from configuration intent
+
+ let entry = RecentsEntry(items: getItems(), configuration: configuration)
+ let refreshDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
+ let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
+ completion(timeline)
+ }
+
+ private func getItems() -> [WidgetData.Item] {
+ guard let account = LocalData.mostRecentAccount() else {
+ return []
+ }
+ return WidgetData.load(account: account).recentItems
+ }
+
+}
+
+struct RecentsEntry: TimelineEntry {
+ let date = Date()
+
+ let items: [WidgetData.Item]
+ let configuration: ConfigurationIntent
+}
+
+struct RecentsEntryView: View {
+ let entry: RecentsEntry
+
+ @Environment(\.widgetFamily) var family
+
+ var body: some View {
+ if entry.items.isEmpty {
+ } else {
+ switch family {
+ case .systemSmall:
+ SquareItemView(item: entry.items[0])
+ .padding()
+
+ case .systemMedium, .systemLarge:
+ VStack {
+ ForEach(Array(entry.items.prefix(family.maxItemCount).enumerated()), id: \.element.id) { (index, item) in
+ if index != 0 {
+ Divider()
+ }
+ ItemListEntryView(item: item)
+ Spacer(minLength: 4)
+ }
+ }
+ .padding()
+
+ case .systemExtraLarge:
+ if #available(iOS 16.0, *) {
+ VStack {
+ ForEach(Array(stride(from: 0, to: min(entry.items.count, family.maxItemCount), by: 2)), id: \.self) { idx in
+ HStack {
+ ItemListEntryView(item: entry.items[idx])
+ if idx + 1 < entry.items.count {
+ Divider()
+ ItemListEntryView(item: entry.items[idx + 1])
+ }
+ }
+ if idx + 2 < entry.items.count {
+ Spacer()
+ Divider()
+ }
+ }
+ }
+ .padding()
+ } else {
+ Text("Requires iOS 16")
+ }
+
+ default:
+ fatalError("unreachable")
+ }
+ }
+ }
+}
+
+private extension WidgetFamily {
+ var maxItemCount: Int {
+ switch self {
+ case .systemSmall:
+ return 1
+ case .systemMedium:
+ return 2
+ case .systemLarge:
+ return 4
+ case .systemExtraLarge:
+ return 8
+ default:
+ return 0
+ }
+ }
+}
+
+private var feedFont = Font.subheadline.weight(.medium).italic()
+private var titleUIFont: UIFont {
+ // TODO: this should use the compressed SF Pro variant, but there's no API to get at it
+ let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .headline).withSymbolicTraits(.traitCondensed)!
+ return UIFont(descriptor: descriptor, size: 0)
+}
+private var titleFont = Font(titleUIFont).leading(.tight)
+
+struct SquareItemView: View {
+ let item: WidgetData.Item
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ HStack {
+ Text(verbatim: item.feedTitle ?? "")
+ .font(feedFont)
+ .foregroundColor(.red)
+ // force the vstack to be as wide as possible
+ Spacer(minLength: 0)
+ }
+ Text(verbatim: item.title ?? "")
+ .font(titleFont)
+ if let published = item.published {
+ Text(published, format: .relative(presentation: .numeric, unitsStyle: .narrow))
+ .font(.caption)
+ }
+ Spacer(minLength: 0)
+ }
+ .widgetURL(item.launchURL)
+ }
+}
+
+struct ItemListEntryView: View {
+ let item: WidgetData.Item
+
+ var body: some View {
+ Link(destination: item.launchURL) {
+ VStack(alignment: .leading) {
+ HStack {
+ Text(verbatim: item.feedTitle ?? "")
+ .font(feedFont)
+ .foregroundColor(.red)
+ Spacer()
+ if let published = item.published {
+ Text(published, format: .relative(presentation: .numeric, unitsStyle: .narrow))
+ .font(.caption)
+ }
+ }
+ Text(verbatim: item.title ?? "")
+ .font(titleFont)
+ .lineLimit(3)
+ Spacer(minLength: 0)
+ }
+ }
+ }
+}
+
+struct Recents_Previews: PreviewProvider {
+ static let item1 = WidgetData.Item(
+ id: "1",
+ feedTitle: "Daring Fireball",
+ title: "There's a Privacy Angle on Apple's Decision to Finance Apple Pay Layer On Its Own",
+ published: Calendar.current.date(byAdding: .hour, value: -1, to: Date())!
+ )
+ static let item2 = WidgetData.Item(
+ id: "2",
+ feedTitle: "Ars Technica",
+ title: "Senate bill would ban data brokers from selling location and health data",
+ published: Calendar.current.date(byAdding: .hour, value: -2, to: Date())!
+ )
+
+ static var previews: some View {
+// RecentsEntryView(entry: RecentsEntry(items: [item1], configuration: ConfigurationIntent()))
+// .previewContext(WidgetPreviewContext(family: .systemSmall))
+
+ RecentsEntryView(entry: RecentsEntry(items: [item1, item2], configuration: ConfigurationIntent()))
+ .previewContext(WidgetPreviewContext(family: .systemMedium))
+ }
+}
diff --git a/Widgets/Widgets.intentdefinition b/Widgets/Widgets.intentdefinition
new file mode 100644
index 0000000..95a5592
--- /dev/null
+++ b/Widgets/Widgets.intentdefinition
@@ -0,0 +1,61 @@
+
+
+
+
+ INEnums
+
+ INIntentDefinitionModelVersion
+ 1.2
+ INIntentDefinitionNamespace
+ 88xZPY
+ INIntentDefinitionSystemVersion
+ 22A5266r
+ INIntentDefinitionToolsBuildVersion
+ 14A5228q
+ INIntentDefinitionToolsVersion
+ 14.0
+ INIntents
+
+
+ INIntentCategory
+ information
+ INIntentDescriptionID
+ tVvJ9c
+ INIntentEligibleForWidgets
+
+ INIntentIneligibleForSuggestions
+
+ INIntentLastParameterTag
+ 2
+ INIntentName
+ Configuration
+ INIntentResponse
+
+ INIntentResponseCodes
+
+
+ INIntentResponseCodeName
+ success
+ INIntentResponseCodeSuccess
+
+
+
+ INIntentResponseCodeName
+ failure
+
+
+
+ INIntentTitle
+ Configuration
+ INIntentTitleID
+ gpCwrM
+ INIntentType
+ Custom
+ INIntentVerb
+ View
+
+
+ INTypes
+
+
+
diff --git a/Widgets/Widgets.swift b/Widgets/Widgets.swift
new file mode 100644
index 0000000..7588db7
--- /dev/null
+++ b/Widgets/Widgets.swift
@@ -0,0 +1,23 @@
+//
+// Widgets.swift
+// Widgets
+//
+// Created by Shadowfacts on 6/19/22.
+//
+
+import WidgetKit
+import SwiftUI
+import Intents
+
+@main
+struct Widgets: Widget {
+ var body: some WidgetConfiguration {
+ IntentConfiguration(kind: "recents", intent: ConfigurationIntent.self, provider: RecentsProvider()) { entry in
+ RecentsEntryView(entry: entry)
+ }
+ .configurationDisplayName("Recent Articles")
+ .description("Recently posted articles from your feeds.")
+ .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
+ }
+}
+
diff --git a/WidgetsExtension.entitlements b/WidgetsExtension.entitlements
new file mode 100644
index 0000000..be06bd4
--- /dev/null
+++ b/WidgetsExtension.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.application-groups
+
+ group.net.shadowfacts.Reader
+
+
+