commit 52e6ffc8d0a3964e67c8e61f082aca5acb385ae5 Author: redstrate <54911369+redstrate@users.noreply.github.com> Date: Tue Feb 18 06:57:35 2020 -0500 Add initial files diff --git a/Gallery.xcodeproj/project.pbxproj b/Gallery.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0f00cff --- /dev/null +++ b/Gallery.xcodeproj/project.pbxproj @@ -0,0 +1,406 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 03093523235660E100E44910 /* ReverseImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03093522235660E100E44910 /* ReverseImageViewController.swift */; }; + 030935252356833A00E44910 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030935242356833A00E44910 /* InfoViewController.swift */; }; + 0361A4F8234690C000639E67 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A4F7234690C000639E67 /* AppDelegate.swift */; }; + 0361A4FA234690C000639E67 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A4F9234690C000639E67 /* SceneDelegate.swift */; }; + 0361A4FC234690C000639E67 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A4FB234690C000639E67 /* ViewController.swift */; }; + 0361A4FF234690C000639E67 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0361A4FD234690C000639E67 /* Main.storyboard */; }; + 0361A502234690C000639E67 /* Gallery.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0361A500234690C000639E67 /* Gallery.xcdatamodeld */; }; + 0361A504234690C100639E67 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0361A503234690C100639E67 /* Assets.xcassets */; }; + 0361A507234690C100639E67 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0361A505234690C100639E67 /* LaunchScreen.storyboard */; }; + 0361A5102346919A00639E67 /* PostViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A50F2346919A00639E67 /* PostViewCell.swift */; }; + 0361A5122346A38100639E67 /* PostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A5112346A38100639E67 /* PostViewController.swift */; }; + 0361A514234828D000639E67 /* EditTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A513234828D000639E67 /* EditTagsViewController.swift */; }; + 0361A51623482A7D00639E67 /* TagViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361A51523482A7D00639E67 /* TagViewCell.swift */; }; + 03F92BA22349635C0000DC1C /* PostCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F92BA12349635C0000DC1C /* PostCollectionView.swift */; }; + 03F92BA4234967E00000DC1C /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F92BA3234967E00000DC1C /* SearchViewController.swift */; }; + 03F92BA6234969800000DC1C /* TagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F92BA5234969800000DC1C /* TagViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 03093522235660E100E44910 /* ReverseImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReverseImageViewController.swift; sourceTree = ""; }; + 030935242356833A00E44910 /* InfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; + 0361A4F4234690C000639E67 /* Gallery.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gallery.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0361A4F7234690C000639E67 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 0361A4F9234690C000639E67 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 0361A4FB234690C000639E67 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 0361A4FE234690C000639E67 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 0361A501234690C000639E67 /* Gallery.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Gallery.xcdatamodel; sourceTree = ""; }; + 0361A503234690C100639E67 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 0361A506234690C100639E67 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 0361A508234690C100639E67 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0361A50E234690F200639E67 /* Gallery.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Gallery.entitlements; sourceTree = ""; }; + 0361A50F2346919A00639E67 /* PostViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewCell.swift; sourceTree = ""; }; + 0361A5112346A38100639E67 /* PostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewController.swift; sourceTree = ""; }; + 0361A513234828D000639E67 /* EditTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTagsViewController.swift; sourceTree = ""; }; + 0361A51523482A7D00639E67 /* TagViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagViewCell.swift; sourceTree = ""; }; + 03F92BA12349635C0000DC1C /* PostCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCollectionView.swift; sourceTree = ""; }; + 03F92BA3234967E00000DC1C /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; + 03F92BA5234969800000DC1C /* TagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0361A4F1234690C000639E67 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0361A4EB234690C000639E67 = { + isa = PBXGroup; + children = ( + 0361A4F6234690C000639E67 /* Gallery */, + 0361A4F5234690C000639E67 /* Products */, + ); + sourceTree = ""; + }; + 0361A4F5234690C000639E67 /* Products */ = { + isa = PBXGroup; + children = ( + 0361A4F4234690C000639E67 /* Gallery.app */, + ); + name = Products; + sourceTree = ""; + }; + 0361A4F6234690C000639E67 /* Gallery */ = { + isa = PBXGroup; + children = ( + 0361A50E234690F200639E67 /* Gallery.entitlements */, + 0361A4F7234690C000639E67 /* AppDelegate.swift */, + 0361A4F9234690C000639E67 /* SceneDelegate.swift */, + 0361A4FB234690C000639E67 /* ViewController.swift */, + 0361A4FD234690C000639E67 /* Main.storyboard */, + 0361A503234690C100639E67 /* Assets.xcassets */, + 0361A505234690C100639E67 /* LaunchScreen.storyboard */, + 0361A508234690C100639E67 /* Info.plist */, + 0361A500234690C000639E67 /* Gallery.xcdatamodeld */, + 0361A50F2346919A00639E67 /* PostViewCell.swift */, + 0361A5112346A38100639E67 /* PostViewController.swift */, + 0361A513234828D000639E67 /* EditTagsViewController.swift */, + 0361A51523482A7D00639E67 /* TagViewCell.swift */, + 03F92BA12349635C0000DC1C /* PostCollectionView.swift */, + 03F92BA3234967E00000DC1C /* SearchViewController.swift */, + 03F92BA5234969800000DC1C /* TagViewController.swift */, + 03093522235660E100E44910 /* ReverseImageViewController.swift */, + 030935242356833A00E44910 /* InfoViewController.swift */, + ); + path = Gallery; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0361A4F3234690C000639E67 /* Gallery */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0361A50B234690C100639E67 /* Build configuration list for PBXNativeTarget "Gallery" */; + buildPhases = ( + 0361A4F0234690C000639E67 /* Sources */, + 0361A4F1234690C000639E67 /* Frameworks */, + 0361A4F2234690C000639E67 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Gallery; + productName = Gallery; + productReference = 0361A4F4234690C000639E67 /* Gallery.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0361A4EC234690C000639E67 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "Joshua Goins"; + TargetAttributes = { + 0361A4F3234690C000639E67 = { + CreatedOnToolsVersion = 11.0; + }; + }; + }; + buildConfigurationList = 0361A4EF234690C000639E67 /* Build configuration list for PBXProject "Gallery" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0361A4EB234690C000639E67; + productRefGroup = 0361A4F5234690C000639E67 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0361A4F3234690C000639E67 /* Gallery */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0361A4F2234690C000639E67 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0361A507234690C100639E67 /* LaunchScreen.storyboard in Resources */, + 0361A504234690C100639E67 /* Assets.xcassets in Resources */, + 0361A4FF234690C000639E67 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0361A4F0234690C000639E67 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03F92BA4234967E00000DC1C /* SearchViewController.swift in Sources */, + 03F92BA22349635C0000DC1C /* PostCollectionView.swift in Sources */, + 0361A5122346A38100639E67 /* PostViewController.swift in Sources */, + 0361A502234690C000639E67 /* Gallery.xcdatamodeld in Sources */, + 0361A4FC234690C000639E67 /* ViewController.swift in Sources */, + 0361A51623482A7D00639E67 /* TagViewCell.swift in Sources */, + 03093523235660E100E44910 /* ReverseImageViewController.swift in Sources */, + 03F92BA6234969800000DC1C /* TagViewController.swift in Sources */, + 0361A4F8234690C000639E67 /* AppDelegate.swift in Sources */, + 030935252356833A00E44910 /* InfoViewController.swift in Sources */, + 0361A514234828D000639E67 /* EditTagsViewController.swift in Sources */, + 0361A4FA234690C000639E67 /* SceneDelegate.swift in Sources */, + 0361A5102346919A00639E67 /* PostViewCell.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 0361A4FD234690C000639E67 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0361A4FE234690C000639E67 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 0361A505234690C100639E67 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0361A506234690C100639E67 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0361A509234690C100639E67 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 0361A50A234690C100639E67 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0361A50C234690C100639E67 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Gallery/Gallery.entitlements; + CODE_SIGN_STYLE = Automatic; + DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; + DEVELOPMENT_TEAM = JM5LKVKH48; + INFOPLIST_FILE = Gallery/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = jorg.Gallery; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0361A50D234690C100639E67 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Gallery/Gallery.entitlements; + CODE_SIGN_STYLE = Automatic; + DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; + DEVELOPMENT_TEAM = JM5LKVKH48; + INFOPLIST_FILE = Gallery/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = jorg.Gallery; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTS_MACCATALYST = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0361A4EF234690C000639E67 /* Build configuration list for PBXProject "Gallery" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0361A509234690C100639E67 /* Debug */, + 0361A50A234690C100639E67 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0361A50B234690C100639E67 /* Build configuration list for PBXNativeTarget "Gallery" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0361A50C234690C100639E67 /* Debug */, + 0361A50D234690C100639E67 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 0361A500234690C000639E67 /* Gallery.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 0361A501234690C000639E67 /* Gallery.xcdatamodel */, + ); + currentVersion = 0361A501234690C000639E67 /* Gallery.xcdatamodel */; + path = Gallery.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 0361A4EC234690C000639E67 /* Project object */; +} diff --git a/Gallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Gallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..76682c8 --- /dev/null +++ b/Gallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Gallery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Gallery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Gallery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Gallery/AppDelegate.swift b/Gallery/AppDelegate.swift new file mode 100644 index 0000000..e444097 --- /dev/null +++ b/Gallery/AppDelegate.swift @@ -0,0 +1,40 @@ +import UIKit +import CoreData + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + + 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) { + } + + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "Gallery") + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + + return container + }() + + func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} + diff --git a/Gallery/Assets.xcassets/AppIcon.appiconset/Contents.json b/Gallery/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/Gallery/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Gallery/Assets.xcassets/Contents.json b/Gallery/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Gallery/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Gallery/Base.lproj/LaunchScreen.storyboard b/Gallery/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Gallery/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gallery/Base.lproj/Main.storyboard b/Gallery/Base.lproj/Main.storyboard new file mode 100644 index 0000000..51e24fb --- /dev/null +++ b/Gallery/Base.lproj/Main.storyboard @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gallery/EditTagsViewController.swift b/Gallery/EditTagsViewController.swift new file mode 100644 index 0000000..82fb6fd --- /dev/null +++ b/Gallery/EditTagsViewController.swift @@ -0,0 +1,70 @@ +import UIKit +import CoreData + +class EditTagsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + var post: Post? + + @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var tagField: UITextField! + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return post!.tags!.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "tagCell", for: indexPath as IndexPath) as! TagViewCell + + cell.label.text = (post?.tags![indexPath.row] as! Tag).name + + return cell + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if(editingStyle == .delete) { + post?.removeFromTags(post?.tags![indexPath.row] as! Tag) + + tableView.reloadData() + } + } + + @IBAction func editingFinished(_ sender: Any) { + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return + } + + let managedContext = appDelegate.persistentContainer.viewContext + + let tag = Tag(context: managedContext) + tag.name = tagField.text + + post?.addToTags(tag) + + tableView.reloadData() + tagField.text?.removeAll() + } + + @IBAction func editAction(_ sender: Any) { + tableView.setEditing(true, animated: true) + } + + @IBAction func doneAction(_ sender: Any) { + dismiss(animated: true, completion: nil) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showTag" { + let newViewController = segue.destination as! TagViewController + let index = self.tableView.indexPathForSelectedRow + + newViewController.tag = (post?.tags![index!.row] as! Tag).name + } + } +} + +extension EditTagsViewController { + static func loadFromStoryboard() -> EditTagsViewController? { + let storyboard = UIStoryboard(name: "Main", bundle: .main) + return storyboard.instantiateViewController(withIdentifier: "EditTagsViewController") as? EditTagsViewController + } +} diff --git a/Gallery/Gallery.entitlements b/Gallery/Gallery.entitlements new file mode 100644 index 0000000..19afff1 --- /dev/null +++ b/Gallery/Gallery.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + + diff --git a/Gallery/Gallery.xcdatamodeld/.xccurrentversion b/Gallery/Gallery.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..931adb4 --- /dev/null +++ b/Gallery/Gallery.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Gallery.xcdatamodel + + diff --git a/Gallery/Gallery.xcdatamodeld/Gallery.xcdatamodel/contents b/Gallery/Gallery.xcdatamodeld/Gallery.xcdatamodel/contents new file mode 100644 index 0000000..a295b04 --- /dev/null +++ b/Gallery/Gallery.xcdatamodeld/Gallery.xcdatamodel/contents @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Gallery/Info.plist b/Gallery/Info.plist new file mode 100644 index 0000000..d8a0554 --- /dev/null +++ b/Gallery/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.utilities + LSRequiresIPhoneOS + + NSUserActivityTypes + + post + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Gallery/InfoViewController.swift b/Gallery/InfoViewController.swift new file mode 100644 index 0000000..d2b6244 --- /dev/null +++ b/Gallery/InfoViewController.swift @@ -0,0 +1,64 @@ +import UIKit +import CoreData +import AVFoundation + +class InfoViewController: UIViewController { + var post: Post? + var image: UIImage? + + @IBOutlet weak var infoLabel: UILabel! + + private func resolutionForLocalVideo(url: URL) -> CGSize? { + guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return nil } + let size = track.naturalSize.applying(track.preferredTransform) + return CGSize(width: abs(size.width), height: abs(size.height)) + } + + + let documentsPath : URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteURL + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(true) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + + var type = "Unavailable" + if(post!.type != nil) { + if(post!.type == "public.image") { + type = "Image" + } else if(post!.type == "public.mpeg-4") { + type = "Video" + } + } + + var date = "Unavailable" + if(post!.date != nil) { + date = dateFormatter.string(from: post!.date!) + } + + var size = "Unavailable" + if(post?.type == "public.mpeg-4") { + let videoPath = documentsPath.appendingPathComponent(post!.value(forKey: "name") as! String).path + + let resolution = resolutionForLocalVideo(url: URL(fileURLWithPath: videoPath)) + + size = String(format: "%.0f", (resolution?.width)!) + "x" + String(format: "%.0f", (resolution?.height)!) + } else if(image != nil) { + size = String(format: "%.0f", (image?.size.width)!) + "x" + String(format: "%.0f", (image?.size.height)!) + } + + infoLabel.text = "Type: " + type + "\nDate: " + date + "\nSize: " + size + } + + @IBAction func exitAction(_ sender: Any) { + dismiss(animated: true, completion: nil) + } +} + +extension InfoViewController { + static func loadFromStoryboard() -> InfoViewController? { + let storyboard = UIStoryboard(name: "Main", bundle: .main) + return storyboard.instantiateViewController(withIdentifier: "InfoViewController") as? InfoViewController + } +} diff --git a/Gallery/PostCollectionView.swift b/Gallery/PostCollectionView.swift new file mode 100644 index 0000000..b2ccb59 --- /dev/null +++ b/Gallery/PostCollectionView.swift @@ -0,0 +1,286 @@ +import UIKit +import CoreData +import AVFoundation + +extension UIActivity.ActivityType { + static let reverseImageSearch = + UIActivity.ActivityType("jorg.Gallery.reverseImage") +} + +class ReverseImageSearchService: UIActivity { + var viewController: UIViewController? + var post: Post? + + override class var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType? { + return .reverseImageSearch + } + + override var activityTitle: String? { + return NSLocalizedString("Reverse Image Search", comment: "activity title") + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override var activityViewController: UIViewController? { + return ReverseImageViewController.loadFromStoryboard(post: self.post!) + } +} + +func generateThumbnail(path: URL) -> UIImage? { + do { + let asset = AVURLAsset(url: path, options: nil) + let imgGenerator = AVAssetImageGenerator(asset: asset) + imgGenerator.appliesPreferredTrackTransform = true + let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) + let thumbnail = UIImage(cgImage: cgImage) + return thumbnail + } catch let error { + print("*** Error generating thumbnail: \(error.localizedDescription)") + return nil + } +} + +class PostCollectionView: UICollectionView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDropDelegate, UICollectionViewDragDelegate { + var posts: [NSManagedObject] = [] + + weak var viewController: UIViewController? + + private let itemsPerRow: CGFloat = 4 + + private let sectionInsets = UIEdgeInsets(top: 10.0, + left: 10.0, + bottom: 10.0, + right: 10.0) + + let documentsPath : URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteURL + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return posts.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let post = posts[indexPath.row] + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "postCell", for: indexPath as IndexPath) as! PostViewCell + + let imagePath = documentsPath.appendingPathComponent(post.value(forKey: "name") as! String).path + + if(FileManager.default.fileExists(atPath: imagePath)) { + let type = (post.value(forKey: "type") as? String) + + if(type == "public.mpeg-4") { + let imagePath = documentsPath.appendingPathComponent(post.value(forKey: "name") as! String).path + cell.imageView.image = generateThumbnail(path: URL(fileURLWithPath: imagePath)) + } else { + cell.imageView.image = UIImage(contentsOfFile: imagePath) + } + } else { + print("could not read " + imagePath) + } + + return cell + } + + func actualInit(tag: String?) { + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return + } + + let managedContext = appDelegate.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "Post") + + if(tag != nil) { + let predicate = NSPredicate(format: "ANY tags.name in %@", [tag]) + + fetchRequest.predicate = predicate + } + + do { + posts = try managedContext.fetch(fetchRequest) + + DispatchQueue.main.async { + self.reloadData() + } + } catch let error as NSError { + print("Could not fetch. \(error), \(error.userInfo)") + } + } + + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + + self.dragInteractionEnabled = true + self.dataSource = self + self.delegate = self + self.dropDelegate = self + self.dragDelegate = self + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + self.dragInteractionEnabled = true + self.dataSource = self + self.delegate = self + self.dropDelegate = self + self.dragDelegate = self + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let paddingSpace = sectionInsets.left * (itemsPerRow + 1) + let availableWidth = frame.width - paddingSpace + let widthPerItem = availableWidth / itemsPerRow + + return CGSize(width: widthPerItem, height: widthPerItem) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + insetForSectionAt section: Int) -> UIEdgeInsets { + return sectionInsets + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return sectionInsets.left + } + + func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { suggestedActions in + return self.makeContextMenu(post: self.posts[indexPath.row]) + }) + } + + func makeContextMenu(post: NSManagedObject) -> UIMenu { + let newWindow = UIAction(title: "Open in New Window", image: UIImage(systemName: "plus.square.on.square")) { action in + + let activity = NSUserActivity(activityType: "post") + activity.userInfo = ["name": post.value(forKey: "name") as! String] + activity.isEligibleForHandoff = true + + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil) + } + + let share = UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up")) { action in + let index = self.posts.firstIndex(of: post) + + let cell = self.cellForItem(at: IndexPath(row: index!, section: 0)) as! PostViewCell + + let imageSearch = ReverseImageSearchService() + imageSearch.viewController = self.window?.rootViewController + imageSearch.post = self.posts[index!] as? Post + + let activityViewController = UIActivityViewController(activityItems: [cell.imageView.image!], applicationActivities: [imageSearch]) + activityViewController.popoverPresentationController?.sourceView = cell.contentView + + self.viewController?.present(activityViewController, animated: true, completion:nil) + } + + let editTags = UIAction(title: "Tags", image: UIImage(systemName: "tag")) { action in + let viewController = EditTagsViewController.loadFromStoryboard() + viewController!.post = post as? Post + + self.viewController?.present(viewController!, animated: true) + } + + let info = UIAction(title: "Info", image: UIImage(systemName: "info.circle")) { action in + let index = self.posts.firstIndex(of: post) + + let cell = self.cellForItem(at: IndexPath(row: index!, section: 0)) as! PostViewCell + + let viewController = InfoViewController.loadFromStoryboard() + viewController!.post = post as? Post + viewController!.image = cell.imageView.image + + self.viewController?.present(viewController!, animated: true) + } + + let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { action in + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return + } + + let managedContext = appDelegate.persistentContainer.viewContext + + managedContext.delete(post) + self.posts.removeAll { (obj: NSManagedObject) -> Bool in + return obj == post + } + + DispatchQueue.main.async { + self.reloadData() + } + } + + #if targetEnvironment(macCatalyst) + return UIMenu(title: "", children: [info, editTags, share, delete]) + #else + if UIDevice.current.userInterfaceIdiom == .pad { + return UIMenu(title: "", children: [newWindow, share, info, editTags, delete]) + } else { + return UIMenu(title: "", children: [share, info, editTags, delete]) + } + #endif + } + + func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { + for item in coordinator.items { + item.dragItem.itemProvider.loadObject(ofClass: UIImage.self, + completionHandler: {(newImage, error) -> Void in + if let image = newImage as? UIImage { + if let data = image.jpegData(compressionQuality: 0.8) { + DispatchQueue.main.async { + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return + } + + let managedContext = appDelegate.persistentContainer.viewContext + + let entity = NSEntityDescription.entity(forEntityName: "Post", in: managedContext)! + + let post = NSManagedObject(entity: entity, insertInto: managedContext) + + let uuid = UUID().uuidString + + let filename = uuid + ".jpg" + + let newPath = self.documentsPath.appendingPathComponent(filename) + + try? data.write(to: newPath) + + post.setValue(filename, forKeyPath: "name") + + try? managedContext.save() + self.posts.append(post) + + self.reloadData() + } + } + } + }) + } + } + + func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + let model = posts[indexPath.item] + let itemProvider = NSItemProvider(object: (cellForItem(at: indexPath) as! PostViewCell).imageView.image!) + itemProvider.suggestedName = model.value(forKey: "name") as? String + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = model //We can set the localObject property for convenience + return [dragItem] + } +} diff --git a/Gallery/PostViewCell.swift b/Gallery/PostViewCell.swift new file mode 100644 index 0000000..73a5ca6 --- /dev/null +++ b/Gallery/PostViewCell.swift @@ -0,0 +1,7 @@ +import UIKit + +class PostViewCell: UICollectionViewCell { + + @IBOutlet weak var imageView: UIImageView! +} + diff --git a/Gallery/PostViewController.swift b/Gallery/PostViewController.swift new file mode 100644 index 0000000..e72e113 --- /dev/null +++ b/Gallery/PostViewController.swift @@ -0,0 +1,147 @@ +import UIKit +import CoreData +import AVFoundation +import AVKit + +class PostViewController: UIViewController { + @IBOutlet weak var imageView: UIImageView! + + var post: NSManagedObject? + var image: UIImage? + var isPopup: Bool = false + @IBOutlet weak var shareButton: UIBarButtonItem! + + let documentsPath : URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteURL + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if(image == nil) { + let imagePath = documentsPath.appendingPathComponent(post!.value(forKey: "name") as! String).path + + if((post?.value(forKey: "type") as? String) == "public.mpeg-4") { + self.image = generateThumbnail(path: URL(fileURLWithPath: imagePath)) + } else { + self.image = UIImage(contentsOfFile: imagePath) + } + } + + imageView.image = self.image + + if(isPopup) { + navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(closePopup)) + } + } + + @objc func closePopup() { + UIApplication.shared.requestSceneSessionDestruction((self.view.window?.windowScene!.session)!, options: nil, errorHandler: nil) + } + + @IBAction func shareAction(_ sender: Any) { + let imageSearch = ReverseImageSearchService() + imageSearch.viewController = parent + imageSearch.post = self.post as? Post + + let activityViewController = UIActivityViewController(activityItems: [self.image!], applicationActivities: [imageSearch]) + activityViewController.popoverPresentationController?.barButtonItem = shareButton + + self.present(activityViewController, animated: true, completion:nil) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showTags" { + let newViewController = segue.destination as! EditTagsViewController + + newViewController.post = self.post as? Post + } else if segue.identifier == "showInfo" { + let newViewController = segue.destination as! InfoViewController + + newViewController.post = self.post as? Post + newViewController.image = self.image + } + } + + @IBAction func playAction(_ sender: Any) { + let imagePath = documentsPath.appendingPathComponent(post!.value(forKey: "name") as! String).path + + // Create an AVPlayer, passing it the HTTP Live Streaming URL. + let player = AVPlayer(url: URL(fileURLWithPath: imagePath)) + + // Create a new AVPlayerViewController and pass it a reference to the player. + let controller = AVPlayerViewController() + controller.player = player + + // Modally present the player and call the player's play() method when complete. + present(controller, animated: true) { + player.play() + } + } +} + +extension PostViewController { + static func loadFromStoryboard() -> PostViewController? { + let storyboard = UIStoryboard(name: "Main", bundle: .main) + return storyboard.instantiateViewController(withIdentifier: "PostViewController") as? PostViewController + } +} + + +#if targetEnvironment(macCatalyst) +private let EditButtonToolbarIdentifier = NSToolbarItem.Identifier(rawValue: "OurButton") +private let ShareButtonToolbarIdentifier = NSToolbarItem.Identifier(rawValue: "OurButton2") +private let InfoButtonToolbarIdentifier = NSToolbarItem.Identifier(rawValue: "OurButton3") + +extension PostViewController: NSToolbarDelegate { + @objc func editTagsAction() { + if(navigationController?.topViewController != self) { + navigationController?.popViewController(animated: true) + } else { + performSegue(withIdentifier: "showTags", sender: nil) + } + } + + @objc func infoAction() { + if(navigationController?.topViewController != self) { + navigationController?.popViewController(animated: true) + } else { + performSegue(withIdentifier: "showInfo", sender: nil) + } + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [NSToolbarItem.Identifier.flexibleSpace, InfoButtonToolbarIdentifier, EditButtonToolbarIdentifier, ShareButtonToolbarIdentifier] + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return toolbarDefaultItemIdentifiers(toolbar) + } + + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + if (itemIdentifier == InfoButtonToolbarIdentifier) { + let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.camera, + target: self, + action: #selector(self.infoAction)) + let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem) + return button + } + + if (itemIdentifier == EditButtonToolbarIdentifier) { + let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.edit, + target: self, + action: #selector(self.editTagsAction)) + let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem) + return button + } + + if (itemIdentifier == ShareButtonToolbarIdentifier) { + let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.action, + target: self, + action: #selector(self.shareAction)) + let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem) + return button + } + + return nil + } +} +#endif diff --git a/Gallery/ReverseImageViewController.swift b/Gallery/ReverseImageViewController.swift new file mode 100644 index 0000000..a5e7a15 --- /dev/null +++ b/Gallery/ReverseImageViewController.swift @@ -0,0 +1,108 @@ +import UIKit +import CoreData +import WebKit +import CoreServices + +extension Data { + + /// Append string to Data + /// + /// Rather than littering my code with calls to `data(using: .utf8)` to convert `String` values to `Data`, this wraps it in a nice convenient little extension to Data. This defaults to converting using UTF-8. + /// + /// - parameter string: The string to be added to the `Data`. + + mutating func append(_ string: String, using encoding: String.Encoding = .utf8) { + if let data = string.data(using: encoding) { + append(data) + } + } +} + +class ReverseImageViewController: UIViewController { + var post: Post? + @IBOutlet weak var webView: WKWebView! + + let documentsPath : URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteURL + + private func mimeType(for path: String) -> String { + let url = URL(fileURLWithPath: path) + let pathExtension = url.pathExtension + + if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as NSString, nil)?.takeRetainedValue() { + if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { + return mimetype as String + } + } + return "application/octet-stream" + } + + private func createBody(with parameters: [String: String]?, filePathKey: String, paths: [String], boundary: String) throws -> Data { + var body = Data() + + if parameters != nil { + for (key, value) in parameters! { + body.append("--\(boundary)\r\n") + body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") + body.append("\(value)\r\n") + } + } + + for path in paths { + let url = URL(fileURLWithPath: path) + let filename = url.lastPathComponent + let data = try Data(contentsOf: url) + let mimetype = mimeType(for: path) + + body.append("--\(boundary)\r\n") + body.append("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(filename)\"\r\n") + body.append("Content-Type: \(mimetype)\r\n\r\n") + body.append(data) + body.append("\r\n") + } + + body.append("--\(boundary)--\r\n") + return body + } + + private func generateBoundaryString() -> String { + return "Boundary-\(UUID().uuidString)" + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + var components = URLComponents(string: "https://iqdb.org") + components?.queryItems = [URLQueryItem(name: "url", value: "true")] + if let result = components?.url { + let boundary = generateBoundaryString() + + var request = URLRequest(url: result) + request.httpMethod = "POST" + request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + let imagePath = documentsPath.appendingPathComponent(post!.value(forKey: "name") as! String).path + + do { + request.httpBody = try createBody(with: nil, filePathKey: "file", paths: [imagePath], boundary: boundary) + } catch let error as NSError { + print(error) + } + + webView!.load(request) + } + } + + @IBAction func cancelAction(_ sender: Any) { + dismiss(animated: true, completion: nil) + } +} + +extension ReverseImageViewController { + static func loadFromStoryboard(post: Post) -> ReverseImageViewController? { + let storyboard = UIStoryboard(name: "Main", bundle: .main) + let viewController = storyboard.instantiateViewController(withIdentifier: "ReverseImageViewController") as? ReverseImageViewController + viewController!.post = post + + return viewController + } +} diff --git a/Gallery/SceneDelegate.swift b/Gallery/SceneDelegate.swift new file mode 100644 index 0000000..72ad55b --- /dev/null +++ b/Gallery/SceneDelegate.swift @@ -0,0 +1,85 @@ +import UIKit +import CoreData + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { + if !configure(window: window, with: userActivity) { + print("Failed to restore from \(userActivity)") + } + } + + #if targetEnvironment(macCatalyst) + guard let windowScene = (scene as? UIWindowScene) else { return } + let toolbar = NSToolbar(identifier: "MyToolbar") + toolbar.delegate = (window?.rootViewController as! UINavigationController).topViewController as? NSToolbarDelegate + windowScene.titlebar!.toolbar = toolbar + windowScene.titlebar!.titleVisibility = .hidden + toolbar.allowsUserCustomization = true + + (window?.rootViewController as! UINavigationController).navigationBar.isHidden = true + + #endif + } + + func sceneDidDisconnect(_ scene: UIScene) { + + } + + func sceneDidBecomeActive(_ scene: UIScene) { + + } + + func sceneWillResignActive(_ scene: UIScene) { + + } + + func sceneWillEnterForeground(_ scene: UIScene) { + + } + + func sceneDidEnterBackground(_ scene: UIScene) { + (UIApplication.shared.delegate as? AppDelegate)?.saveContext() + } + + func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool { + if activity.activityType == "post" { + if let photoID = activity.userInfo?["name"] as? String { + if let photoDetailViewController = PostViewController.loadFromStoryboard() { + + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return false + } + + let managedContext = + appDelegate.persistentContainer.viewContext + + let request = NSFetchRequest(entityName: "Post") + + request.predicate = NSPredicate(format: "name = %@", photoID) + request.returnsObjectsAsFaults = false + + do { + let result = try managedContext.fetch(request) + + photoDetailViewController.post = result[0] as? NSManagedObject + photoDetailViewController.isPopup = true + + if let navigationController = window?.rootViewController as? UINavigationController { + navigationController.pushViewController(photoDetailViewController, animated: false) + + return true + } + } catch _ as NSError { + return false + } + } + } + } + + return false + } +} diff --git a/Gallery/SearchViewController.swift b/Gallery/SearchViewController.swift new file mode 100644 index 0000000..473500f --- /dev/null +++ b/Gallery/SearchViewController.swift @@ -0,0 +1,50 @@ +import UIKit +import CoreData + +class SearchViewController: UIViewController { + @IBOutlet weak var collectionView: PostCollectionView! + + @IBOutlet weak var searchField: UITextField! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + collectionView.viewController = self + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showPost" { + let newViewController = segue.destination as! PostViewController + let index = self.collectionView.indexPathsForSelectedItems?.first + + newViewController.post = self.collectionView.posts[index!.row] + newViewController.image = (self.collectionView.cellForItem(at: index!) as! PostViewCell).imageView.image; + } + } + + @IBAction func searchAction(_ sender: Any) { + collectionView.actualInit(tag: searchField.text) + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if identifier == "showPost" { + #if targetEnvironment(macCatalyst) + let index = self.collectionView.indexPathsForSelectedItems?.first + + let post = self.collectionView.posts[index!.row] + + let activity = NSUserActivity(activityType: "post") + activity.userInfo = ["name": post.value(forKey: "name") as! String] + activity.isEligibleForHandoff = true + + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil) + + return false + #else + return true + #endif + } + + return super.shouldPerformSegue(withIdentifier: identifier, sender: sender) + } +} diff --git a/Gallery/TagViewCell.swift b/Gallery/TagViewCell.swift new file mode 100644 index 0000000..3a42ca6 --- /dev/null +++ b/Gallery/TagViewCell.swift @@ -0,0 +1,8 @@ +import UIKit + +class TagViewCell: UITableViewCell { + + @IBOutlet weak var label: UILabel! + +} + diff --git a/Gallery/TagViewController.swift b/Gallery/TagViewController.swift new file mode 100644 index 0000000..aac9673 --- /dev/null +++ b/Gallery/TagViewController.swift @@ -0,0 +1,25 @@ +import UIKit +import CoreData + +class TagViewController: UIViewController { + var tag: String? + + @IBOutlet weak var collectionView: PostCollectionView! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + collectionView.actualInit(tag: self.tag) + collectionView.viewController = self + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showPost" { + let newViewController = segue.destination as! PostViewController + let index = self.collectionView.indexPathsForSelectedItems?.first + + newViewController.post = self.collectionView.posts[index!.row] + newViewController.image = (self.collectionView.cellForItem(at: index!) as! PostViewCell).imageView.image; + } + } +} diff --git a/Gallery/ViewController.swift b/Gallery/ViewController.swift new file mode 100644 index 0000000..31d9a58 --- /dev/null +++ b/Gallery/ViewController.swift @@ -0,0 +1,150 @@ +import UIKit +import CoreData + +class ViewController: UIViewController, UIDocumentPickerDelegate { + @IBOutlet weak var collectionView: PostCollectionView! + + let documentsPath : URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].absoluteURL + + func importFile(path: URL) { + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { + return + } + + let managedContext = appDelegate.persistentContainer.viewContext + + let entity = NSEntityDescription.entity(forEntityName: "Post", in: managedContext)! + + let post = NSManagedObject(entity: entity, insertInto: managedContext) + + let oldPath = path + let newPath = documentsPath.appendingPathComponent(path.lastPathComponent) + + do { + try FileManager.default.copyItem(at: oldPath, to: newPath) + + if let resourceValues = try? path.resourceValues(forKeys: [.typeIdentifierKey]), + let uti = resourceValues.typeIdentifier { + post.setValue(uti, forKeyPath: "type") + } + + post.setValue(Date(), forKeyPath: "date") + post.setValue(path.lastPathComponent, forKeyPath: "name") + + try managedContext.save() + collectionView.posts.append(post) + + DispatchQueue.main.async { + self.collectionView.reloadData() + } + } catch let error as NSError { + print("Could not save. \(error), \(error.userInfo)") + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + collectionView.actualInit(tag: nil) + collectionView.viewController = self + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.window?.windowScene!.title = "Home" + } + + @IBAction func importAction(_ sender: Any) { + let documentPicker = UIDocumentPickerViewController(documentTypes: ["public.image", "public.movie"], in: .import) + documentPicker.delegate = self as UIDocumentPickerDelegate + documentPicker.allowsMultipleSelection = true + + present(documentPicker, animated: true) + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + for url in urls { + importFile(path: url) + } + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showPost" { + let newViewController = segue.destination as! PostViewController + let index = self.collectionView.indexPathsForSelectedItems?.first + + newViewController.post = self.collectionView.posts[index!.row] + newViewController.image = (self.collectionView.cellForItem(at: index!) as! PostViewCell).imageView.image; + } + } + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + if identifier == "showPost" { + #if targetEnvironment(macCatalyst) + let index = self.collectionView.indexPathsForSelectedItems?.first + + let post = self.collectionView.posts[index!.row] + + let activity = NSUserActivity(activityType: "post") + activity.userInfo = ["name": post.value(forKey: "name") as! String] + activity.isEligibleForHandoff = true + + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil, errorHandler: nil) + + return false + #else + return true + #endif + } + + return super.shouldPerformSegue(withIdentifier: identifier, sender: sender) + } +} + +#if targetEnvironment(macCatalyst) +private let OurButtonToolbarIdentifier = NSToolbarItem.Identifier(rawValue: "OurButton") +private let OurButtonToolbarIdentifier2 = NSToolbarItem.Identifier(rawValue: "OurButton2") + +extension ViewController: NSToolbarDelegate { + @objc func searchAction() { + if(navigationController?.topViewController != self) { + navigationController?.popViewController(animated: true) + } else { + performSegue(withIdentifier: "search", sender: nil) + } + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [OurButtonToolbarIdentifier2, NSToolbarItem.Identifier.flexibleSpace, + OurButtonToolbarIdentifier] + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return toolbarDefaultItemIdentifiers(toolbar) + } + + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + if (itemIdentifier == OurButtonToolbarIdentifier2) { + let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, + target: self, + action: #selector(self.importAction)) + let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem) + button.label = "Add"; + + return button + } + + if (itemIdentifier == OurButtonToolbarIdentifier) { + let barButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.search, + target: self, + action: #selector(self.searchAction)) + let button = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: barButtonItem) + button.label = "Search"; + return button + } + return nil + } +} +#endif diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bdecdd0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Joshua Goins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccd6010 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Gallery + +A local media gallery. You can import images and videos, and even tag them for searching and filtering. + +![showcase image](https://raw.githubusercontent.com/redstrate/gallery/master/misc/showcase.png) + +The app is written in Swift, and uses UIKit and CoreData. Thanks to Catalyst, it can run on macOS as well as iOS and iPadOS. diff --git a/misc/showcase.png b/misc/showcase.png new file mode 100644 index 0000000..c9a0606 Binary files /dev/null and b/misc/showcase.png differ