feat: home screen widget for iOS. (#494)

This commit is contained in:
Jojo Feng
2025-01-20 01:12:44 -08:00
committed by GitHub
parent 0897abf27e
commit fdce94f2e7
35 changed files with 1642 additions and 25 deletions

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@ -22,6 +22,11 @@
E530B1AD283B54DA004E8EB6 /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E530B1AC283B54DA004E8EB6 /* ActionViewController.swift */; };
E530B1B0283B54DA004E8EB6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E530B1AE283B54DA004E8EB6 /* MainInterface.storyboard */; };
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E573DDF82D3E273F00831A51 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E573DDF72D3E273F00831A51 /* WidgetKit.framework */; };
E573DDFA2D3E273F00831A51 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E573DDF92D3E273F00831A51 /* SwiftUI.framework */; };
E573DE072D3E274000831A51 /* Story Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E573DE392D3E282700831A51 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = E573DE382D3E282700831A51 /* Alamofire */; };
E573DE3E2D3E28CD00831A51 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = E573DE3D2D3E28CD00831A51 /* SwiftSoup */; };
E575B6F127EBC6DB002B1508 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E575B6F027EBC6DA002B1508 /* CloudKit.framework */; };
/* End PBXBuildFile section */
@ -40,12 +45,19 @@
remoteGlobalIDString = E530B1A5283B54DA004E8EB6;
remoteInfo = "Action Extension";
};
E573DE052D3E274000831A51 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = E573DDF52D3E273F00831A51;
remoteInfo = StoryWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 12;
dstPath = "";
dstSubfolderSpec = 10;
files = (
@ -60,6 +72,7 @@
dstSubfolderSpec = 13;
files = (
E51D52B7283B464E00FC8DD8 /* Share Extension.appex in Embed App Extensions */,
E573DE072D3E274000831A51 /* Story Widget Extension.appex in Embed App Extensions */,
E530B1B4283B54DA004E8EB6 /* Action Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
@ -96,11 +109,28 @@
E530B1AF283B54DA004E8EB6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
E530B1B1283B54DA004E8EB6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E530B1B9283B54E4004E8EB6 /* Action Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Action Extension.entitlements"; sourceTree = "<group>"; };
E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Story Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
E573DDF72D3E273F00831A51 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
E573DDF92D3E273F00831A51 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
E575B6EF27EBC6C6002B1508 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E575B6F027EBC6DA002B1508 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
E59F28EE283B477D00512089 /* Share Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Share Extension.entitlements"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
E573DE0B2D3E274000831A51 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = E573DDF52D3E273F00831A51 /* Story Widget Extension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
E573DDFB2D3E273F00831A51 /* StoryWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (E573DE0B2D3E274000831A51 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = StoryWidget; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -126,6 +156,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E573DDF32D3E273F00831A51 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E573DE392D3E282700831A51 /* Alamofire in Frameworks */,
E573DDFA2D3E273F00831A51 /* SwiftUI.framework in Frameworks */,
E573DDF82D3E273F00831A51 /* WidgetKit.framework in Frameworks */,
E573DE3E2D3E28CD00831A51 /* SwiftSoup in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -147,6 +188,7 @@
97C146F01CF9000F007C117D /* Runner */,
E51D52AE283B464E00FC8DD8 /* Share Extension */,
E530B1A9283B54DA004E8EB6 /* Action Extension */,
E573DDFB2D3E273F00831A51 /* StoryWidget */,
97C146EF1CF9000F007C117D /* Products */,
D79CD63C88FF49EF451AFDDF /* Pods */,
B3F4F49CF582C662A01499C0 /* Frameworks */,
@ -159,6 +201,7 @@
97C146EE1CF9000F007C117D /* Runner.app */,
E51D52AD283B464E00FC8DD8 /* Share Extension.appex */,
E530B1A6283B54DA004E8EB6 /* Action Extension.appex */,
E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -185,6 +228,8 @@
E575B6F027EBC6DA002B1508 /* CloudKit.framework */,
E530B1A7283B54DA004E8EB6 /* UniformTypeIdentifiers.framework */,
8BF0A917F40A838BF30D8F4C /* Pods_Runner.framework */,
E573DDF72D3E273F00831A51 /* WidgetKit.framework */,
E573DDF92D3E273F00831A51 /* SwiftUI.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -244,6 +289,7 @@
dependencies = (
E51D52B6283B464E00FC8DD8 /* PBXTargetDependency */,
E530B1B3283B54DA004E8EB6 /* PBXTargetDependency */,
E573DE062D3E274000831A51 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@ -284,13 +330,37 @@
productReference = E530B1A6283B54DA004E8EB6 /* Action Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
E573DDF52D3E273F00831A51 /* Story Widget Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = E573DE0C2D3E274000831A51 /* Build configuration list for PBXNativeTarget "Story Widget Extension" */;
buildPhases = (
E573DDF22D3E273F00831A51 /* Sources */,
E573DDF32D3E273F00831A51 /* Frameworks */,
E573DDF42D3E273F00831A51 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
E573DDFB2D3E273F00831A51 /* StoryWidget */,
);
name = "Story Widget Extension";
packageProductDependencies = (
E573DE382D3E282700831A51 /* Alamofire */,
E573DE3D2D3E28CD00831A51 /* SwiftSoup */,
);
productName = StoryWidgetExtension;
productReference = E573DDF62D3E273F00831A51 /* Story Widget Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1330;
LastSwiftUpdateCheck = 1610;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@ -304,6 +374,9 @@
E530B1A5283B54DA004E8EB6 = {
CreatedOnToolsVersion = 13.3;
};
E573DDF52D3E273F00831A51 = {
CreatedOnToolsVersion = 16.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@ -315,6 +388,10 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
E573DE372D3E282700831A51 /* XCRemoteSwiftPackageReference "Alamofire" */,
E573DE3C2D3E28CD00831A51 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -322,6 +399,7 @@
97C146ED1CF9000F007C117D /* Runner */,
E51D52AC283B464E00FC8DD8 /* Share Extension */,
E530B1A5283B54DA004E8EB6 /* Action Extension */,
E573DDF52D3E273F00831A51 /* Story Widget Extension */,
);
};
/* End PBXProject section */
@ -355,6 +433,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E573DDF42D3E273F00831A51 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@ -372,7 +457,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
@ -456,6 +541,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
E573DDF22D3E273F00831A51 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -469,6 +561,11 @@
target = E530B1A5283B54DA004E8EB6 /* Action Extension */;
targetProxy = E530B1B2283B54DA004E8EB6 /* PBXContainerItemProxy */;
};
E573DE062D3E274000831A51 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = E573DDF52D3E273F00831A51 /* Story Widget Extension */;
targetProxy = E573DE052D3E274000831A51 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -575,7 +672,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -718,7 +815,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -753,7 +850,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Hacki;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.news";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -789,7 +886,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -831,7 +928,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -869,7 +966,7 @@
INFOPLIST_FILE = "Share Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Share Extension";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -908,7 +1005,7 @@
INFOPLIST_FILE = "Action Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -952,7 +1049,7 @@
INFOPLIST_FILE = "Action Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -992,7 +1089,7 @@
INFOPLIST_FILE = "Action Extension/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Hacki";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1010,6 +1107,136 @@
};
name = Profile;
};
E573DE082D3E274000831A51 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StoryWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = StoryWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Widget-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
E573DE092D3E274000831A51 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = QMWX3X2NF7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StoryWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = StoryWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Widget-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.jiaqi.hacki.Widget-Extension";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
E573DE0A2D3E274000831A51 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StoryWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = StoryWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.jiaqi.hacki.Widget-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -1053,7 +1280,49 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E573DE0C2D3E274000831A51 /* Build configuration list for PBXNativeTarget "Story Widget Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E573DE082D3E274000831A51 /* Debug */,
E573DE092D3E274000831A51 /* Release */,
E573DE0A2D3E274000831A51 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
E573DE372D3E282700831A51 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.10.2;
};
};
E573DE3C2D3E28CD00831A51 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.7.6;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
E573DE382D3E282700831A51 /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = E573DE372D3E282700831A51 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
E573DE3D2D3E28CD00831A51 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency;
package = E573DE3C2D3E28CD00831A51 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = SwiftSoup;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E530B1A5283B54DA004E8EB6"
BuildableName = "Action Extension.appex"
BlueprintName = "Action Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</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">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E51D52AC283B464E00FC8DD8"
BuildableName = "Share Extension.appex"
BlueprintName = "Share Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</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">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E573DDF52D3E273F00831A51"
BuildableName = "Story Widget Extension.appex"
BlueprintName = "Story Widget Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</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 = "E573DDF52D3E273F00831A51"
BuildableName = "Story Widget Extension.appex"
BlueprintName = "Story Widget Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "_XCWidgetKind"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetDefaultView"
value = "timeline"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "_XCWidgetFamily"
value = "systemMedium"
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 = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,24 @@
{
"originHash" : "1002c245c0fdae6ca9c33705b8fc0eaeec1eff55818735c136a20ed23937d94f",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
"version" : "5.10.2"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "0837db354faf9c9deb710dc597046edaadf5360f",
"version" : "2.7.6"
}
}
],
"version" : 3
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,36 @@
import Foundation
extension Optional where Wrapped: Collection {
var isMoreThanOne: Bool {
guard let unwrapped = self else {
return false
}
if unwrapped.count > 1 {
return true
} else {
return false
}
}
var isNullOrEmpty: Bool {
guard let unwrapped = self else {
return true
}
return unwrapped.isEmpty
}
var isNotNullOrEmpty: Bool {
return !isNullOrEmpty
}
var countOrZero: Int {
guard let unwrapped = self else {
return 0
}
return unwrapped.count
}
}

