From 72c02f0ce0d9f04c597a33441fdf210190c7ebd2 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 3 Jul 2022 16:00:17 -0700 Subject: [PATCH] iOS --- MastoSearch.xcodeproj/project.pbxproj | 190 +++++++++++++++- .../xcschemes/MastoSearchMobile.xcscheme | 78 +++++++ .../xcschemes/xcschememanagement.plist | 10 + MastoSearchCore/Package.swift | 1 + MastoSearchMobile/AppDelegate.swift | 32 +++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 ++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 ++ MastoSearchMobile/Info.plist | 23 ++ MastoSearchMobile/SceneDelegate.swift | 69 ++++++ MastoSearchMobile/StatusTableHeaderView.swift | 75 ++++++ .../StatusTableRowCollectionViewCell.swift | 83 +++++++ MastoSearchMobile/ViewController.swift | 213 ++++++++++++++++++ 14 files changed, 828 insertions(+), 1 deletion(-) create mode 100644 MastoSearch.xcodeproj/xcshareddata/xcschemes/MastoSearchMobile.xcscheme create mode 100644 MastoSearchMobile/AppDelegate.swift create mode 100644 MastoSearchMobile/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 MastoSearchMobile/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 MastoSearchMobile/Assets.xcassets/Contents.json create mode 100644 MastoSearchMobile/Base.lproj/LaunchScreen.storyboard create mode 100644 MastoSearchMobile/Info.plist create mode 100644 MastoSearchMobile/SceneDelegate.swift create mode 100644 MastoSearchMobile/StatusTableHeaderView.swift create mode 100644 MastoSearchMobile/StatusTableRowCollectionViewCell.swift create mode 100644 MastoSearchMobile/ViewController.swift diff --git a/MastoSearch.xcodeproj/project.pbxproj b/MastoSearch.xcodeproj/project.pbxproj index 588d3e0..b934ddb 100644 --- a/MastoSearch.xcodeproj/project.pbxproj +++ b/MastoSearch.xcodeproj/project.pbxproj @@ -7,6 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + D626E6C628724258000E1AF5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626E6C528724258000E1AF5 /* AppDelegate.swift */; }; + D626E6C828724258000E1AF5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626E6C728724258000E1AF5 /* SceneDelegate.swift */; }; + D626E6CA28724258000E1AF5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626E6C928724258000E1AF5 /* ViewController.swift */; }; + D626E6CF28724259000E1AF5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D626E6CE28724259000E1AF5 /* Assets.xcassets */; }; + D626E6D228724259000E1AF5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D626E6D028724259000E1AF5 /* LaunchScreen.storyboard */; }; + D626E6D8287242F0000E1AF5 /* MastoSearchCore in Frameworks */ = {isa = PBXBuildFile; productRef = D626E6D7287242F0000E1AF5 /* MastoSearchCore */; }; + D626E6DA287242F2000E1AF5 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D626E6D9287242F2000E1AF5 /* SwiftSoup */; }; + D626E6DC28724610000E1AF5 /* StatusTableRowCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626E6DB28724610000E1AF5 /* StatusTableRowCollectionViewCell.swift */; }; + D626E6DE28724D4C000E1AF5 /* StatusTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626E6DD28724D4C000E1AF5 /* StatusTableHeaderView.swift */; }; D6451241276981A40046CCD2 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6451240276981A40046CCD2 /* WindowController.swift */; }; D6559A5228721BAF000EEB4D /* MastoSearchCore in Frameworks */ = {isa = PBXBuildFile; productRef = D6559A5128721BAF000EEB4D /* MastoSearchCore */; }; D669039E2769236F00819C4D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D669039C2769236F00819C4D /* ViewController.swift */; }; @@ -29,6 +38,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + D626E6C328724258000E1AF5 /* MastoSearchMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MastoSearchMobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D626E6C528724258000E1AF5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D626E6C728724258000E1AF5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + D626E6C928724258000E1AF5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + D626E6CE28724259000E1AF5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + D626E6D128724259000E1AF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + D626E6D328724259000E1AF5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D626E6DB28724610000E1AF5 /* StatusTableRowCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableRowCollectionViewCell.swift; sourceTree = ""; }; + D626E6DD28724D4C000E1AF5 /* StatusTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableHeaderView.swift; sourceTree = ""; }; D6451240276981A40046CCD2 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; D669039C2769236F00819C4D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; D66903BD2769250B00819C4D /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; @@ -43,6 +61,15 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + D626E6C028724258000E1AF5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D626E6D8287242F0000E1AF5 /* MastoSearchCore in Frameworks */, + D626E6DA287242F2000E1AF5 /* SwiftSoup in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6A4B8AA27C2B1770016F458 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -62,6 +89,21 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D626E6C428724258000E1AF5 /* MastoSearchMobile */ = { + isa = PBXGroup; + children = ( + D626E6C528724258000E1AF5 /* AppDelegate.swift */, + D626E6C728724258000E1AF5 /* SceneDelegate.swift */, + D626E6C928724258000E1AF5 /* ViewController.swift */, + D626E6DB28724610000E1AF5 /* StatusTableRowCollectionViewCell.swift */, + D626E6DD28724D4C000E1AF5 /* StatusTableHeaderView.swift */, + D626E6CE28724259000E1AF5 /* Assets.xcassets */, + D626E6D028724259000E1AF5 /* LaunchScreen.storyboard */, + D626E6D328724259000E1AF5 /* Info.plist */, + ); + path = MastoSearchMobile; + sourceTree = ""; + }; D6559A5028721BAF000EEB4D /* Frameworks */ = { isa = PBXGroup; children = ( @@ -84,6 +126,7 @@ D6E77D1428721A7600D8B732 /* MastoSearchCore */, D6B24DE727640CE100BA23B8 /* MastoSearch */, D6A4B8AE27C2B1770016F458 /* MastoSearchTests */, + D626E6C428724258000E1AF5 /* MastoSearchMobile */, D6B24DE627640CE100BA23B8 /* Products */, D6559A5028721BAF000EEB4D /* Frameworks */, ); @@ -94,6 +137,7 @@ children = ( D6B24DE527640CE100BA23B8 /* MastoSearch.app */, D6A4B8AD27C2B1770016F458 /* MastoSearchTests.xctest */, + D626E6C328724258000E1AF5 /* MastoSearchMobile.app */, ); name = Products; sourceTree = ""; @@ -114,6 +158,27 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + D626E6C228724258000E1AF5 /* MastoSearchMobile */ = { + isa = PBXNativeTarget; + buildConfigurationList = D626E6D628724259000E1AF5 /* Build configuration list for PBXNativeTarget "MastoSearchMobile" */; + buildPhases = ( + D626E6BF28724258000E1AF5 /* Sources */, + D626E6C028724258000E1AF5 /* Frameworks */, + D626E6C128724258000E1AF5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MastoSearchMobile; + packageProductDependencies = ( + D626E6D7287242F0000E1AF5 /* MastoSearchCore */, + D626E6D9287242F2000E1AF5 /* SwiftSoup */, + ); + productName = MastoSearchMobile; + productReference = D626E6C328724258000E1AF5 /* MastoSearchMobile.app */; + productType = "com.apple.product-type.application"; + }; D6A4B8AC27C2B1770016F458 /* MastoSearchTests */ = { isa = PBXNativeTarget; buildConfigurationList = D6A4B8B327C2B1770016F458 /* Build configuration list for PBXNativeTarget "MastoSearchTests" */; @@ -160,9 +225,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1320; + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1320; TargetAttributes = { + D626E6C228724258000E1AF5 = { + CreatedOnToolsVersion = 14.0; + }; D6A4B8AC27C2B1770016F458 = { CreatedOnToolsVersion = 13.2; TestTargetID = D6B24DE427640CE100BA23B8; @@ -191,11 +259,21 @@ targets = ( D6B24DE427640CE100BA23B8 /* MastoSearch */, D6A4B8AC27C2B1770016F458 /* MastoSearchTests */, + D626E6C228724258000E1AF5 /* MastoSearchMobile */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + D626E6C128724258000E1AF5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D626E6D228724259000E1AF5 /* LaunchScreen.storyboard in Resources */, + D626E6CF28724259000E1AF5 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6A4B8AB27C2B1770016F458 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -215,6 +293,18 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + D626E6BF28724258000E1AF5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D626E6DE28724D4C000E1AF5 /* StatusTableHeaderView.swift in Sources */, + D626E6DC28724610000E1AF5 /* StatusTableRowCollectionViewCell.swift in Sources */, + D626E6CA28724258000E1AF5 /* ViewController.swift in Sources */, + D626E6C628724258000E1AF5 /* AppDelegate.swift in Sources */, + D626E6C828724258000E1AF5 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D6A4B8A927C2B1770016F458 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -244,7 +334,87 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + D626E6D028724259000E1AF5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D626E6D128724259000E1AF5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ + D626E6D428724259000E1AF5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HGYVAQA9FW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MastoSearchMobile/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MastoSearch; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.MastoSearchMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 2; + }; + name = Debug; + }; + D626E6D528724259000E1AF5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = HGYVAQA9FW; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MastoSearchMobile/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MastoSearch; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.MastoSearchMobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 2; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; D6A4B8B427C2B1770016F458 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -453,6 +623,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + D626E6D628724259000E1AF5 /* Build configuration list for PBXNativeTarget "MastoSearchMobile" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D626E6D428724259000E1AF5 /* Debug */, + D626E6D528724259000E1AF5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D6A4B8B327C2B1770016F458 /* Build configuration list for PBXNativeTarget "MastoSearchTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -502,6 +681,15 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + D626E6D7287242F0000E1AF5 /* MastoSearchCore */ = { + isa = XCSwiftPackageProductDependency; + productName = MastoSearchCore; + }; + D626E6D9287242F2000E1AF5 /* SwiftSoup */ = { + isa = XCSwiftPackageProductDependency; + package = D66903BF27692EAB00819C4D /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; + }; D6559A5128721BAF000EEB4D /* MastoSearchCore */ = { isa = XCSwiftPackageProductDependency; productName = MastoSearchCore; diff --git a/MastoSearch.xcodeproj/xcshareddata/xcschemes/MastoSearchMobile.xcscheme b/MastoSearch.xcodeproj/xcshareddata/xcschemes/MastoSearchMobile.xcscheme new file mode 100644 index 0000000..00c3066 --- /dev/null +++ b/MastoSearch.xcodeproj/xcshareddata/xcschemes/MastoSearchMobile.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MastoSearch.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist b/MastoSearch.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist index cb22af5..8fe1f8e 100644 --- a/MastoSearch.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/MastoSearch.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,9 +9,19 @@ orderHint 0 + MastoSearchMobile.xcscheme_^#shared#^_ + + orderHint + 2 + SuppressBuildableAutocreation + D626E6C228724258000E1AF5 + + primary + + D6A4B8AC27C2B1770016F458 primary diff --git a/MastoSearchCore/Package.swift b/MastoSearchCore/Package.swift index 6bde9d1..81e3c69 100644 --- a/MastoSearchCore/Package.swift +++ b/MastoSearchCore/Package.swift @@ -7,6 +7,7 @@ let package = Package( name: "MastoSearchCore", platforms: [ .macOS(.v12), + .iOS(.v15), ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. diff --git a/MastoSearchMobile/AppDelegate.swift b/MastoSearchMobile/AppDelegate.swift new file mode 100644 index 0000000..4e09183 --- /dev/null +++ b/MastoSearchMobile/AppDelegate.swift @@ -0,0 +1,32 @@ +// +// AppDelegate.swift +// MastoSearchMobile +// +// Created by Shadowfacts on 7/3/22. +// + +import UIKit +import MastoSearchCore + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + DatabaseController.shared.initialize() + + return true + } + + func applicationWillTerminate(_ application: UIApplication) { + DatabaseController.shared.close() + } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + } + +} diff --git a/MastoSearchMobile/Assets.xcassets/AccentColor.colorset/Contents.json b/MastoSearchMobile/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/MastoSearchMobile/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastoSearchMobile/Assets.xcassets/AppIcon.appiconset/Contents.json b/MastoSearchMobile/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/MastoSearchMobile/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/MastoSearchMobile/Assets.xcassets/Contents.json b/MastoSearchMobile/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/MastoSearchMobile/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MastoSearchMobile/Base.lproj/LaunchScreen.storyboard b/MastoSearchMobile/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/MastoSearchMobile/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MastoSearchMobile/Info.plist b/MastoSearchMobile/Info.plist new file mode 100644 index 0000000..0eb786d --- /dev/null +++ b/MastoSearchMobile/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/MastoSearchMobile/SceneDelegate.swift b/MastoSearchMobile/SceneDelegate.swift new file mode 100644 index 0000000..e76aa92 --- /dev/null +++ b/MastoSearchMobile/SceneDelegate.swift @@ -0,0 +1,69 @@ +// +// SceneDelegate.swift +// MastoSearchMobile +// +// Created by Shadowfacts on 7/3/22. +// + +import UIKit +import MastoSearchCore + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = (scene as? UIWindowScene) else { return } + + window = UIWindow(windowScene: windowScene) + + let nav = UINavigationController(rootViewController: ViewController()) + nav.navigationBar.scrollEdgeAppearance = nav.navigationBar.standardAppearance + window!.rootViewController = nav + + window!.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + + syncStatuses() + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + func syncStatuses() { + SyncController.shared.syncStatuses { error in + let alert = UIAlertController(title: "Error syncing statuses", message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Ok", style: .default)) + self.window!.rootViewController!.present(alert, animated: true) + } + } + +} + diff --git a/MastoSearchMobile/StatusTableHeaderView.swift b/MastoSearchMobile/StatusTableHeaderView.swift new file mode 100644 index 0000000..ca479e0 --- /dev/null +++ b/MastoSearchMobile/StatusTableHeaderView.swift @@ -0,0 +1,75 @@ +// +// StatusTableHeaderView.swift +// MastoSearchMobile +// +// Created by Shadowfacts on 7/3/22. +// + +import UIKit + +class StatusTableHeaderView: UICollectionReusableView { + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .systemBackground + + let dateLabel = UILabel() + dateLabel.text = "Date" + dateLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout).withSymbolicTraits(.traitBold)!, size: 0) + dateLabel.textColor = .tintColor + dateLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(dateLabel) + + let contentWarningLabel = UILabel() + contentWarningLabel.text = "Content Warning" + contentWarningLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout).withSymbolicTraits(.traitBold)!, size: 0) + contentWarningLabel.textColor = .tintColor + contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(contentWarningLabel) + + let contentLabel = UILabel() + contentLabel.text = "Content" + contentLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout).withSymbolicTraits(.traitBold)!, size: 0) + contentLabel.textColor = .tintColor + contentLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(contentLabel) + + let fakeDateLabel = UILabel() + fakeDateLabel.text = "07/03/2022, 02:31 PM" + fakeDateLabel.translatesAutoresizingMaskIntoConstraints = false + fakeDateLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular) + fakeDateLabel.isHidden = true + addSubview(fakeDateLabel) + + let separator = UIView() + separator.backgroundColor = .separator + separator.translatesAutoresizingMaskIntoConstraints = false + addSubview(separator) + + NSLayoutConstraint.activate([ + dateLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2), + dateLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + + fakeDateLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2), + fakeDateLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + contentWarningLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: fakeDateLabel.trailingAnchor, multiplier: 2), + contentWarningLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + + contentLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: centerXAnchor, multiplier: 1), + contentLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + + separator.leadingAnchor.constraint(equalTo: leadingAnchor), + separator.trailingAnchor.constraint(equalTo: trailingAnchor), + separator.bottomAnchor.constraint(equalTo: bottomAnchor), + separator.heightAnchor.constraint(equalToConstant: 0.5), + + heightAnchor.constraint(greaterThanOrEqualToConstant: 32), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/MastoSearchMobile/StatusTableRowCollectionViewCell.swift b/MastoSearchMobile/StatusTableRowCollectionViewCell.swift new file mode 100644 index 0000000..1afd959 --- /dev/null +++ b/MastoSearchMobile/StatusTableRowCollectionViewCell.swift @@ -0,0 +1,83 @@ +// +// StatusTableRowCollectionViewCell.swift +// MastoSearchMobile +// +// Created by Shadowfacts on 7/3/22. +// + +import UIKit +import MastoSearchCore +import SwiftSoup + +class StatusTableRowCollectionViewCell: UICollectionViewCell { + +// private static let formatter: DateFormatter = { +// let f = DateFormatter() +// f.locale = .current +// f.setLocalizedDateFormatFromTemplate("yyyy-MM-dd hh:mm a") +// return f +// }() + private static let dateStyle: Date.FormatStyle = { + Date.FormatStyle() + .year(.extended(minimumLength: 4)) + .month(.twoDigits) + .day(.twoDigits) + .hour(.twoDigits(amPM: .abbreviated)) + .minute(.twoDigits) + }() + + private let dateLabel = UILabel() + private let contentWarningLabel = UILabel() + private let contentLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + dateLabel.translatesAutoresizingMaskIntoConstraints = false + dateLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular) + addSubview(dateLabel) + + contentWarningLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(contentWarningLabel) + + let spacer = UIView() + spacer.translatesAutoresizingMaskIntoConstraints = false + addSubview(spacer) + + contentLabel.translatesAutoresizingMaskIntoConstraints = false + contentLabel.font = .preferredFont(forTextStyle: .callout) + contentLabel.numberOfLines = 2 + addSubview(contentLabel) + + NSLayoutConstraint.activate([ + dateLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2), + dateLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + + contentWarningLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: dateLabel.trailingAnchor, multiplier: 2), + contentWarningLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + + spacer.leadingAnchor.constraint(equalTo: contentWarningLabel.trailingAnchor), + centerXAnchor.constraint(equalToSystemSpacingAfter: spacer.trailingAnchor, multiplier: 1), + + contentLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: centerXAnchor, multiplier: 1), + contentLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + contentLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + contentLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 4), + bottomAnchor.constraint(greaterThanOrEqualTo: contentLabel.bottomAnchor, constant: 4), + + heightAnchor.constraint(greaterThanOrEqualToConstant: 32), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateUI(status: Status) { + dateLabel.text = status.published.formatted(StatusTableRowCollectionViewCell.dateStyle) + contentWarningLabel.text = status.summary ?? "" + let doc = try! SwiftSoup.parseBodyFragment(status.content) + contentLabel.text = try! doc.body()!.text() + } + +} diff --git a/MastoSearchMobile/ViewController.swift b/MastoSearchMobile/ViewController.swift new file mode 100644 index 0000000..2a8c1e5 --- /dev/null +++ b/MastoSearchMobile/ViewController.swift @@ -0,0 +1,213 @@ +// +// ViewController.swift +// MastoSearchMobile +// +// Created by Shadowfacts on 7/3/22. +// + +import UIKit +import MastoSearchCore +import Combine +import SafariServices + +class ViewController: UIViewController { + + private let searchQueue = DispatchQueue(label: "Search", qos: .userInitiated) + + private var collectionView: UICollectionView! + private var dataSource: UICollectionViewDiffableDataSource! + + private var searchQuerySubject = PassthroughSubject() + private var cancellables = Set() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + navigationItem.title = "MastoSearch" + navigationItem.leadingItemGroups = [ + UIBarButtonItemGroup(barButtonItems: [ + UIBarButtonItem(title: "Account", menu: createAccountMenu()), + UIBarButtonItem(title: "Import", style: .plain, target: self, action: #selector(importPressed)), + ], representativeItem: nil) + ] + let searchController = UISearchController(searchResultsController: nil) + searchController.searchResultsUpdater = self + navigationItem.searchController = searchController + + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.headerMode = .supplementary + config.itemSeparatorHandler = { indexPath, config in + if indexPath.row == 0 { + var config = config + config.topSeparatorVisibility = .hidden + return config + } else { + return config + } + } + let layout = UICollectionViewCompositionalLayout.list(using: config) + collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.allowsMultipleSelection = true + collectionView.delegate = self + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + let header = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in + } + let cell = UICollectionView.CellRegistration { cell, indexPath, status in + cell.updateUI(status: status) + } + + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in + collectionView.dequeueConfiguredReusableCell(using: cell, for: indexPath, item: item.status) + } + dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in + guard elementKind == UICollectionView.elementKindSectionHeader else { + return nil + } + return collectionView.dequeueConfiguredReusableSupplementary(using: header, for: indexPath) + } + + searchQuerySubject + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) + .sink { [unowned self] query in + self.updateStatuses(query: query) + } + .store(in: &cancellables) + + updateStatuses(query: "") + } + + private func updateStatuses(query: String) { + searchQueue.async { + if query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + DatabaseController.shared.getStatuses(sortDescriptor: NSSortDescriptor(key: "published", ascending: false)) { seq in + self.applyUpdate(statuses: seq) + } + } else { + DatabaseController.shared.getStatuses(query: query, sortDescriptor: NSSortDescriptor(key: "published", ascending: false)) { seq in + self.applyUpdate(statuses: seq) + } + } + } + } + + private func applyUpdate(statuses: StatusSequence) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.statuses]) + snapshot.appendItems(statuses.map { Item(status: $0) }) + DispatchQueue.main.async { + self.dataSource.apply(snapshot, animatingDifferences: false) + } + } + + private func createAccountMenu() -> UIMenu { + if let account = LocalData.account { + return UIMenu(children: [ + UIAction(title: "Logged in to \(account.instanceURL.host!)", attributes: .disabled, handler: { _ in }), + UIAction(title: "Log out", attributes: .destructive, handler: { [unowned self] _ in + self.logout() + }), + ]) + } else { + return UIMenu(children: [ + UIAction(title: "Log in...", handler: { [unowned self] _ in + self.login() + }), + ]) + } + } + + private func login() { + + } + + private func logout() { + + } + + @objc private func importPressed() { + + } + +} + +extension ViewController { + enum Section { + case statuses + } + struct Item: Equatable, Hashable { + let status: Status + + static func ==(lhs: Item, rhs: Item) -> Bool { + return lhs.status.url == rhs.status.url + } + + func hash(into hasher: inout Hasher) { + hasher.combine(status.url) + } + } +} + +extension ViewController: UISearchResultsUpdating { + func updateSearchResults(for searchController: UISearchController) { + searchQuerySubject.send(searchController.searchBar.text ?? "") + } +} + +extension ViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let status = dataSource.itemIdentifier(for: indexPath)?.status else { + return + } + present(SFSafariViewController(url: URL(string: status.url)!), animated: true) + } + + func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? { + let statuses = indexPaths.compactMap { dataSource.itemIdentifier(for: $0)?.status } + switch statuses.count { + case 0: + return nil + case 1: + let url = URL(string: statuses.first!.url)! + return UIContextMenuConfiguration { + SFSafariViewController(url: url) + } actionProvider: { _ in + UIMenu(children: [ + UIAction(title: "Open in Safari", image: UIImage(systemName: "safari"), handler: { [unowned self] _ in + self.present(SFSafariViewController(url: url), animated: true) + }), + UIAction(title: "Copy URL", image: UIImage(systemName: "list.bullet.clipboard"), handler: { _ in + UIPasteboard.general.url = url + }) + ]) + } + default: + return UIContextMenuConfiguration(actionProvider: { _ in + UIMenu(children: [ + UIAction(title: "Copy URLs", image: UIImage(systemName: "list.bullet.clipboard"), handler: { _ in + UIPasteboard.general.urls = statuses.map { URL(string: $0.url)! } + }) + ]) + }) + } + } + + func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + if let viewController = animator.previewViewController, + viewController is SFSafariViewController { + animator.preferredCommitStyle = .pop + animator.addCompletion { + self.present(viewController, animated: true) + } + } + } +}