) {
17 | self._isPresented = isPresented
18 | }
19 |
20 | func item(title: String, description: String, symbol: String) -> some View {
21 | HStack(alignment: .center) {
22 | Image(systemName: symbol)
23 | .resizable()
24 | .aspectRatio(1.0, contentMode: .fit)
25 | .foregroundColor(.redditOrange)
26 | .frame(width: 30)
27 | .padding(5)
28 | (Text(title).font(.headline)
29 | + Text("\n\(description)"))
30 | .frame(width: 300)
31 | }
32 | }
33 |
34 | public var body: some View {
35 | VStack {
36 | Text("What's New?")
37 | .font(.largeTitle)
38 | .fontWeight(.heavy)
39 | .padding(.top)
40 | .frame(width: 300)
41 | Text("Tweaks for Reddit v\(TweaksForReddit.version)")
42 | .font(.callout)
43 | .foregroundColor(.gray)
44 | .padding(.bottom)
45 |
46 | VStack(alignment: .leading, spacing: 10) {
47 | self.item(
48 | title: "Menu bar access",
49 | description: "TFR now lives in your menu bar providing quick access to your favorite subreddits.",
50 | symbol: "menubar.arrow.up.rectangle"
51 | )
52 | }
53 | Button("Cool! \(Image(systemName: "checkmark"))") {
54 | isPresented = false
55 | TweaksForReddit.defaults.set(TweaksForReddit.version, forKey: "lastWhatsNewVersion")
56 | }
57 | .buttonStyle(RedditweaksButtonStyle())
58 | .padding(.top, 30)
59 | }
60 | .frame(width: 400, height: 300)
61 | }
62 | }
63 |
64 | struct WhatsNewView_Previews: PreviewProvider {
65 | static var previews: some View {
66 | WhatsNewView(isPresented: .constant(true))
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | A Safari App Extension that makes Reddit suck just a little bit less on Safari 13+.
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ### CI Status
11 |
12 | [](https://github.com/bermudalocket/Tweaks-for-Reddit/actions/workflows/build.yml)
13 |
14 | ## Background
15 |
16 | This project started in June 2019 as an attempt to port the entirety of the Reddit Enhancement Suite to the Safari App Extension framework. Since then, this project has diverted from that path and instead implements a select swath of features. Requests are welcome: simply open an issue. Pull requests are also welcome, as are code reviews.
17 |
18 | ## Requirements
19 |
20 | As of version 1.7.2, Tweaks for Reddit is only supported on macOS 11.x (Big Sur) and macOS 12.x (Monterey).
21 |
22 | As of version 1.4, redditweaks is only supported on macOS 10.15 (Catalina) and 11 (Big Sur). This is due to the adoption of SwiftUI and Combine in version 1.4.
23 |
24 | Versions 1.3 and below were written in UIKit with the help of [SnapKit](https://github.com/SnapKit/SnapKit).
25 |
26 | ## Installation
27 | 1. Download the latest release from the [releases page](https://github.com/bermudalocket/redditweaks/releases).
28 | 2. Unzip the archive, and move `redditweaks.app` into `/Applications`.
29 | 3. Launch `redditweaks.app` and follow the prompt to enable the extension in Safari.
30 | 4. You may then close the app. It is not required to be open for the extension to work, but you cannot delete it.
31 |
32 | ## Development
33 |
34 |
35 |
36 | ## Historical Screenshots
37 |
38 | https://i.imgur.com/wytyfjh.jpg
39 | https://i.imgur.com/RLFPr6i.jpg
40 | https://i.imgur.com/VNxAfgB.jpg
41 | https://i.imgur.com/Mgz1lbk.png
42 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Icon-16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "Icon-33.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "Icon-32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "Icon-64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "Icon-128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "Icon-257.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "Icon-256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "Icon-513.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "Icon-512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "Icon-1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-128.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-16.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-256.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-257.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-32.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-33.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-512.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-513.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-513.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/AppIcon.appiconset/Icon-64.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Color.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.337",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | },
20 | "properties" : {
21 | "localizable" : true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Icon.imageset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/Icon.imageset/64.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Icon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "64.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
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 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/livecommentpreviews.imageset/CleanShot 2021-05-21 at 15.15.05@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/livecommentpreviews.imageset/CleanShot 2021-05-21 at 15.15.05@2x.jpg
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/livecommentpreviews.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "filename" : "CleanShot 2021-05-21 at 15.15.05@2x.jpg",
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 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/snoovatar_mock.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "snoovatar_mock.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
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 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/snoovatar_mock.imageset/snoovatar_mock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Resources/Assets.xcassets/snoovatar_mock.imageset/snoovatar_mock.png
--------------------------------------------------------------------------------
/Resources/Background.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSBackgroundOnly
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Resources/Extension Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Tweaks for Reddit Extension
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | 20220304.220403
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSExtension
26 |
27 | NSExtensionPointIdentifier
28 | com.apple.Safari.extension
29 | NSExtensionPrincipalClass
30 | $(PRODUCT_MODULE_NAME).SafariExtensionHandler
31 | SFSafariContentScript
32 |
33 |
34 | Script
35 | snudown.js
36 |
37 |
38 | Script
39 | redditweaks-script.js
40 |
41 |
42 | SFSafariStyleSheet
43 |
44 |
45 | Style Sheet
46 | redditweaks.css
47 |
48 |
49 | SFSafariToolbarItem
50 |
51 | Action
52 | Popover
53 | Identifier
54 | redditweaks
55 | Image
56 | toolbar.pdf
57 | Label
58 | Tweaks for Reddit
59 |
60 | SFSafariWebsiteAccess
61 |
62 | Allowed Domains
63 |
64 | www.reddit.com
65 | *.reddit.com
66 |
67 | Level
68 | Some
69 |
70 |
71 | NSHumanReadableCopyright
72 | Copyright © 2019 - 2021 Michael Rippe. All rights reserved.
73 | NSHumanReadableDescription
74 | An extension that adds miscellaneous features to make Reddit suck a little less in Safari 13+.
75 |
76 |
77 |
--------------------------------------------------------------------------------
/Resources/Extension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.bermudalocket.redditweaks
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 | com.apple.developer.ubiquity-kvstore-identifier
16 | $(TeamIdentifierPrefix)com.bermudalocket.redditweaks
17 | com.apple.security.app-sandbox
18 |
19 | com.apple.security.application-groups
20 |
21 | group.com.bermudalocket.redditweaks
22 |
23 | com.apple.security.network.client
24 |
25 | keychain-access-groups
26 |
27 | $(AppIdentifierPrefix)group.com.bermudalocket.tweaksforreddit
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Resources/Main App Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleURLTypes
22 |
23 |
24 | CFBundleTypeRole
25 | Viewer
26 | CFBundleURLName
27 | rdtwks
28 | CFBundleURLSchemes
29 |
30 | rdtwks
31 |
32 |
33 |
34 | CFBundleVersion
35 | 20220304.220403
36 | ITSAppUsesNonExemptEncryption
37 |
38 | LSApplicationCategoryType
39 | public.app-category.social-networking
40 | LSMinimumSystemVersion
41 | $(MACOSX_DEPLOYMENT_TARGET)
42 | LSUIElement
43 |
44 | NSHumanReadableCopyright
45 | Copyright © 2019 Michael Rippe. All rights reserved.
46 | NSSupportsAutomaticTermination
47 |
48 | NSSupportsSuddenTermination
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/Resources/Main App.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.developer.aps-environment
6 | development
7 | com.apple.developer.icloud-container-identifiers
8 |
9 | iCloud.com.bermudalocket.redditweaks
10 |
11 | com.apple.developer.icloud-services
12 |
13 | CloudKit
14 |
15 | com.apple.developer.ubiquity-kvstore-identifier
16 | $(TeamIdentifierPrefix)$(CFBundleIdentifier)
17 | com.apple.security.app-sandbox
18 |
19 | com.apple.security.application-groups
20 |
21 | group.com.bermudalocket.redditweaks
22 |
23 | com.apple.security.network.client
24 |
25 | keychain-access-groups
26 |
27 | $(AppIdentifierPrefix)group.com.bermudalocket.tweaksforreddit
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/TFRCore/AppStoreService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppStoreService.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 8/24/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import Foundation
11 | import StoreKit
12 |
13 | public enum InAppPurchase: CaseIterable {
14 | case liveCommentPreview
15 |
16 | public var productId: String {
17 | switch self {
18 | case .liveCommentPreview: return "livecommentpreview"
19 | }
20 | }
21 |
22 | init?(string: String) {
23 | for iap in InAppPurchase.allCases {
24 | if iap.productId == string {
25 | self = iap
26 | }
27 | }
28 | return nil
29 | }
30 | }
31 |
32 | public class AppStoreService: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
33 |
34 | private let paymentQueue = SKPaymentQueue.default()
35 |
36 | private let restoreCompletePublisher = PassthroughSubject()
37 |
38 | private(set) var products = [SKProduct]()
39 |
40 | override init() {
41 | super.init()
42 | paymentQueue.add(self)
43 | logService("Added service to payment queue", service: .appStore)
44 |
45 | self.productRequest.start()
46 | }
47 |
48 | deinit {
49 | paymentQueue.remove(self)
50 | logService("Removed service from payment queue", service: .appStore)
51 | }
52 |
53 | public var productRequest: SKProductsRequest {
54 | let request = SKProductsRequest(productIdentifiers: Set(InAppPurchase.allCases.map(\.productId)))
55 | request.delegate = self
56 | return request
57 | }
58 |
59 | public func purchase(_ item: InAppPurchase) {
60 | logService("Purchase: \(item)", service: .appStore)
61 | switch item {
62 | case .liveCommentPreview:
63 | guard let item = self.products.first else {
64 | logService("Tried to purchase \(item) but self.products.first is null.", service: .appStore)
65 | return
66 | }
67 | paymentQueue.add(SKPayment(product: item))
68 | logService("Item added to payment queue", service: .appStore)
69 | }
70 | }
71 |
72 | public func restorePurchases() -> AnyPublisher {
73 | paymentQueue.restoreCompletedTransactions()
74 | logService("Restoring completed transactions", service: .appStore)
75 | return self.restoreCompletePublisher.eraseToAnyPublisher()
76 | }
77 |
78 | public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
79 | logService("Received products request response with \(response.products.count) product(s)", service: .appStore)
80 | self.products = response.products
81 | }
82 |
83 | public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
84 | logService("Received \(transactions.count) transaction(s)", service: .appStore)
85 | for transaction in transactions {
86 | switch transaction.transactionState {
87 | case .purchased, .restored:
88 | NSUbiquitousKeyValueStore.default.set(true, forKey: InAppPurchase.liveCommentPreview.productId)
89 |
90 | default:
91 | NSUbiquitousKeyValueStore.default.set(false, forKey: InAppPurchase.liveCommentPreview.productId)
92 | }
93 | }
94 | }
95 |
96 | public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
97 | self.restoreCompletePublisher.send(true)
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/TFRCore/AppStoreValidationRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppStoreValidationRequest.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/2/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct AppStoreValidationRequest: Encodable {
12 | let receipt: String
13 | let identifier: UUID
14 |
15 | public init(receipt: String, identifier: UUID) {
16 | self.receipt = receipt
17 | self.identifier = identifier
18 | }
19 | }
20 |
21 | public struct AppStoreValidationResponse: Decodable {
22 |
23 | public let id: String
24 |
25 | var quantity: Int {
26 | Int(internalQuantity) ?? -1
27 | }
28 | var transactionId: Int {
29 | Int(internalTransactionId) ?? -1
30 | }
31 | var purchaseDate: Date {
32 | Date(timeIntervalSince1970: Double(internalPurchaseDate) ?? -1)
33 | }
34 | var isTrial: Bool {
35 | internalIsTrial == "true"
36 | }
37 |
38 | private let internalQuantity: String
39 | let internalTransactionId: String
40 | let internalPurchaseDate: String
41 | let internalIsTrial: String
42 |
43 | enum CodingKeys: String, CodingKey {
44 | case id = "product_id"
45 | case internalQuantity = "quantity"
46 | case internalTransactionId = "transaction_id"
47 | case internalPurchaseDate = "original_purchase_date_ms"
48 | case internalIsTrial = "is_trial_period"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/TFRCore/CoreDataService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoreDataService.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 2/15/21.
6 | //
7 |
8 | import Combine
9 | import CoreData
10 |
11 | public class CoreDataService: ObservableObject {
12 |
13 | public static let shared = CoreDataService(inMemory: false)
14 |
15 | public let container: NSPersistentContainer
16 |
17 | public init(inMemory: Bool = false) {
18 | let bundle = Bundle.allFrameworks.first { $0.bundleIdentifier == "com.bermudalocket.TFRCore" }
19 | let url = bundle!.url(forResource: "Tweaks for Reddit", withExtension: "momd")!
20 | let model = NSManagedObjectModel(contentsOf: url)!
21 |
22 | let container = NSPersistentContainer(name: "Tweaks for Reddit", managedObjectModel: model)
23 | container.persistentStoreDescriptions.forEach {
24 | if inMemory {
25 | $0.url = URL(fileURLWithPath: "/dev/null")
26 | } else {
27 | $0.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
28 | $0.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
29 | $0.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.bermudalocket.redditweaks")
30 | }
31 | }
32 | container.loadPersistentStores { description, error in
33 | if let error = error {
34 | fatalError("\(error)")
35 | }
36 | log("NSPersistentContainer successfully created, persistent stores loaded.")
37 | container.viewContext.automaticallyMergesChangesFromParent = true
38 | }
39 | self.container = container
40 | }
41 |
42 | public var favoriteSubreddits: [FavoriteSubreddit] {
43 | do {
44 | let request = NSFetchRequest(entityName: "FavoriteSubreddit")
45 | return try container.viewContext.fetch(request)
46 | } catch {
47 | logError("Failed to fetch favorite subreddits: \(error)")
48 | return []
49 | }
50 | }
51 |
52 | public func userKarma(for user: String) -> Int? {
53 | let request = NSFetchRequest(entityName: "KarmaMemory")
54 | request.predicate = NSPredicate(format: "user = %@", user)
55 | do {
56 | return try container.viewContext
57 | .fetch(request)
58 | .map(\.karma)
59 | .compactMap { Int($0) }
60 | .reduce(0, +) // i love swift so much
61 | } catch {
62 | logError("Failed to fetch user karma for user \(user): \(error)")
63 | return nil
64 | }
65 | }
66 |
67 | public func saveUserKarma(for user: String, karma: Int) {
68 | let newKarma = KarmaMemory(context: container.viewContext)
69 | newKarma.user = user
70 | newKarma.karma = Int64(karma)
71 | do {
72 | try container.viewContext.save()
73 | } catch {
74 | logError("Error saving user karma: \(error)")
75 | }
76 | }
77 |
78 | public func commentCount(for thread: String) -> Int? {
79 | let request = NSFetchRequest(entityName: "ThreadCommentCount")
80 | request.predicate = NSPredicate(format: "thread = %@", thread)
81 | request.sortDescriptors = [
82 | .init(keyPath: \ThreadCommentCount.timestamp, ascending: false)
83 | ]
84 | request.fetchLimit = 1
85 | do {
86 | return try container.viewContext.fetch(request).map(\.count).first
87 | } catch {
88 | logError("Failed to fetch comment count for thread \(thread): \(error)")
89 | return nil
90 | }
91 | }
92 |
93 | public func saveCommentCount(for thread: String, count: Int) {
94 | let threadCommentCount = ThreadCommentCount(context: container.viewContext)
95 | threadCommentCount.thread = thread
96 | threadCommentCount.count = count
97 | threadCommentCount.timestamp = Date()
98 | do {
99 | try container.viewContext.save()
100 | } catch {
101 | logError("Failed to save comment count: \(error)")
102 | }
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/TFRCore/Debugger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Debugger.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/3/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os
11 |
12 | fileprivate let infoLog = OSLog(subsystem: "TfR", category: "info")
13 |
14 | public func log(_ message: String) {
15 | os_log(.default, log: infoLog, "[TfR] %{public}s", message)
16 | }
17 |
18 | public enum LogService {
19 | case background
20 | case defaults
21 | case keychain
22 | case appStore
23 | case notifications
24 |
25 | var format: StaticString {
26 | switch self {
27 | case .appStore: return "[TfR][] %{public}s"
28 | case .background: return "[TfR][👀] %{public}s"
29 | case .defaults: return "[TfR][⚙️] %{public}s"
30 | case .keychain: return "[TfR][🔑] %{public}s"
31 | case .notifications: return "[TfR][🔔] %{public}s"
32 | }
33 | }
34 | }
35 |
36 | public func logService(_ message: String, service: LogService) {
37 | os_log(.default, log: infoLog, service.format, message)
38 | }
39 |
40 | public func logInit(_ name: String) {
41 | os_log(.default, log: infoLog, "[TfR][⭐] %{public}s", name)
42 | }
43 |
44 | public func logDeinit(_ name: String) {
45 | os_log(.default, log: infoLog, "[TfR][🗑] %{public}s", name)
46 | }
47 |
48 | public func mockLog(_ message: String, level: OSLogType = .default) {
49 | os_log(level, log: infoLog, "[TfR][🔸 - mocked] %{public}s", message)
50 | }
51 |
52 | public func logSend(_ message: String) {
53 | os_log(.default, log: infoLog, "[TfR][🟩] %{public}s", message)
54 | }
55 |
56 | public func logError(_ message: String) {
57 | os_log(.error, log: infoLog, "[TfR][🚨] %{public}s", message)
58 | }
59 |
60 | public func logReducer(_ message: String) {
61 | os_log(.default, log: infoLog, "[TfR][🟪] %{public}s", message)
62 | }
63 |
--------------------------------------------------------------------------------
/TFRCore/DefaultsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultsService.swift
3 | // redditweaks
4 | //
5 | // Created by Michael Rippe on 6/23/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol DefaultsService {
12 | func bool(forKey: String) -> Bool
13 | func double(forKey: String) -> Double
14 | func getObject(_ key: String) -> Any?
15 | func get(_ forKey: DefaultsKey) -> Any?
16 | func set(_ value: Any?, forKey: String)
17 | }
18 |
19 | public extension DefaultsService {
20 | func get(_ forKey: DefaultsKey) -> Any? {
21 | getObject(forKey.rawValue)
22 | }
23 | func exists(_ forKey: DefaultsKey) -> Bool {
24 | get(forKey) != nil
25 | }
26 | func set(_ value: Any?, forKey: DefaultsKey) {
27 | set(value, forKey: forKey.rawValue)
28 | }
29 | }
30 |
31 | public enum DefaultsKey: String {
32 | case firstLaunch
33 | case selectedTab
34 | case oauthStatusCode
35 | case didCompleteOAuth
36 | case lastWhatsNewVersion
37 | case firstLaunchIsDefinite
38 | case lastReviewRequestTimestamp
39 | case favoriteSubredditListHeight
40 | case favoriteSubredditListSortingMethod
41 | case didPurchaseLiveCommentPreviews
42 | }
43 |
44 | class DefaultsServiceLive: DefaultsService {
45 | func bool(forKey: String) -> Bool {
46 | TweaksForReddit.defaults.bool(forKey: forKey)
47 | }
48 | func double(forKey: String) -> Double {
49 | TweaksForReddit.defaults.double(forKey: forKey)
50 | }
51 | func getObject(_ key: String) -> Any? {
52 | TweaksForReddit.defaults.object(forKey: key)
53 | }
54 | func set(_ value: Any?, forKey: String) {
55 | TweaksForReddit.defaults.set(value, forKey: forKey)
56 | }
57 | }
58 |
59 | public class DefaultsServiceMock: DefaultsService {
60 | public init() { }
61 | private var internalStorage = [String: Any]()
62 | public func bool(forKey: String) -> Bool {
63 | internalStorage[forKey] as? Bool ?? false
64 | }
65 | public func double(forKey: String) -> Double {
66 | internalStorage[forKey] as? Double ?? 0
67 | }
68 | public func getObject(_ key: String) -> Any? {
69 | internalStorage[key]
70 | }
71 | public func set(_ value: Any?, forKey: String) {
72 | logService("Set key \(forKey) to \(value.debugDescription)", service: .defaults)
73 | internalStorage[forKey] = value
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/TFRCore/Extension/Color+redditOrange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+redditOrange.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 5/21/21.
6 | // Copyright © 2021 Michael Rippe. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension Color {
12 | public static let redditOrange = Color(red: 1, green: 86.0/255.0, blue: 0.0)
13 | }
14 |
--------------------------------------------------------------------------------
/TFRCore/Extension/URL+expressibleAsStringLiteral.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+expressibleAsStringLiteral.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 2/20/22.
6 | // Copyright © 2022 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension URL: ExpressibleByStringLiteral {
12 |
13 | public init(stringLiteral value: StringLiteralType) {
14 | self = URL(string: value)!
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/TFRCore/Extension/URL+queryParameters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+queryParameters.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/24/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension URL {
12 | public var queryParameters: [String: String]? {
13 | var url = self.absoluteString
14 | guard let qmark = url.lastIndex(of: "?") else {
15 | return nil
16 | }
17 | url.removeSubrange(url.indices.startIndex...qmark)
18 |
19 | var queryParameters = [String: String]()
20 | for entry in url.split(separator: "&") {
21 | let kv = entry.split(separator: "=")
22 | guard let key = kv.first, let value = kv.last else {
23 | continue
24 | }
25 | queryParameters[String(key)] = String(value)
26 | }
27 |
28 | return queryParameters
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/TFRCore/Extension/URL+redditURLs.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+redditURLs.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/25/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension URL {
12 |
13 | static let accessToken = Self(string: "https://www.reddit.com/api/v1/access_token")!
14 | static let refreshToken = accessToken
15 |
16 |
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/TFRCore/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSHumanReadableCopyright
22 | Copyright © 2021 bermudalocket. All rights reserved.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/TFRCore/KeychainService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeychainService.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/23/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Security
11 | import KeychainAccess
12 |
13 | public protocol KeychainService {
14 | func getTokens() -> Tokens?
15 | func setTokens(_ tokens: Tokens)
16 | }
17 |
18 | class KeychainServiceLive: KeychainService {
19 |
20 | private let keychain = Keychain(
21 | service: "com.bermudalocket.tweaksforreddit",
22 | accessGroup: "2VZ489BR9H.group.com.bermudalocket.tweaksforreddit"
23 | )
24 | .synchronizable(true)
25 | .accessibility(.always)
26 |
27 | func get(_ key: String) -> String? {
28 | try? keychain.get(key)
29 | }
30 |
31 | func set(_ key: String, value: String) {
32 | try? keychain.set(value, key: key)
33 | }
34 |
35 | func getTokens() -> Tokens? {
36 | guard let accessToken = get("accessToken") else {
37 | logError("Access token not found")
38 | return nil
39 | }
40 | guard let refreshToken = get("refreshToken") else {
41 | logError("Refresh token not found")
42 | return nil
43 | }
44 | return Tokens(accessToken: accessToken, refreshToken: refreshToken)
45 | }
46 |
47 | func setTokens(_ tokens: Tokens) {
48 | do {
49 | guard let accessToken = tokens.accessToken, let refreshToken = tokens.refreshToken else {
50 | throw RedditError.noToken
51 | }
52 | set("accessToken", value: accessToken)
53 | set("refreshToken", value: refreshToken)
54 | } catch {
55 | logService("Keychain error saving: \(error)", service: .keychain)
56 | }
57 | }
58 |
59 | }
60 |
61 | public class KeychainServiceMock: KeychainService {
62 |
63 | public init() { }
64 |
65 | private var tokens = Tokens(accessToken: "access-token-mocked", refreshToken: "refresh-token-mocked")
66 |
67 | public func getTokens() -> Tokens? {
68 | tokens
69 | }
70 |
71 | public func setTokens(_ tokens: Tokens) {
72 | self.tokens = tokens
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/TFRCore/NotificationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationService.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 8/20/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import AppKit
10 | import Foundation
11 | import UserNotifications
12 |
13 | public class NotificationService: NSObject, UNUserNotificationCenterDelegate {
14 |
15 | public static let shared = NotificationService()
16 |
17 | private override init() {
18 | super.init()
19 | let action = UNNotificationAction(identifier: "OPEN_URL", title: "Open URL", options: .init(rawValue: 0))
20 | let category = UNNotificationCategory(identifier: "MESSAGES", actions: [action], intentIdentifiers: [], options: .init())
21 | UNUserNotificationCenter.current().setNotificationCategories([category])
22 | UNUserNotificationCenter.current().delegate = self
23 | }
24 |
25 | public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
26 | guard let urlStr = response.notification.request.content.userInfo["URL"] as? String,
27 | let url = URL(string: "https://www.reddit.com" + urlStr) else {
28 | return
29 | }
30 | NSWorkspace.shared.open(url)
31 | }
32 |
33 | public func send(msg: UnreadMessage) {
34 | let id = String((msg.author + msg.body + msg.subreddit).hashValue)
35 |
36 | let notification = UNMutableNotificationContent()
37 | notification.title = "New message"
38 | notification.subtitle = msg.author + " in r/" + msg.subreddit
39 | notification.sound = .default
40 | notification.body = msg.body
41 | notification.userInfo = [ "URL": msg.context ]
42 | notification.categoryIdentifier = "MESSAGES"
43 | // if #available(macOS 12.0, *) {
44 | // notification.interruptionLevel = .active
45 | // }
46 |
47 | UNUserNotificationCenter.current().getDeliveredNotifications { notifications in
48 | if notifications.filter({ $0.request.identifier == id }).count == 0 {
49 | let request = UNNotificationRequest(
50 | identifier: id,
51 | content: notification,
52 | trigger: UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
53 | )
54 | UNUserNotificationCenter.current().add(request) { error in
55 | if let error = error {
56 | logError("NotificationService error: \(error)")
57 | } else {
58 | logService("Notification delivered with ID \(id)", service: .notifications)
59 | }
60 | }
61 | } else {
62 | logService("This message has already been delivered", service: .notifications)
63 | }
64 | }
65 |
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/TFRCore/OAuth/Listing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Listing.swift
3 | // Listing
4 | //
5 | // Created by Michael Rippe on 9/13/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Listing: Decodable where T: Decodable {
12 | public let kind: String
13 | public let data: ListingGuts
14 |
15 | public struct ListingGuts: Decodable where T: Decodable {
16 | public let after: String?
17 | public let count: Int
18 | public let contents: [ListingGutsChildren]
19 |
20 | public struct ListingGutsChildren: Decodable where T: Decodable {
21 | public let kind: String
22 | public let data: T
23 | }
24 |
25 | public enum CodingKeys: String, CodingKey {
26 | case after
27 | case count = "dist"
28 | case contents = "children"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/TFRCore/OAuth/Post.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Post.swift
3 | // Post
4 | //
5 | // Created by Michael Rippe on 9/11/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Post: Codable, Equatable, Hashable {
12 | public let subreddit: String
13 | public let title: String
14 | public let permalink: String
15 |
16 | /// The name of this post, e.g. t3_abcdef
17 | public let name: String
18 |
19 | public init(subreddit: String, title: String, permalink: String, name: String) {
20 | self.subreddit = subreddit
21 | self.title = title
22 | self.permalink = permalink
23 | self.name = name
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/TFRCore/OAuth/RedditError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OAuthError.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/24/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum RedditError: Error, Equatable {
12 |
13 | case badResponse(code: Int? = nil)
14 |
15 | case noToken
16 |
17 | case wrapping(message: String)
18 |
19 | // indicates the token needs to be refreshed
20 | case unauthorized
21 |
22 | // indicates the snoovatar failed to download
23 | case downloadFailed
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/TFRCore/OAuth/Tokens.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tokens.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/24/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct Tokens: Decodable, Equatable {
12 | public let accessToken: String?
13 | public let refreshToken: String?
14 |
15 | enum CodingKeys: String, CodingKey {
16 | case accessToken = "access_token"
17 | case refreshToken = "refresh_token"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/TFRCore/OAuth/UnreadMessages.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnreadMessages.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/24/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct UnreadMessagesResponse: Codable, Equatable {
12 |
13 | public let kind: String
14 | public let data: UnreadMessagesData
15 |
16 | public struct UnreadMessagesData: Codable, Equatable {
17 | public let children: [UnreadMessageParent]
18 |
19 | public struct UnreadMessageParent: Codable, Equatable {
20 | public let kind: String
21 | public let data: UnreadMessage
22 | }
23 | }
24 |
25 | }
26 |
27 | public struct UnreadMessage: Codable, Equatable, Hashable {
28 | public let author: String
29 | public let body: String
30 | public let subreddit: String
31 | public let subject: String // "comment reply", "post reply", ...
32 | public let context: String
33 | public let created: Double
34 |
35 | public init(author: String, body: String, subreddit: String, subject: String, context: String, created: Double) {
36 | self.author = author
37 | self.body = body
38 | self.subreddit = subreddit
39 | self.subject = subject
40 | self.context = context
41 | self.created = created
42 | }
43 | }
44 |
45 | extension UnreadMessage {
46 | public var createdTimestamp: Date {
47 | Date(timeIntervalSince1970: self.created)
48 | }
49 | }
50 |
51 | extension UnreadMessagesResponse {
52 | public static let empty = Self(kind: "empty", data: UnreadMessagesData(children: []))
53 | }
54 |
--------------------------------------------------------------------------------
/TFRCore/OAuth/UserData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserData.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/17/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct UserData: Codable, Equatable {
12 |
13 | public let username: String?
14 | public let snoovatarImageURL: String?
15 | public let postKarma: Int?
16 | public let commentKarma: Int?
17 | public let inboxCount: Int?
18 |
19 | enum CodingKeys: String, CodingKey {
20 | case username = "name"
21 | case snoovatarImageURL = "snoovatar_img"
22 | case postKarma = "link_karma"
23 | case commentKarma = "comment_karma"
24 | case inboxCount = "inbox_count"
25 | }
26 |
27 | }
28 |
29 | extension UserData {
30 | public var snoovatarUrl: URL? {
31 | guard let url = snoovatarImageURL else {
32 | return nil
33 | }
34 | return URL(string: url)
35 | }
36 | }
37 |
38 | extension UserData {
39 | public static let mock = UserData(
40 | username: "thebermudamocket",
41 | snoovatarImageURL: "https://i.redd.it/snoovatar/snoovatars/2d193ec6-03ef-4a80-a651-637f7ed0dd93.png",
42 | postKarma: 1234,
43 | commentKarma: 98765,
44 | inboxCount: 3
45 | )
46 | }
47 |
48 | /*
49 |
50 | {
51 | "kind": "t2",
52 | "data": {
53 | "snoovatar_img": "https://i.redd.it/snoovatar/snoovatars/2d193ec6-03ef-4a80-a651-637f7ed0dd93.png",
54 | "snoovatar_size": [
55 | 380,
56 | 600
57 | ],
58 | "gold_expiration": 1626459655,
59 | "new_modmail_exists": false,
60 | "has_mod_mail": false,
61 | "coins": 2600,
62 | "awarder_karma": 116,
63 | "awardee_karma": 526,
64 | "link_karma": 6082,
65 | "total_karma": 17510,
66 | "comment_karma": 10786,
67 | "inbox_count": 0,
68 | "has_mail": false,
69 | "created": 1414988589,
70 | }
71 | }
72 |
73 | */
74 |
--------------------------------------------------------------------------------
/TFRCore/Persistence/FavoriteSubreddit+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteSubreddit+CoreDataClass.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/27/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 | //
9 |
10 | import Foundation
11 | import CoreData
12 | import AppKit
13 |
14 | @objc(FavoriteSubreddit)
15 | public class FavoriteSubreddit: NSManagedObject {
16 |
17 | }
18 |
19 | public extension FavoriteSubreddit {
20 | @objc func open() {
21 | guard let name = self.name, let url = URL(string: "https://www.reddit.com/r/\(name)") else {
22 | return
23 | }
24 | NSWorkspace.shared.open(url)
25 | }
26 | }
27 |
28 | public class FavoriteSubredditMock: FavoriteSubreddit {
29 |
30 | var stubbedName: String?
31 | var stubbedPosition: Int?
32 |
33 | public convenience init(name: String = " ", position: Int = 0) {
34 | self.init()
35 | self.stubbedName = name
36 | self.stubbedPosition = position
37 | }
38 |
39 | public override var name: String? {
40 | get {
41 | stubbedName
42 | }
43 | set {
44 | stubbedName = newValue
45 | }
46 | }
47 |
48 | override public var internalPosition: Int16 {
49 | get {
50 | Int16(stubbedPosition ?? 0)
51 | }
52 | set {
53 | stubbedPosition = Int(newValue)
54 | }
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/TFRCore/Persistence/FavoriteSubreddit+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteSubreddit+CoreDataProperties.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/27/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 | //
9 |
10 | import Foundation
11 | import CoreData
12 |
13 |
14 | extension FavoriteSubreddit {
15 |
16 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
17 | return NSFetchRequest(entityName: "FavoriteSubreddit")
18 | }
19 |
20 | @NSManaged public var internalPosition: Int16
21 | @NSManaged public var name: String?
22 |
23 | }
24 |
25 | extension FavoriteSubreddit : Identifiable {
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/TFRCore/Persistence/FavoriteSubreddit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteSubreddit.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 5/31/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | extension FavoriteSubreddit {
13 | public var position: Int {
14 | get { Int(internalPosition) }
15 | set { internalPosition = Int16(newValue) }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/TFRCore/Persistence/KarmaMemory+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KarmaMemory+CoreDataClass.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/27/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 | //
9 |
10 | import Foundation
11 | import CoreData
12 |
13 | @objc(KarmaMemory)
14 | public class KarmaMemory: NSManagedObject {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/TFRCore/Persistence/KarmaMemory+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KarmaMemory+CoreDataProperties.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/27/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 | //
9 |
10 | import Foundation
11 | import CoreData
12 |
13 |
14 | extension KarmaMemory {
15 |
16 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
17 | return NSFetchRequest(entityName: "KarmaMemory")
18 | }
19 |
20 | @NSManaged public var karma: Int64
21 | @NSManaged public var user: String?
22 |
23 | }
24 |
25 | extension KarmaMemory : Identifiable {
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/TFRCore/Persistence/ThreadCommentCount+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThreadCommentCount+CoreDataClass.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/27/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 | //
9 |
10 | import Foundation
11 | import CoreData
12 |
13 | @objc(ThreadCommentCount)
14 | public class ThreadCommentCount: NSManagedObject, Identifiable {
15 |
16 | @NSManaged public var internalCount: Int64
17 | @NSManaged public var thread: String?
18 | @NSManaged public var timestamp: Date?
19 |
20 | }
21 |
22 | extension ThreadCommentCount {
23 | public var count: Int {
24 | get { Int(internalCount) }
25 | set { internalCount = Int64(newValue) }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/TFRCore/RedditweaksButtonStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tweaks for RedditButtonStyle.swift
3 | // Tweaks for Reddit
4 | //
5 | // Created by Michael Rippe on 4/23/21.
6 | // Copyright © 2021 Michael Rippe. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct RedditweaksButtonStyle: ButtonStyle {
12 |
13 | @Environment(\.colorScheme) private var colorScheme
14 |
15 | @Environment(\.isEnabled) private var isEnabled
16 |
17 | @State private var isHovered = false
18 |
19 | private var textColor: Color {
20 | if isEnabled {
21 | return isHovered ? Color(.windowBackgroundColor) : .white
22 | }
23 | return Color(.placeholderTextColor)
24 | }
25 |
26 | private func calculateScale(with configuration: Configuration) -> CGFloat {
27 | var scale: CGFloat = 1.0
28 | if isEnabled {
29 | if configuration.isPressed {
30 | scale -= 0.1
31 | } else if isHovered {
32 | scale -= 0.05
33 | }
34 | }
35 | return scale
36 | }
37 |
38 | public init() { }
39 |
40 | public func makeBody(configuration: Configuration) -> some View {
41 | configuration
42 | .label
43 | .font(.system(.title3).bold())
44 | .padding(10)
45 | .foregroundColor(textColor)
46 | .transition(.opacity)
47 | .background(
48 | RoundedRectangle(cornerRadius: 12, style: .continuous)
49 | .foregroundColor(isEnabled ? .redditOrange : Color(.disabledControlTextColor))
50 | .transition(.opacity)
51 | )
52 | .onHover { isHovered in
53 | withAnimation(.easeInOut) {
54 | self.isHovered = isHovered
55 | }
56 | }
57 | .scaleEffect(calculateScale(with: configuration))
58 | }
59 |
60 | }
61 |
62 | // swiftlint:disable:next type_name
63 | struct RedditweaksButtonStyle_Preview: PreviewProvider {
64 |
65 | static var previews: some View {
66 | Button { } label: {
67 | Text("Preview")
68 | }
69 | .buttonStyle(RedditweaksButtonStyle())
70 | .padding()
71 | Button { } label: {
72 | Text("Preview")
73 | }
74 | .buttonStyle(RedditweaksButtonStyle())
75 | .disabled(true)
76 | .padding()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/TFRCore/TFRCore.h:
--------------------------------------------------------------------------------
1 | //
2 | // TFRCore.h
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/27/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for TFRCore.
12 | FOUNDATION_EXPORT double TFRCoreVersionNumber;
13 |
14 | //! Project version string for TFRCore.
15 | FOUNDATION_EXPORT const unsigned char TFRCoreVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
--------------------------------------------------------------------------------
/TFRCore/TFREnvironment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TFREnvironment.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 6/25/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SafariServices
11 |
12 | public struct TFREnvironment {
13 |
14 | public static let shared = Self(
15 | oauth: RedditService(),
16 | coreData: CoreDataService(inMemory: false),
17 | defaults: DefaultsServiceLive(),
18 | keychain: KeychainServiceLive(),
19 | appStore: AppStoreService()
20 | )
21 |
22 | public var reddit: RedditCommunicating
23 | public var coreData: CoreDataService
24 | public var defaults: DefaultsService
25 | public var keychain: KeychainService
26 | public var appStore: AppStoreService
27 |
28 | init(oauth: RedditCommunicating, coreData: CoreDataService, defaults: DefaultsService, keychain: KeychainService, appStore: AppStoreService) {
29 | self.reddit = oauth
30 | self.coreData = coreData
31 | self.defaults = defaults
32 | self.keychain = keychain
33 | self.appStore = appStore
34 | }
35 |
36 | }
37 |
38 | import Combine
39 |
40 | extension TFREnvironment {
41 |
42 | /// Used for SwiftUI preview providers only.
43 | public static func mocked() -> TFREnvironment {
44 | TFREnvironment(
45 | oauth: RedditServiceMocked(),
46 | coreData: CoreDataService(inMemory: true),
47 | defaults: DefaultsServiceMock(),
48 | keychain: KeychainServiceMock(),
49 | appStore: AppStoreService()
50 | )
51 | }
52 |
53 | private struct RedditServiceMocked: RedditCommunicating {
54 | func begin(state: String) { }
55 | func exchangeCodeForTokens(code: String) -> AnyPublisher {
56 | Just(Tokens(accessToken: "mocked-Access-Token", refreshToken: "mocked-Refresh-Token"))
57 | .setFailureType(to: RedditError.self)
58 | .eraseToAnyPublisher()
59 | }
60 | func getUserData(tokens: Tokens) -> AnyPublisher {
61 | Just(UserData.mock)
62 | .setFailureType(to: RedditError.self)
63 | .eraseToAnyPublisher()
64 | }
65 | func getMessages(tokens: Tokens) -> AnyPublisher<[UnreadMessage], RedditError> {
66 | Just([])
67 | .setFailureType(to: RedditError.self)
68 | .eraseToAnyPublisher()
69 | }
70 | func getHiddenPosts(tokens: Tokens, username: String, after: Post?, before: Post?) -> AnyPublisher<[Post], RedditError> {
71 | Just([
72 | Post(subreddit: "mocking", title: "This is a mocked post", permalink: "", name: "t3_mocked")
73 | ])
74 | .setFailureType(to: RedditError.self)
75 | .eraseToAnyPublisher()
76 | }
77 | func unhide(tokens: Tokens, posts: [Post]) -> AnyPublisher {
78 | Just(true)
79 | .setFailureType(to: RedditError.self)
80 | .eraseToAnyPublisher()
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/TFRCore/Tweaks for Reddit.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | Persistence 6.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TFRCore/Tweaks for Reddit.xcdatamodeld/Persistence 6.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/TFRCore/TweaksForReddit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TweaksForReddit.swift
3 | // TFRCore
4 | //
5 | // Created by Michael Rippe on 7/9/20.
6 | // Copyright © 2020 Michael Rippe. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import os
11 |
12 | public enum TweaksForReddit {
13 |
14 | public static let bundleId = "com.bermudalocket.redditweaks"
15 |
16 | public static let extensionId = "\(bundleId).extension"
17 |
18 | public static let groupId = "group.\(bundleId)"
19 |
20 | public static let defaults = UserDefaults(suiteName: groupId)!
21 |
22 | public static var version: String {
23 | let major = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
24 | let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
25 | return "\(major)-\(build)"
26 | }
27 |
28 | public static var identifier: UUID {
29 | guard let stored = defaults.string(forKey: "identifier"), let uuid = UUID(uuidString: stored) else {
30 | let newId = UUID()
31 | defaults.setValue(newId.uuidString, forKey: "identifier")
32 | return newId
33 | }
34 | return uuid
35 | }
36 |
37 | public static let popoverWidth: CGFloat = 350.0
38 |
39 | public static var debugInfo: String {
40 | let osVersion = ProcessInfo.processInfo.operatingSystemVersionString
41 | return "\n\nTFR \(self.version), macOS \(osVersion)"
42 | }
43 |
44 | public static func symbolForSubreddit(_ subreddit: String) -> String {
45 | sfSymbolsMap[subreddit] ?? "doc"
46 | }
47 |
48 | private static let sfSymbolsMap: [String: String] = [
49 | "apple": "applelogo",
50 | "art": "paintbrush.fill",
51 | "books": "books.vertical.fill",
52 | "consulting": "rectangle.3.offgrid.bubble.left.fill",
53 | "earthporn": "globe",
54 | "explainlikeimfive": "questionmark.circle.fill",
55 | "gaming": "gamecontroller.fill",
56 | "math": "function",
57 | "movies": "film.fill",
58 | "music": "music.quarternote.3",
59 | "news": "newspaper.fill",
60 | "pics": "photo.fill",
61 | "photography": "photo.fill",
62 | "space": "moon.stars.fill",
63 | "sports": "sportscourt.fill",
64 | "television": "tv.fill",
65 | "todayilearned": "lightbulb.fill",
66 | "worldnews": "newspaper.fill",
67 |
68 | // apple products
69 | "iphone": "iphone",
70 | "ipad": "ipad",
71 | "ipados": "ipad",
72 | "ipadosbeta": "ipad",
73 | "macos": "desktopcomputer",
74 | "macosbeta": "desktopcomputer",
75 | "ios": "ipad",
76 | "iosbeta": "ipad",
77 | "homepod": "homepod.fill",
78 | ]
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Tweaks for Reddit Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 12
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tweaks for Reddit Tests/TweaksForRedditTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tweaks for RedditTests.swift
3 | // Tweaks for RedditTests
4 | // 5.0
5 | // 10.16
6 | //
7 | // Created by Michael Rippe on 6/26/20.
8 | // Copyright © 2020 Michael Rippe. All rights reserved.
9 | //
10 |
11 | import Combine
12 | import XCTest
13 | @testable import Tweaks_for_Reddit
14 | @testable import Tweaks_for_Reddit_Extension
15 | @testable import TFRCore
16 | @testable import TFRCompose
17 | @testable import Tweaks_for_Reddit_Popover
18 |
19 | extension Task where Success == Never, Failure == Never {
20 | public static func sleep(_ seconds: Int) async throws {
21 | try await Task.sleep(nanoseconds: .init(UInt64(seconds) * NSEC_PER_SEC))
22 | }
23 | }
24 |
25 | class TweaksForRedditTests: XCTestCase {
26 |
27 | private var cancellables = Set()
28 |
29 | func testICloudKeyValueStore() async throws {
30 | let randomId = UUID().uuidString
31 | let iCloud = NSUbiquitousKeyValueStore.default
32 |
33 | iCloud.set(true, forKey: randomId)
34 |
35 | iCloud.synchronize()
36 |
37 | XCTAssert(iCloud.bool(forKey: randomId))
38 | }
39 |
40 | func testReceipt() async throws {
41 | let store = Store(
42 | initialState: MainAppState(tab: .liveCommentPreview),
43 | reducer: mainAppReducer,
44 | environment: .shared
45 | )
46 |
47 | NSUbiquitousKeyValueStore.default.set(true, forKey: InAppPurchase.liveCommentPreview.productId)
48 |
49 | store.send(.restorePurchases)
50 |
51 | XCTAssert(store.state.receiptValidationStatus == .valid)
52 | }
53 |
54 | func testOAuth() {
55 | let waiter = XCTestExpectation(description: "oauth")
56 |
57 | let store: MainAppStore = MainAppStore(
58 | initialState: .init(),
59 | reducer: .none,
60 | environment: TFREnvironment(
61 | oauth: RedditService(),
62 | coreData: CoreDataService.init(inMemory: true),
63 | defaults: DefaultsServiceMock(),
64 | keychain: KeychainServiceMock(),
65 | appStore: AppStoreService()
66 | )
67 | )
68 |
69 | store.$state.sink { newState in
70 | if newState.oauthState == .completed {
71 | waiter.fulfill()
72 | }
73 | }.store(in: &cancellables)
74 |
75 | store.send(.beginOAuth)
76 | store.send(.exchangeCodeForTokens(incomingUrl: "rdtwks://oauth?code=mocked"))
77 |
78 | wait(for: [waiter], timeout: 10)
79 |
80 | XCTAssertTrue(store.state.oauthState == .completed)
81 | }
82 |
83 | func testAllFeaturesAreAssignedAPageType() {
84 | Feature.features.forEach { feature in
85 | XCTAssertTrue(RedditPageType.allCases.map { $0.features.contains(feature) }.count >= 1)
86 | }
87 | }
88 | //
89 | // func testFetchRequest() {
90 | // let env = TFREnvironment(
91 | // oauth: .mock,
92 | // iap: .shared,
93 | // coreData: .preview,
94 | // defaults: .mock
95 | // )
96 | // let testSub = FavoriteSubreddit(context: PersistenceController.shared.container.viewContext)
97 | // testSub.name = "Apple"
98 | // XCTAssertTrue(PersistenceController.shared.favoriteSubreddits.contains { $0.name == "Apple" })
99 | // }
100 | //
101 | // func testCoreDataAddDelete() {
102 | // let vc = PersistenceController.shared.container.viewContext
103 | //
104 | // let uuid = UUID().uuidString
105 | //
106 | // // add
107 | // let sub = FavoriteSubreddit(context: vc)
108 | // sub.name = uuid
109 | // XCTAssertTrue(PersistenceController.shared.favoriteSubreddits.contains { $0.name == uuid })
110 | //
111 | // // delete
112 | // vc.delete(sub)
113 | // XCTAssertTrue(!PersistenceController.shared.favoriteSubreddits.contains { $0.name == uuid })
114 | // }
115 |
116 | func testBuildJavascript() {
117 | // let safari = SafariExtensionHandler()
118 | // XCTAssertEqual(safari.buildJavascriptFunction(for: .collapseAutoModerator), "collapseAutoModerator()")
119 | // XCTAssertEqual(safari.buildJavascriptFunction(for: .collapseChildComments), "collapseChildComments()")
120 | // XCTAssertEqual(safari.buildJavascriptFunction(for: .hideAds), "hideAds()")
121 | // XCTAssertEqual(safari.buildJavascriptFunction(for: .hideNewRedditButton), "hideNewRedditButton()")
122 | // XCTAssertEqual(safari.buildJavascriptFunction(for: .hideRedditPremiumBanner), "hideRedditPremiumBanner()")
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "KeychainAccess",
6 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
10 | "version": "4.2.2"
11 | }
12 | },
13 | {
14 | "package": "Kingfisher",
15 | "repositoryURL": "https://github.com/onevcat/Kingfisher",
16 | "state": {
17 | "branch": null,
18 | "revision": "d06df9adf50ed8cde5786d935836a5f445f780ba",
19 | "version": "6.3.1"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildLocationStyle
6 | UseAppPreferences
7 | CustomBuildLocationType
8 | RelativeToDerivedData
9 | DerivedDataLocationStyle
10 | Default
11 | IssueFilterStyle
12 | ShowActiveSchemeOnly
13 | LiveSourceIssuesEnabled
14 |
15 | ShowSharedSchemesAutomaticallyEnabled
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/xcdebugger/Expressions.xcexplist:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
8 |
10 |
11 |
12 |
13 |
15 |
16 |
18 |
19 |
21 |
22 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/mikerippe.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Tweaks for Reddit.xcodeproj/project.xcworkspace/xcuserdata/mikerippe.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcbaselines/895233BE24A6AC0300B72CE8.xcbaseline/E97D8360-7C44-4A54-81E9-01D39D4CD8F2.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | classNames
6 |
7 | TweaksForRedditTests
8 |
9 | testCoreData()
10 |
11 | com.apple.XCTPerformanceMetric_WallClockTime
12 |
13 | baselineAverage
14 | 0.0035121
15 | baselineIntegrationDisplayName
16 | Local Baseline
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcbaselines/895233BE24A6AC0300B72CE8.xcbaseline/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | runDestinationsByUUID
6 |
7 | E97D8360-7C44-4A54-81E9-01D39D4CD8F2
8 |
9 | localComputer
10 |
11 | busSpeedInMHz
12 | 400
13 | cpuCount
14 | 1
15 | cpuKind
16 | 6-Core Intel Core i7
17 | cpuSpeedInMHz
18 | 2600
19 | logicalCPUCoresPerPackage
20 | 12
21 | modelCode
22 | MacBookPro16,1
23 | physicalCPUCoresPerPackage
24 | 6
25 | platformIdentifier
26 | com.apple.platform.macosx
27 |
28 | targetArchitecture
29 | x86_64
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcbaselines/89DD691F24AA8AE800599B28.xcbaseline/8761E753-CE72-475C-9BBA-89955EF0A75A.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | classNames
6 |
7 | redditweaksUITests
8 |
9 | testLaunchPerformance()
10 |
11 | com.apple.dt.XCTMetric_ApplicationLaunch-ApplicationLaunchExtended.duration
12 |
13 | baselineAverage
14 | 1.0681
15 | baselineIntegrationDisplayName
16 | Local Baseline
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcbaselines/89DD691F24AA8AE800599B28.xcbaseline/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | runDestinationsByUUID
6 |
7 | 8761E753-CE72-475C-9BBA-89955EF0A75A
8 |
9 | localComputer
10 |
11 | busSpeedInMHz
12 | 400
13 | cpuCount
14 | 1
15 | cpuKind
16 | 6-Core Intel Core i7
17 | cpuSpeedInMHz
18 | 2600
19 | logicalCPUCoresPerPackage
20 | 12
21 | modelCode
22 | MacBookPro16,1
23 | physicalCPUCoresPerPackage
24 | 6
25 | platformIdentifier
26 | com.apple.platform.macosx
27 |
28 | targetArchitecture
29 | x86_64
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/Main App UI Tests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
17 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
44 |
45 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/Main App Unit Tests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
11 |
15 |
16 |
17 |
18 |
19 |
25 |
31 |
32 |
33 |
34 |
35 |
42 |
43 |
49 |
50 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
81 |
82 |
83 |
85 |
86 |
87 |
88 |
89 |
90 |
100 |
101 |
107 |
108 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/Popover.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/TFRCore Tests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
14 |
15 |
17 |
23 |
24 |
25 |
26 |
27 |
37 |
39 |
45 |
46 |
47 |
48 |
54 |
55 |
61 |
62 |
63 |
64 |
66 |
67 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/TFRCore.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/TFRIntents.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
45 |
46 |
47 |
48 |
60 |
62 |
68 |
69 |
70 |
71 |
78 |
80 |
86 |
87 |
88 |
89 |
91 |
92 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcshareddata/xcschemes/Tweaks for Reddit Extension.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
45 |
46 |
47 |
48 |
58 |
62 |
63 |
64 |
70 |
71 |
72 |
73 |
76 |
77 |
80 |
81 |
84 |
85 |
88 |
89 |
92 |
93 |
96 |
97 |
98 |
99 |
106 |
108 |
114 |
115 |
116 |
117 |
119 |
120 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/bermudalocket.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
16 |
17 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
42 |
43 |
44 |
46 |
53 |
54 |
55 |
56 |
57 |
59 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/bermudalocket.xcuserdatad/xcschemes/TweaksForRedditTests.testICloudKeyValueStore.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
63 |
64 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/bermudalocket.xcuserdatad/xcschemes/TweaksForRedditTests.testReceipt.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
63 |
64 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/bermudalocket.xcuserdatad/xcschemes/TweaksForRedditTests.testValidateReceipt.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
63 |
64 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/bermudalocket.xcuserdatad/xcschemes/[DEBUG] Tweaks for Reddit.app.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
44 |
46 |
52 |
53 |
54 |
55 |
58 |
59 |
62 |
63 |
66 |
67 |
70 |
71 |
74 |
75 |
76 |
78 |
79 |
80 |
86 |
87 |
93 |
94 |
95 |
96 |
98 |
99 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/bermudalocket.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Main App UI Tests.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 6
11 |
12 | Main App Unit Tests.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 5
16 |
17 | Popover.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 2
21 |
22 | SnapKitPlayground (Playground) 1.xcscheme
23 |
24 | isShown
25 |
26 | orderHint
27 | 15
28 |
29 | SnapKitPlayground (Playground) 2.xcscheme
30 |
31 | isShown
32 |
33 | orderHint
34 | 17
35 |
36 | SnapKitPlayground (Playground).xcscheme
37 |
38 | isShown
39 |
40 | orderHint
41 | 8
42 |
43 | TFRBackgroundFetcher.xcscheme_^#shared#^_
44 |
45 | orderHint
46 | 13
47 |
48 | TFRCore Tests.xcscheme_^#shared#^_
49 |
50 | orderHint
51 | 10
52 |
53 | TFRCore.xcscheme_^#shared#^_
54 |
55 | orderHint
56 | 3
57 |
58 | TFRIntents.xcscheme_^#shared#^_
59 |
60 | orderHint
61 | 11
62 |
63 | TfRGlobals Tests.xcscheme_^#shared#^_
64 |
65 | orderHint
66 | 16
67 |
68 | TfRGlobals.xcscheme_^#shared#^_
69 |
70 | orderHint
71 | 9
72 |
73 | Tweaks for Reddit Extension.xcscheme_^#shared#^_
74 |
75 | orderHint
76 | 1
77 |
78 | Tweaks for Reddit Notifications.xcscheme_^#shared#^_
79 |
80 | orderHint
81 | 12
82 |
83 | Tweaks for Reddit.app.xcscheme_^#shared#^_
84 |
85 | orderHint
86 | 0
87 |
88 | TweaksForRedditTests.testICloudKeyValueStore.xcscheme
89 |
90 | isShown
91 |
92 | orderHint
93 | 9
94 |
95 | TweaksForRedditTests.testReceipt.xcscheme
96 |
97 | isShown
98 |
99 | orderHint
100 | 8
101 |
102 | TweaksForRedditTests.testValidateReceipt.xcscheme
103 |
104 | isShown
105 |
106 | orderHint
107 | 7
108 |
109 | [DEBUG] Tweaks for Reddit.app.xcscheme
110 |
111 | orderHint
112 | 4
113 |
114 | block.xcscheme_^#shared#^_
115 |
116 | orderHint
117 | 14
118 |
119 |
120 | SuppressBuildableAutocreation
121 |
122 | 8925682726ED27AD00268E76
123 |
124 | primary
125 |
126 |
127 | 8925FD0526337E0A00D83FF1
128 |
129 | primary
130 |
131 |
132 | 895233BE24A6AC0300B72CE8
133 |
134 | primary
135 |
136 |
137 | 897A296226E6FD2700268E76
138 |
139 | primary
140 |
141 |
142 | 898A38F72688FE5E00268E76
143 |
144 | primary
145 |
146 |
147 | 898A390E2688FFBA00268E76
148 |
149 | primary
150 |
151 |
152 | 898F0574267EBDE600268E76
153 |
154 | primary
155 |
156 |
157 | 89E03BF626E93CDC00268E76
158 |
159 | primary
160 |
161 |
162 | 89EAB7412687F24000268E76
163 |
164 | primary
165 |
166 |
167 | 99C7C2EC235D81A700ABA6EA
168 |
169 | primary
170 |
171 |
172 | 99C7C2FF235D81AA00ABA6EA
173 |
174 | primary
175 |
176 |
177 |
178 |
179 |
180 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/mikerippe.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
23 |
25 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcodeproj/xcuserdata/mikerippe.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | redditweaks Extension.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | redditweaks.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 1
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 99C7C2EC235D81A700ABA6EA
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "KeychainAccess",
6 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "84e546727d66f1adc5439debad16270d0fdd04e7",
10 | "version": "4.2.2"
11 | }
12 | },
13 | {
14 | "package": "Kingfisher",
15 | "repositoryURL": "https://github.com/onevcat/Kingfisher",
16 | "state": {
17 | "branch": null,
18 | "revision": "d06df9adf50ed8cde5786d935836a5f445f780ba",
19 | "version": "6.3.1"
20 | }
21 | },
22 | {
23 | "package": "TFRCompose",
24 | "repositoryURL": "https://github.com/bermudalocket/TFRCompose",
25 | "state": {
26 | "branch": "main",
27 | "revision": "8c32053078689c0a8bda6ed81340d8e1004bdee3",
28 | "version": null
29 | }
30 | },
31 | {
32 | "package": "TFRPrivate",
33 | "repositoryURL": "https://github.com/bermudalocket/TFRPrivate",
34 | "state": {
35 | "branch": "main",
36 | "revision": "5c71bb9c6b1f438b64d7902f8a6e91f53843082a",
37 | "version": null
38 | }
39 | }
40 | ]
41 | },
42 | "version": 1
43 | }
44 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Tweaks for Reddit.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildLocationStyle
6 | UseAppPreferences
7 | CustomBuildLocationType
8 | RelativeToDerivedData
9 | DerivedDataLocationStyle
10 | Default
11 | IssueFilterStyle
12 | ShowActiveSchemeOnly
13 | LiveSourceIssuesEnabled
14 |
15 | ShowSharedSchemesAutomaticallyEnabled
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/xcdebugger/Expressions.xcexplist:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
8 |
10 |
11 |
12 |
13 |
15 |
16 |
18 |
19 |
21 |
22 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcuserdata/bermudalocket.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Tweaks for Reddit.xcworkspace/xcuserdata/mikerippe.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Tweaks for Reddit.xcworkspace/xcuserdata/mikerippe.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Tweaks for RedditUITests/Tweaks_for_RedditUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tweaks_for_RedditUITests.swift
3 | // Tweaks for RedditUITests
4 | //
5 | // Created by Michael Rippe on 6/19/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class Tweaks_for_RedditUITests: XCTestCase {
12 |
13 | var app: XCUIApplication!
14 |
15 | override func setUpWithError() throws {
16 | continueAfterFailure = false
17 | app = XCUIApplication()
18 | // app.launchArguments = ["--testing"]
19 | app.launch()
20 | }
21 |
22 | func testOAuth() {
23 | app.buttons["Reddit API Access"].click()
24 | let startButton = app.buttons["Start OAuth"]
25 | if startButton.isEnabled {
26 | app.buttons["Start OAuth"].click()
27 | } else {
28 | app.buttons["Restart OAuth"].click()
29 | }
30 |
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Tweaks for RedditUITests/WindowAlwaysComesToFrontTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WindowAlwaysComesToFrontTest.swift
3 | // WindowAlwaysComesToFrontTest
4 | //
5 | // Created by Michael Rippe on 9/14/21.
6 | // Copyright © 2021 bermudalocket. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class WindowAlwaysComesToFrontTest: XCTestCase {
12 |
13 | func testWindowAlwaysComesToFront() throws {
14 | let app = XCUIApplication()
15 | app.launch()
16 | let windows = app.windows
17 | var foundFront = false
18 | for i in 0...windows.count {
19 | let window = windows.element(boundBy: i)
20 | if window.isHittable {
21 | foundFront = true
22 | break
23 | }
24 | }
25 | XCTAssertTrue(foundFront)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------