View File

@ -0,0 +1,9 @@
import Foundation
extension Date {
var timeAgoString: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
return formatter.localizedString(for: self, relativeTo: Date())
}
}

View File

@ -0,0 +1,10 @@
import Foundation
extension Int? {
var orZero: Int {
guard let unwrapped = self else {
return 0
}
return unwrapped
}
}

View File

@ -0,0 +1,88 @@
import Foundation
import UIKit
import SwiftSoup
public extension String {
var isNotEmpty: Bool {
!isEmpty
}
var htmlStripped: String {
do {
let pRegex = try Regex("<p>")
let iRegex = try Regex(#"\<i\>(.*?)\<\/i\>"#)
let codeRegex = try Regex(#"\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>"#)
.dotMatchesNewlines(true)
let linkRegex = try Regex(#"\<a href=\"(.*?)\".*?\>.*?\<\/a\>"#)
let res = try Entities.unescape(self)
.replacing(pRegex, with: { match in
"\n"
})
.replacing(iRegex, with: { match in
if let m = match[1].substring {
let matchedStr = String(m)
return "**\(matchedStr)**"
}
return String()
})
.replacing(linkRegex, with: { match in
if let m = match[1].substring {
let matchedStr = String(m)
return matchedStr
}
return String()
})
.withExtraLineBreak
.replacing(codeRegex, with: { match in
if let m = match[1].substring {
let matchedStr = String(m)
return "```" + String(matchedStr.replacing("\n\n", with: "``` \n ``` \n").dropLast(1)) + "```"
}
return String()
})
return res
} catch {
return error.localizedDescription
}
}
func toJSON() -> Any? {
guard let data = self.data(using: .utf8, allowLossyConversion: false) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
}
private var withExtraLineBreak: String {
if isEmpty { return self }
let range = startIndex..<index(endIndex, offsetBy: -1)
var str = String(replacingOccurrences(of: "\n", with: "\n\n", range: range))
while str.last?.isWhitespace == true {
str = String(str.dropLast())
}
return str
}
}
public extension Optional where Wrapped == String {
var orEmpty: String {
guard let unwrapped = self else {
return ""
}
return unwrapped
}
var htmlStripped: String{
guard let unwrapped = self else {
return ""
}
return unwrapped.htmlStripped
}
var isNotNullOrEmpty: Bool {
guard let unwrapped = self else {
return false
}
return unwrapped.isNotEmpty
}
}

View File

@ -0,0 +1,11 @@
<?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>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,54 @@
public struct Comment: Item {
public let id: Int
public let parent: Int?
public let text: String?
public let type: String?
public let by: String?
public let time: Int
public let kids: [Int]?
public let level: Int?
public var metadata: String {
if let count = kids?.count, count != 0 {
return "\(count) cmt\(count > 1 ? "s":"") | \(timeAgo) by \(by.orEmpty)"
} else {
return "\(timeAgo) by \(by.orEmpty)"
}
}
/// Values below will always be nil for `Comment`.
public let title: String?
public let url: String?
public let descendants: Int?
public let score: Int?
init(id: Int, parent: Int?, text: String?, by: String?, time: Int, kids: [Int]? = [Int](), level: Int? = 0) {
self.id = id
self.parent = parent
self.text = text
self.by = by
self.time = time
self.kids = kids
self.level = level
self.type = "comment"
self.title = nil
self.url = nil
self.descendants = nil
self.score = nil
}
// Empty initializer
init() {
self.init(id: 0, parent: 0, text: "", by: "", time: 0)
}
public func copyWith(text: String? = nil, level: Int? = nil) -> Comment {
Comment(id: id,
parent: parent,
text: text ?? self.text,
by: by,
time: time,
kids: kids ?? [Int](),
level: level ?? self.level)
}
}

View File

@ -0,0 +1,59 @@
import Foundation
public protocol Item: Codable, Identifiable, Hashable {
var id: Int { get }
var parent: Int? { get }
var title: String? { get }
var text: String? { get }
var url: String? { get }
var type: String? { get }
var by: String? { get }
var score: Int? { get }
var descendants: Int? { get }
var time: Int { get }
var kids: [Int]? { get }
var metadata: String { get }
}
public extension Item {
var createdAtDate: Date {
let date = Date(timeIntervalSince1970: Double(time))
return date
}
var createdAt: String {
let date = Date(timeIntervalSince1970: Double(time))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d, yyyy"
return dateFormatter.string(from: date)
}
var timeAgo: String {
let date = Date(timeIntervalSince1970: Double(time))
return date.timeAgoString
}
var formattedTime: String {
Date(timeIntervalSince1970: Double(time)).formatted()
}
var itemUrl: String {
"https://news.ycombinator.com/item?id=\(self.id)"
}
var readableUrl: String? {
if let url = self.url {
let domain = URL(string: url)?.host
return domain
}
return nil
}
var isJob: Bool {
return type == "job"
}
var isJobWithUrl: Bool {
return type == "job" && text.isNullOrEmpty && url.isNotNullOrEmpty
}
}

View File

@ -0,0 +1,48 @@
import Foundation
public enum SearchFilter: Equatable, Hashable {
case story
case comment
case dateRange(Date, Date)
var query: String {
switch(self){
case .story:
return "story"
case .comment:
return "comment"
case .dateRange(let startDate, let endDate):
let startTimestamp = Int(startDate.timeIntervalSince1970.rounded())
let endTimestamp = Int(endDate.timeIntervalSince1970.rounded())
if startTimestamp != endTimestamp {
return "created_at_i>=\(startTimestamp),created_at_i<=\(endTimestamp)"
} else {
let updatedStartDate = Calendar.current.date(
byAdding: .hour,
value: -24,
to: startDate)
let updatedStartTimestamp = updatedStartDate?.timeIntervalSince1970
if let updatedStartTimestamp = updatedStartTimestamp?.rounded() {
return "created_at_i>=\(Int(updatedStartTimestamp)),created_at_i<=\(endTimestamp)"
}
return .init()
}
}
}
var isNumericFilter: Bool {
switch(self){
case .story, .comment:
return false
case .dateRange:
return true
}
}
var isTagFilter: Bool {
!isNumericFilter
}
}

View File

@ -0,0 +1,62 @@
import Foundation
public class SearchParams: Equatable {
public let page: Int
public let query: String
public let sorted: Bool
public let filters: Set<SearchFilter>
public var filteredQuery: String {
var buffer = String()
if sorted {
buffer.append("search_by_date?query=\(query)")
} else {
buffer.append("search?query=\(query)")
}
if !filters.isEmpty {
let numericFilters = filters.filter({ $0.isNumericFilter })
let tagFilters = filters.filter({ $0.isTagFilter })
if !numericFilters.isEmpty {
buffer.append("&numericFilters=")
for filter in filters.filter({ $0.isNumericFilter }) {
buffer.append(filter.query)
buffer.append(",")
}
buffer = String(buffer.dropLast(1))
}
if !tagFilters.isEmpty {
buffer.append("&tags=(")
for filter in filters.filter({ $0.isTagFilter }) {
buffer.append(filter.query)
buffer.append(",")
}
buffer = String(buffer.dropLast(1))
buffer.append(")")
}
}
buffer.append("&page=\(page)");
return buffer
}
public init(page: Int, query: String, sorted: Bool, filters: Set<SearchFilter>) {
self.page = page
self.query = query
self.sorted = sorted
self.filters = filters
}
public func copyWith(page: Int? = nil, query: String? = nil, sorted: Bool? = nil, filters: Set<SearchFilter>? = nil) -> SearchParams {
return SearchParams(page: page ?? self.page, query: query ?? self.query, sorted: sorted ?? self.sorted, filters: filters ?? self.filters)
}
public static func == (lhs: SearchParams, rhs: SearchParams) -> Bool {
return lhs.page == rhs.page && lhs.query == rhs.query && lhs.sorted == rhs.sorted && lhs.filters == rhs.filters
}
}

View File

@ -0,0 +1,98 @@
public extension Story {
var shortMetadata: String {
if isJob {
return "\(timeAgo)"
} else {
return "\(score.orZero) | \(descendants.orZero) | \(timeAgo)"
}
}
}
public struct Story: Item {
public let id: Int
public let parent: Int?
public let title: String?
public let text: String?
public let url: String?
public let type: String?
public let by: String?
public let score: Int?
public let descendants: Int?
public let time: Int
public let kids: [Int]?
public var metadata: String {
if isJob {
return "\(timeAgo) by \(by.orEmpty)"
} else {
return "\(score.orZero) pt\(score.orZero > 1 ? "s":"") | \(descendants.orZero) cmt\(descendants.orZero > 1 ? "s":"") | \(timeAgo) by \(by.orEmpty)"
}
}
public init(id: Int,
parent: Int? = nil,
title: String?,
text: String?,
url: String?,
type: String?,
by: String?,
score: Int?,
descendants: Int?,
time: Int,
kids: [Int]? = [Int]()) {
self.id = id
self.parent = parent
self.title = title
self.text = text
self.url = url
self.type = type
self.score = score
self.by = by
self.descendants = descendants
self.time = time
self.kids = kids
}
// Empty initializer
public init() {
self.init(
id: 0,
parent: 0,
title: "",
text: "",
url: "",
type: "story",
by: "",
score: 0,
descendants: 0,
time: 0
)
}
func copyWith(text: String?) -> Story {
.init(
id: id,
parent: parent,
title: title,
text: text,
url: url,
type: type,
by: by,
score: score,
descendants: descendants,
time: time,
kids: kids
)
}
public static let errorPlaceholder: Story = .init(
id: 0,
title: "Something went wrong...",
text: nil,
url: "retrying...",
type: "story",
by: nil,
score: nil,
descendants: nil,
time: 0
)
}

View File

@ -0,0 +1,56 @@
import AppIntents
import SwiftData
public enum StoryType: String, Equatable, CaseIterable, AppEnum, Codable {
case top = "top"
case best = "best"
case new = "new"
case ask = "ask"
case show = "show"
case jobs = "job"
public var icon: String {
switch self {
case .top:
return "flame"
case .best:
return "medal"
case .new:
return "rectangle.dashed"
case .ask:
return "questionmark.bubble"
case .show:
return "sparkles.tv"
case .jobs:
return "briefcase"
}
}
public var label: String {
switch self {
case .jobs:
return "jobs"
default:
return self.rawValue
}
}
public var isDownloadable: Bool {
switch self {
case .top, .ask, .best:
return true
default:
return false
}
}
public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Story Type"
public static var caseDisplayRepresentations: [StoryType : DisplayRepresentation] = [
.top: "Top",
.best: "Best",
.new: "New",
.ask: "Ask HN",
.show: "Show HN",
.jobs: "YC Jobs"
]
}

View File

@ -0,0 +1,53 @@
import Foundation
public struct User: Decodable, Equatable {
public let id: String?
public let about: String?
public let created: Int?
public let delay: Int?
public let karma: Int?
public let submitted: [Int]?
public init() {
self.id = .init()
self.about = .init()
self.created = .init()
self.delay = .init()
self.karma = .init()
self.submitted = .init()
}
/// If a user does not have any activity, the user endpoint will not return anything.
/// in that case, we create a user with only username.
public init(id: String) {
self.id = id
self.about = .init()
self.created = .init()
self.delay = .init()
self.karma = .init()
self.submitted = .init()
}
init(id: String?, about: String?, created: Int?, delay: Int?, karma: Int?, submitted: [Int]?) {
self.id = id
self.about = about
self.created = created
self.delay = delay
self.karma = karma
self.submitted = submitted
}
func copyWith(about: String? = nil) -> User {
return User(id: id, about: about ?? self.about, created: created, delay: delay, karma: karma, submitted: submitted)
}
}
public extension User {
var createdAt: String? {
guard let created = created else { return nil }
let date = Date(timeIntervalSince1970: Double(created))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d, yyyy"
return dateFormatter.string(from: date)
}
}

View File

@ -0,0 +1,17 @@
import AppIntents
struct SelectStoryTypeIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Story Type"
static var description = IntentDescription("Select the type of story you want to see.")
@Parameter(title: "Story Type", default: StorySource.top)
var source: StorySource
init(source: StorySource) {
self.source = source
}
init() {
self.source = .top
}
}

View File

@ -0,0 +1,14 @@
import WidgetKit
import Foundation
struct StoryEntry: TimelineEntry {
let date: Date
let story: Story
let source: StorySource
static let errorPlaceholder: StoryEntry = StoryEntry(
date: .now,
story: .errorPlaceholder,
source: .top
)
}

View File

@ -0,0 +1,130 @@
import Foundation
import Alamofire
public class StoryRepository {
public static let shared: StoryRepository = .init()
private let baseUrl: String = "https://hacker-news.firebaseio.com/v0/"
private init() {}
// MARK: - Story related.
public func fetchAllStories(from storyType: StoryType, onStoryFetched: @escaping (Story) -> Void) async -> Void {
let storyIds = await fetchStoryIds(from: storyType)
for id in storyIds {
let story = await self.fetchStory(id)
if let story = story {
onStoryFetched(story)
}
}
}
public func fetchStoryIds(from storyType: StoryType) async -> [Int] {
let response = await AF.request("\(self.baseUrl)\(storyType.rawValue)stories.json").serializingString().response
guard response.data != nil else { return [Int]() }
let storyIds = try? JSONDecoder().decode([Int].self, from: response.data!)
return storyIds ?? [Int]()
}
public func fetchStoryIds(from storyType: String) async -> [Int] {
let response = await AF.request("\(self.baseUrl)\(storyType)stories.json").serializingString().response
guard response.data != nil else { return [Int]() }
let storyIds = try? JSONDecoder().decode([Int].self, from: response.data!)
return storyIds ?? [Int]()
}
public func fetchStories(ids: [Int], onStoryFetched: @escaping (Story) -> Void) async -> Void {
for id in ids {
let story = await fetchStory(id)
if let story = story {
onStoryFetched(story)
}
}
}
public func fetchStory(_ id: Int) async -> Story?{
let response = await AF.request("\(self.baseUrl)item/\(id).json").serializingString().response
if let data = response.data,
var story = try? JSONDecoder().decode(Story.self, from: data) {
let formattedText = story.text.htmlStripped
story = story.copyWith(text: formattedText)
return story
} else {
return nil
}
}
// MARK: - Comment related.
public func fetchComments(ids: [Int], onCommentFetched: @escaping (Comment) -> Void) async -> Void {
for id in ids {
let comment = await fetchComment(id)
if let comment = comment {
onCommentFetched(comment)
}
}
}
public func fetchComment(_ id: Int) async -> Comment? {
let response = await AF.request("\(self.baseUrl)item/\(id).json").serializingString().response
if let data = response.data,
var comment = try? JSONDecoder().decode(Comment.self, from: data) {
let formattedText = comment.text.htmlStripped
comment = comment.copyWith(text: formattedText)
return comment
} else {
return nil
}
}
// MARK: - Item related.
public func fetchItems(ids: [Int], filtered: Bool = true, onItemFetched: @escaping (any Item) -> Void) async -> Void {
for id in ids {
let item = await fetchItem(id)
guard let item = item else { continue }
if let story = item as? Story {
onItemFetched(story)
} else if let cmt = item as? Comment {
onItemFetched(cmt)
}
}
}
public func fetchItem(_ id: Int) async -> (any Item)? {
let response = await AF.request("\(self.baseUrl)item/\(id).json").serializingString().response
if let data = response.data,
let result = try? response.result.get(),
let map = result.toJSON() as? [String: AnyObject],
let type = map["type"] as? String {
switch type {
case "story":
let story = try? JSONDecoder().decode(Story.self, from: data)
let formattedText = story?.text.htmlStripped
return story?.copyWith(text: formattedText)
case "comment":
let comment = try? JSONDecoder().decode(Comment.self, from: data)
let formattedText = comment?.text.htmlStripped
return comment?.copyWith(text: formattedText)
default:
return nil
}
} else {
return nil
}
}
// MARK: - User related.
public func fetchUser(_ id: String) async -> User? {
let response = await AF.request("\(self.baseUrl)/user/\(id).json").serializingString().response
if let data = response.data,
let user = try? JSONDecoder().decode(User.self, from: data) {
let formattedText = user.about.orEmpty.htmlStripped
return user.copyWith(about: formattedText)
} else {
return nil
}
}
}

View File

@ -0,0 +1,21 @@
import AppIntents
enum StorySource: String, AppEnum {
case top
case best
case new
case ask
case show
case job
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Story Source"
static var caseDisplayRepresentations: [StorySource : DisplayRepresentation] = [
.top: "Top",
.best: "Best",
.new: "New",
.ask: "Ask",
.show: "Show",
.job: "Jobs"
]
}

View File

@ -0,0 +1,42 @@
import WidgetKit
struct StoryTimelineProvider: AppIntentTimelineProvider {
func snapshot(for configuration: SelectStoryTypeIntent, in context: Context) async -> StoryEntry {
let ids = await StoryRepository.shared.fetchStoryIds(from: configuration.source.rawValue)
guard let first = ids.first else { return .errorPlaceholder }
let story = await StoryRepository.shared.fetchStory(first)
guard let story = story else { return .errorPlaceholder }
let entry = StoryEntry(date: Date(), story: story, source: configuration.source)
return entry
}
func placeholder(in context: Context) -> StoryEntry {
let story = Story(
id: 0,
title: "This is a placeholder story",
text: "text",
url: "",
type: "story",
by: "Hacki",
score: 100,
descendants: 24,
time: Int(Date().timeIntervalSince1970)
)
return StoryEntry(date: Date(), story: story, source: .top)
}
func timeline(for configuration: SelectStoryTypeIntent, in context: Context) async -> Timeline<StoryEntry> {
let ids = await StoryRepository.shared.fetchStoryIds(from: configuration.source.rawValue)
guard let first = ids.first else {
return Timeline(entries: [.errorPlaceholder], policy: .atEnd)
}
let story = await StoryRepository.shared.fetchStory(first)
guard let story = story else {
return Timeline(entries: [.errorPlaceholder], policy: .atEnd)
}
let entry = StoryEntry(date: Date(), story: story, source: configuration.source)
let timeline = Timeline(entries: [entry], policy: .atEnd)
return timeline
}
}

View File

@ -0,0 +1,84 @@
import WidgetKit
import SwiftUI
import AppIntents
struct StoryWidgetView : View {
@Environment(\.widgetFamily) var family
@Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
var story: Story
var source: StorySource
var body: some View {
switch family {
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 0) {
Text(story.shortMetadata)
.font(.caption)
Text(story.title.orEmpty)
.font(.caption).fontWeight(.bold)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
Spacer(minLength: 0)
}
.containerBackground(for: .widget) {
Color(UIColor.secondarySystemBackground)
}
.widgetURL(URL(string: "\(story.id)"))
default:
HStack {
VStack {
Text(story.title.orEmpty)
.font(family == .systemSmall ? .system(size: 14) : .body)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
if let text = story.text, text.isNotEmpty {
HStack {
Text(text.replacingOccurrences(of: "\n", with: " "))
.font(.footnote)
.lineLimit(3)
.foregroundColor(.gray)
Spacer()
}
}
Spacer()
HStack {
if let url = story.readableUrl {
Text(url)
.font(family == .systemSmall ? .system(size: 8) : .footnote)
.foregroundColor(.orange)
}
Spacer()
}
Divider().frame(maxWidth: .infinity)
HStack(alignment: .center) {
Text("\(source.rawValue.uppercased()) | \(story.metadata)")
.font(family == .systemSmall ? showsWidgetContainerBackground ? .system(size: 10) : .system(size: 8) : .caption)
.padding(.top, showsWidgetContainerBackground ? 2 : .zero)
Spacer()
}
}
}
.padding(.all, showsWidgetContainerBackground ? .zero : nil)
.containerBackground(for: .widget) {
Color(UIColor.secondarySystemBackground)
}
.widgetURL(URL(string: "\(story.id)"))
}
}
}
struct StoryWidget: Widget {
let kind: String = "StoryWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: SelectStoryTypeIntent.self,
provider: StoryTimelineProvider()) { entry in
StoryWidgetView(story: entry.story, source: entry.source)
}
.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
.configurationDisplayName("Story on Hacker News")
.description("Watch out. It's hot.")
}
}

