├── .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 `` document section
33 | ///
34 | /// This is a sample HTML string
35 | /// ```
36 | ///
37 | /// 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 |
--------------------------------------------------------------------------------