├── .gitignore ├── Account ├── Package.swift ├── README.md ├── Sources │ └── Account │ │ ├── Account.swift │ │ └── AccountManager.swift └── Tests │ └── AccountTests │ └── DummyFile.swift ├── LICENSE ├── Links ├── Package.swift ├── README.md ├── Sources │ └── Links │ │ ├── Extensions │ │ ├── +Color.swift │ │ └── +LPLinkMetadata.swift │ │ ├── Group.swift │ │ ├── Link.swift │ │ ├── Tag.swift │ │ └── Utils │ │ └── LinkActivity.swift └── Tests │ └── LinksTests │ ├── GroupsTests.swift │ ├── LinksTests.swift │ └── TagsTests.swift ├── LinksDatabase ├── Package.swift ├── README.md ├── Sources │ └── LinksDatabase │ │ ├── DatabaseQueue.swift │ │ ├── DatabaseTable.swift │ │ ├── LinksDatabase.swift │ │ ├── LinksTable.swift │ │ ├── Migrations.swift │ │ ├── MigrationsTable.swift │ │ ├── OrderBy.swift │ │ ├── SQL.swift │ │ └── SQLUtils.swift └── Tests │ └── LinksDatabaseTests │ ├── DatabaseSQLTests.swift │ └── SQLTests.swift ├── LinksMetadata ├── .gitignore ├── Package.swift ├── README.md ├── Sources │ └── LinksMetadata │ │ ├── HTMLParser.swift │ │ ├── LinkDataQueue.swift │ │ └── OpenGraph.swift └── Tests │ └── LinksMetadataTests │ ├── HTMLParserTests.swift │ ├── LinkDataQueueTests.swift │ └── LinksMetadataTests.swift ├── README.md ├── Shared ├── Model │ ├── AppData.swift │ ├── AppIcon.swift │ └── Category.swift ├── Redirector │ └── URLRedirector.swift ├── SharedExtension │ ├── ExtensionsAddLinkRequests.swift │ └── ExtensionsAddLinkRequestsManager.swift └── Utils │ ├── +Data.swift │ ├── +Date.swift │ ├── +FileManager.swift │ ├── +URL.swift │ ├── +UserDefaults.swift │ ├── AppReviewManager.swift │ ├── IAPManager.swift │ ├── ImageCacheStorage.swift │ ├── Logging.swift │ ├── Paths.swift │ ├── SFSymbols.swift │ ├── Tasks │ ├── MoveToProtectedContainerTask.swift │ ├── RegisterInitialValuesForUserDefaultsStartupTask.swift │ └── StartupTask.swift │ ├── ThemeController.swift │ └── UserDefaultsWrapper.swift ├── Ulry.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── AddToUlryAction.xcscheme │ ├── Ulry.xcscheme │ ├── UlryIntents.xcscheme │ ├── UlryShareExtension.xcscheme │ └── UlryTests.xcscheme ├── Ulry ├── AddToUlryAction │ ├── AddToUlryAction.entitlements │ ├── AddToUlryActionViewController.swift │ ├── Base.lproj │ │ └── MainInterface.storyboard │ ├── GetURL.js │ ├── Info.plist │ └── Media.xcassets │ │ ├── AppIcon.imageset │ │ ├── 1024 1.png │ │ ├── 1024 2.png │ │ ├── 1024.png │ │ └── Contents.json │ │ ├── Contents.json │ │ └── ExtensionAppIcon.appiconset │ │ ├── 1024.png │ │ ├── 20@2x.png │ │ ├── 20@3x.png │ │ ├── 29@2x.png │ │ ├── 29@3x.png │ │ ├── 40@2x.png │ │ ├── 40@3x.png │ │ ├── 60@2x.png │ │ ├── 60@3x.png │ │ └── Contents.json ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 167.png │ │ ├── 20@2x.png │ │ ├── 20@3x.png │ │ ├── 29@2x.png │ │ ├── 29@3x.png │ │ ├── 38@2x.png │ │ ├── 38@3x.png │ │ ├── 40@2x.png │ │ ├── 40@3x.png │ │ ├── 60@2x.png │ │ ├── 60@3x.png │ │ ├── 64@2x.png │ │ ├── 64@3x.png │ │ ├── 68@2x.png │ │ ├── 76@2x.png │ │ └── Contents.json │ ├── Contents.json │ ├── astronaut.dataset │ │ ├── Contents.json │ │ └── astronaut.json │ ├── colors │ │ ├── Contents.json │ │ ├── bg-color.colorset │ │ │ └── Contents.json │ │ ├── list-bg-color.colorset │ │ │ └── Contents.json │ │ ├── list-cell-bg-color.colorset │ │ │ └── Contents.json │ │ └── list-cell-selected-bg-color.colorset │ │ │ └── Contents.json │ ├── custom-app-icons │ │ ├── Contents.json │ │ ├── dark_red.appiconset │ │ │ ├── 1024.png │ │ │ ├── 167.png │ │ │ ├── 20@2x.png │ │ │ ├── 20@3x.png │ │ │ ├── 29@2x.png │ │ │ ├── 29@3x.png │ │ │ ├── 38@2x.png │ │ │ ├── 38@3x.png │ │ │ ├── 40@2x.png │ │ │ ├── 40@3x.png │ │ │ ├── 60@2x.png │ │ │ ├── 60@3x.png │ │ │ ├── 64@2x.png │ │ │ ├── 64@3x.png │ │ │ ├── 68@2x.png │ │ │ ├── 76@2x.png │ │ │ └── Contents.json │ │ ├── light_black.appiconset │ │ │ ├── 1024.png │ │ │ ├── 167.png │ │ │ ├── 20@2x.png │ │ │ ├── 20@3x.png │ │ │ ├── 29@2x.png │ │ │ ├── 29@3x.png │ │ │ ├── 38@2x.png │ │ │ ├── 38@3x.png │ │ │ ├── 40@2x.png │ │ │ ├── 40@3x.png │ │ │ ├── 60@2x.png │ │ │ ├── 60@3x.png │ │ │ ├── 64@2x.png │ │ │ ├── 64@3x.png │ │ │ ├── 68@2x.png │ │ │ ├── 76@2x.png │ │ │ └── Contents.json │ │ ├── light_blue.appiconset │ │ │ ├── 1024.png │ │ │ ├── 167.png │ │ │ ├── 20@2x.png │ │ │ ├── 20@3x.png │ │ │ ├── 29@2x.png │ │ │ ├── 29@3x.png │ │ │ ├── 38@2x.png │ │ │ ├── 38@3x.png │ │ │ ├── 40@2x.png │ │ │ ├── 40@3x.png │ │ │ ├── 60@2x.png │ │ │ ├── 60@3x.png │ │ │ ├── 64@2x.png │ │ │ ├── 64@3x.png │ │ │ ├── 68@2x.png │ │ │ ├── 76@2x.png │ │ │ └── Contents.json │ │ ├── light_orange.appiconset │ │ │ ├── 1024.png │ │ │ ├── 167.png │ │ │ ├── 20@2x.png │ │ │ ├── 20@3x.png │ │ │ ├── 29@2x.png │ │ │ ├── 29@3x.png │ │ │ ├── 38@2x.png │ │ │ ├── 38@3x.png │ │ │ ├── 40@2x.png │ │ │ ├── 40@3x.png │ │ │ ├── 60@2x.png │ │ │ ├── 60@3x.png │ │ │ ├── 64@2x.png │ │ │ ├── 64@3x.png │ │ │ ├── 68@2x.png │ │ │ ├── 76@2x.png │ │ │ └── Contents.json │ │ └── thumbnails │ │ │ ├── Contents.json │ │ │ ├── thumb-dark_red.imageset │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ │ ├── thumb-default.imageset │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ │ ├── thumb-light_black.imageset │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ │ ├── thumb-light_blue.imageset │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ │ └── thumb-light_orange.imageset │ │ │ ├── 1024@2x.png │ │ │ └── Contents.json │ ├── github-logo.imageset │ │ ├── Contents.json │ │ ├── GitHub-Mark-Light-64px 1.png │ │ ├── GitHub-Mark-Light-64px 2.png │ │ └── GitHub-Mark-Light-64px.png │ ├── launchscreen-1.0.imageset │ │ ├── 1024.png │ │ └── Contents.json │ ├── onboarding-link-details.imageset │ │ ├── Contents.json │ │ └── onboarding-link-details.jpg │ └── onboarding-share-extension.imageset │ │ ├── Contents.json │ │ └── onboarding-share-extension.jpeg ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── Intents │ ├── AddURLIntentHandler.swift │ └── Intents.intentdefinition ├── IntentsExtension │ ├── Info.plist │ ├── IntentHandler.swift │ └── UlryIntents.entitlements ├── SceneDelegate.swift ├── UIKit Extensions │ ├── +Color.swift │ ├── +Date.swift │ ├── +NSMutableAttributedString.swift │ ├── +Set.swift │ ├── +String.swift │ ├── +UIActivityViewController.swift │ ├── +UIAlertController.swift │ ├── +UIButton.swift │ ├── +UICellAccessory.swift │ ├── +UICollectionLayoutListConfiguration.swift │ ├── +UICollectionView.swift │ ├── +UIColor.swift │ ├── +UIFont.swift │ ├── +UIImage.swift │ ├── +UIScreen.swift │ ├── +UIViewController.swift │ ├── SwiftUICollectionViewCell.swift │ └── UICollectionViewCellCustomBackground.swift ├── Ulry.entitlements ├── Ulry.xcdatamodeld │ ├── .xccurrentversion │ └── Ulry.xcdatamodel │ │ └── contents ├── Urly.xcdatamodeld │ ├── .xccurrentversion │ └── Urly.xcdatamodel │ │ └── contents ├── ViewControllers │ ├── AddCategoryViewController.swift │ ├── AddLinkViewController.swift │ ├── ChangelogViewController.swift │ ├── GenericCollectionList │ │ └── GenericCollectionList.swift │ ├── Home │ │ ├── BackgroundSupplementaryView.swift │ │ ├── Cells │ │ │ ├── BouncyCollectionViewCell.swift │ │ │ ├── GroupCollectionViewCell.swift │ │ │ ├── HomeHeaderCollectionViewCell.swift │ │ │ ├── MainCategoryCollectionViewCell.swift │ │ │ └── TagCollectionViewCell.swift │ │ ├── HomeCollectionViewController.swift │ │ └── Layout.swift │ ├── Links │ │ ├── Cells │ │ │ ├── LinkCell.swift │ │ │ └── LinkCollectionViewCell.swift │ │ └── LinksTableViewController.swift │ ├── MultipleSelectionList.swift │ ├── OnboardingView.swift │ ├── SFSymbolsCollectionView.swift │ ├── Settings │ │ ├── AboutViewController.swift │ │ ├── AppIconsViewController.swift │ │ ├── AppearanceViewController.swift │ │ ├── BackupViewController.swift │ │ ├── GeneralSettingsViewController.swift │ │ ├── LinkCellAppearenceViewController.swift │ │ ├── SettingsViewController.swift │ │ ├── TipsViewController.swift │ │ └── URLRedirectorSettings.swift │ ├── SingleSelectionList.swift │ ├── SpinnerViewController.swift │ └── UlryInfoViewController.swift ├── Views │ ├── BackgroundImage.swift │ ├── CategoryTitleView.swift │ ├── EmptinessView.swift │ ├── FaviconHostnameView.swift │ ├── LinkDetailView.swift │ ├── LinkImagePreview.swift │ ├── SymbolCollectionViewCell.swift │ ├── UrlTextFieldAccessoryView.swift │ └── WeeklyAddedLinksGraph.swift └── changelog.json ├── UlryIntents └── Info.plist ├── UlryShareExtension ├── Base.lproj │ └── MainInterface.storyboard ├── Info.plist ├── ShareViewController.swift └── UlryShareExtension.entitlements └── UlryTests ├── +ColorsTest.swift └── Shared └── URLRedirectorTest.swift /Account/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Account", 8 | platforms: [.iOS(SupportedPlatform.IOSVersion.v15)], 9 | products: [ 10 | .library(name: "Account", type: .dynamic, targets: ["Account"]), 11 | ], 12 | dependencies: [ 13 | .package(path: "../LinksDatabase"), 14 | .package(path: "../Links"), 15 | .package(path: "../LinksMetadata") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "Account", 20 | dependencies: ["LinksDatabase", "Links", "LinksMetadata"] 21 | ), 22 | .testTarget( 23 | name: "AccountTests", 24 | dependencies: [ 25 | "Account", 26 | "Links" 27 | ] 28 | ) 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /Account/README.md: -------------------------------------------------------------------------------- 1 | # Account 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Account/Sources/Account/AccountManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 15/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class AccountManager { 12 | public static var shared: AccountManager! 13 | 14 | private let accountsFolder: String 15 | private var accountsDictionary = [String:Account]() 16 | 17 | public var activeAccounts: [Account] { 18 | precondition(Thread.isMainThread) 19 | return Array(accountsDictionary.values.filter { $0.isActive }) 20 | } 21 | 22 | public var accounts: [Account] { 23 | Array(accountsDictionary.values) 24 | } 25 | 26 | public init(accountsFolder: String) { 27 | self.accountsFolder = accountsFolder 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Account/Tests/AccountTests/DummyFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 07/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Mattia Righetti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Links/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Links", 8 | platforms: [.iOS(SupportedPlatform.IOSVersion.v15)], 9 | products: [ 10 | .library( 11 | name: "Links", 12 | type: .dynamic, 13 | targets: ["Links"] 14 | ), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/ccgus/fmdb", .upToNextMinor(from: "2.7.7")), 18 | ], 19 | targets: [ 20 | .target( 21 | name: "Links", 22 | dependencies: [ 23 | .product(name: "FMDB", package: "fmdb") 24 | ] 25 | ), 26 | .testTarget( 27 | name: "LinksTests", 28 | dependencies: [ 29 | "Links", 30 | .product(name: "FMDB", package: "fmdb") 31 | ] 32 | ), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /Links/README.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Links/Sources/Links/Extensions/+Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 07/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Color { 12 | public static func randomHexColorCode() -> String { 13 | let a = ["1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"]; 14 | return 15 | "#" 16 | .appending(a[Int(arc4random_uniform(15))]) 17 | .appending(a[Int(arc4random_uniform(15))]) 18 | .appending(a[Int(arc4random_uniform(15))]) 19 | .appending(a[Int(arc4random_uniform(15))]) 20 | .appending(a[Int(arc4random_uniform(15))]) 21 | .appending(a[Int(arc4random_uniform(15))]) 22 | } 23 | 24 | // MARK: - Initializers 25 | init(decimalRed red: Double, green: Double, blue: Double) { 26 | self.init(red: red / 255, green: green / 255, blue: blue / 255) 27 | } 28 | 29 | init?(hex: String) { 30 | var hexNormalized = hex.trimmingCharacters(in: .whitespacesAndNewlines) 31 | hexNormalized = hexNormalized.replacingOccurrences(of: "#", with: "") 32 | 33 | // Helpers 34 | var rgb: UInt64 = 0 35 | var r: CGFloat = 0.0 36 | var g: CGFloat = 0.0 37 | var b: CGFloat = 0.0 38 | var a: CGFloat = 1.0 39 | let length = hexNormalized.count 40 | 41 | // Create Scanner 42 | Scanner(string: hexNormalized).scanHexInt64(&rgb) 43 | 44 | if length == 6 { 45 | r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 46 | g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 47 | b = CGFloat(rgb & 0x0000FF) / 255.0 48 | 49 | } else if length == 8 { 50 | r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0 51 | g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0 52 | b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0 53 | a = CGFloat(rgb & 0x000000FF) / 255.0 54 | 55 | } else { 56 | return nil 57 | } 58 | 59 | let uiColor = UIColor(red: r, green: g, blue: b, alpha: a) 60 | self.init(uiColor) 61 | } 62 | 63 | var toHex: String? { 64 | // Extract Components 65 | guard let components = cgColor?.components, components.count >= 3 else { 66 | return nil 67 | } 68 | 69 | // Helpers 70 | let r = Float(components[0]) 71 | let g = Float(components[1]) 72 | let b = Float(components[2]) 73 | var a = Float(1.0) 74 | 75 | if components.count >= 4 { 76 | a = Float(components[3]) 77 | } 78 | 79 | // Create Hex String 80 | let hex = String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) 81 | 82 | return hex 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Links/Sources/Links/Extensions/+LPLinkMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 09/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import LinkPresentation 10 | 11 | public protocol LinkImageProvider { 12 | func getImageFileURL(for link: Link) -> URL? 13 | } 14 | 15 | public extension LPLinkMetadata { 16 | convenience init(_ link: Link, imageProvider: LinkImageProvider?) { 17 | self.init() 18 | self.url = URL(string: link.hostname) 19 | self.originalURL = URL(string: link.url) 20 | self.title = link.ogTitle 21 | if let imageUrl = imageProvider?.getImageFileURL(for: link) { 22 | self.imageProvider = NSItemProvider(contentsOf: imageUrl) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Links/Sources/Links/Group.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Group.swift 3 | // Urly 4 | // 5 | // Created by Mattia Righetti on 12/25/21. 6 | // Copyright © 2021 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | public final class Group: Hashable, Equatable { 13 | public var id: UUID 14 | public var name: String 15 | public var colorHex: String 16 | public var iconName: String 17 | public var links: Set? 18 | 19 | public init(id: UUID = UUID(), colorHex: String, iconName: String, name: String, links: Set? = nil) { 20 | self.id = id 21 | self.colorHex = colorHex 22 | self.iconName = iconName 23 | self.name = name 24 | self.links = links 25 | } 26 | 27 | public init?(from res: FMResultSet) { 28 | self.id = UUID(uuidString: res.string(forColumn: "id")!)! 29 | self.colorHex = res.string(forColumn: "color")! 30 | self.iconName = res.string(forColumn: "icon")! 31 | self.name = res.string(forColumn: "name")! 32 | } 33 | 34 | public static func == (lhs: Group, rhs: Group) -> Bool { 35 | lhs.id == rhs.id && lhs === rhs 36 | } 37 | 38 | public static func ===(lhs: Group, rhs: Group) -> Bool { 39 | lhs.name == rhs.name && 40 | lhs.colorHex == rhs.colorHex && 41 | lhs.iconName == rhs.iconName 42 | } 43 | 44 | public func hash(into hasher: inout Hasher) { 45 | hasher.combine(id) 46 | } 47 | } 48 | 49 | extension Group: Codable {} 50 | -------------------------------------------------------------------------------- /Links/Sources/Links/Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag.swift 3 | // Urly 4 | // 5 | // Created by Mattia Righetti on 12/25/21. 6 | // Copyright © 2021 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import FMDB 11 | 12 | public final class Tag: Hashable, Equatable { 13 | public var id: UUID 14 | public var name: String 15 | public var colorHex: String 16 | public var links: Set? = nil 17 | 18 | public init(id: UUID = UUID(), colorHex: String, name: String) { 19 | self.id = id 20 | self.colorHex = colorHex 21 | self.name = name 22 | } 23 | 24 | public init?(from res: FMResultSet) { 25 | self.id = UUID(uuidString: res.string(forColumn: "id")!)! 26 | self.colorHex = res.string(forColumn: "color")! 27 | self.name = res.string(forColumn: "name")! 28 | } 29 | 30 | public static func == (lhs: Tag, rhs: Tag) -> Bool { 31 | lhs.id == rhs.id && lhs === rhs 32 | } 33 | 34 | public static func ===(lhs: Tag, rhs: Tag) -> Bool { 35 | lhs.name == rhs.name && 36 | lhs.colorHex == rhs.colorHex 37 | } 38 | 39 | public func hash(into hasher: inout Hasher) { 40 | hasher.combine(id) 41 | } 42 | } 43 | 44 | extension Tag: Codable {} 45 | -------------------------------------------------------------------------------- /Links/Sources/Links/Utils/LinkActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 09/12/2022. 6 | // 7 | 8 | import UIKit 9 | import LinkPresentation 10 | 11 | public class LinkActivity: NSObject, UIActivityItemSource { 12 | var link: Link 13 | var imageProvider: LinkImageProvider 14 | 15 | public init(_ link: Link, imageProvider: LinkImageProvider) { 16 | self.link = link 17 | self.imageProvider = imageProvider 18 | } 19 | 20 | public func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { 21 | return link.hostname 22 | } 23 | 24 | public func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { 25 | return link.url 26 | } 27 | 28 | public func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { 29 | LPLinkMetadata(link, imageProvider: imageProvider) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Links/Tests/LinksTests/GroupsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 16/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Links 11 | 12 | class GroupsTests: XCTestCase { 13 | func testGroupEqual() { 14 | let group1 = Group(colorHex: "#aaffaa", iconName: "icon1", name: "name") 15 | let group2 = Group(colorHex: "#aaffaa", iconName: "icon1", name: "name") 16 | let group3 = Group(colorHex: "#aafafa", iconName: "icon1", name: "name") 17 | XCTAssertTrue(group1 === group2) 18 | XCTAssertFalse(group1 === group3) 19 | XCTAssertFalse(group2 === group3) 20 | } 21 | 22 | func testGroupToJson() { 23 | let group = Group(colorHex: "333333", iconName: "icon", name: "name") 24 | 25 | XCTAssertNoThrow { 26 | try JSONEncoder().encode(group) 27 | } 28 | 29 | let data = try! JSONEncoder().encode(group) 30 | print(String(data: data, encoding: .utf8)!) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Links/Tests/LinksTests/LinksTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Links 3 | 4 | final class LinksTests: XCTestCase { 5 | func testLinkEqual() { 6 | let link1 = Link(url: "https://example.com") 7 | let link2 = Link(url: "https://example.com") 8 | XCTAssertFalse(link1 === link2) 9 | 10 | let link3 = Link(url: "https://example.com") 11 | link3.colorHex = "#aaffaa" 12 | let link4 = Link(url: "https://example.com") 13 | link4.colorHex = "#aaffaa" 14 | XCTAssertTrue(link3 === link4) 15 | 16 | let tags1 = [Tag(colorHex: "#ffaaff", name: "tag1")] 17 | let tags2 = [Tag(colorHex: "#ffaaff", name: "tag2")] 18 | link3.tags = Set(tags1) 19 | XCTAssertFalse(link3 === link4) 20 | link4.tags = Set(tags2) 21 | XCTAssertFalse(link3 === link4) 22 | link3.tags = Set(tags2) 23 | XCTAssertTrue(link3 === link4) 24 | 25 | link3.group = Group(colorHex: "#aaffff", iconName: "icon", name: "name") 26 | XCTAssertFalse(link3 === link4) 27 | link3.group = nil 28 | XCTAssertTrue(link3 === link4) 29 | } 30 | 31 | func testLinkToJson() { 32 | let link = Link(url: "https://example.com", note: "some note") 33 | link.group = Group(colorHex: "312121", iconName: "icon", name: "name") 34 | link.tags = Set([ 35 | Tag(colorHex: "aaaaaa", name: "namea"), 36 | Tag(colorHex: "bbbbbb", name: "nameb") 37 | ]) 38 | 39 | XCTAssertNoThrow { 40 | try JSONEncoder().encode(link) 41 | } 42 | 43 | let data = try! JSONEncoder().encode(link) 44 | print(String(data: data, encoding: .utf8)!) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Links/Tests/LinksTests/TagsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 16/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Links 11 | 12 | class TagsTests: XCTestCase { 13 | func testTagEquals() { 14 | let tag1 = Tag(colorHex: "#aaffaa", name: "name1") 15 | let tag2 = Tag(colorHex: "#aaffaa", name: "name1") 16 | let tag3 = Tag(colorHex: "#aafaaa", name: "name1") 17 | let tag4 = Tag(colorHex: "#aaffaa", name: "name") 18 | XCTAssertTrue(tag1 === tag2) 19 | XCTAssertFalse(tag3 === tag2) 20 | XCTAssertFalse(tag3 === tag1) 21 | XCTAssertFalse(tag4 === tag2) 22 | XCTAssertFalse(tag4 === tag1) 23 | XCTAssertFalse(tag4 === tag3) 24 | } 25 | 26 | func testTagToJson() { 27 | let tag = Tag(colorHex: "333333", name: "name") 28 | 29 | XCTAssertNoThrow { 30 | try JSONEncoder().encode(tag) 31 | } 32 | 33 | let data = try! JSONEncoder().encode(tag) 34 | print(String(data: data, encoding: .utf8)!) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LinksDatabase/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LinksDatabase", 8 | platforms: [.iOS(SupportedPlatform.IOSVersion.v15)], 9 | products: [ 10 | .library( 11 | name: "LinksDatabase", 12 | type: .dynamic, 13 | targets: ["LinksDatabase"] 14 | ) 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/ccgus/fmdb", .upToNextMinor(from: "2.7.7")), 18 | .package(path: "../Links") 19 | ], 20 | targets: [ 21 | .target( 22 | name: "LinksDatabase", 23 | dependencies: [ 24 | .product(name: "FMDB", package: "fmdb"), 25 | "Links" 26 | ] 27 | ), 28 | .testTarget( 29 | name: "LinksDatabaseTests", 30 | dependencies: [ 31 | "LinksDatabase", 32 | .product(name: "FMDB", package: "fmdb") 33 | ] 34 | ) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /LinksDatabase/README.md: -------------------------------------------------------------------------------- 1 | # LinksDatabase 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /LinksDatabase/Sources/LinksDatabase/DatabaseTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseTable.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 06/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol DatabaseTable { 12 | var name: String { get } 13 | } 14 | -------------------------------------------------------------------------------- /LinksDatabase/Sources/LinksDatabase/Migrations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 25/01/2023. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Restructure of database tables, introduced primary keys instead of unique 13 | and removed use of foreign keys in favor of triggers 14 | 15 | This update will also load current links in the search virtual table 16 | */ 17 | let migration_1674682062 = """ 18 | CREATE TABLE IF NOT EXISTS category_copy (id text not null primary key, name varchar(50) not null, icon varchar(50) not null, color char(7) not null); 19 | INSERT INTO category_copy SELECT * FROM category; 20 | 21 | CREATE TABLE IF NOT EXISTS tag_copy (id text not null primary key, name varchar(50) not null, color char(7) not null); 22 | INSERT INTO tag_copy SELECT * FROM tag; 23 | 24 | CREATE TABLE IF NOT EXISTS link_copy (id text not null primary key, url text not null, starred bool not null default false, unread bool not null default true, note text, color char(7) not null, ogTitle text, ogDescription text, ogImageUrl text, created_at integer not null, updated_at integer not null); 25 | INSERT INTO link_copy SELECT * FROM link; 26 | 27 | CREATE TABLE IF NOT EXISTS category_link_copy (link_id text not null, category_id text not null, primary key (link_id, category_id)); 28 | INSERT INTO category_link_copy SELECT * FROM category_link; 29 | 30 | CREATE TABLE IF NOT EXISTS tag_link_copy (link_id text not null, tag_id text not null, primary key (link_id, tag_id)); 31 | INSERT INTO tag_link_copy SELECT * FROM tag_link; 32 | 33 | DROP TABLE if EXISTS category_link;DROP TABLE if EXISTS category;DROP TABLE if EXISTS tag;DROP TABLE if EXISTS link;DROP TABLE if EXISTS tag_link; 34 | ALTER TABLE category_link_copy RENAME TO category_link;ALTER TABLE category_copy RENAME TO category;ALTER TABLE tag_copy RENAME TO tag;ALTER TABLE link_copy RENAME TO link;ALTER TABLE tag_link_copy RENAME TO tag_link; 35 | 36 | INSERT INTO search SELECT id, url, ogTitle, ogDescription FROM link; 37 | INSERT INTO migrations (version) values (1674682062); 38 | 39 | ALTER TABLE link ADD COLUMN archived bool not null default false; 40 | 41 | CREATE TRIGGER if not EXISTS on_link_update_update_lookup AFTER UPDATE ON link BEGIN update search set title = NEW.ogTitle, url = NEW.url, description = NEW.ogDescription where id = NEW.id; END; 42 | CREATE TRIGGER if not EXISTS on_link_insert_add_lookup AFTER INSERT ON link BEGIN insert into search values (NEW.id, NEW.url, NEW.ogTitle, NEW.ogDescription); END; 43 | CREATE TRIGGER if not EXISTS on_link_delete_delete_from_tag_and_category AFTER DELETE ON link BEGIN delete from category_link where link_id = OLD.id; delete from tag_link where link_id = OLD.id; delete from search where id = OLD.id; END; 44 | CREATE TRIGGER if not EXISTS on_category_delete AFTER DELETE ON category BEGIN delete from category_link where category_id = OLD.id; END; 45 | CREATE TRIGGER if not EXISTS on_tag_delete AFTER DELETE ON tag BEGIN delete from tag_link where tag_id = OLD.id; END; 46 | """ 47 | 48 | let migrations: [Int32:String] = [ 49 | 1674682062: migration_1674682062 50 | ] 51 | 52 | func getMigrations(lastrun: Int32) -> [String] { 53 | Array(migrations.filter { i, stmt in i > lastrun }.map { $1 }) 54 | } 55 | -------------------------------------------------------------------------------- /LinksDatabase/Sources/LinksDatabase/MigrationsTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 26/01/2023. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import FMDB 10 | import Foundation 11 | 12 | final class MigrationsTable: DatabaseTable { 13 | let name: String 14 | private let queue: DatabaseQueue 15 | 16 | init(name: String, queue: DatabaseQueue) { 17 | self.name = name 18 | self.queue = queue 19 | } 20 | 21 | func fetchLastRunMigration() throws -> Int32? { 22 | return try fetchSingleGeneric { database in 23 | guard let resultSet = database.executeQuery("select max(version) as version from migrations;", withArgumentsIn: []) else { return nil } 24 | if resultSet.next() { 25 | return resultSet.int(forColumn: "version") 26 | } 27 | return nil 28 | } 29 | } 30 | 31 | private func fetchSingleGeneric(_ fetchMethod: @escaping ((FMDatabase) -> T?)) throws -> T? { 32 | var t: T? = nil 33 | var error: DatabaseError? = nil 34 | 35 | queue.runInDatabaseSync { databaseResult in 36 | if case .failure(let databaseError) = databaseResult { 37 | error = databaseError 38 | } 39 | 40 | if case .success(let database) = databaseResult { 41 | t = fetchMethod(database) 42 | } 43 | } 44 | 45 | if let error = error { 46 | throw(error) 47 | } 48 | 49 | return t 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LinksDatabase/Sources/LinksDatabase/OrderBy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderBy.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 7/6/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum OrderBy: String { 12 | case name 13 | case lastUpdated 14 | case oldest 15 | case newest 16 | 17 | var orderByClause: (String, String) { 18 | switch self { 19 | case .name: 20 | return ("ogTitle", "asc") 21 | case .lastUpdated: 22 | return ("updated_at", "desc") 23 | case .oldest: 24 | return ("created_at", "asc") 25 | case .newest: 26 | return ("created_at", "desc") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LinksDatabase/Sources/LinksDatabase/SQLUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 15/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - SQL 12 | 13 | func _sql_select(_ s: String, from: String, where: String?, orderBy: (String, String)?) -> String { 14 | var query = "select \(s) from \(from)" 15 | if let w = `where` { 16 | query += " where " + w 17 | } 18 | if let oc = orderBy { 19 | query += " order by \(oc.0) collate nocase \(oc.1)" 20 | } 21 | return query 22 | } 23 | 24 | func _sql_delete(from: String, where: String?) -> String { 25 | var query = "delete from \(from)" 26 | if let w = `where` { 27 | query += " where \(w)" 28 | } 29 | return query 30 | } 31 | 32 | func _sql_insert(into table: String, fields: [String]) -> String { 33 | return "insert into \(table) (" + fields.joined(separator: ",") + ") values (" + [String](repeating: "?", count: fields.count).joined(separator: ",") + ")" 34 | } 35 | 36 | func _sql_update(_ table: String, fields: [String], where: String) -> String { 37 | guard fields.count > 0 else { fatalError("update function must have at least a sql field to update") } 38 | 39 | var query = "update \(table) set " + fields.map { "\($0) = ?" }.joined(separator: ",") 40 | query += " where \(`where`)" 41 | return query 42 | } 43 | -------------------------------------------------------------------------------- /LinksDatabase/Tests/LinksDatabaseTests/SQLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 15/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import LinksDatabase 11 | 12 | final class SQLTests: XCTestCase { 13 | func testSelectQueryGenerator() { 14 | XCTAssertEqual("select * from table", _sql_select("*", from: "table", where: nil, orderBy: nil)) 15 | XCTAssertEqual("select * from table where id = 4", _sql_select("*", from: "table", where: "id = 4", orderBy: nil)) 16 | XCTAssertEqual("select * from table where id = 4 order by val collate nocase desc", _sql_select("*", from: "table", where: "id = 4", orderBy: ("val", "desc"))) 17 | XCTAssertEqual("select * from table order by o collate nocase asc", _sql_select("*", from: "table", where: nil, orderBy: ("o", "asc"))) 18 | } 19 | 20 | func testUpdateQueryGenerator() { 21 | XCTAssertEqual( 22 | "update link set url = ?,starred = ?,unread = ?,color = ?,updated_at = ?,ogTitle = ?,ogImageUrl = ?,ogDescription = ? where id = ?", 23 | _sql_update("link", fields: ["url", "starred", "unread", "color", "updated_at", "ogTitle", "ogImageUrl", "ogDescription"], where: "id = ?") 24 | ) 25 | } 26 | 27 | func testInsertQueryGenerator() { 28 | XCTAssertEqual("insert into link (first,second,third) values (?,?,?)", _sql_insert(into: "link", fields: ["first", "second", "third"])) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LinksMetadata/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /LinksMetadata/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LinksMetadata", 8 | platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_13), .iOS(SupportedPlatform.IOSVersion.v15)], 9 | products: [ 10 | .library(name: "LinksMetadata", targets: ["LinksMetadata"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.2"), 14 | .package(path: "../Links") 15 | ], 16 | targets: [ 17 | .target( 18 | name: "LinksMetadata", 19 | dependencies: [ 20 | .product(name: "Kanna", package: "kanna"), 21 | "Links" 22 | ], 23 | linkerSettings: [ 24 | .unsafeFlags(["-Xlinker", "-no_application_extension"]) 25 | ] 26 | ), 27 | .testTarget(name: "LinksMetadataTests", dependencies: ["LinksMetadata"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /LinksMetadata/README.md: -------------------------------------------------------------------------------- 1 | # LinksMetadata 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /LinksMetadata/Sources/LinksMetadata/HTMLParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mattia Righetti on 18/03/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Kanna 11 | 12 | 13 | public class HTMLParser { 14 | private let document: HTMLDocument 15 | 16 | public init?(html: String) { 17 | guard let document = try? Kanna.HTML(html: html, encoding: .utf8) else { return nil } 18 | self.document = document 19 | } 20 | 21 | /// Returns content of `` tag 22 | func contentFromMetatag(metatag: String) -> String? { 23 | guard let content = document.head?.xpath(xpathForMetatag(metatag)).first?["content"] else { return nil } 24 | return content.isEmpty ? nil : content 25 | } 26 | 27 | /// Returns page ``, null in case none is found 28 | func pageTitle() -> String? { 29 | return document.title 30 | } 31 | 32 | /// Returns content of an HTML tag located in `<head>` document section 33 | /// 34 | /// This is a sample HTML string 35 | /// ``` 36 | /// <head> 37 | /// <title>Some Title 38 | /// ... 39 | /// 40 | /// ``` 41 | /// 42 | /// calling `contentFromTag(tag: "title")` will return `Some Title` 43 | func contentFromTag(tag: String) -> String? { 44 | return document.head?.xpath("//\(tag)").first?.text 45 | } 46 | 47 | private func xpathForMetatag(_ metatag: String) -> String { 48 | return "//meta[@property='\(metatag)'] | //meta[@name='\(metatag)']" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LinksMetadata/Sources/LinksMetadata/OpenGraph.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matt on 13/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol OpenGraph { 12 | var url: String? { get } 13 | var ogImageUrl: String? { get set } 14 | var ogTitle: String? { get set } 15 | var ogDescription: String? { get set } 16 | var ogSiteName: String? { get set } 17 | } 18 | 19 | public struct DefaultOpenGraphData: OpenGraph { 20 | public var url: String? 21 | public var ogImageUrl: String? 22 | public var ogTitle: String? 23 | public var ogDescription: String? 24 | public var ogSiteName: String? 25 | 26 | public init?(html: String) { 27 | guard let parser = HTMLParser(html: html) else { return nil } 28 | url = parser.contentFromMetatag(metatag: "og:url") 29 | ogTitle = parser.contentFromMetatag(metatag: "og:title") ?? parser.pageTitle() ?? nil 30 | ogImageUrl = parser.contentFromMetatag(metatag: "og:image") 31 | ogDescription = parser.contentFromMetatag(metatag: "og:description") 32 | ogSiteName = parser.contentFromMetatag(metatag: "og:site_name") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LinksMetadata/Tests/LinksMetadataTests/HTMLParserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Mattia Righetti on 20/03/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import LinksMetadata 11 | 12 | private let urls: [URL] = [ 13 | "https://www.avanderlee.com/swift/operations/", 14 | "https://www.swiftbysundell.com/podcast/121/", 15 | "https://www.rfc-editor.org/rfc/rfc6762#page-5", 16 | "https://matheducators.stackexchange.com/questions/7985/solving-linear-equations-by-factoring", 17 | ] 18 | .map { URL(string: $0)! } 19 | 20 | let html1 = """ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Hacker News 29 | Content 30 | 31 | 32 | 33 | """ 34 | 35 | final class HTMLParserTests: XCTestCase { 36 | func testTitleUrl1() throws { 37 | let html = try String(contentsOf: urls[2]) 38 | let parser = HTMLParser(html: html)! 39 | XCTAssertEqual(nil, parser.contentFromMetatag(metatag: "og:url")) 40 | XCTAssertEqual("RFC 6762: Multicast DNS", parser.pageTitle()!) 41 | } 42 | 43 | func testTitleUrl2() throws { 44 | let html = try String(contentsOf: urls[3]) 45 | let parser = HTMLParser(html: html)! 46 | XCTAssertEqual("https://matheducators.stackexchange.com/questions/7985/solving-linear-equations-by-factoring", parser.contentFromMetatag(metatag: "og:url")!) 47 | XCTAssertEqual("secondary education - Solving linear equations by factoring - Mathematics Educators Stack Exchange", parser.pageTitle()!) 48 | } 49 | 50 | func testTagUrl3() throws { 51 | let parser = HTMLParser(html: html1) 52 | XCTAssertEqual("Content", parser?.contentFromTag(tag: "sometag")) 53 | XCTAssertEqual("Hacker News", parser?.contentFromTag(tag: "title")) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LinksMetadata/Tests/LinksMetadataTests/LinksMetadataTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import LinksMetadata 3 | 4 | private let urls: [URL] = [ 5 | "https://www.avanderlee.com/swift/operations/", 6 | "https://www.swiftbysundell.com/podcast/121/", 7 | "https://www.rfc-editor.org/rfc/rfc6762#page-5", 8 | "https://stackoverflow.com/questions/75790728/how-to-write-list-of-json-string-to-json-file-in-java" 9 | ] 10 | .map { URL(string: $0)! } 11 | 12 | final class LinksMetadataTests: XCTestCase { 13 | let queue = LinkDataQueue(headerFields: ["User-Agent":"Ulry"]) 14 | 15 | func testWebsite1() throws { 16 | let html = try String(contentsOf: urls[0]) 17 | guard let og = DefaultOpenGraphData(html: html) else { XCTFail(); return } 18 | XCTAssertEqual(Optional("Getting started with Operations and OperationQueues in Swift"), og.ogTitle) 19 | XCTAssertEqual(Optional("Get the most out of operations and the OperationQueue in Swift. Separate concern, add dependencies, track progress and completion with custom operations."), og.ogDescription) 20 | XCTAssertEqual(Optional("SwiftLee"), og.ogSiteName) 21 | XCTAssertEqual(Optional("https://www.avanderlee.com/swift/operations/"), og.url) 22 | XCTAssertEqual(Optional("https://swiftlee-banners.herokuapp.com/imagegenerator.php?title=Getting+started+with+Operations+and+OperationQueues+in+Swift"), og.ogImageUrl) 23 | } 24 | 25 | func testWebsite2() throws { 26 | let html = try String(contentsOf: urls[1]) 27 | guard let og = DefaultOpenGraphData(html: html) else { XCTFail(); return } 28 | XCTAssertEqual(Optional("121: “Responsive and smooth UIs”, with special guest Adam Bell | Swift by Sundell"), og.ogTitle) 29 | XCTAssertEqual(Optional("Adam Bell returns to the podcast to discuss different techniques and approaches for optimizing UI code, and how to utilize tools like animations in order to build iOS apps that feel fast and responsive."), og.ogDescription) 30 | XCTAssertEqual(Optional("Swift by Sundell"), og.ogSiteName) 31 | XCTAssertEqual(Optional("https://www.swiftbysundell.com/podcast/121"), og.url) 32 | XCTAssertEqual(Optional("https://www.swiftbysundell.com/images/podcast/121.png"), og.ogImageUrl) 33 | XCTAssertEqual(Optional("121: “Responsive and smooth UIs”, with special guest Adam Bell | Swift by Sundell"), og.ogTitle) 34 | } 35 | 36 | func testWebsite3() throws { 37 | let html = try String(contentsOf: urls[2]) 38 | guard let og = DefaultOpenGraphData(html: html) else { XCTFail(); return } 39 | XCTAssertEqual(Optional("RFC 6762: Multicast DNS"), og.ogTitle) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ulry 2 | Lightweight and fast read-it-later app for iOS 3 | 4 |

5 | 6 | 7 | 8 |

9 | 10 | ### Download 11 | 12 | Ulry is [available on the App Store](https://apps.apple.com/it/app/ulry/id1603982621?l=en) for iOS. 13 | -------------------------------------------------------------------------------- /Shared/Model/AppData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppData.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 21/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct AppData { 12 | /// String version of the app, i.e. `0.4.1` 13 | public static var appVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String 14 | /// Build version of the app, i.e. `64` 15 | public static var appBundleVersion: String = Bundle.main.infoDictionary?["CFBundleVersion"] as! String 16 | 17 | public static var userAgentSignature: String = "Ulry/\(appVersion)(\(appBundleVersion)" 18 | 19 | public static var supabaseApiKey: String { 20 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpnc295dmR2emRpd3lhdmN1dW9zIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODE5ODQ1NjAsImV4cCI6MTk5NzU2MDU2MH0.oeU3VqoeRrvsw6Oo5bb22hHqxJKa11tsiz6Sen_IcD8" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Shared/Model/AppIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIcon.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/27/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum AppIcon: String, CaseIterable, Hashable { 12 | case `default` = "AppIcon" 13 | case dark_red = "dark_red" 14 | case light_black = "light_black" 15 | case light_blue = "light_blue" 16 | case light_orange = "light_orange" 17 | 18 | static var currentAppIcon: AppIcon { 19 | guard let name = UIApplication.shared.alternateIconName else { return .default } 20 | 21 | guard let appIcon = AppIcon(rawValue: name) else { 22 | fatalError("Provided unknown app icon value") 23 | } 24 | 25 | return appIcon 26 | } 27 | 28 | var thumbnail: UIImage { 29 | if self == .default { 30 | return UIImage(named: "thumb-default")! 31 | } else { 32 | return UIImage(named: "thumb-" + self.rawValue)! 33 | } 34 | } 35 | 36 | var title: String { 37 | switch self { 38 | case .default: 39 | return "Default" 40 | case .light_black: 41 | return "Light Black" 42 | case .light_blue: 43 | return "Light Blue" 44 | case .dark_red: 45 | return "Dark Red" 46 | case .light_orange: 47 | return "Light Orange" 48 | } 49 | } 50 | 51 | var subtitle: String? { 52 | switch self { 53 | case .default: 54 | return nil 55 | case .dark_red, .light_black, .light_blue, .light_orange: 56 | return nil 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Shared/Model/Category.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Category.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/9/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Links 10 | import UIKit 11 | 12 | struct CategoryCellContent { 13 | var title: String 14 | var backgroundColor: UIColor 15 | var icon: String? 16 | 17 | init(title: String, backgroundColor: UIColor, icon: String? = nil) { 18 | self.title = title 19 | self.backgroundColor = backgroundColor 20 | self.icon = icon 21 | } 22 | } 23 | 24 | public enum Category: Hashable { 25 | case all 26 | case unread 27 | case starred 28 | case archived 29 | case group(Group) 30 | case tag(Tag) 31 | 32 | public init?(rawValue: (String, UIColor, String?)) { 33 | return nil 34 | } 35 | 36 | var cellContent: CategoryCellContent { 37 | switch self { 38 | case .all: 39 | return CategoryCellContent(title: "All", backgroundColor: .init(hex: "D46C4E")!, icon: "list.bullet") 40 | case .unread: 41 | return CategoryCellContent(title: "Unread", backgroundColor: .init(hex: "CCABD8")!, icon: "archivebox") 42 | case .starred: 43 | return CategoryCellContent(title: "Starred", backgroundColor: .init(hex: "FFCE35")!, icon: "star") 44 | case .archived: 45 | return CategoryCellContent(title: "Archived", backgroundColor: .lightGray, icon: "tray") 46 | case .group(let group): 47 | return CategoryCellContent(title: group.name, backgroundColor: UIColor(hex: group.colorHex)!, icon: group.iconName) 48 | case .tag(let tag): 49 | return CategoryCellContent(title: tag.name, backgroundColor: UIColor(hex: tag.colorHex)!) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Shared/SharedExtension/ExtensionsAddLinkRequests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensionsContainer.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 27/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct ExtensionsAddLinkRequests: Codable { 12 | let url: String 13 | let note: String? 14 | let date: Int 15 | 16 | init(url: String, note: String?) { 17 | self.url = url 18 | self.note = note 19 | self.date = Int(Date().timeIntervalSince1970) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Shared/Utils/+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +Data.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 08/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | /// String value representing the data size in bytes 13 | var sizeString: String { 14 | let bcf = ByteCountFormatter() 15 | bcf.allowedUnits = [.useAll] 16 | bcf.countStyle = .file 17 | return bcf.string(fromByteCount: Int64(self.count)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Shared/Utils/+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +Date.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 27/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Date { 12 | func from(unix: Int) -> Date? { 13 | return Date() 14 | } 15 | 16 | func toUnixTime() { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Shared/Utils/+FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +FileManager.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 03/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import os 10 | import Foundation 11 | 12 | enum FileManagerError: Error { 13 | case fileIsPresent 14 | } 15 | 16 | extension FileManager { 17 | func secureCopyItem(at srcURL: URL, to dstURL: URL) -> Bool { 18 | do { 19 | if FileManager.default.fileExists(atPath: dstURL.path) { 20 | os_log(.error, "file is already present at \(dstURL.path), removing object so that new one can by copied over") 21 | throw FileManagerError.fileIsPresent 22 | } 23 | 24 | try FileManager.default.copyItem(at: srcURL, to: dstURL) 25 | } catch (let error) { 26 | os_log(.error, "Cannot copy item at \(srcURL) to \(dstURL): \(error)") 27 | return false 28 | } 29 | 30 | return true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Shared/Utils/+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +URL.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/12/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension URL { 12 | /// Returns a URL for the given app group and database pointing to the sqlite database. 13 | static func storeURL(for appGroup: String, folders: [String]? = nil, filename: String) -> URL { 14 | guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { 15 | fatalError("Shared file container could not be created.") 16 | } 17 | 18 | var path: URL = fileContainer 19 | if let folders = folders { 20 | for folder in folders { 21 | path = path.appendingPathExtension(folder) 22 | } 23 | } 24 | return path.appendingPathComponent(filename) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Shared/Utils/+UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UserDefaults.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/22/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | 13 | func save(customObject object: T, inKey key: String) { 14 | let encoder = JSONEncoder() 15 | if let encoded = try? encoder.encode(object) { 16 | self.set(encoded, forKey: key) 17 | } 18 | } 19 | 20 | func retrieve(object type:T.Type, fromKey key: String) -> T? { 21 | if let data = self.data(forKey: key) { 22 | let decoder = JSONDecoder() 23 | if let object = try? decoder.decode(type, from: data) { 24 | return object 25 | } else { 26 | print("Couldnt decode object") 27 | return nil 28 | } 29 | } else { 30 | print("Couldn't find key") 31 | return nil 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Shared/Utils/AppReviewManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppReviewRequest.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 11/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import StoreKit 11 | 12 | struct AppReviewManager: Logging { 13 | private let appId = "1603982621" 14 | 15 | private func isReviewActive() async -> Bool { 16 | var components = URLComponents() 17 | components.scheme = "https" 18 | components.host = "zgsoyvdvzdiwyavcuuos.functions.supabase.co" 19 | components.path = "/isAppPublished" 20 | 21 | guard let url = components.url else { return false } 22 | 23 | var request = URLRequest(url: url) 24 | request.setValue("Bearer \(AppData.supabaseApiKey)", forHTTPHeaderField: "Authorization") 25 | request.setValue(AppData.userAgentSignature, forHTTPHeaderField: "User-Agent") 26 | request.timeoutInterval = 2 27 | 28 | do { 29 | let (data, _) = try await URLSession.shared.data(for: request) 30 | let res = try JSONDecoder().decode(Bool.self, from: data) 31 | return res 32 | } catch { 33 | logger.debug("There was an error contacting Ulry APIs: \(error)") 34 | return false 35 | } 36 | } 37 | 38 | func openAppStoreForReview() { 39 | guard let writeReviewURL = URL(string: "https://apps.apple.com/app/id\(appId)?action=write-review") 40 | else { fatalError("Expected a valid URL") } 41 | 42 | UIApplication.shared.open(writeReviewURL, options: [:], completionHandler: nil) 43 | } 44 | 45 | let minimumReviewWorthyActionCount = 30.0 46 | 47 | func registerReviewWorthyAction(weighted: Double = 1.0) async { 48 | var actionCount: Double = UserDefaultsWrapper().get(key: .reviewWorthyActionCount) 49 | actionCount += 1.0 * weighted 50 | UserDefaultsWrapper().set(actionCount, forKey: .reviewWorthyActionCount) 51 | } 52 | 53 | public func requestReviewIfAppropriate(in view: UIView) async { 54 | let actionCount: Double = UserDefaultsWrapper().get(key: .reviewWorthyActionCount) 55 | 56 | guard actionCount >= minimumReviewWorthyActionCount else { 57 | return 58 | } 59 | 60 | let bundleVersionKey = kCFBundleVersionKey as String 61 | let currentVersion = Bundle.main.object(forInfoDictionaryKey: bundleVersionKey) as? String 62 | let lastVersion: String? = UserDefaultsWrapper().optionalGet(key: .lastReviewRequestAppVersion) 63 | 64 | guard lastVersion == nil || lastVersion != currentVersion else { 65 | return 66 | } 67 | 68 | if let windowScene = await view.window?.windowScene { 69 | await SKStoreReviewController.requestReview(in: windowScene) 70 | } 71 | 72 | UserDefaultsWrapper().set(0, forKey: .reviewWorthyActionCount) 73 | UserDefaultsWrapper().set(currentVersion, forKey: .lastReviewRequestAppVersion) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Shared/Utils/IAPManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IAPManager.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 3/2/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import StoreKit 11 | 12 | final class IAPManager { 13 | static let shared = IAPManager() 14 | var task: Task? 15 | 16 | init() { 17 | task = listenForStoreKitUpdates() 18 | } 19 | 20 | enum ProductsIdentifiers: String, CaseIterable { 21 | case coffee = "coffee_tip" 22 | case donut = "donut_tip" 23 | case pizza = "pizza_tip" 24 | case `super` = "super_tip" 25 | 26 | var value: String { 27 | return "com.mattrighetti.Ulry.iap" + "." + self.rawValue 28 | } 29 | } 30 | 31 | func fetchProducts() async throws -> [Product] { 32 | let products = try await Product.products(for: [ 33 | ProductsIdentifiers.coffee.value, 34 | ProductsIdentifiers.donut.value, 35 | ProductsIdentifiers.pizza.value, 36 | ProductsIdentifiers.super.value 37 | ]) 38 | 39 | return products 40 | } 41 | 42 | func purchase(_ product: Product) async throws -> Transaction { 43 | let result = try await product.purchase() 44 | 45 | switch result { 46 | case .pending: 47 | throw Product.PurchaseError.purchaseNotAllowed 48 | case .success(let verification): 49 | switch verification { 50 | case .verified(let transaction): 51 | await transaction.finish() 52 | return transaction 53 | case .unverified: 54 | throw Product.PurchaseError.invalidOfferSignature 55 | } 56 | case .userCancelled: 57 | throw Product.PurchaseError.purchaseNotAllowed 58 | @unknown default: 59 | assertionFailure("Unexpected result") 60 | throw Product.PurchaseError.purchaseNotAllowed 61 | } 62 | } 63 | 64 | func listenForStoreKitUpdates() -> Task { 65 | return Task.detached { 66 | for await result in Transaction.updates { 67 | switch result { 68 | case .verified(let transaction): 69 | print("Transaction verified in listener") 70 | await transaction.finish() 71 | case .unverified: 72 | print("Transaction unverified") 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Shared/Utils/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 27/11/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import os.log 11 | 12 | 13 | /// The `Logging` Protocol provides a convenient way to 14 | /// log your app's behaviour. 15 | /// 16 | /// Types that conform to the `Logging` protocol 17 | /// have access to a [`Logger`](https://developer.apple.com/documentation/os/logger) 18 | /// variable or static variable. 19 | /// The `logger` can be used to log messages about the 20 | /// app's behaviour. 21 | @available(macOS 11, *) 22 | @available(macOSApplicationExtension 11, *) 23 | @available(iOS 14, *) 24 | @available(iOSApplicationExtension 14, *) 25 | public protocol Logging { 26 | 27 | var logger: Logger { get } 28 | static var logger: Logger { get } 29 | 30 | } 31 | 32 | @available(macOS 11, *) 33 | @available(macOSApplicationExtension 11, *) 34 | @available(iOS 14, *) 35 | @available(iOSApplicationExtension 14, *) 36 | public extension Logging { 37 | 38 | var logger: Logger { 39 | Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: type(of: self))) 40 | } 41 | 42 | static var logger: Logger { 43 | Logger(subsystem: Bundle.main.bundleIdentifier!, category: String(describing: type(of: self))) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Shared/Utils/Paths.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Paths.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 04/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Paths { 12 | #if os(iOS) 13 | public static let dataFolder: URL = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /Shared/Utils/Tasks/MoveToProtectedContainerTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MoveToProtectedContainerTask.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 16/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MoveToProtectedContainerTask: StartupTask, Logging { 12 | var runOnce: Bool = true 13 | var key: String = "MoveToProtectedContainerTask" 14 | 15 | // MARK: - Current 16 | 17 | private let srcRootFolder: URL = { 18 | FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 19 | }() 20 | 21 | private var currentDbPath: URL { 22 | srcRootFolder.appendingPathComponent("ulry").appendingPathExtension("sqlite") 23 | } 24 | 25 | private var currentImageFolderPath: URL { 26 | srcRootFolder.appendingPathComponent("images") 27 | } 28 | 29 | // MARK: - Destination 30 | 31 | private let destRootFolder: URL = { 32 | FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 33 | }() 34 | 35 | private var destDbPath: URL { 36 | destRootFolder.appendingPathComponent("ulry").appendingPathExtension("sqlite") 37 | } 38 | 39 | private var destImageFolderPath: URL { 40 | destRootFolder.appendingPathComponent("images") 41 | } 42 | 43 | // MARK: - Task 44 | 45 | public func execTask() throws { 46 | logger.debug("app folder is: \(FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!)") 47 | 48 | guard FileManager.default.fileExists(atPath: currentDbPath.path) else { 49 | logger.info("no database in document folder, killing task") 50 | return 51 | } 52 | logger.info("file exists at \(currentDbPath.path), moving operation about to start...") 53 | 54 | try createNewFolder(at: destRootFolder.path) 55 | try createNewFolder(at: destImageFolderPath.path) 56 | 57 | guard FileManager.default.secureCopyItem(at: currentDbPath, to: destDbPath) else { 58 | logger.error("cannot copy database, encountered error") 59 | fatalError() 60 | } 61 | 62 | guard let imageFiles = try? FileManager.default.contentsOfDirectory(atPath: currentImageFolderPath.path) else { 63 | logger.info("image folder does not exist, skipping copy of image folder") 64 | return 65 | } 66 | 67 | var failed = [String]() 68 | for imageFile in imageFiles { 69 | let src = currentImageFolderPath.appendingPathComponent(imageFile) 70 | let dst = destImageFolderPath.appendingPathComponent(imageFile) 71 | if !FileManager.default.secureCopyItem(at: src, to: dst) { 72 | failed.append(imageFile) 73 | } 74 | } 75 | 76 | if failed.count > 0 { 77 | logger.error("\(failed.count) images could not be copied to new destination: \(failed)") 78 | } 79 | } 80 | 81 | private func createNewFolder(at path: String) throws { 82 | if !FileManager.default.fileExists(atPath: path) { 83 | logger.info("creating application support folder at \(path)") 84 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false) 85 | } else { 86 | logger.debug("folder at \(path) is already present, unless this is a dev device this should not happen") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Shared/Utils/Tasks/RegisterInitialValuesForUserDefaultsStartupTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegisterInitialValuesForUserDefaultsStartupTask.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 16/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct RegisterInitialValuesForUserDefaultsStartupTask: StartupTask { 12 | var runOnce: Bool = false 13 | var key: String = "RegisterInitialValuesForUserDefaultsStartupTask" 14 | 15 | func execTask() throws { 16 | UserDefaultsWrapper().registerDefaults() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Shared/Utils/Tasks/StartupTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartupTask.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 03/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import os 10 | import Foundation 11 | 12 | public protocol StartupTask { 13 | var runOnce: Bool { get } 14 | var key: String { get } 15 | 16 | func execTask() throws 17 | } 18 | 19 | extension StartupTask { 20 | func run() { 21 | guard shouldRun() else { return } 22 | os_log(.info, "running task: \(key)") 23 | 24 | do { 25 | try execTask() 26 | } catch { 27 | fatalError("[TASK][ERROR] -> [\(key)] failed because: \(error)") 28 | } 29 | 30 | setRun() 31 | } 32 | 33 | func shouldRun() -> Bool { 34 | // If not runOnce then always run 35 | !runOnce || 36 | // If runOnce, run only if it hasn't been run before 37 | (runOnce && !UserDefaults(suiteName: "com.mattrighetti.Ulry.startup-tasks")!.bool(forKey: key)) 38 | } 39 | 40 | func setRun() { 41 | if runOnce { 42 | UserDefaults(suiteName: "com.mattrighetti.Ulry.startup-tasks")!.set(true, forKey: key) 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Shared/Utils/ThemeController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeController.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/21/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Theme: String { 12 | case light 13 | case dark 14 | case system 15 | 16 | var uiInterfaceStyle: UIUserInterfaceStyle { 17 | switch self { 18 | case .light: 19 | return .light 20 | case .dark: 21 | return .dark 22 | case .system: 23 | return .unspecified 24 | } 25 | } 26 | } 27 | 28 | class ThemeController: NSObject { 29 | private(set) lazy var currentTheme = loadTheme() 30 | private let defaults: UserDefaults 31 | private let defaultsKey = "theme" 32 | var handler: ((UIUserInterfaceStyle) -> Void)? 33 | 34 | init(defaults: UserDefaults = .standard) { 35 | self.defaults = defaults 36 | super.init() 37 | self.defaults.addObserver(self, forKeyPath: DefaultsKey.theme.key, options: [.old, .new], context: nil) 38 | } 39 | 40 | func changeTheme(to theme: Theme) { 41 | currentTheme = theme 42 | defaults.setValue(theme.rawValue, forKey: defaultsKey) 43 | } 44 | 45 | private func loadTheme() -> Theme { 46 | let rawValue = defaults.string(forKey: defaultsKey) 47 | return rawValue.flatMap(Theme.init) ?? .light 48 | } 49 | 50 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 51 | guard 52 | handler != nil, 53 | let change = change, 54 | object != nil, 55 | keyPath == DefaultsKey.theme.key, 56 | let themeValue = change[.newKey] as? String, 57 | let theme = Theme(rawValue: themeValue)?.uiInterfaceStyle 58 | else { return } 59 | 60 | handler!(theme) 61 | } 62 | 63 | deinit { 64 | UserDefaults.standard.removeObserver(self, forKeyPath: DefaultsKey.theme.key, context: nil) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Ulry.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Ulry.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Ulry.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "fmdb", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/ccgus/fmdb", 7 | "state" : { 8 | "revision" : "61e51fde7f7aab6554f30ab061cc588b28a97d04", 9 | "version" : "2.7.7" 10 | } 11 | }, 12 | { 13 | "identity" : "kanna", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/tid-kijyun/Kanna.git", 16 | "state" : { 17 | "revision" : "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", 18 | "version" : "5.2.7" 19 | } 20 | }, 21 | { 22 | "identity" : "lottie-ios", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/airbnb/lottie-ios", 25 | "state" : { 26 | "revision" : "a4622b4d6fdbe76cc5487bc27e84029abaf07217", 27 | "version" : "4.0.1" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Ulry.xcodeproj/xcshareddata/xcschemes/Ulry.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Ulry.xcodeproj/xcshareddata/xcschemes/UlryTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 28 | 34 | 35 | 36 | 38 | 44 | 45 | 46 | 48 | 54 | 55 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 78 | 79 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/AddToUlryAction.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.mattrighetti.Ulry 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/AddToUlryActionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionViewController.swift 3 | // AddToUlryAction 4 | // 5 | // Created by Mattia Righetti on 1/10/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import os 10 | import UIKit 11 | import SwiftUI 12 | import CoreData 13 | import MobileCoreServices 14 | import UniformTypeIdentifiers 15 | 16 | class ActionViewController: UIViewController { 17 | 18 | let file = ExtensionsAddLinkRequestsManager() 19 | 20 | lazy var titleLabel: UILabel = { 21 | let label = UILabel() 22 | label.text = "Fetching data..." 23 | label.font = UIFont.rounded(ofSize: 18, weight: .bold) 24 | label.translatesAutoresizingMaskIntoConstraints = false 25 | return label 26 | }() 27 | 28 | lazy var image: UIImageView = { 29 | let imageview = UIImageView() 30 | imageview.image = UIImage(named: "AppIcon") 31 | imageview.layer.cornerRadius = 35 32 | imageview.clipsToBounds = true 33 | imageview.contentMode = .scaleToFill 34 | imageview.translatesAutoresizingMaskIntoConstraints = false 35 | return imageview 36 | }() 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | navigationItem.title = "Action" 42 | 43 | let extensionItem = extensionContext?.inputItems.first as! NSExtensionItem 44 | let itemProvider = (extensionItem.attachments?.first)! as NSItemProvider 45 | 46 | let propertyList = String(describing: UTType.propertyList) 47 | if itemProvider.hasItemConformingToTypeIdentifier(propertyList) { 48 | itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil) { item, error in 49 | let dictionary = item as! NSDictionary 50 | OperationQueue.main.addOperation { [weak self] in 51 | if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary { 52 | self?.handleData(dict: results) 53 | } 54 | } 55 | } 56 | } else { 57 | os_log(.error, "Encountered error while trying to insert link from action") 58 | } 59 | 60 | view.addSubview(titleLabel) 61 | view.addSubview(image) 62 | 63 | NSLayoutConstraint.activate([ 64 | titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), 65 | titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), 66 | image.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), 67 | image.bottomAnchor.constraint(equalTo: titleLabel.topAnchor, constant: -20), 68 | image.widthAnchor.constraint(equalToConstant: 150), 69 | image.heightAnchor.constraint(equalToConstant: 150) 70 | ]) 71 | } 72 | 73 | private func handleData(dict: NSDictionary) { 74 | var delay: DispatchTime = .now() + 1.0 75 | if file.canSaveMoreLinks { 76 | let urlString = dict["url"] as! String 77 | guard let _ = URL(string: urlString) else { return } 78 | file.add(urlString, note: nil) 79 | titleLabel.text = "Saved correctly" 80 | } else { 81 | let conf = UIImage.SymbolConfiguration(paletteColors: [.yellow]) 82 | image.image = UIImage(systemName: "exclamationmark.triangle.fill", withConfiguration: conf) 83 | titleLabel.text = "Can't save anymore links" 84 | titleLabel.textColor = .systemRed 85 | delay = .now() + 3.0 86 | } 87 | 88 | DispatchQueue.main.asyncAfter(deadline: delay, execute: { 89 | self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/GetURL.js: -------------------------------------------------------------------------------- 1 | var GetURL = function() {}; 2 | 3 | GetURL.prototype = { 4 | run: function(arguments) { 5 | let url = document.URL 6 | let title 7 | 8 | try { 9 | title = document.getElementsByTagName("title")[0].text; 10 | } catch (err) { 11 | title = "" 12 | } 13 | 14 | try { 15 | var meta = document.getElementsByTagName("meta"); 16 | var dict = {}; 17 | 18 | for (var i = 0; i < meta.length; i++) { 19 | let key = meta[i].name === "" ? meta[i].getAttribute('property') : meta[i].name; 20 | let content = meta[i].content; 21 | 22 | if (key !== "" && content !== "") { 23 | dict[key] = content; 24 | } 25 | } 26 | } catch (err) { 27 | dict = { "empty": true } 28 | } 29 | 30 | arguments.completionFunction({ 31 | "url" : document.URL, 32 | "title": title, 33 | "dictData": dict 34 | }); 35 | } 36 | }; 37 | 38 | var ExtensionPreprocessingJS = new GetURL; 39 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsWebPageWithMaxCount 12 | 1 13 | 14 | NSExtensionJavaScriptPreprocessingFile 15 | GetURL 16 | NSExtensionServiceAllowsFinderPreviewItem 17 | 18 | NSExtensionServiceAllowsTouchBarItem 19 | 20 | NSExtensionServiceFinderPreviewIconName 21 | NSActionTemplate 22 | NSExtensionServiceTouchBarBezelColorName 23 | TouchBarBezel 24 | NSExtensionServiceTouchBarIconName 25 | NSActionTemplate 26 | 27 | NSExtensionMainStoryboard 28 | MainInterface 29 | NSExtensionPointIdentifier 30 | com.apple.ui-services 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/1024 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/1024 1.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/1024 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/1024 2.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/1024.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/AppIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1024 1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "1024 2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "1024.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/20@2x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/20@3x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/29@2x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/29@3x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/40@2x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/40@3x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/60@2x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/60@3x.png -------------------------------------------------------------------------------- /Ulry/AddToUlryAction/Media.xcassets/ExtensionAppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "1024.png", 53 | "idiom" : "ios-marketing", 54 | "scale" : "1x", 55 | "size" : "1024x1024" 56 | } 57 | ], 58 | "info" : { 59 | "author" : "xcode", 60 | "version" : 1 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Ulry/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Urly 4 | // 5 | // Created by Mattia Righetti on 12/23/21. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import os 10 | import UIKit 11 | import CoreData 12 | import LinksDatabase 13 | import FMDB 14 | 15 | @main 16 | class AppDelegate: UIResponder, UIApplicationDelegate { 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | let startupTasks: [StartupTask] = [ 20 | RegisterInitialValuesForUserDefaultsStartupTask() 21 | ] 22 | 23 | startupTasks.forEach { $0.run() } 24 | 25 | Task { 26 | await AppReviewManager().registerReviewWorthyAction(weighted: 0.1) 27 | } 28 | 29 | return true 30 | } 31 | 32 | // MARK: - UISceneSession Lifecycle 33 | 34 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 35 | // Called when a new scene session is being created. 36 | // Use this method to select a configuration to create the new scene with. 37 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/20@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/20@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/29@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/29@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/38@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/38@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/40@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/40@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/60@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/60@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/64@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/64@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/68@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/AppIcon.appiconset/76@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "167.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "1024.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/astronaut.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "astronaut.json", 5 | "idiom" : "universal", 6 | "universal-type-identifier" : "public.json" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/colors/bg-color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.110", 9 | "green" : "0.110", 10 | "red" : "0.137" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "light" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "luminosity", 37 | "value" : "dark" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "srgb", 42 | "components" : { 43 | "alpha" : "1.000", 44 | "blue" : "0.110", 45 | "green" : "0.110", 46 | "red" : "0.137" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | } 51 | ], 52 | "info" : { 53 | "author" : "xcode", 54 | "version" : 1 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/colors/list-bg-color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF7", 9 | "green" : "0xF2", 10 | "red" : "0xF2" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.110", 27 | "green" : "0.110", 28 | "red" : "0.137" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/colors/list-cell-bg-color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.100", 26 | "blue" : "255", 27 | "green" : "255", 28 | "red" : "255" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/colors/list-cell-selected-bg-color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD6", 9 | "green" : "0xD1", 10 | "red" : "0xD1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.200", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/167.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/20@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/20@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/29@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/29@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/38@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/38@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/40@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/40@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/60@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/60@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/64@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/64@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/68@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/76@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/dark_red.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "167.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "1024.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/167.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/20@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/20@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/29@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/29@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/38@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/38@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/40@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/40@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/60@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/60@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/64@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/64@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/68@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/76@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_black.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "167.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "1024.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/167.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/20@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/20@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/29@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/29@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/38@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/38@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/40@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/40@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/60@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/60@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/64@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/64@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/68@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/76@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_blue.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "167.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "1024.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/167.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/20@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/20@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/29@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/29@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/38@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/38@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/40@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/40@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/60@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/60@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/64@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/64@3x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/68@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/76@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/light_orange.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "scale" : "2x", 8 | "size" : "20x20" 9 | }, 10 | { 11 | "filename" : "20@3x.png", 12 | "idiom" : "universal", 13 | "platform" : "ios", 14 | "scale" : "3x", 15 | "size" : "20x20" 16 | }, 17 | { 18 | "filename" : "29@2x.png", 19 | "idiom" : "universal", 20 | "platform" : "ios", 21 | "scale" : "2x", 22 | "size" : "29x29" 23 | }, 24 | { 25 | "filename" : "29@3x.png", 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "scale" : "3x", 29 | "size" : "29x29" 30 | }, 31 | { 32 | "filename" : "38@2x.png", 33 | "idiom" : "universal", 34 | "platform" : "ios", 35 | "scale" : "2x", 36 | "size" : "38x38" 37 | }, 38 | { 39 | "filename" : "38@3x.png", 40 | "idiom" : "universal", 41 | "platform" : "ios", 42 | "scale" : "3x", 43 | "size" : "38x38" 44 | }, 45 | { 46 | "filename" : "40@2x.png", 47 | "idiom" : "universal", 48 | "platform" : "ios", 49 | "scale" : "2x", 50 | "size" : "40x40" 51 | }, 52 | { 53 | "filename" : "40@3x.png", 54 | "idiom" : "universal", 55 | "platform" : "ios", 56 | "scale" : "3x", 57 | "size" : "40x40" 58 | }, 59 | { 60 | "filename" : "60@2x.png", 61 | "idiom" : "universal", 62 | "platform" : "ios", 63 | "scale" : "2x", 64 | "size" : "60x60" 65 | }, 66 | { 67 | "filename" : "60@3x.png", 68 | "idiom" : "universal", 69 | "platform" : "ios", 70 | "scale" : "3x", 71 | "size" : "60x60" 72 | }, 73 | { 74 | "filename" : "64@2x.png", 75 | "idiom" : "universal", 76 | "platform" : "ios", 77 | "scale" : "2x", 78 | "size" : "64x64" 79 | }, 80 | { 81 | "filename" : "64@3x.png", 82 | "idiom" : "universal", 83 | "platform" : "ios", 84 | "scale" : "3x", 85 | "size" : "64x64" 86 | }, 87 | { 88 | "filename" : "68@2x.png", 89 | "idiom" : "universal", 90 | "platform" : "ios", 91 | "scale" : "2x", 92 | "size" : "68x68" 93 | }, 94 | { 95 | "filename" : "76@2x.png", 96 | "idiom" : "universal", 97 | "platform" : "ios", 98 | "scale" : "2x", 99 | "size" : "76x76" 100 | }, 101 | { 102 | "filename" : "167.png", 103 | "idiom" : "universal", 104 | "platform" : "ios", 105 | "scale" : "2x", 106 | "size" : "83.5x83.5" 107 | }, 108 | { 109 | "filename" : "1024.png", 110 | "idiom" : "universal", 111 | "platform" : "ios", 112 | "size" : "1024x1024" 113 | } 114 | ], 115 | "info" : { 116 | "author" : "xcode", 117 | "version" : 1 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-dark_red.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-dark_red.imageset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-dark_red.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "1024.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-default.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-default.imageset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-default.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "1024.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_black.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_black.imageset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "1024.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_blue.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_blue.imageset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_blue.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "1024.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_orange.imageset/1024@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_orange.imageset/1024@2x.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/custom-app-icons/thumbnails/thumb-light_orange.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "1024@2x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/github-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "GitHub-Mark-Light-64px 1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "GitHub-Mark-Light-64px.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "GitHub-Mark-Light-64px 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/github-logo.imageset/GitHub-Mark-Light-64px 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/github-logo.imageset/GitHub-Mark-Light-64px 1.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/github-logo.imageset/GitHub-Mark-Light-64px 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/github-logo.imageset/GitHub-Mark-Light-64px 2.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/github-logo.imageset/GitHub-Mark-Light-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/github-logo.imageset/GitHub-Mark-Light-64px.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/launchscreen-1.0.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/launchscreen-1.0.imageset/1024.png -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/launchscreen-1.0.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "1024.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/onboarding-link-details.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "onboarding-link-details.jpg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/onboarding-link-details.imageset/onboarding-link-details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/onboarding-link-details.imageset/onboarding-link-details.jpg -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/onboarding-share-extension.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "onboarding-share-extension.jpeg", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Assets.xcassets/onboarding-share-extension.imageset/onboarding-share-extension.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattrighetti/Ulry/56ec80dcf9f6d778d2ef3650959fc0642f1e7f5c/Ulry/Assets.xcassets/onboarding-share-extension.imageset/onboarding-share-extension.jpeg -------------------------------------------------------------------------------- /Ulry/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Ulry/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | NSAppTransportSecurity 8 | 9 | NSAllowsArbitraryLoads 10 | 11 | 12 | NSUserActivityTypes 13 | 14 | AddURLIntent 15 | 16 | UIApplicationSceneManifest 17 | 18 | UIApplicationSupportsMultipleScenes 19 | 20 | UISceneConfigurations 21 | 22 | UIWindowSceneSessionRoleApplication 23 | 24 | 25 | UISceneConfigurationName 26 | Default Configuration 27 | UISceneDelegateClassName 28 | $(PRODUCT_MODULE_NAME).SceneDelegate 29 | 30 | 31 | 32 | 33 | UIFileSharingEnabled 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Ulry/Intents/AddURLIntentHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddURLIntentHandler.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/31/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Intents 10 | 11 | public enum AddURLIntentHandlerError: LocalizedError { 12 | case communicationFailure 13 | 14 | public var errorDescription: String? { 15 | switch self { 16 | case .communicationFailure: 17 | return NSLocalizedString("Unable to communicate with Ulry.", comment: "Communication failure") 18 | } 19 | } 20 | } 21 | 22 | 23 | 24 | public class AddURLIntentHandler: NSObject, AddURLIntentHandling { 25 | public func resolveUrl(for intent: AddURLIntent, with completion: @escaping (AddURLUrlResolutionResult) -> Void) { 26 | guard let url = intent.url else { 27 | completion(.unsupported(forReason: .required)) 28 | return 29 | } 30 | completion(.success(with: url)) 31 | } 32 | 33 | public func handle(intent: AddURLIntent, completion: @escaping (AddURLIntentResponse) -> Void) { 34 | let file = ExtensionsAddLinkRequestsManager() 35 | 36 | guard let urlString = intent.url, let _ = URL(string: urlString) else { 37 | completion(AddURLIntentResponse(code: .isNotValidUrl, userActivity: nil)) 38 | return 39 | } 40 | 41 | guard file.canSaveMoreLinks else { 42 | completion(AddURLIntentResponse(code: .cannotSaveToExternalFile, userActivity: nil)) 43 | return 44 | } 45 | 46 | file.add(urlString, note: nil) 47 | completion(AddURLIntentResponse(code: .success, userActivity: nil)) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Ulry/IntentsExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | IntentsRestrictedWhileLocked 10 | 11 | IntentsRestrictedWhileProtectedDataUnavailable 12 | 13 | IntentsSupported 14 | 15 | AddURLIntent 16 | 17 | 18 | NSExtensionPointIdentifier 19 | com.apple.intents-service 20 | NSExtensionPrincipalClass 21 | $(PRODUCT_MODULE_NAME).IntentHandler 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Ulry/IntentsExtension/IntentHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntentHandler.swift 3 | // UlryIntents 4 | // 5 | // Created by Mattia Righetti on 1/18/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Intents 10 | 11 | // As an example, this class is set up to handle Message intents. 12 | // You will want to replace this or add other intents as appropriate. 13 | // The intents you wish to handle must be declared in the extension's Info.plist. 14 | 15 | class IntentHandler: INExtension { 16 | override func handler(for intent: INIntent) -> Any? { 17 | switch intent { 18 | case is AddURLIntent: 19 | return AddURLIntentHandler() 20 | default: 21 | fatalError("Unhandled intent type: \(intent)") 22 | } 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Ulry/IntentsExtension/UlryIntents.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | group.com.mattrighetti.Ulry 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +Date.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 17/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Date { 12 | func getFormattedDate(format: String) -> String { 13 | let dateformat = DateFormatter() 14 | dateformat.dateFormat = format 15 | return dateformat.string(from: self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+NSMutableAttributedString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +NSMutableAttributedString.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 2/1/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension NSMutableAttributedString { 12 | convenience init(coloredStrings: [(String, UIColor)], separator: String? = " ") { 13 | let text = coloredStrings.map(\.0).sorted(by: { $0 <= $1 }).joined(separator: separator!) as String 14 | let attributedText = NSMutableAttributedString(string: text) 15 | 16 | for val in coloredStrings { 17 | let range = (text as NSString).range(of: val.0) 18 | attributedText.addAttribute(.foregroundColor, value: val.1, range: range) 19 | } 20 | 21 | self.init(attributedString: attributedText) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+Set.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +Set.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 11/5/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Set { 12 | public mutating func toggle(_ element: Element) { 13 | if self.contains(element) { 14 | self.remove(element) 15 | } else { 16 | self.insert(element) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +String.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 16/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var nilIfEmpty: String? { 13 | if self.isEmpty { 14 | return nil 15 | } else { 16 | return self 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIActivityViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIActivityViewController.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 23/02/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIActivityViewController { 12 | public static func share(file: URL, title: String) -> UIActivityViewController { 13 | let activity = UIActivityViewController(activityItems: [file], applicationActivities: nil) 14 | activity.title = title 15 | activity.excludedActivityTypes = [ 16 | .addToReadingList, 17 | .assignToContact, 18 | .markupAsPDF, 19 | .openInIBooks, 20 | .saveToCameraRoll, 21 | .postToFacebook, 22 | .postToVimeo, 23 | .postToWeibo, 24 | .postToFlickr, 25 | .postToTwitter, 26 | .postToTencentWeibo 27 | ] 28 | 29 | if #available(iOS 15.4, *) { 30 | activity.excludedActivityTypes?.append(.sharePlay) 31 | } 32 | 33 | return activity 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIAlertController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIAlertController.swift 3 | // Ulry 4 | // 5 | // Created by Matt on 14/12/2022. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIAlertController { 12 | static func okAlert(title: String?, message: String?) -> UIAlertController { 13 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 14 | alert.addAction(UIAlertAction(title: "Ok", style: .default)) 15 | return alert 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIButton.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 24/03/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIButton { 12 | public static func capsule(text: String, color: UIColor) -> UIButton { 13 | var configuration = UIButton.Configuration.filled() 14 | configuration.cornerStyle = .capsule 15 | configuration.baseBackgroundColor = color 16 | configuration.buttonSize = .mini 17 | configuration.title = text 18 | return UIButton(configuration: configuration) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UICellAccessory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UICellAccessory.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 24/03/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICellAccessory { 12 | public static func pill(text: String, color: UIColor) -> UICellAccessory.CustomViewConfiguration { 13 | let pill = UIButton.capsule(text: text, color: color) 14 | pill.isUserInteractionEnabled = false 15 | return UICellAccessory.CustomViewConfiguration(customView: pill, placement: .trailing()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UICollectionLayoutListConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionLayoutListConfiguration.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 17/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionLayoutListConfiguration { 12 | static func withCustomBackground(appearance: Self.Appearance) -> Self { 13 | var layout = self.init(appearance: appearance) 14 | layout.backgroundColor = UIColor(named: "list-bg-color") 15 | return layout 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UICollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UICollectionView.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 17/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class UICollectionViewCustomBackground: UICollectionView { 12 | override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 13 | super.init(frame: frame, collectionViewLayout: layout) 14 | backgroundColor = UIColor(named: "bg-color") 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIColor.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/8/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | // MARK: - Initializers 13 | convenience init(decimalRed red: Double, green: Double, blue: Double) { 14 | self.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1.0) 15 | } 16 | 17 | convenience init?(hex: String) { 18 | var hexNormalized = hex.trimmingCharacters(in: .whitespacesAndNewlines) 19 | hexNormalized = hexNormalized.replacingOccurrences(of: "#", with: "") 20 | 21 | // Helpers 22 | var rgb: UInt64 = 0 23 | var r: CGFloat = 0.0 24 | var g: CGFloat = 0.0 25 | var b: CGFloat = 0.0 26 | var a: CGFloat = 1.0 27 | let length = hexNormalized.count 28 | 29 | // Create Scanner 30 | Scanner(string: hexNormalized).scanHexInt64(&rgb) 31 | 32 | if length == 6 { 33 | r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 34 | g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 35 | b = CGFloat(rgb & 0x0000FF) / 255.0 36 | 37 | } else if length == 8 { 38 | r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0 39 | g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0 40 | b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0 41 | a = CGFloat(rgb & 0x000000FF) / 255.0 42 | 43 | } else { 44 | return nil 45 | } 46 | 47 | self.init(red: r, green: g, blue: b, alpha: a) 48 | } 49 | 50 | var toHex: String? { 51 | // Extract Components 52 | guard let components = cgColor.components, components.count >= 3 else { 53 | return nil 54 | } 55 | 56 | // Helpers 57 | let r = Float(components[0]) 58 | let g = Float(components[1]) 59 | let b = Float(components[2]) 60 | var a = Float(1.0) 61 | 62 | if components.count >= 4 { 63 | a = Float(components[3]) 64 | } 65 | 66 | // Create Hex String 67 | let hex = String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) 68 | 69 | return hex 70 | } 71 | 72 | public static var random: UIColor { 73 | let randomHex = self.randomHexColorCode() 74 | return UIColor(hex: randomHex)! 75 | } 76 | 77 | public static func randomHexColorCode() -> String { 78 | let a = ["1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"]; 79 | return 80 | "#" 81 | .appending(a[Int(arc4random_uniform(15))]) 82 | .appending(a[Int(arc4random_uniform(15))]) 83 | .appending(a[Int(arc4random_uniform(15))]) 84 | .appending(a[Int(arc4random_uniform(15))]) 85 | .appending(a[Int(arc4random_uniform(15))]) 86 | .appending(a[Int(arc4random_uniform(15))]) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIFont.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/9/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIFont { 12 | class func rounded(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont { 13 | let systemFont = UIFont.systemFont(ofSize: size, weight: weight) 14 | let font: UIFont 15 | 16 | if let descriptor = systemFont.fontDescriptor.withDesign(.rounded) { 17 | font = UIFont(descriptor: descriptor, size: size) 18 | } else { 19 | font = systemFont 20 | } 21 | return font 22 | } 23 | } 24 | 25 | extension UIFont { 26 | 27 | static func preferredFont(for style: TextStyle, weight: Weight, italic: Bool = false) -> UIFont { 28 | 29 | // Get the style's default pointSize 30 | let traits = UITraitCollection(preferredContentSizeCategory: .large) 31 | let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traits) 32 | 33 | // Get the font at the default size and preferred weight 34 | var font = UIFont.systemFont(ofSize: desc.pointSize, weight: weight) 35 | if italic == true { 36 | font = font.with([.traitItalic]) 37 | } 38 | 39 | // Setup the font to be auto-scalable 40 | let metrics = UIFontMetrics(forTextStyle: style) 41 | return metrics.scaledFont(for: font) 42 | } 43 | 44 | private func with(_ traits: UIFontDescriptor.SymbolicTraits...) -> UIFont { 45 | guard let descriptor = fontDescriptor.withSymbolicTraits(UIFontDescriptor.SymbolicTraits(traits).union(fontDescriptor.symbolicTraits)) else { 46 | return self 47 | } 48 | return UIFont(descriptor: descriptor, size: 0) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIImage.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/8/22. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIImage { 11 | convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { 12 | let renderer = UIGraphicsImageRenderer(size: size) 13 | let img = renderer.image { ctx in 14 | ctx.cgContext.setFillColor(UIColor.red.cgColor) 15 | 16 | let rectangle = CGRect(origin: .zero, size: size) 17 | ctx.cgContext.addEllipse(in: rectangle) 18 | ctx.cgContext.drawPath(using: .fillStroke) 19 | } 20 | 21 | self.init(cgImage: img.cgImage!) 22 | } 23 | 24 | func imageWith(newSize: CGSize) -> UIImage { 25 | let image = UIGraphicsImageRenderer(size: newSize).image { _ in 26 | draw(in: CGRect(origin: .zero, size: newSize)) 27 | } 28 | return image.withRenderingMode(renderingMode) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIScreen.swift 3 | // Urly 4 | // 5 | // Created by Mattia Righetti on 1/3/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScreen { 12 | static let screenWidth = UIScreen.main.bounds.size.width 13 | static let screenHeight = UIScreen.main.bounds.size.height 14 | static let screenSize = UIScreen.main.bounds.size 15 | } 16 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/+UIViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +UIViewController.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/10/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | extension UIViewController { 13 | func add(_ parent: UIViewController, frame: CGRect?) { 14 | parent.addChild(self) 15 | parent.view.addSubview(view) 16 | if let frame = frame { 17 | view.frame = frame 18 | } 19 | didMove(toParent: parent) 20 | } 21 | 22 | func remove() { 23 | guard parent != nil else { return } 24 | 25 | willMove(toParent: nil) 26 | removeFromParent() 27 | view.removeFromSuperview() 28 | } 29 | } 30 | 31 | extension UIViewController { 32 | func present(_ swiftuiView: Content, animated: Bool, config: ((UIHostingController) -> Void)? = nil) { 33 | let hostingController = UIHostingController(rootView: swiftuiView) 34 | config?(hostingController) 35 | present(hostingController, animated: animated) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/SwiftUICollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUICollectionViewCell.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 17/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | final class SwiftUICollectionViewCell: UICollectionViewListCell { 13 | func host(_ hostingController: UIHostingController) { 14 | backgroundColor = UIColor(named: "list-bg-color") 15 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 16 | hostingController.view.backgroundColor = UIColor(named: "list-cell-bg-color") 17 | addSubview(hostingController.view) 18 | 19 | let constraints = [ 20 | hostingController.view.topAnchor.constraint(equalTo: self.topAnchor), 21 | hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor), 22 | hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor), 23 | hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor), 24 | ] 25 | NSLayoutConstraint.activate(constraints) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Ulry/UIKit Extensions/UICollectionViewCellCustomBackground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewCellCustomBackground.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 17/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UICollectionViewCellCustomBackground: UICollectionViewListCell { 12 | override func updateConfiguration(using state: UICellConfigurationState) { 13 | var back = UIBackgroundConfiguration.listPlainCell().updated(for: state) 14 | if state.isSelected || state.isHighlighted { 15 | back.backgroundColor = UIColor(named: "list-cell-selected-bg-color") 16 | } else { 17 | back.backgroundColor = UIColor(named: "list-cell-bg-color") 18 | } 19 | backgroundConfiguration = back 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Ulry/Ulry.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.icloud-container-identifiers 6 | 7 | com.apple.developer.icloud-services 8 | 9 | com.apple.developer.siri 10 | 11 | com.apple.security.application-groups 12 | 13 | group.com.mattrighetti.Ulry 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Ulry/Ulry.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Ulry/Ulry.xcdatamodeld/Ulry.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Ulry/Urly.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Ulry/Urly.xcdatamodeld/Urly.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/ChangelogViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChangelogViewController.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/26/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct Release: Hashable, Decodable { 12 | var version: String 13 | var description: String? 14 | var changes: [String]? 15 | } 16 | 17 | struct ChangelogViewController: View { 18 | 19 | private var releases: [Release] = { 20 | guard 21 | let path = Bundle.main.path(forResource: "changelog", ofType: "json"), 22 | let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe), 23 | let res = try? JSONDecoder().decode([Release].self, from: data) 24 | else { 25 | return [] 26 | } 27 | 28 | return res 29 | }() 30 | 31 | var body: some View { 32 | List { 33 | AppHeader() 34 | .padding(.bottom, 20) 35 | .listRowSeparator(.hidden) 36 | .padding(.top, 20) 37 | ForEach(releases, id: \.self) { release in 38 | ReleaseView(version: release.version, description: release.description, changelogs: release.changes) 39 | .listRowSeparator(.hidden) 40 | } 41 | } 42 | .listStyle(.plain) 43 | } 44 | } 45 | 46 | struct AppHeader: View { 47 | var body: some View { 48 | HStack() { 49 | Spacer() 50 | ZStack { 51 | RoundedRectangle(cornerRadius: 25.0) 52 | .foregroundColor(.gray.opacity(0.5)) 53 | .frame(width: 105, height: 105) 54 | Image("thumb-default") 55 | .resizable() 56 | .frame(width: 100, height: 100) 57 | .cornerRadius(23.0) 58 | .padding(.horizontal, 10) 59 | } 60 | 61 | VStack(alignment: .leading) { 62 | Text("What's new in") 63 | Text("Ulry") 64 | } 65 | .font(.system(size: 25, weight: .bold)) 66 | 67 | Spacer() 68 | } 69 | } 70 | } 71 | 72 | struct ReleaseView: View { 73 | var version: String 74 | var description: String? 75 | var changelogs: [String]? 76 | 77 | var body: some View { 78 | VStack(alignment: .leading) { 79 | Divider() 80 | 81 | Text(version) 82 | .font(.title) 83 | .fontWeight(.bold) 84 | .padding(.top, 10) 85 | .padding(.bottom, 3) 86 | 87 | if let description = description { 88 | Text(description) 89 | .font(.body) 90 | .fontWeight(.semibold) 91 | .foregroundColor(.secondary) 92 | .padding(.bottom, 3) 93 | } 94 | 95 | if let changelogs = changelogs { 96 | ForEach(changelogs, id: \.self) { changelog in 97 | HStack { 98 | Text("-") 99 | Text((try! AttributedString(markdown: changelog))) 100 | .font(.body) 101 | .padding(.bottom, 1) 102 | } 103 | .foregroundColor(.secondary) 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | struct ChangelogViewController_Previews: PreviewProvider { 111 | static var previews: some View { 112 | ChangelogViewController() 113 | .preferredColorScheme(.dark) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Home/BackgroundSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundSupplementaryView.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 17/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A basic supplementary view used for section backgrounds. 12 | final class BackgroundSupplementaryView: UICollectionReusableView { 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | 16 | layer.cornerRadius = 15 17 | backgroundColor = UIColor(hex: "111111") 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Home/Cells/BouncyCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BouncyCollectionViewCell.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 16/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BouncyCollectionViewCell: UICollectionViewCell { 12 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 13 | super.touchesBegan(touches, with: event) 14 | UIView.animate(withDuration: 0.1) { 15 | self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) 16 | } 17 | } 18 | 19 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 20 | super.touchesCancelled(touches, with: event) 21 | UIView.animate(withDuration: 0.1) { 22 | self.transform = .identity 23 | } 24 | } 25 | 26 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 27 | super.touchesEnded(touches, with: event) 28 | UIView.animate(withDuration: 0.1) { 29 | self.transform = .identity 30 | } 31 | } 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Home/Cells/GroupCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupCategoryCollectionViewCell.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/10/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GroupCollectionViewCell: BouncyCollectionViewCell { 12 | 13 | var longPressAction: (() -> Void)? = nil 14 | 15 | lazy var label: UILabel = { 16 | let label = UILabel() 17 | label.font = UIFont.rounded(ofSize: 15, weight: .regular) 18 | label.translatesAutoresizingMaskIntoConstraints = false 19 | return label 20 | }() 21 | 22 | lazy var sfsymbolImage: UIImageView = { 23 | let imgview = UIImageView() 24 | imgview.layer.cornerRadius = 10 25 | imgview.layer.backgroundColor = UIColor.clear.cgColor 26 | imgview.tintColor = .label 27 | imgview.translatesAutoresizingMaskIntoConstraints = false 28 | return imgview 29 | }() 30 | 31 | override init(frame: CGRect) { 32 | super.init(frame: frame) 33 | 34 | layer.cornerRadius = 15 35 | 36 | contentView.addSubview(label) 37 | contentView.addSubview(sfsymbolImage) 38 | 39 | let lpr = UILongPressGestureRecognizer(target: self, action: #selector(longPressGesture)) 40 | lpr.minimumPressDuration = 0.5 41 | addGestureRecognizer(lpr) 42 | 43 | NSLayoutConstraint.activate([ 44 | sfsymbolImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 45 | sfsymbolImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), 46 | sfsymbolImage.widthAnchor.constraint(equalToConstant: 20), 47 | 48 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 49 | label.leadingAnchor.constraint(equalTo: sfsymbolImage.trailingAnchor, constant: 10), 50 | label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5) 51 | ]) 52 | } 53 | 54 | required init?(coder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | func update(with category: Category) { 59 | label.text = category.cellContent.title 60 | sfsymbolImage.tintColor = category.cellContent.backgroundColor 61 | 62 | backgroundColor = .lightGray.withAlphaComponent(0.2) 63 | 64 | if let icon = category.cellContent.icon { 65 | sfsymbolImage.image = UIImage(systemName: icon) 66 | } 67 | } 68 | 69 | override func prepareForReuse() { 70 | label.text = nil 71 | sfsymbolImage.image = nil 72 | longPressAction = nil 73 | } 74 | 75 | @objc private func longPressGesture(gesture : UILongPressGestureRecognizer!) { 76 | guard gesture.state == .began else { return } 77 | longPressAction?() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Home/Cells/HomeHeaderCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewHeader.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 9/25/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol HomeHeaderCollectionViewCellDelegate: NSObject { 12 | func toggle(section: HomeCollectionViewSection) 13 | } 14 | 15 | class HomeHeaderCollectionViewCell: UICollectionViewListCell { 16 | weak var delegate: HomeHeaderCollectionViewCellDelegate? 17 | var section: HomeCollectionViewSection! 18 | var isExpanded: Bool = false { 19 | didSet { 20 | UIView.animate(withDuration: 0.2) { 21 | self.arrow.transform = CGAffineTransformMakeRotation(!self.isExpanded ? Double.pi / 2 : 0) 22 | } 23 | } 24 | } 25 | 26 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 27 | super.touchesEnded(touches, with: event) 28 | didTap() 29 | } 30 | 31 | override init(frame: CGRect) { 32 | super.init(frame: frame) 33 | let customView = UICellAccessory.CustomViewConfiguration(customView: arrow, placement: .trailing(), reservedLayoutWidth: .custom(40), maintainsFixedSize: false) 34 | self.accessories = [.customView(configuration: customView)] 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | @objc private func didTap() { 42 | delegate?.toggle(section: section) 43 | isExpanded.toggle() 44 | } 45 | 46 | private lazy var arrow: UILabel = { 47 | let label = UILabel() 48 | let attachment = NSTextAttachment() 49 | let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 17, weight: .regular, scale: .medium) 50 | attachment.image = UIImage(systemName: "chevron.forward.circle", withConfiguration: symbolConfiguration)?.withTintColor(.systemBlue) 51 | label.attributedText = NSMutableAttributedString(attachment: attachment) 52 | return label 53 | }() 54 | } 55 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Home/Cells/MainCategoryCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainCategoryCollectionViewCell.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/10/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | 13 | class MainCategoryCollectionViewCell: BouncyCollectionViewCell { 14 | 15 | lazy var textLabel: UILabel = { 16 | let label = UILabel() 17 | label.font = UIFont.rounded(ofSize: 17, weight: .semibold) 18 | label.textColor = .label 19 | label.translatesAutoresizingMaskIntoConstraints = false 20 | return label 21 | }() 22 | 23 | lazy var sfsymbolImage: UIImageView = { 24 | let imgview = UIImageView() 25 | imgview.layer.cornerRadius = 10 26 | imgview.layer.backgroundColor = UIColor.clear.cgColor 27 | imgview.tintColor = .label 28 | imgview.translatesAutoresizingMaskIntoConstraints = false 29 | return imgview 30 | }() 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | 35 | contentView.addSubview(textLabel) 36 | contentView.addSubview(sfsymbolImage) 37 | 38 | layer.borderColor = UIColor.white.withAlphaComponent(0.2).cgColor 39 | layer.borderWidth = 2 40 | layer.cornerRadius = 15 41 | clipsToBounds = true 42 | 43 | NSLayoutConstraint.activate([ 44 | sfsymbolImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), 45 | sfsymbolImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), 46 | 47 | textLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -15), 48 | textLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20) 49 | ]) 50 | } 51 | 52 | required init?(coder: NSCoder) { 53 | fatalError("init(coder:) has not been implemented") 54 | } 55 | 56 | func update(with category: Category) { 57 | backgroundColor = category.cellContent.backgroundColor 58 | textLabel.text = category.cellContent.title 59 | 60 | if let icon = category.cellContent.icon { 61 | sfsymbolImage.image = UIImage(systemName: icon) 62 | } 63 | } 64 | 65 | override func prepareForReuse() { 66 | textLabel.text = nil 67 | sfsymbolImage.image = nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Home/Cells/TagCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagCategoryCollectionViewCell.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/10/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TagCollectionViewCell: BouncyCollectionViewCell { 12 | 13 | var longPressAction: (() -> Void)? = nil 14 | 15 | lazy var label: UILabel = { 16 | let label = UILabel() 17 | label.font = UIFont.rounded(ofSize: 15, weight: .regular) 18 | label.translatesAutoresizingMaskIntoConstraints = false 19 | return label 20 | }() 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | 25 | let lpr = UILongPressGestureRecognizer(target: self, action: #selector(longPressGesture)) 26 | lpr.minimumPressDuration = 0.5 27 | addGestureRecognizer(lpr) 28 | 29 | layer.borderColor = UIColor.white.withAlphaComponent(0.1).cgColor 30 | layer.borderWidth = 1 31 | layer.cornerRadius = 15 32 | 33 | contentView.addSubview(label) 34 | 35 | NSLayoutConstraint.activate([ 36 | label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), 37 | label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), 38 | label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10) 39 | ]) 40 | } 41 | 42 | required init?(coder: NSCoder) { 43 | fatalError("init(coder:) has not been implemented") 44 | } 45 | 46 | func update(with category: Category) { 47 | backgroundColor = category.cellContent.backgroundColor 48 | label.text = category.cellContent.title 49 | } 50 | 51 | override func prepareForReuse() { 52 | label.text = nil 53 | longPressAction = nil 54 | } 55 | 56 | @objc private func longPressGesture(gesture : UILongPressGestureRecognizer!) { 57 | guard gesture.state == .began else { return } 58 | longPressAction?() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/Settings/GeneralSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralSettingsVC.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/25/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GeneralSettingsViewController: GenericCollectionList { 12 | 13 | private lazy var markAsReadOnOpenButton: CollectionViewCellContent = { 14 | let uiswitch = UISwitch() 15 | uiswitch.isOn = UserDefaultsWrapper().get(key: .markReadOnOpen) 16 | 17 | let action = UIAction { _ in 18 | UserDefaultsWrapper().set(uiswitch.isOn, forKey: .markReadOnOpen) 19 | } 20 | 21 | uiswitch.addAction(action, for: .valueChanged) 22 | 23 | let customSwitchView = UICellAccessory.CustomViewConfiguration(customView: uiswitch, placement: .trailing()) 24 | return .setting(CVSetting(image: "checkmark", text: "Read on open", secondaryText: "Automatically set as read link when opened", accessories: [.customView(configuration: customSwitchView)], hexColor: "D8335B")) 25 | }() 26 | 27 | private lazy var openLinksInAppButton: CollectionViewCellContent = { 28 | let uiswitch = UISwitch() 29 | 30 | uiswitch.isOn = UserDefaultsWrapper().get(key: .openInApp) 31 | 32 | let action = UIAction { _ in 33 | UserDefaultsWrapper().set(uiswitch.isOn, forKey: .openInApp) 34 | } 35 | 36 | uiswitch.addAction(action, for: .valueChanged) 37 | 38 | let customSwitchView = UICellAccessory.CustomViewConfiguration(customView: uiswitch, placement: .trailing()) 39 | return .setting(CVSetting(image: "safari.fill", text: "Open links in-app", secondaryText: "Open links in browser without leaving the app", accessories: [.customView(configuration: customSwitchView)], hexColor: "FF6766")) 40 | }() 41 | 42 | private lazy var readerMode: CollectionViewCellContent = { 43 | let uiswitch = UISwitch() 44 | 45 | uiswitch.isOn = UserDefaultsWrapper().get(key: .readMode) 46 | 47 | let action = UIAction { _ in 48 | UserDefaultsWrapper().set(uiswitch.isOn, forKey: .readMode) 49 | } 50 | 51 | uiswitch.addAction(action, for: .valueChanged) 52 | 53 | let customSwitchView = UICellAccessory.CustomViewConfiguration(customView: uiswitch, placement: .trailing()) 54 | return .setting(CVSetting(image: "quote.opening", text: "Reader mode", secondaryText: "If website is compatible, automatically activate Reader Mode when opening link", accessories: [.customView(configuration: customSwitchView)], hexColor: "3D8EB9")) 55 | }() 56 | 57 | private lazy var urlRedirectorSettings: CollectionViewCellContent = { 58 | var cvsetting = CVSetting( 59 | image: "arrow.triangle.branch", 60 | text: "Privacy Redirector", 61 | secondaryText: "Redirect social media platforms and paywalled websites to their privacy respecting and free alternatives", 62 | accessories: [.disclosureIndicator()], 63 | hexColor: "EF9688", 64 | isSelectable: true 65 | ) 66 | 67 | return .navigate(cvsetting, URLRedirectorSettings()) 68 | }() 69 | 70 | override func viewDidLoad() { 71 | super.viewDidLoad() 72 | 73 | navigationItem.title = "General" 74 | 75 | content = [ 76 | [markAsReadOnOpenButton], 77 | [openLinksInAppButton, readerMode], 78 | [urlRedirectorSettings] 79 | ] 80 | 81 | super.setup() 82 | } 83 | 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /Ulry/ViewControllers/SpinnerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpinnerViewController.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 18/03/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SpinnerViewController: UIViewController { 12 | private lazy var spinner: UIActivityIndicatorView = { 13 | let spinner = UIActivityIndicatorView(style: .large) 14 | spinner.translatesAutoresizingMaskIntoConstraints = false 15 | return spinner 16 | }() 17 | 18 | private lazy var label: UILabel = { 19 | let label = UILabel() 20 | label.numberOfLines = 2 21 | label.textAlignment = .center 22 | label.lineBreakMode = .byWordWrapping 23 | label.translatesAutoresizingMaskIntoConstraints = false 24 | return label 25 | }() 26 | 27 | override func loadView() { 28 | view = UIView() 29 | view.backgroundColor = UIColor(white: 0, alpha: 0.7) 30 | 31 | spinner.startAnimating() 32 | view.addSubview(spinner) 33 | view.addSubview(label) 34 | 35 | NSLayoutConstraint.activate([ 36 | spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor), 37 | spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor), 38 | label.centerXAnchor.constraint(equalTo: spinner.centerXAnchor), 39 | label.topAnchor.constraint(equalTo: spinner.bottomAnchor, constant: 15), 40 | label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25), 41 | label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -25) 42 | ]) 43 | } 44 | 45 | func showText(texts: [String]) { 46 | let interval = 10.0 47 | var curr = 0.0 48 | 49 | for text in texts { 50 | DispatchQueue.main.asyncAfter(deadline: .now() + curr) { 51 | self.label.text = text 52 | } 53 | curr += interval 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Ulry/Views/BackgroundImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/25/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct BackgroundImage: View { 12 | var systemName: String 13 | var hex: String 14 | var height: CGFloat? = 30 15 | var width: CGFloat? = 30 16 | 17 | var body: some View { 18 | ZStack { 19 | Color(hex: hex) 20 | if (systemName.starts(with: "asset:")) { 21 | Image(systemName.replacingOccurrences(of: "asset:", with: "")) 22 | .foregroundColor(.white) 23 | } else { 24 | Image(systemName: systemName) 25 | .font(.system(size: 16)) 26 | .foregroundColor(.white) 27 | } 28 | } 29 | .clipShape(RoundedRectangle(cornerRadius: 8)) 30 | .frame(width: width, height: height) 31 | } 32 | 33 | public static func getHostingViewController(icon systemName: String, hex: String) -> UIView { 34 | let view = UIHostingController(rootView: BackgroundImage(systemName: systemName, hex: hex)).view 35 | view!.backgroundColor = .clear 36 | return view! 37 | } 38 | } 39 | 40 | struct BackgroundImage_Previews: PreviewProvider { 41 | static var previews: some View { 42 | BackgroundImage(systemName: "checkmark.seal", hex: "fcde44") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Ulry/Views/CategoryTitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryTitleView.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 16/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CategoryTitleView: View { 12 | var category: Category 13 | 14 | var body: some View { 15 | HStack { 16 | if let iconName = category.cellContent.icon { 17 | Image(systemName: iconName) 18 | .foregroundColor(Color(uiColor: category.cellContent.backgroundColor)) 19 | } else { 20 | Circle() 21 | .foregroundColor(Color(uiColor: category.cellContent.backgroundColor)) 22 | .frame(width: 15) 23 | } 24 | 25 | Text(category.cellContent.title) 26 | } 27 | } 28 | 29 | static func getView(for category: Category) -> UIView { 30 | let view = UIHostingController(rootView: CategoryTitleView(category: category)).view 31 | view!.backgroundColor = .clear 32 | return view! 33 | } 34 | } 35 | 36 | struct CategoryTitleView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | CategoryTitleView(category: .starred) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Ulry/Views/EmptinessView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptinessView.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/16/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Lottie 11 | 12 | class EmptinessView: UIView { 13 | 14 | lazy var vstack: UIStackView = { 15 | let vstack = UIStackView() 16 | vstack.axis = .vertical 17 | vstack.spacing = 10 18 | vstack.alignment = .center 19 | vstack.distribution = .fillProportionally 20 | vstack.translatesAutoresizingMaskIntoConstraints = false 21 | return vstack 22 | }() 23 | 24 | lazy var animationView: LottieAnimationView = { 25 | let view = LottieAnimationView(asset: "astronaut") 26 | view.contentMode = .scaleAspectFit 27 | view.loopMode = .loop 28 | view.animationSpeed = 1 29 | return view 30 | }() 31 | 32 | lazy var emptinessLabel: UILabel = { 33 | let label = UILabel() 34 | label.font = UIFont.preferredFont(for: .title3, weight: .bold) 35 | label.text = "Nothing to see here" 36 | return label 37 | }() 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | 42 | addSubview(vstack) 43 | 44 | vstack.addArrangedSubview(animationView) 45 | vstack.addArrangedSubview(emptinessLabel) 46 | 47 | let hc = animationView.heightAnchor.constraint(equalToConstant: 300) 48 | hc.priority = .init(999) 49 | hc.isActive = true 50 | 51 | let wc = animationView.widthAnchor.constraint(equalToConstant: 300) 52 | wc.priority = .init(999) 53 | wc.isActive = true 54 | 55 | let lc = emptinessLabel.heightAnchor.constraint(equalToConstant: 40) 56 | lc.priority = .init(999) 57 | lc.isActive = true 58 | 59 | NSLayoutConstraint.activate([ 60 | vstack.centerYAnchor.constraint(equalTo: centerYAnchor), 61 | vstack.centerXAnchor.constraint(equalTo: centerXAnchor) 62 | ]) 63 | 64 | animationView.play() 65 | } 66 | 67 | required init?(coder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Ulry/Views/FaviconHostnameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FaviconHostnameView.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 1/9/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct FaviconHostnameView: View { 12 | @State private var favicon: UIImage? = nil 13 | 14 | var hostname: String 15 | 16 | var body: some View { 17 | HStack { 18 | if let favicon = favicon { 19 | Image(uiImage: favicon) 20 | .resizable() 21 | .aspectRatio(contentMode: .fit) 22 | .frame(width: 10, height: 10, alignment: .center) 23 | } else { 24 | Image(systemName: "link.circle") 25 | .font(.system(size: 10)) 26 | .aspectRatio(contentMode: .fit) 27 | .frame(width: 10, height: 10, alignment: .center) 28 | } 29 | 30 | Text(hostname) 31 | .font(.system(size: 10, weight: .semibold, design: .monospaced)) 32 | .foregroundColor(.blue) 33 | 34 | Spacer() 35 | } 36 | .padding(.horizontal) 37 | .task { 38 | guard let data = await fetchFavicon(of: hostname) else { return } 39 | self.favicon = UIImage(data: data) 40 | } 41 | } 42 | 43 | private func fetchFavicon(of hostname: String) async -> Data? { 44 | guard let faviconUrl = URL(string: "https://" + hostname + "/favicon.ico") else { return nil } 45 | do { 46 | let (data, response) = try await URLSession.shared.data(from: faviconUrl) 47 | guard (response as? HTTPURLResponse)?.statusCode == 200 else { return nil } 48 | return data 49 | } catch { 50 | return nil 51 | } 52 | } 53 | } 54 | 55 | struct FaviconHostnameView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | FaviconHostnameView(hostname: "facebook.com") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Ulry/Views/LinkImagePreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkImagePreview.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 10/15/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LinkImagePreview: UIView { 12 | enum Kind { 13 | case image(UIImage) 14 | case color(UIColor, String) 15 | } 16 | 17 | var kind: Kind? = nil { 18 | didSet { 19 | switch kind { 20 | case .color(let color, let letter): 21 | image.backgroundColor = color 22 | urlLetterLabel.text = letter 23 | image.image = nil 24 | case .image(let uiimage): 25 | image.image = uiimage 26 | image.backgroundColor = nil 27 | urlLetterLabel.text = nil 28 | default: 29 | fatalError() 30 | } 31 | } 32 | } 33 | 34 | var action: (() -> Void)? 35 | 36 | lazy var image: UIImageView = { 37 | let imageView = UIImageView() 38 | imageView.layer.cornerRadius = 10 39 | imageView.contentMode = .scaleAspectFill 40 | imageView.isUserInteractionEnabled = true 41 | imageView.translatesAutoresizingMaskIntoConstraints = false 42 | return imageView 43 | }() 44 | 45 | lazy var urlLetterLabel: UILabel = { 46 | let label = UILabel() 47 | label.font = UIFont.rounded(ofSize: 23, weight: .black) 48 | label.textColor = .white 49 | label.translatesAutoresizingMaskIntoConstraints = false 50 | return label 51 | }() 52 | 53 | override init(frame: CGRect) { 54 | super.init(frame: frame) 55 | image.clipsToBounds = true 56 | 57 | addSubview(image) 58 | addSubview(urlLetterLabel) 59 | 60 | addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onInfoButtonPressed))) 61 | 62 | NSLayoutConstraint.activate([ 63 | urlLetterLabel.centerXAnchor.constraint(equalTo: centerXAnchor), 64 | urlLetterLabel.centerYAnchor.constraint(equalTo: centerYAnchor) 65 | ]) 66 | } 67 | 68 | override func layoutSubviews() { 69 | super.layoutSubviews() 70 | image.frame = self.bounds 71 | } 72 | 73 | required init?(coder: NSCoder) { 74 | fatalError("init(coder:) has not been implemented") 75 | } 76 | 77 | @objc private func onInfoButtonPressed() { 78 | UIView.animate(withDuration: 0.1) { 79 | self.transform = .identity.scaledBy(x: 1.5, y: 1.5) 80 | } 81 | 82 | UIView.animate(withDuration: 0.1) { 83 | self.transform = .identity 84 | } 85 | 86 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { 87 | self.action?() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Ulry/Views/SymbolCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SymbolCollectionViewCell.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 9/25/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SymbolCollectionViewCell: UICollectionViewCell { 12 | var symbol: String? { 13 | didSet { 14 | symbolLabel.text = symbol 15 | } 16 | } 17 | 18 | var text: String? { 19 | didSet { 20 | textLablel.text = text 21 | } 22 | } 23 | 24 | var color: UIColor? { 25 | didSet { 26 | contentView.backgroundColor = color 27 | } 28 | } 29 | 30 | lazy var symbolLabel: UILabel = { 31 | let label = UILabel() 32 | label.translatesAutoresizingMaskIntoConstraints = false 33 | return label 34 | }() 35 | 36 | lazy var textLablel: UILabel = { 37 | let label = UILabel() 38 | label.translatesAutoresizingMaskIntoConstraints = false 39 | return label 40 | }() 41 | 42 | override init(frame: CGRect) { 43 | super.init(frame: frame) 44 | addSubview(symbolLabel) 45 | addSubview(textLablel) 46 | 47 | NSLayoutConstraint.activate([ 48 | symbolLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), 49 | symbolLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), 50 | symbolLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), 51 | 52 | textLablel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), 53 | textLablel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), 54 | textLablel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10) 55 | ]) 56 | } 57 | 58 | required init?(coder: NSCoder) { 59 | fatalError("init(coder:) has not been implemented") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Ulry/Views/WeeklyAddedLinksGraph.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseStatsViewController.swift 3 | // Ulry 4 | // 5 | // Created by Mattia Righetti on 27/03/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Charts 11 | import Account 12 | import LinksDatabase 13 | 14 | struct WeeklyAddedLinksGraph: View { 15 | 16 | @State var sevenDaysStats: [LinkAddedPerDay] 17 | 18 | var body: some View { 19 | if #available(iOS 16.0, *) { 20 | let max = sevenDaysStats.map { $0.value }.max() ?? 10 21 | 22 | VStack(alignment: .leading) { 23 | Text("This week") 24 | .font(.headline) 25 | Text("The number of links you added this week") 26 | .font(.caption) 27 | .foregroundColor(.secondary) 28 | .padding(.bottom, 2) 29 | 30 | Chart(sevenDaysStats, id: \.self) { item in 31 | LineMark( 32 | x: .value("Date", String(item.date.dropFirst(5))), 33 | y: .value("Value", item.value) 34 | ) 35 | .symbol(.circle) 36 | .foregroundStyle(.teal) 37 | } 38 | .chartYAxis{ 39 | AxisMarks(position: .trailing, values: topPaddingData(max: max)) 40 | } 41 | .frame(height: 240) 42 | } 43 | .padding(15) 44 | .background(Color("list-cell-bg-color")) 45 | } else { 46 | Text("Can't be displayed on this iOS version") 47 | .font(.headline) 48 | } 49 | } 50 | 51 | private func topPaddingData(max: Int) -> [Int] { 52 | let step = max <= 5 ? 1 : max / 5 53 | let to = max == 0 ? 10 : max + 2 * step 54 | return stride(from: 0, to: to, by: step).map { $0 } 55 | } 56 | } 57 | 58 | // MARK: - Data Structure 59 | struct LinkAddedPerDay: Hashable { 60 | var date: String 61 | var value: Int 62 | } 63 | 64 | struct DatabaseStatsViewController_Previews: PreviewProvider { 65 | static var previews: some View { 66 | ZStack { 67 | Color("list-bg-color") 68 | WeeklyAddedLinksGraph(sevenDaysStats: [ 69 | LinkAddedPerDay(date: "2023-11-10", value: 0), 70 | LinkAddedPerDay(date: "2023-11-11", value: 15), 71 | LinkAddedPerDay(date: "2023-11-12", value: 0), 72 | LinkAddedPerDay(date: "2023-11-13", value: 4), 73 | LinkAddedPerDay(date: "2023-11-14", value: 30), 74 | LinkAddedPerDay(date: "2023-11-15", value: 10), 75 | LinkAddedPerDay(date: "2023-11-16", value: 1) 76 | ]).preferredColorScheme(.dark) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Ulry/changelog.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "1.0.0", 4 | "description": "Welcome to the very first version of Ulry 🎉" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /UlryIntents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | IntentsRestrictedWhileLocked 10 | 11 | IntentsRestrictedWhileProtectedDataUnavailable 12 | 13 | IntentsSupported 14 | 15 | AddURLIntent 16 | 17 | 18 | NSExtensionPointIdentifier 19 | com.apple.intents-service 20 | NSExtensionPrincipalClass 21 | $(PRODUCT_MODULE_NAME).IntentHandler 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /UlryShareExtension/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /UlryShareExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsWebURLWithMaxCount 12 | 1 13 | 14 | 15 | NSExtensionMainStoryboard 16 | MainInterface 17 | NSExtensionPointIdentifier 18 | com.apple.share-services 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /UlryShareExtension/UlryShareExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.com.mattrighetti.Ulry 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /UlryTests/+ColorsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // +ColorsTest.swift 3 | // UlryTests 4 | // 5 | // Created by Mattia Righetti on 11/04/23. 6 | // Copyright © 2023 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | @testable import Ulry 13 | 14 | class ColorTest: XCTestCase { 15 | func testInvalidColor() throws { 16 | XCTAssertNil(UIColor(hex: "a")) 17 | XCTAssertNil(UIColor(hex: "aa")) 18 | XCTAssertNil(UIColor(hex: "abb")) 19 | XCTAssertNil(UIColor(hex: "aaa")) 20 | XCTAssertNil(UIColor(hex: "accc")) 21 | XCTAssertNil(UIColor(hex: "abddeee")) 22 | XCTAssertNil(UIColor(hex: "#a")) 23 | } 24 | 25 | func testValidColor() throws { 26 | XCTAssertNotNil(UIColor(hex: "#aabbbb")) 27 | XCTAssertNotNil(UIColor(hex: "#aabbaaaa")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /UlryTests/Shared/URLRedirectorTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRedirectorTest.swift 3 | // UlryTests 4 | // 5 | // Created by Mattia Righetti on 10/19/22. 6 | // Copyright © 2022 Mattia Righetti. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import XCTest 12 | @testable import Ulry 13 | 14 | class URLRedirectorTest: XCTestCase { 15 | 16 | func testRegexRedirectUrl() { 17 | var redirector = URLRedirector() 18 | redirector.setCustomRedirect([ 19 | .twitter: true, 20 | .youtube: true, 21 | .reddit: true, 22 | .medium: true 23 | ]) 24 | 25 | let youtubeUrl = URL(string: "https://www.youtube.com/watch?v=ZSZmibBPj2A")! 26 | let twitterUrl = URL(string: "https://twitter.com/randompicker")! 27 | 28 | let invidiousRedirect = redirector.redirect(youtubeUrl) 29 | let nitterRedirect = redirector.redirect(twitterUrl) 30 | 31 | XCTAssertNotNil(nitterRedirect) 32 | XCTAssertNotNil(invidiousRedirect) 33 | 34 | XCTAssertEqual(URL(string: "https://farside.link/piped/watch?v=ZSZmibBPj2A")!, invidiousRedirect) 35 | XCTAssertEqual(URL(string: "https://farside.link/nitter/randompicker"), nitterRedirect) 36 | } 37 | 38 | func testRegexRedirectMapUrl() { 39 | var redirector = URLRedirector() 40 | redirector.setCustomRedirect([ 41 | .twitter: true, 42 | .youtube: true, 43 | .reddit: true, 44 | .medium: true 45 | ]) 46 | 47 | let youtubeUrl = URL(string: "https://www.youtube.com/watch?v=ZSZmibBPj2A")! 48 | let twitterUrl = URL(string: "https://twitter.com/randompicker")! 49 | let hdblogitUrl = URL(string: "https://hdblog.it/randompicker")! 50 | let hdblogcomUrl = URL(string: "https://www.hdblog.net/randompicker")! 51 | let hdblognetUrl = URL(string: "https://hdblog.it/randompicker")! 52 | 53 | let ytRedirect = redirector.redirect(youtubeUrl) 54 | let nitterRedirect = redirector.redirect(twitterUrl) 55 | let hdblogitRedirect = redirector.redirect(hdblogitUrl) 56 | let hdblogcomRedirect = redirector.redirect(hdblogcomUrl) 57 | let hdblognetRedirect = redirector.redirect(hdblognetUrl) 58 | 59 | XCTAssertNotNil(nitterRedirect) 60 | XCTAssertNotNil(ytRedirect) 61 | XCTAssertNotNil(hdblogitRedirect) 62 | XCTAssertNotNil(hdblogcomRedirect) 63 | XCTAssertNotNil(hdblognetRedirect) 64 | 65 | XCTAssertEqual(URL(string: "https://farside.link/piped/watch?v=ZSZmibBPj2A")!, ytRedirect) 66 | XCTAssertEqual(URL(string: "https://farside.link/nitter/randompicker"), nitterRedirect) 67 | XCTAssertEqual(URL(string: "https://hdblog.it/randompicker"), hdblogitRedirect) 68 | XCTAssertEqual(URL(string: "https://hdblog.it/randompicker"), hdblognetRedirect) 69 | XCTAssertEqual(URL(string: "https://www.hdblog.net/randompicker"), hdblogcomRedirect) 70 | } 71 | } 72 | --------------------------------------------------------------------------------