View File

@ -0,0 +1,9 @@
import WidgetKit
import SwiftUI
@main
struct StoryWidgetBundle: WidgetBundle {
var body: some Widget {
StoryWidget()
}
}

View File

@ -0,0 +1,8 @@
import WidgetKit
extension Timeline where EntryType == StoryEntry {
static let errorPlaceholder: Timeline<StoryEntry> = .init(
entries: [.errorPlaceholder],
policy: .atEnd
)
}

View File

@ -0,0 +1,18 @@
import AppIntents
import HackerNewsKit
struct SelectStoryTypeIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Select Story Type"
static var description = IntentDescription("Select the type of story you want to see.")
@Parameter(title: "Story Type", default: StorySource.top)
var source: StorySource
init(source: StorySource) {
self.source = source
}
init() {
self.source = .top
}
}

View File

@ -0,0 +1,21 @@
import AppIntents
enum StorySource: String, AppEnum {
case top
case best
case new
case ask
case show
case job
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Story Source"
static var caseDisplayRepresentations: [StorySource : DisplayRepresentation] = [
.top: "Top",
.best: "Best",
.new: "New",
.ask: "Ask",
.show: "Show",
.job: "Jobs"
]
}

View File

@ -1,5 +1,5 @@
app_identifier("com.jiaqi.hacki") # The bundle identifier of your app
apple_id("georgefung78@Live.com") # Your Apple Developer Portal username
apple_id("georgefung78@live.com") # Your Apple Developer Portal username
itc_team_id("120097292") # App Store Connect Team ID
team_id("QMWX3X2NF7") # Developer Portal Team ID

View File

@ -34,7 +34,7 @@ platform :ios do
# Download code signing certificates using `match` (and the `MATCH_PASSWORD` secret)
sync_code_signing(
type: "appstore",
app_identifier: [APP_IDENTIFIER, "#{APP_IDENTIFIER}.Share-Extension", "#{APP_IDENTIFIER}.Action-Extension"],
app_identifier: [APP_IDENTIFIER, "#{APP_IDENTIFIER}.Share-Extension", "#{APP_IDENTIFIER}.Action-Extension", "#{APP_IDENTIFIER}.Widget-Extension"],
readonly: true
)

View File

@ -297,12 +297,11 @@ packages:
feature_discovery:
dependency: "direct main"
description:
path: "."
ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
resolved-ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
url: "https://github.com/livinglist/feature_discovery"
source: git
version: "0.14.1"
name: feature_discovery
sha256: "54c2e0c7cc229c4a7149ef21d933fa7115cf034b985926477488aa2d0619321b"
url: "https://pub.dev"
source: hosted
version: "0.14.2"
ffi:
dependency: transitive
description:

View File

@ -21,10 +21,7 @@ dependencies:
dio_smart_retry: ^7.0.1
equatable: ^2.0.5
fast_gbk: ^1.0.0
feature_discovery:
git:
url: https://github.com/livinglist/feature_discovery
ref: bcf4ef28542acb0c98ec7dfa9d20d8fa7a0a594b
feature_discovery: ^0.14.2
flutter:
sdk: flutter
flutter_bloc: ^9.0.0