├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── AppStoreSubmissionLog.csv ├── Configuration.storekit ├── Extension ├── Info.plist ├── Message.swift ├── Resources │ ├── .vscode │ │ └── tasks.json │ ├── redditweaks-script.js │ ├── redditweaks.css │ ├── script.js │ ├── script.ts │ ├── snudown.js │ ├── toolbar 2.pdf │ └── toolbar.pdf └── SafariExtensionHandler.swift ├── InternetAccessPolicy.json ├── LICENSE ├── Main App ├── Main.swift ├── MainAppStore.swift ├── SelectedTab.swift └── View │ ├── LogoView.swift │ ├── MacDevicesSymbol.swift │ ├── MainView.swift │ ├── Pages │ ├── ConnectToSafariView.swift │ ├── InAppPurchasesView.swift │ ├── NextTabButton.swift │ ├── NotificationsView.swift │ ├── OAuthView.swift │ ├── SafariPopoverView.swift │ ├── SafariPopoverViewArt.swift │ ├── WelcomeView.swift │ └── iCloudView.swift │ ├── RoutingView.swift │ └── TitleView.swift ├── Popover ├── FavoriteSubredditListHeight.swift ├── FavoriteSubredditSortingMethod.swift ├── Feature.swift ├── Info.plist ├── PopoverStore.swift ├── PopoverViewWrapper.swift ├── RedditPageType.swift ├── RedditStore.swift ├── TFRPopover.h └── View │ ├── AsyncImage.swift │ ├── FavoriteSubredditView.swift │ ├── FavoriteSubredditsSectionView.swift │ ├── FeaturesListView.swift │ ├── HiddenPostsView.swift │ ├── PopoverView.swift │ ├── PostView.swift │ ├── RedditInfoView.swift │ ├── RedditMailView.swift │ ├── SectionBackgroundView.swift │ ├── SettingsView.swift │ ├── VersionView.swift │ └── WhatsNewView.swift ├── README.md ├── Resources ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ ├── Icon-128.png │ │ ├── Icon-16.png │ │ ├── Icon-256.png │ │ ├── Icon-257.png │ │ ├── Icon-32.png │ │ ├── Icon-33.png │ │ ├── Icon-512.png │ │ ├── Icon-513.png │ │ └── Icon-64.png │ ├── Color.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── Icon.imageset │ │ ├── 64.png │ │ └── Contents.json │ ├── livecommentpreviews.imageset │ │ ├── CleanShot 2021-05-21 at 15.15.05@2x.jpg │ │ └── Contents.json │ └── snoovatar_mock.imageset │ │ ├── Contents.json │ │ └── snoovatar_mock.png ├── Background.plist ├── Extension Info.plist ├── Extension.entitlements ├── Main App Info.plist └── Main App.entitlements ├── TFRCore Tests └── TFRCore_Tests.swift ├── TFRCore ├── AppStoreService.swift ├── AppStoreValidationRequest.swift ├── CoreDataService.swift ├── Debugger.swift ├── DefaultsService.swift ├── Extension │ ├── Color+redditOrange.swift │ ├── URL+expressibleAsStringLiteral.swift │ ├── URL+queryParameters.swift │ └── URL+redditURLs.swift ├── Info.plist ├── KeychainService.swift ├── NotificationService.swift ├── OAuth │ ├── Listing.swift │ ├── Post.swift │ ├── RedditError.swift │ ├── Tokens.swift │ ├── UnreadMessages.swift │ └── UserData.swift ├── Persistence │ ├── FavoriteSubreddit+CoreDataClass.swift │ ├── FavoriteSubreddit+CoreDataProperties.swift │ ├── FavoriteSubreddit.swift │ ├── KarmaMemory+CoreDataClass.swift │ ├── KarmaMemory+CoreDataProperties.swift │ └── ThreadCommentCount+CoreDataClass.swift ├── RedditService.swift ├── RedditweaksButtonStyle.swift ├── TFRCore.h ├── TFREnvironment.swift ├── Tweaks for Reddit.xcdatamodeld │ ├── .xccurrentversion │ └── Persistence 6.xcdatamodel │ │ └── contents └── TweaksForReddit.swift ├── Tweaks for Reddit Tests ├── Info.plist └── TweaksForRedditTests.swift ├── Tweaks for Reddit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── bermudalocket.xcuserdatad │ │ ├── IDEFindNavigatorScopes.plist │ │ ├── UserInterfaceState.xcuserstate │ │ ├── WorkspaceSettings.xcsettings │ │ ├── xcdebugger │ │ │ └── Expressions.xcexplist │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── mikerippe.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ ├── xcbaselines │ │ ├── 895233BE24A6AC0300B72CE8.xcbaseline │ │ │ ├── E97D8360-7C44-4A54-81E9-01D39D4CD8F2.plist │ │ │ └── Info.plist │ │ └── 89DD691F24AA8AE800599B28.xcbaseline │ │ │ ├── 8761E753-CE72-475C-9BBA-89955EF0A75A.plist │ │ │ └── Info.plist │ └── xcschemes │ │ ├── Main App UI Tests.xcscheme │ │ ├── Main App Unit Tests.xcscheme │ │ ├── Popover.xcscheme │ │ ├── TFRCore Tests.xcscheme │ │ ├── TFRCore.xcscheme │ │ ├── TFRIntents.xcscheme │ │ ├── Tweaks for Reddit Extension.xcscheme │ │ └── Tweaks for Reddit.app.xcscheme └── xcuserdata │ ├── bermudalocket.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ ├── TweaksForRedditTests.testICloudKeyValueStore.xcscheme │ │ ├── TweaksForRedditTests.testReceipt.xcscheme │ │ ├── TweaksForRedditTests.testValidateReceipt.xcscheme │ │ ├── [DEBUG] Tweaks for Reddit.app.xcscheme │ │ └── xcschememanagement.plist │ └── mikerippe.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Tweaks for Reddit.xcworkspace ├── contents.xcworkspacedata ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ ├── bermudalocket.xcuserdatad │ ├── IDEFindNavigatorScopes.plist │ ├── UserInterfaceState.xcuserstate │ ├── WorkspaceSettings.xcsettings │ ├── xcdebugger │ │ └── Expressions.xcexplist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── mikerippe.xcuserdatad │ └── UserInterfaceState.xcuserstate └── Tweaks for RedditUITests ├── Tweaks_for_RedditUITests.swift └── WindowAlwaysComesToFrontTest.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bermudalocket] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: xcodebuild 2 | 3 | on: 4 | push: 5 | branches: [ main, testflight ] 6 | pull_request: 7 | branches: [ main, testflight ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | submodules: recursive 19 | token: ${{ secrets.ACCESS_TOKEN }} 20 | 21 | - name: Set up keys 22 | run: | 23 | echo -n "${{ secrets.SSH_KEY }}" | base64 --decode --output ~/.ssh/id_rsa 24 | chmod 400 ~/.ssh/id_rsa 25 | git config --global url.ssh://git@github.com/.insteadOf https://github.com/ 26 | 27 | - name: Import cert & provisioning profiles 28 | env: 29 | BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE }} 30 | P12_PASSWORD: ${{ secrets.P12_PASSWORD }} 31 | BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} 32 | BUILD_PROVISION_PROFILE_BASE64_EXT: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64_EXT }} 33 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 34 | run: | 35 | # create variables 36 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 37 | PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile 38 | PP_PATH_EXT=$RUNNER_TEMP/build_pp_ext.provisionprofile 39 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 40 | 41 | # import certificate and provisioning profile from secrets 42 | echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH 43 | echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH 44 | echo -n "$BUILD_PROVISION_PROFILE_BASE64_EXT" | base64 --decode --output $PP_PATH_EXT 45 | 46 | # create temporary keychain 47 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 48 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 49 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 50 | 51 | # import certificate to keychain 52 | security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 53 | security list-keychain -d user -s $KEYCHAIN_PATH 54 | 55 | # apply provisioning profile 56 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 57 | cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles 58 | cp $PP_PATH_EXT ~/Library/MobileDevice/Provisioning\ Profiles 59 | 60 | - name: Resolve package dependencies 61 | run: xcodebuild -resolvePackageDependencies 62 | 63 | - name: Tweaks for Reddit.app 64 | run: xcodebuild clean build -destination generic/platform=macOS -scheme "Tweaks for Reddit.app" -workspace "Tweaks for Reddit.xcworkspace" 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | NoCommit.swift 4 | .vscode 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Compose"] 2 | path = Compose 3 | url = https://github.com/bermudalocket/TFRCompose 4 | [submodule "TFRPrivate"] 5 | path = TFRPrivate 6 | url = https://github.com/bermudalocket/TFRPrivate 7 | -------------------------------------------------------------------------------- /AppStoreSubmissionLog.csv: -------------------------------------------------------------------------------- 1 | Timestamp,Version,Comment 2 | Sat May 22 2021 13:37:28,1.11.1,Submitted for review 3 | Mon May 31 2021 13:08:45,1.12,Submitted for review 4 | Fri Jun 04 2021 12:41:00,1.12.1,Submitted for review 5 | Thu Jun 10 2021 19:45:54,1.12.2,Submitted for review 6 | Wed Jun 16 2021 12:50:37,1.12.3,Submitted for review 7 | -------------------------------------------------------------------------------- /Configuration.storekit: -------------------------------------------------------------------------------- 1 | { 2 | "identifier" : "2FDC76E6", 3 | "nonRenewingSubscriptions" : [ 4 | 5 | ], 6 | "products" : [ 7 | { 8 | "displayPrice" : "0.99", 9 | "familyShareable" : true, 10 | "internalID" : "812EF264", 11 | "localizations" : [ 12 | { 13 | "description" : "", 14 | "displayName" : "", 15 | "locale" : "en_US" 16 | } 17 | ], 18 | "productID" : "livecommentpreview", 19 | "referenceName" : "Live Comment Preview", 20 | "type" : "NonConsumable" 21 | } 22 | ], 23 | "settings" : { 24 | 25 | }, 26 | "subscriptionGroups" : [ 27 | 28 | ], 29 | "version" : { 30 | "major" : 1, 31 | "minor" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Extension/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message.swift 3 | // Tweaks for Reddit Extension 4 | // 5 | // Created by Michael Rippe on 6/30/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Message: CaseIterable { 12 | case begin 13 | case end 14 | case script 15 | case userKarmaFetchRequest, userKarmaSaveRequest, userKarmaFetchRequestResponse 16 | case threadCommentCountFetchRequest, threadCommentCountSaveRequest, threadCommentCountFetchRequestResponse 17 | 18 | var key: String { 19 | "\(self)" 20 | } 21 | 22 | static func fromString(_ string: String) -> Message? { 23 | Message.allCases.first { "\($0)" == string } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Extension/Resources/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "xcodebuild", 8 | "type": "shell", 9 | "command": "xcodebuild -project ../../redditweaks.xcodeproj -scheme redditweaks -configuration Debug -sdk macosx11.3 build", 10 | "problemMatcher": [], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Extension/Resources/redditweaks.css: -------------------------------------------------------------------------------- 1 | 2 | .tweaksForRedditLivePreviewContainer { 3 | background: rgba(0, 0, 0, 0.05); 4 | border-radius: 5px; 5 | position: relative; 6 | } 7 | 8 | .tweaksForRedditLivePreviewContainer div { 9 | padding: 10px !important; 10 | } 11 | 12 | textarea { 13 | padding: 10px; 14 | line-height: 1.5; 15 | border-radius: 5px; 16 | border: 1px solid #ccc; 17 | box-shadow: 1px 1px 1px #999; 18 | } 19 | 20 | .rdtwks-downvotes { 21 | font-size: large; 22 | font-weight: bold; 23 | color: tomato; 24 | padding: 0px 0px 5px 5px !important; 25 | } 26 | 27 | .rdtwks-upvotes { 28 | font-size: large; 29 | font-weight: bold; 30 | color: mediumseagreen; 31 | padding: 5px 0px 0px 5px !important; 32 | } 33 | 34 | .rdtwks-negativeKarmaMemory { 35 | font-weight: bold; 36 | color: tomato; 37 | } 38 | 39 | .rdtwks-positiveKarmaMemory { 40 | font-weight: bold; 41 | color: mediumseagreen; 42 | } 43 | -------------------------------------------------------------------------------- /Extension/Resources/script.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var uuid_1 = require("uuid"); 4 | console.log("---------------------------------------------------------"); 5 | console.log("- Tweaks for Reddit is initializing... -"); 6 | console.log("---------------------------------------------------------"); 7 | function debug(msg) { 8 | console.log("[Tweaks for Reddit][DEBUG] " + msg); 9 | } 10 | function ready(callback) { 11 | if (document.readyState != "loading") { 12 | callback(); 13 | } 14 | else { 15 | document.addEventListener("DOMContentLoaded", callback); 16 | } 17 | } 18 | // test 19 | debug("this is a test"); 20 | console.log("[TFR Test] uuidv4 = " + uuid_1.v4()); 21 | var Feature; 22 | (function (Feature) { 23 | Feature[Feature["autoExpandImages"] = 0] = "autoExpandImages"; 24 | Feature[Feature["hideAds"] = 1] = "hideAds"; 25 | Feature[Feature["hidePromotedPosts"] = 2] = "hidePromotedPosts"; 26 | Feature[Feature["hideHappeningNowBanners"] = 3] = "hideHappeningNowBanners"; 27 | Feature[Feature["rememberUserVotes"] = 4] = "rememberUserVotes"; 28 | })(Feature || (Feature = {})); 29 | -------------------------------------------------------------------------------- /Extension/Resources/script.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | console.log("---------------------------------------------------------"); 4 | console.log("- Tweaks for Reddit is initializing... -"); 5 | console.log("---------------------------------------------------------"); 6 | 7 | function debug(msg: string): void { 8 | console.log(`[Tweaks for Reddit][DEBUG] ${msg}`); 9 | } 10 | 11 | function ready(callback: () => void): void { 12 | if (document.readyState != "loading") { 13 | callback(); 14 | } else { 15 | document.addEventListener("DOMContentLoaded", callback); 16 | } 17 | } 18 | 19 | // test 20 | debug("this is a test"); 21 | console.log("[TFR Test] uuidv4 = " + uuidv4()); 22 | 23 | interface Feature { 24 | name: string; 25 | enabled: boolean; 26 | run(subject?: HTMLElement): void; 27 | } 28 | 29 | const expandableDomains = [ "i.redd.it", "reddit.com", "i.imgur.com", "pbs.twimg.com" ]; 30 | 31 | const autoExpandImages: Feature = { 32 | name: "Auto-expand images", 33 | enabled: false, 34 | run: (subject?: HTMLElement) => { 35 | (subject ?? document).querySelectorAll(".thing").forEach(e => { 36 | const dataUrl = e.getAttribute("data-url"); 37 | if (dataUrl === null) return; 38 | for (const i in expandableDomains) { 39 | const domain = expandableDomains[i] 40 | if (dataUrl.includes(domain)) { 41 | const expando = e.querySelector(".expando-button:not(.expanded)"); 42 | if (expando instanceof HTMLElement) { 43 | expando.click(); 44 | break; 45 | } 46 | } 47 | } 48 | }); 49 | } 50 | }; 51 | 52 | const oldReddit: Feature = { 53 | name: "Always use old Reddit", 54 | enabled: false, 55 | run: (subject?: HTMLElement) => { 56 | const isOldReddit = document.querySelector("ul.sr-bar") !== null; 57 | if (!isOldReddit) { 58 | const url = window.location.href; 59 | if (url.includes("/poll/")) { 60 | return 61 | } else if (url.startsWith("https://www.reddit")) { 62 | window.location.href = url.replace("www", "old"); 63 | } else if (url.startsWith("https://reddit.com")) { 64 | window.location.href = url.replace("reddit.com", "old.reddit.com") 65 | } 66 | } 67 | } 68 | }; 69 | 70 | const favoriteSubredditBar: Feature = { 71 | name: "Favorite subreddits bar", 72 | enabled: false, 73 | run: (subject?: HTMLElement) => { 74 | const bar = document.querySelector("ul.sr-bar"); 75 | if (bar === null) return; 76 | for (let i = 3; i < bar.childElementCount; i++) { 77 | const node = bar.childNodes[i]; 78 | if (node instanceof HTMLElement) node.style.display = "none"; 79 | } 80 | } 81 | }; 82 | 83 | const allFeatures = [ 84 | autoExpandImages, 85 | oldReddit 86 | ]; 87 | 88 | -------------------------------------------------------------------------------- /Extension/Resources/toolbar 2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Extension/Resources/toolbar 2.pdf -------------------------------------------------------------------------------- /Extension/Resources/toolbar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bermudalocket/Tweaks-for-Reddit/fa8caf8898b1d02a975d28f37dba7a146c13af2a/Extension/Resources/toolbar.pdf -------------------------------------------------------------------------------- /InternetAccessPolicy.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApplicationDescription" : "Tweaks for Reddit is a Safari app extension for reddit.com.", 3 | "Connections" : [ 4 | { 5 | "Host" : "reddit.com", 6 | "Port" : "443", 7 | "NetworkProtocol" : "TCP", 8 | "IsIncoming" : false, 9 | "Purpose" : "The app and extension may need to access the Reddit API based on the features you enable." 10 | "DenyConsequences" : "The app and extension will not be able to access the OAuth2-based Reddit API.", 11 | }, 12 | { 13 | "Host": "bermudalocket.com", 14 | "Port": "443", 15 | "NetworkProtocol": "TCP", 16 | "IsIncoming": false, 17 | "Purpose": "The app needs to communicate with our middleman server to verify your in-app purchase receipt(s) with Apple." 18 | "DenyConsequences": "The app will not be able to verify your in-app purchase receipt(s) with Apple.", 19 | }, 20 | { 21 | "Host": "i.redd.it", 22 | "Port": "443", 23 | "NetworkProtocol": "TCP", 24 | "IsIncoming": false, 25 | "Purpose": "The extension may need to download your Snoovatar from Reddit's image server based on features you enable." 26 | "DenyConsequences": "The extension will not be able to download and display your Snoovatar.", 27 | }, 28 | { 29 | "Host" : "oauth.reddit.com", 30 | "Port" : "443", 31 | "NetworkProtocol" : "TCP", 32 | "IsIncoming" : false, 33 | "Purpose" : "The app and extension may need to access the Reddit API based on the features you enable." 34 | "DenyConsequences" : "The app and extension will not be able to access the OAuth2-based Reddit API.", 35 | }, 36 | ], 37 | "DeveloperName" : "bermudalocket" 38 | } 39 | -------------------------------------------------------------------------------- /Main App/SelectedTab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectedTab.swift 3 | // redditweaks 4 | // 5 | // Created by Michael Rippe on 6/24/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | enum SelectedTab: String, Codable, CaseIterable, RawRepresentable { 13 | 14 | case welcome = "Welcome" 15 | case connectToSafari = "Connect to Safari" 16 | case oauth = "Reddit API Access" 17 | case notifications = "Notifications" 18 | case toolbar = "The Toolbar Popover" 19 | case iCloud 20 | case liveCommentPreview = "Live Comment Previews" 21 | 22 | var name: String { 23 | self.rawValue 24 | } 25 | 26 | var symbol: String { 27 | switch self { 28 | case .welcome: return "hand.wave.fill" 29 | case .connectToSafari: return "safari.fill" 30 | case .oauth: return "key.fill" 31 | case .toolbar: return "menubar.arrow.up.rectangle" 32 | case .iCloud: return "cloud.fill" 33 | case .liveCommentPreview: return "sparkles.square.fill.on.square" 34 | case .notifications: return "bell.badge.fill" 35 | } 36 | } 37 | 38 | var next: SelectedTab { 39 | switch self { 40 | case .welcome: 41 | return .connectToSafari 42 | case .connectToSafari: 43 | return .oauth 44 | case .oauth: 45 | return .notifications 46 | case .notifications: 47 | return .toolbar 48 | case .toolbar: 49 | return .iCloud 50 | case .iCloud: 51 | return .liveCommentPreview 52 | case .liveCommentPreview: 53 | return .welcome 54 | } 55 | } 56 | 57 | var view: some View { 58 | Group { 59 | switch self { 60 | case .notifications: 61 | NotificationsView() 62 | 63 | case .oauth: 64 | OAuthView() 65 | 66 | case .connectToSafari: 67 | ConnectToSafariView() 68 | 69 | case .liveCommentPreview: 70 | InAppPurchasesView() 71 | 72 | case .welcome: 73 | WelcomeView() 74 | 75 | case .iCloud: 76 | iCloudView() 77 | 78 | case .toolbar: 79 | SafariPopoverView() 80 | } 81 | } 82 | } 83 | 84 | init?(rawValue: String) { 85 | guard let tab = SelectedTab.allCases.filter({ $0.name == rawValue }).first else { 86 | return nil 87 | } 88 | self = tab 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Main App/View/LogoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 5/15/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct LogoView: View { 12 | 13 | @State private var isIconAnimating = false 14 | 15 | var body: some View { 16 | VStack(alignment: .center) { 17 | Image("Icon") 18 | .rotation3DEffect( 19 | .init(degrees: isIconAnimating ? 45 : -45), 20 | axis: (x: isIconAnimating ? -1 : 1, 21 | y: isIconAnimating ? 1 : -1, 22 | z: isIconAnimating ? 1 : 0) 23 | ) 24 | .animation(.easeInOut(duration: 5).repeatForever(autoreverses: true), value: isIconAnimating) 25 | Text("Tweaks for Reddit") 26 | .font(.system(size: 20, weight: .bold, design: .rounded)) 27 | } 28 | .onAppear { 29 | DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: .milliseconds(500))) { 30 | isIconAnimating.toggle() 31 | } 32 | } 33 | } 34 | } 35 | 36 | struct LogoView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | LogoView() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Main App/View/MacDevicesSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacDevicesSymbol.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 9/15/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MacDevicesSymbol: View { 12 | var body: some View { 13 | ZStack { 14 | Image(systemName: "laptopcomputer") 15 | .offset(x: -10, y: 7.5) 16 | Image(systemName: "laptopcomputer") 17 | .offset(x: 0, y: -7.5) 18 | Image(systemName: "laptopcomputer") 19 | .offset(x: 10, y: 7.5) 20 | } 21 | } 22 | } 23 | 24 | struct MacDevicesSymbol_Previews: PreviewProvider { 25 | static var previews: some View { 26 | MacDevicesSymbol() 27 | .padding() 28 | .frame(width: 50, height: 50) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Main App/View/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 4/17/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MainView: View { 12 | 13 | @EnvironmentObject private var store: MainAppStore 14 | 15 | var body: some View { 16 | NavigationView { 17 | VStack { 18 | TitleView() 19 | .padding(.top, 30) 20 | List(SelectedTab.allCases, id: \.self) { tab in 21 | NavigationLink( 22 | destination: tab.view, 23 | tag: tab, 24 | selection: store.binding(for: \.tab, transform: MainAppAction.setTab) 25 | ) { 26 | HStack { 27 | Image(systemName: tab.symbol) 28 | .renderingMode(.original) 29 | .frame(width: 25) 30 | Text(tab.name) 31 | } 32 | .font(.title3) 33 | .accessibilityLabel(tab.name) 34 | } 35 | } 36 | } 37 | .listStyle(SidebarListStyle()) 38 | .frame(width: 240) 39 | } 40 | .navigationViewStyle(DoubleColumnNavigationViewStyle()) 41 | .frame(width: 825, height: 450) 42 | } 43 | 44 | } 45 | 46 | import TFRCore 47 | 48 | struct MainView_Previews: PreviewProvider { 49 | static var previews: some View { 50 | MainView() 51 | .environmentObject( 52 | MainAppStore( 53 | initialState: MainAppState(), 54 | reducer: .none, 55 | environment: .shared 56 | ) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Main App/View/Pages/ConnectToSafariView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectToSafariView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 4/17/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import SafariServices.SFSafariApplication 11 | import TFRCompose 12 | import TFRCore 13 | 14 | struct ConnectToSafariView: View { 15 | 16 | @EnvironmentObject private var store: MainAppStore 17 | 18 | @State private var isAnimating = false 19 | 20 | @State private var isExtensionEnabled = false 21 | 22 | @Sendable 23 | private func checkSafariExtensionState() async { 24 | Task { 25 | isExtensionEnabled = await SFSafariExtensionManager.getSafariExtensionState(id: TweaksForReddit.extensionId) 26 | } 27 | } 28 | 29 | private func openSafariPreferences() { 30 | SFSafariApplication.showPreferencesForExtension(withIdentifier: TweaksForReddit.extensionId) 31 | } 32 | 33 | var body: some View { 34 | VStack { 35 | VStack(spacing: 10) { 36 | Image(systemName: "safari") 37 | .font(.system(size: 68)) 38 | .foregroundColor(.redditOrange) 39 | .rotationEffect(isAnimating ? .zero : .degrees(360*5)) 40 | .onAppear { 41 | withAnimation(.easeInOut(duration: 3.0).delay(1.25).repeatForever()) { 42 | isAnimating.toggle() 43 | } 44 | } 45 | Text("Connect to Safari") 46 | .font(.system(size: 32, weight: .bold)) 47 | } 48 | .padding([.horizontal, .bottom]) 49 | VStack(spacing: 10) { 50 | Text("Connecting to Safari is easy.") 51 | Text("All you have to do is click a checkbox in Safari's preferences.\nClick the button below to have Safari open to the right spot.") 52 | .multilineTextAlignment(.center) 53 | .padding(.bottom) 54 | if store.state.isSafariExtensionEnabled { 55 | Text("The extension is enabled!") 56 | .font(.title2) 57 | .bold() 58 | } 59 | HStack { 60 | Spacer() 61 | Button("Open in Safari \(Image(systemName: "safari.fill"))", action: openSafariPreferences) 62 | .buttonStyle(RedditweaksButtonStyle()) 63 | .disabled(store.state.isSafariExtensionEnabled) 64 | NextTabButton() 65 | Spacer() 66 | } 67 | } 68 | } 69 | .padding() 70 | .onAppear { 71 | Task(priority: .userInitiated, operation: checkSafariExtensionState) 72 | } 73 | } 74 | 75 | } 76 | 77 | struct ConnectToSafariView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | Group { 80 | ConnectToSafariView() 81 | .environmentObject(MainAppStore( 82 | initialState: MainAppState(isSafariExtensionEnabled: false), 83 | reducer: mainAppReducer, 84 | environment: .shared 85 | )) 86 | ConnectToSafariView() 87 | .environmentObject(MainAppStore( 88 | initialState: MainAppState(isSafariExtensionEnabled: true), 89 | reducer: mainAppReducer, 90 | environment: .shared 91 | )) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Main App/View/Pages/NextTabButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NextTabButton.swift 3 | // NextTabButton 4 | // 5 | // Created by Michael Rippe on 8/23/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCore 11 | 12 | struct NextTabButton: View { 13 | 14 | @EnvironmentObject private var store: MainAppStore 15 | 16 | var body: some View { 17 | Button(action: { 18 | store.send(.nextTab) 19 | }, label: { 20 | Text("Next \(Image(systemName: "arrow.right"))") 21 | }) 22 | .buttonStyle(RedditweaksButtonStyle()) 23 | } 24 | } 25 | 26 | struct NextTabButton_Previews: PreviewProvider { 27 | static var previews: some View { 28 | NextTabButton() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Main App/View/Pages/NotificationsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationsView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 8/23/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import UserNotifications 11 | import TFRCore 12 | 13 | struct NotificationsView: View { 14 | 15 | @EnvironmentObject private var store: MainAppStore 16 | 17 | var body: some View { 18 | VStack(alignment: .center, spacing: 20) { 19 | VStack(spacing: 10) { 20 | Image(systemName: SelectedTab.notifications.symbol) 21 | .renderingMode(.original) 22 | .font(.system(size: 68)) 23 | .foregroundColor(.accentColor) 24 | Text(SelectedTab.notifications.name) 25 | .font(.system(size: 32, weight: .bold)) 26 | } 27 | .padding(.horizontal) 28 | 29 | Text("Tweaks for Reddit offers an alternative notification system\nto replace the broken Reddit system on Safari.") 30 | .multilineTextAlignment(.center) 31 | 32 | Text("Please note that notification-related features are currently in beta.") 33 | 34 | if store.state.notificationsEnabled { 35 | VStack { 36 | Text("Notifications are enabled!") 37 | .font(.title2) 38 | .bold() 39 | Text("Changes can be made in System Preferences.") 40 | .font(.callout) 41 | .foregroundColor(.gray) 42 | } 43 | } 44 | if store.state.oauthState != .completed { 45 | HStack { 46 | Text("This feature requires OAuth authorization.") 47 | .font(.title2) 48 | .bold() 49 | Button("Go \(Image(systemName: "arrow.right"))") { 50 | store.send(.setTab(.oauth)) 51 | } 52 | } 53 | } 54 | HStack { 55 | Button("Request notifications \(Image(systemName: "lock"))") { 56 | store.send(.requestNotificationAuthorization) 57 | }.buttonStyle(RedditweaksButtonStyle()) 58 | .disabled(store.state.notificationsEnabled || store.state.oauthState != .completed) 59 | NextTabButton() 60 | } 61 | }.onAppear { 62 | store.send(.checkNotificationsEnabled) 63 | } 64 | } 65 | 66 | } 67 | 68 | struct NotificationsView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | Group { 71 | NotificationsView() 72 | .environmentObject(MainAppStore( 73 | initialState: MainAppState(notificationsEnabled: false), 74 | reducer: .none, 75 | environment: .shared)) 76 | NotificationsView() 77 | .environmentObject(MainAppStore( 78 | initialState: MainAppState(notificationsEnabled: true), 79 | reducer: .none, 80 | environment: .shared)) 81 | } 82 | .padding() 83 | .frame(width: 510) 84 | .accentColor(.redditOrange) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Main App/View/Pages/OAuthView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OAuthView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 6/13/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCompose 11 | import TFRCore 12 | 13 | struct OAuthView: View { 14 | 15 | @EnvironmentObject private var store: Store 16 | 17 | var body: some View { 18 | VStack(alignment: .center, spacing: 10) { 19 | VStack(spacing: 10) { 20 | Image(systemName: "key.fill") 21 | .font(.system(size: 60)) 22 | .foregroundColor(.accentColor) 23 | .rotationEffect(.degrees(45)) 24 | Text("Authorize API Access") 25 | .font(.largeTitle.bold()) 26 | } 27 | .padding(.horizontal) 28 | 29 | VStack(spacing: 10) { 30 | Text("Tweaks for Reddit will request permission to access the Reddit API on your behalf.\n") 31 | + Text("This permission is necessary for certain features to work.").bold() 32 | Text("We will ") + Text("never").bold().italic().underline(true, color: .redditOrange) + Text(" create, modify, delete, or vote on content or respond to direct messages,\nchat requests, or ModMail delivered to your account without your permission.") 33 | } 34 | .font(.body) 35 | .multilineTextAlignment(.center) 36 | 37 | Group { 38 | switch store.state.oauthState { 39 | case .started: 40 | HStack { 41 | ProgressView() 42 | .progressViewStyle(CircularProgressViewStyle()) 43 | .scaleEffect(0.8) 44 | .padding() 45 | Text("Waiting to hear back from Reddit...") 46 | } 47 | 48 | case .exchanging: 49 | Text("Exchanging code for tokens...") 50 | 51 | case .failed: 52 | Text("Authorization failed :(") 53 | 54 | case .notStarted: 55 | Text("") 56 | 57 | case .completed: 58 | Text("You've successfully authorized API access!") 59 | } 60 | } 61 | .font(.title2.bold()) 62 | .padding() 63 | 64 | HStack { 65 | Button("Start authorizing \(Image(systemName: "lock"))") { 66 | store.send(.beginOAuth) 67 | } 68 | .disabled(store.state.oauthState == .completed) 69 | .accessibilityLabel("Start OAuth") 70 | if store.state.oauthState == .completed { 71 | Button("Reauthorize \(Image(systemName: "lock.rotation"))") { 72 | store.send(.beginOAuth) 73 | } 74 | .accessibilityLabel("Restart OAuth") 75 | } 76 | NextTabButton() 77 | } 78 | 79 | } 80 | .buttonStyle(RedditweaksButtonStyle()) 81 | } 82 | 83 | } 84 | 85 | struct OAuthView_Preview: PreviewProvider { 86 | static var previews: some View { 87 | Group { 88 | OAuthView() 89 | .environmentObject( 90 | MainAppStore.init( 91 | initialState: MainAppState(), 92 | reducer: .none, 93 | environment: .shared 94 | ) 95 | ) 96 | OAuthView() 97 | .environmentObject(MainAppStore( 98 | initialState: .init(oauthState: .completed), 99 | reducer: .none, 100 | environment: .shared 101 | )) 102 | }.padding() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Main App/View/Pages/SafariPopoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 4/19/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SafariPopoverView: View { 12 | 13 | var body: some View { 14 | VStack(spacing: 20) { 15 | VStack(spacing: 10) { 16 | Image(systemName: "bubble.middle.top") 17 | .font(.system(size: 68)) 18 | .foregroundColor(.accentColor) 19 | Text("The Popover") 20 | .font(.system(size: 32, weight: .bold)) 21 | } 22 | .padding(.horizontal) 23 | 24 | VStack(spacing: 12) { 25 | Text("The extension can be accessed in Safari via the toolbar.") 26 | Text("From there, you can enable individual features via their checkboxes.") 27 | Text("Keep in mind you must be using \"Old Reddit\" in order for the extension to work.") 28 | .bold() 29 | } 30 | 31 | ArtSafariToolbarView() 32 | .padding(.vertical) 33 | 34 | NextTabButton() 35 | } 36 | } 37 | } 38 | 39 | struct PageView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | SafariPopoverView() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Main App/View/Pages/SafariPopoverViewArt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariToolbarView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 4/20/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ArtSafariToolbarView: View { 12 | 13 | @Environment(\.colorScheme) private var colorScheme 14 | 15 | private var background: Color { 16 | switch colorScheme { 17 | case .light: return Color(red: 0.933, green: 0.913, blue: 0.918, opacity: 1.0) 18 | case .dark: return Color(red: 0.213, green: 0.179, blue: 0.188, opacity: 1.0) 19 | @unknown default: 20 | return Color(red: 0.933, green: 0.913, blue: 0.918, opacity: 1.0) 21 | } 22 | } 23 | 24 | private var urlBar: Color { 25 | switch colorScheme { 26 | case .light: return Color(red: 0.882, green: 0.862, blue: 0.867, opacity: 1.0) 27 | case .dark: return Color(red: 0.264, green: 0.23, blue: 0.239, opacity: 1.0) 28 | @unknown default: 29 | return Color(red: 0.882, green: 0.862, blue: 0.867, opacity: 1.0) 30 | } 31 | } 32 | 33 | private var refresh: Color { 34 | switch colorScheme { 35 | case .light: return Color(red: 0.449, green: 0.439, blue: 0.443, opacity: 1.0) 36 | case .dark: return Color(red: 0.891, green: 0.886, blue: 0.886, opacity: 1.0) 37 | @unknown default: 38 | return Color(red: 0.449, green: 0.439, blue: 0.443, opacity: 1.0) 39 | } 40 | } 41 | 42 | var body: some View { 43 | ZStack { 44 | Rectangle() 45 | .fill(background) 46 | .frame(height: 52) 47 | HStack { 48 | ZStack { 49 | RoundedRectangle(cornerRadius: 10) 50 | .fill(urlBar) 51 | HStack { 52 | Spacer() 53 | Image(systemName: "arrow.clockwise") 54 | .foregroundColor(refresh) 55 | .padding(.trailing, 10) 56 | .opacity(0.75) 57 | } 58 | } 59 | Image("Icon") 60 | .resizable() 61 | .frame(width: 26, height: 26) 62 | .padding(.leading, 10) 63 | } 64 | .frame(height: 28) 65 | .offset(x: -250, y: 0) 66 | } 67 | } 68 | } 69 | 70 | struct SafariToolbarView_Previews: PreviewProvider { 71 | static var previews: some View { 72 | ArtSafariToolbarView() 73 | .frame(width: 700, height: 400) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Main App/View/Pages/WelcomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeView.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 | struct WelcomeView: View { 12 | 13 | @State private var isAnimating = false 14 | 15 | var body: some View { 16 | VStack { 17 | Spacer() 18 | Image(systemName: "hand.wave.fill") 19 | .resizable() 20 | .rotationEffect(Angle.init(degrees: isAnimating ? 0 : 30), anchor: .bottomTrailing) 21 | .scaledToFit() 22 | .frame(width: 50) 23 | .foregroundColor(.redditOrange) 24 | Text("Welcome to") 25 | .font(.title2) 26 | .onAppear { 27 | withAnimation(.linear(duration: 1).delay(0.5).repeatForever()) { 28 | self.isAnimating = true 29 | } 30 | } 31 | Text("Tweaks for Reddit") 32 | .font(.title) 33 | .fontWeight(.heavy) 34 | 35 | Text("The next few pages will guide you through setting up the app.") 36 | .padding(.vertical) 37 | 38 | NextTabButton() 39 | .padding() 40 | Spacer() 41 | } 42 | .accessibilityLabel("Welcome to Tweaks for Reddit") 43 | } 44 | } 45 | 46 | struct WelcomeView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | WelcomeView() 49 | .frame(width: 500) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Main App/View/Pages/iCloudView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iCloudView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 5/17/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import CloudKit 10 | import SwiftUI 11 | 12 | // swiftlint:disable type_name 13 | 14 | struct iCloudView: View { 15 | 16 | private var isConnected: Bool { 17 | FileManager.default.ubiquityIdentityToken != nil 18 | } 19 | 20 | var body: some View { 21 | VStack(alignment: .center, spacing: 20) { 22 | VStack(spacing: 10) { 23 | Image(systemName: isConnected ? "checkmark.icloud" : "icloud") 24 | .font(.system(size: 68)) 25 | .foregroundColor(.accentColor) 26 | Text("Connect to iCloud") 27 | .font(.system(size: 32, weight: .bold)) 28 | } 29 | .padding(.horizontal) 30 | 31 | Text("Tweaks for Reddit can store your favorite subreddits in iCloud\nso you can use them on other devices you're signed into.") 32 | .multilineTextAlignment(.center) 33 | 34 | if isConnected { 35 | Text("You're signed in and connected to iCloud!") 36 | .font(.title2) 37 | .bold() 38 | } else { 39 | Text("Looks like you're not signed in to iCloud on this device.") 40 | .font(.title2) 41 | .bold() 42 | } 43 | 44 | NextTabButton() 45 | } 46 | } 47 | 48 | } 49 | 50 | struct iCloudView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | iCloudView() 53 | .frame(width: 510) 54 | .padding() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Main App/View/RoutingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DestinationView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 5/28/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct RoutingView: View { 12 | 13 | let tab: SelectedTab 14 | 15 | var body: some View { 16 | switch tab { 17 | case .notifications: 18 | NotificationsView() 19 | 20 | case .oauth: 21 | OAuthView() 22 | 23 | case .connectToSafari: 24 | ConnectToSafariView() 25 | 26 | case .liveCommentPreview: 27 | InAppPurchasesView() 28 | 29 | case .welcome: 30 | WelcomeView() 31 | 32 | case .iCloud: 33 | iCloudView() 34 | 35 | case .toolbar: 36 | SafariPopoverView() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Main App/View/TitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 3/2/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCore 11 | 12 | struct TitleView: View { 13 | 14 | var body: some View { 15 | VStack { 16 | Text("Tweaks for Reddit") 17 | .font(.system(.title, design: .rounded)) 18 | .fontWeight(.heavy) 19 | Text("Version \(TweaksForReddit.version)") 20 | .font(.subheadline) 21 | } 22 | .padding(.horizontal) 23 | .padding(.vertical, 5) 24 | .frame(minWidth: 0, maxWidth: .infinity) 25 | } 26 | } 27 | 28 | struct TitleView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | TitleView() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Popover/FavoriteSubredditListHeight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteSubredditListHeight.swift 3 | // Tweaks_for_Reddit_Popover 4 | // 5 | // Created by Michael Rippe on 6/29/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct FavoriteSubredditListHeight: Equatable, Hashable { 12 | public let heightInPixels: Int 13 | public let displayName: String 14 | } 15 | 16 | extension FavoriteSubredditListHeight { 17 | static func fromRawValue(_ value: Int) -> FavoriteSubredditListHeight? { 18 | allCases.first { $0.heightInPixels == value } 19 | } 20 | } 21 | 22 | extension FavoriteSubredditListHeight { 23 | public static let small = FavoriteSubredditListHeight(heightInPixels: 125, displayName: "a few") 24 | public static let medium = FavoriteSubredditListHeight(heightInPixels: 200, displayName: "a bunch") 25 | public static let large = FavoriteSubredditListHeight(heightInPixels: 320, displayName: "a lot") 26 | 27 | public static let allCases = [ small, medium, large ] 28 | } 29 | -------------------------------------------------------------------------------- /Popover/FavoriteSubredditSortingMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteSubredditSortingMethod.swift 3 | // Tweaks_for_Reddit_Popover 4 | // 5 | // Created by Michael Rippe on 7/6/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum FavoriteSubredditSortingMethod: CaseIterable { 12 | case alphabetical 13 | case manual 14 | 15 | var description: String { 16 | switch self { 17 | case .alphabetical: 18 | return "alphabetically" 19 | 20 | case .manual: 21 | return "manually" 22 | } 23 | } 24 | 25 | static func fromDescription(_ description: String) -> FavoriteSubredditSortingMethod? { 26 | allCases.filter { $0.description == description }.first 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Popover/Feature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Michael Rippe on 1/25/20. 3 | // Copyright (c) 2020 Michael Rippe. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | import TFRCore 8 | 9 | public struct Feature: Hashable, Comparable { 10 | 11 | /// User defaults key 12 | public let key: String 13 | 14 | /// Description shown in popover 15 | let description: String 16 | 17 | /// Whether or not this is an in-app purchase 18 | let premium: Bool 19 | 20 | /// A string to display to the user as a tooltip on hover 21 | let help: String 22 | 23 | init(key: String, description: String, premium: Bool = false, help: String = "") { 24 | self.key = key 25 | self.description = description 26 | self.premium = premium 27 | self.help = help 28 | } 29 | 30 | public var isEnabled: Bool { 31 | TweaksForReddit.defaults.bool(forKey: key) 32 | } 33 | 34 | /// Comparable alphabetically 35 | public static func < (lhs: Feature, rhs: Feature) -> Bool { 36 | lhs.description < rhs.description 37 | } 38 | 39 | } 40 | 41 | extension Feature { 42 | 43 | public static let features: [Feature] = [ 44 | .showEstimatedDownvotes, 45 | .autoExpandImages, 46 | .endlessScroll, 47 | .showNewComments, 48 | .rememberUserVotes, 49 | .noChat, 50 | .showKarma, 51 | .customSubredditBar, 52 | .hideUsername, 53 | .collapseChildComments, 54 | .collapseAutoModerator, 55 | .oldRedditRedirect, 56 | .hideJunk 57 | ] 58 | 59 | // in-app purchase 60 | public static let liveCommentPreview = Feature( 61 | key: "liveCommentPreview", 62 | description: "Preview reply in markdown", 63 | premium: true 64 | ) 65 | 66 | public static let hideJunk = Feature( 67 | key: "hideJunk", 68 | description: "Hide junk", 69 | help: "Hides visual junk" 70 | ) 71 | 72 | public static let showEstimatedDownvotes = Feature( 73 | key: "showEstimatedDownvotes", 74 | description: "Show estimated downvotes", 75 | help: "When viewing a post, Tweaks for Reddit will estimate and display the number of downvotes" 76 | ) 77 | 78 | public static let autoExpandImages = Feature( 79 | key: "autoExpandImages", 80 | description: "Automatically expand images", 81 | help: "Tweaks for Reddit will automatically expand all expandable image posts" 82 | ) 83 | 84 | public static let endlessScroll = Feature( 85 | key: "endlessScroll", 86 | description: "Endless scrolling", 87 | help: "Tweaks for Reddit will automatically load the next 25 posts as you near the bottom of the page" 88 | ) 89 | 90 | public static let showNewComments = Feature( 91 | key: "showNewComments", 92 | description: "Track new comments on visited posts", 93 | help: "Tweaks for Reddit will show the number of new comments made on threads since you visited." 94 | ) 95 | 96 | public static let rememberUserVotes = Feature( 97 | key: "rememberUserVotes", 98 | description: "Remember up/downvoting users", 99 | help: "Tweaks for Reddit will remember when you upvote and downvote users and display their net vote count (if not 0) on their posts and comments" 100 | ) 101 | 102 | public static let oldRedditRedirect = Feature( 103 | key: "oldReddit", 104 | description: "Always use old.reddit.com", 105 | help: "Tweaks for Reddit will attempt to bring you to the Old Reddit version of a page if it detects New Reddit" 106 | ) 107 | 108 | public static let noChat = Feature( 109 | key: "noChat", 110 | description: "Remove chat" 111 | ) 112 | 113 | public static let showKarma = Feature( 114 | key: "showKarma", 115 | description: "Show comment and post karma in user bar", 116 | help: "Tweaks for Reddit will parse your profile to include your comment and post karma in the user bar" 117 | ) 118 | 119 | public static let customSubredditBar = Feature( 120 | key: "customSubredditBar", 121 | description: "Favorite subreddits bar", 122 | help: "Tweaks for Reddit will replace the subreddit bar at the top of every page with a list of your favorite subreddits" 123 | ) 124 | 125 | public static let hideUsername = Feature( 126 | key: "hideUsername", 127 | description: "Remove username from user bar", 128 | help: "Tweaks for Reddit will help you guard your privacy from peering eyes by removing your username from the user bar" 129 | ) 130 | 131 | public static let collapseAutoModerator = Feature( 132 | key: "collapseAutoModerator", 133 | description: "Collapse AutoModerator", 134 | help: "Tweaks for Reddit will collapse all comments made by an AutoModerator account" 135 | ) 136 | 137 | public static let collapseChildComments = Feature( 138 | key: "collapseChildComments", 139 | description: "Collapse top-level replies", 140 | help: "Tweaks for Reddit will collapse subcomments on posts" 141 | ) 142 | 143 | } 144 | -------------------------------------------------------------------------------- /Popover/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 | -------------------------------------------------------------------------------- /Popover/PopoverViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopoverViewWrapper.swift 3 | // Tweaks for Reddit Extension 4 | // 5.0 5 | // 10.16 6 | // 7 | // Created by Michael Rippe on 7/6/20. 8 | // Copyright © 2020 Michael Rippe. All rights reserved. 9 | // 10 | 11 | import SafariServices 12 | import SwiftUI 13 | import TFRCompose 14 | import TFRCore 15 | 16 | /** 17 | A bridge to SwiftUI via NSHostingView. 18 | */ 19 | public class PopoverViewWrapper: SFSafariExtensionViewController { 20 | 21 | public init() { 22 | super.init(nibName: nil, bundle: nil) 23 | logInit("PopoverViewWrapper") 24 | 25 | let environment = TFREnvironment.shared 26 | 27 | let store = PopoverStore( 28 | initialState: PopoverState( 29 | redditState: .init(), 30 | favoriteSubreddits: environment.coreData.favoriteSubreddits 31 | ), 32 | reducer: popoverReducer, 33 | environment: environment 34 | ) 35 | 36 | var activity: NSBackgroundActivityScheduler { 37 | let activity = NSBackgroundActivityScheduler(identifier: "com.bermudalocket.redditweaks.checkForMessagesTask") 38 | activity.interval = 60 39 | activity.qualityOfService = .background 40 | activity.repeats = true 41 | activity.tolerance = 20 42 | activity.schedule { completion in 43 | if activity.shouldDefer { 44 | logService("Deferring task", service: .background) 45 | completion(.deferred) 46 | } else { 47 | logService("Checking for messages...", service: .background) 48 | store.send(.reddit(.checkForMessages)) 49 | completion(.finished) 50 | } 51 | } 52 | return activity 53 | } 54 | 55 | let view = PopoverView(store: store) 56 | // .environmentObject(store) 57 | .environment(\.managedObjectContext, environment.coreData.container.viewContext) 58 | .accentColor(.redditOrange) 59 | 60 | self.view = NSHostingView(rootView: view) 61 | } 62 | 63 | deinit { 64 | logDeinit("PopoverViewWrapper") 65 | } 66 | 67 | required init?(coder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Popover/RedditPageType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedditPageType.swift 3 | // Tweaks for Reddit Extension 4 | // 5 | // Created by Michael Rippe on 6/30/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum RedditPageType: CaseIterable { 12 | 13 | case feed, subreddit, post, user 14 | 15 | public var features: [Feature] { 16 | let base: [Feature] = [.customSubredditBar, .hideJunk, .hideUsername, .noChat, .oldRedditRedirect, .showKarma, .showNewComments, .rememberUserVotes] 17 | switch self { 18 | case .feed: 19 | return base + [.autoExpandImages, .endlessScroll] 20 | 21 | case .post: 22 | return base + [.collapseAutoModerator, .collapseChildComments, .liveCommentPreview, .showEstimatedDownvotes] 23 | 24 | case .subreddit: 25 | return base + [.autoExpandImages, .endlessScroll] 26 | 27 | default: 28 | return base 29 | } 30 | } 31 | 32 | public static func forURL(_ url: URL) -> RedditPageType? { 33 | let urlStr = url.absoluteString 34 | guard urlStr.contains("reddit") else { 35 | return nil 36 | } 37 | if urlStr.contains("/r/") { 38 | return urlStr.contains("/comments/") ? .post : .subreddit 39 | } 40 | if urlStr.contains("/user/") { 41 | return .user 42 | } 43 | return .feed 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Popover/RedditStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedditStore.swift 3 | // redditweaks 4 | // 5 | // Created by Michael Rippe on 6/21/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Combine 11 | import Foundation 12 | import TFRCompose 13 | import TFRCore 14 | 15 | typealias RedditStore = Store 16 | 17 | struct RedditState: Equatable { 18 | var isShowingMailView = false 19 | 20 | var userData: UserData? 21 | var unreadMessages: [UnreadMessage]? 22 | var hiddenPosts: [Post]? 23 | var hiddenPostsPage: Int = 1 24 | var postsBeingUnhidden = [Post]() 25 | 26 | var oauthError: RedditError? = nil 27 | } 28 | 29 | public enum RedditAction: Equatable { 30 | case openPostHistory 31 | case openCommentHistory 32 | 33 | case setIsShowingMailView(_ state: Bool) 34 | 35 | case setOAuthError(_ error: RedditError) 36 | 37 | case fetchUserData 38 | case updateUserData(_ userData: UserData) 39 | 40 | case fetchHiddenPosts(after: Post? = nil, before: Post? = nil) 41 | case updateHiddenPosts(_ posts: [Post]) 42 | case unhidePosts(_ posts: [Post]) 43 | 44 | case checkForMessages 45 | case updateMessages(_ messages: [UnreadMessage]) 46 | } 47 | 48 | let redditReducer = Reducer { state, action, env in 49 | 50 | guard let tokens = env.keychain.getTokens() else { 51 | state.oauthError = .noToken 52 | return .none 53 | } 54 | 55 | logReducer("redditReducer: \(action)") 56 | switch action { 57 | case .unhidePosts(let posts): 58 | state.postsBeingUnhidden.append(contentsOf: posts) 59 | return env.reddit.unhide(tokens: tokens, posts: posts) 60 | .map { result in 61 | if !result { 62 | logError("failed to unhide post") 63 | } 64 | return RedditAction.fetchHiddenPosts() 65 | } 66 | .catch { AnyPublisher(value: RedditAction.setOAuthError($0)) } 67 | .eraseToAnyPublisher() 68 | 69 | case .fetchHiddenPosts(after: let after, before: let before): 70 | state.hiddenPosts = nil 71 | guard let username = state.userData?.username else { 72 | state.oauthError = .noToken 73 | return .none 74 | } 75 | if let _ = after { 76 | state.hiddenPostsPage += 1 77 | } else if let _ = before { 78 | state.hiddenPostsPage -= 1 79 | } 80 | return env.reddit.getHiddenPosts(tokens: tokens, username: username, after: after, before: before) 81 | .map(RedditAction.updateHiddenPosts) 82 | .catch { error in 83 | return AnyPublisher(value: RedditAction.setOAuthError(RedditError.wrapping(message: "\(error)"))) 84 | } 85 | .eraseToAnyPublisher() 86 | 87 | case .updateHiddenPosts(let posts): 88 | state.postsBeingUnhidden = [] 89 | state.hiddenPosts = posts 90 | 91 | case .setIsShowingMailView(let isShowingMailView): 92 | state.isShowingMailView = isShowingMailView 93 | 94 | case .openPostHistory: 95 | guard let username = state.userData?.username else { 96 | return .none 97 | } 98 | NSWorkspace.shared.open(URL(string: "https://www.reddit.com/user/\(username)/submitted/")!) 99 | 100 | case .openCommentHistory: 101 | guard let username = state.userData?.username else { 102 | return .none 103 | } 104 | NSWorkspace.shared.open(URL(string: "https://www.reddit.com/user/\(username)/comments/")!) 105 | 106 | case .setOAuthError(let error): 107 | state.oauthError = error 108 | 109 | case .updateMessages(let messages): 110 | env.defaults.set(messages.count, forKey: "newMessageCount") 111 | messages.forEach(NotificationService.shared.send(msg:)) 112 | state.unreadMessages = messages 113 | 114 | case .checkForMessages: 115 | env.defaults.set(nil, forKey: "newMessageCount") 116 | return env.reddit.getMessages(tokens: tokens) 117 | .map(RedditAction.updateMessages) 118 | .catch { AnyPublisher(value: RedditAction.setOAuthError($0)) } 119 | .eraseToAnyPublisher() 120 | 121 | case .updateUserData(let userData): 122 | state.userData = userData 123 | 124 | case .fetchUserData: 125 | return env.reddit.getUserData(tokens: tokens) 126 | .map(RedditAction.updateUserData) 127 | .catch { AnyPublisher(value: RedditAction.setOAuthError($0)) } 128 | .eraseToAnyPublisher() 129 | } 130 | return .none 131 | } 132 | -------------------------------------------------------------------------------- /Popover/TFRPopover.h: -------------------------------------------------------------------------------- 1 | // 2 | // Tweaks_for_Reddit_Popover.h 3 | // Tweaks_for_Reddit_Popover 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 Tweaks_for_Reddit_Popover. 12 | FOUNDATION_EXPORT double Tweaks_for_Reddit_PopoverVersionNumber; 13 | 14 | //! Project version string for Tweaks_for_Reddit_Popover. 15 | FOUNDATION_EXPORT const unsigned char Tweaks_for_Reddit_PopoverVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Popover/View/AsyncImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncImage.swift 3 | // TFRCore 4 | // 5 | // Created by Michael Rippe on 6/18/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import SwiftUI 11 | import TFRCore 12 | 13 | class AsyncImageLoader: ObservableObject { 14 | 15 | let publisher = PassthroughSubject() 16 | 17 | private var cancellables = Set() 18 | 19 | func load(url: URL) { 20 | let fm = FileManager.default 21 | guard let base = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { 22 | return 23 | } 24 | let sub = base.appendingPathComponent("Tweaks for Reddit") 25 | if !fm.fileExists(atPath: sub.path) { 26 | try? fm.createDirectory(at: sub, withIntermediateDirectories: true, attributes: nil) 27 | } 28 | let path = sub.appendingPathComponent("snoovatar.png").path 29 | 30 | Future { promise in 31 | if FileManager.default.fileExists(atPath: path), let attrs = try? fm.attributesOfItem(atPath: path) { 32 | if let lastModDate = attrs[.modificationDate] as? NSDate, 33 | NSDate.now.timeIntervalSince1970 - lastModDate.timeIntervalSince1970 > 1000*60*60 { 34 | try? fm.removeItem(atPath: path) 35 | } 36 | if let data = FileManager.default.contents(atPath: path) { 37 | return promise(.success(data)) 38 | } 39 | } 40 | URLSession.shared.downloadTask(with: url) { location, _, error in 41 | if let error = error { 42 | return promise(.failure(error)) 43 | } 44 | guard let location = location, let data = fm.contents(atPath: location.path) else { 45 | return promise(.failure(RedditError.downloadFailed)) 46 | } 47 | fm.createFile(atPath: path, contents: data, attributes: nil) 48 | return promise(.success(data)) 49 | }.resume() 50 | } 51 | .compactMap(NSImage.init(data:)) 52 | .map(Image.init(nsImage:)) 53 | .receive(on: DispatchQueue.main) 54 | .sink { completion in 55 | switch completion { 56 | case .finished: break 57 | case .failure(_): 58 | self.publisher.send(Image(systemName: "xmark")) 59 | } 60 | } receiveValue: { 61 | self.publisher.send($0) 62 | } 63 | .store(in: &cancellables) 64 | } 65 | 66 | } 67 | 68 | struct AsyncImage: View { 69 | 70 | @StateObject private var imageLoader = AsyncImageLoader() 71 | 72 | @State private var image: Image? 73 | 74 | let url: URL 75 | 76 | init(url: URL) { 77 | self.url = url 78 | } 79 | 80 | var body: some View { 81 | VStack { 82 | if let image = image { 83 | image 84 | .resizable() 85 | .scaledToFit() 86 | } else { 87 | ProgressView() 88 | } 89 | } 90 | .onAppear { 91 | imageLoader.load(url: url) 92 | } 93 | .onReceive(imageLoader.publisher) { 94 | self.image = $0 95 | } 96 | } 97 | } 98 | 99 | struct AsyncImage_Previews: PreviewProvider { 100 | static var previews: some View { 101 | AsyncImage( 102 | url: URL(string: "https://i.redd.it/snoovatar/snoovatars/2d193ec6-03ef-4a80-a651-637f7ed0dd93.png")! 103 | ) 104 | .frame(width: 500, height: 500) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Popover/View/FavoriteSubredditView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteSubredditView.swift 3 | // Tweaks for Reddit Extension 4 | // 5.0 5 | // 10.16 6 | // 7 | // Created by Michael Rippe on 7/8/20. 8 | // Copyright © 2020 Michael Rippe. All rights reserved. 9 | // 10 | 11 | import SwiftUI 12 | import TFRCore 13 | 14 | extension View { 15 | func eraseToAnyView() -> AnyView { 16 | AnyView(self) 17 | } 18 | } 19 | 20 | struct FavoriteSubredditView: View { 21 | 22 | @EnvironmentObject private var store: PopoverStore 23 | 24 | let subreddit: FavoriteSubreddit 25 | 26 | @State private var isHovered = false 27 | 28 | private var icon: AnyView { 29 | if isHovered { 30 | return Image(systemName: "arrowshape.turn.up.left.fill") 31 | .eraseToAnyView() 32 | } 33 | guard let name = subreddit.name else { 34 | return Image(systemName: "questionmark").eraseToAnyView() 35 | } 36 | return Image(systemName: TweaksForReddit.symbolForSubreddit(name)) 37 | .frame(width: 20) 38 | .eraseToAnyView() 39 | } 40 | 41 | var body: some View { 42 | HStack(alignment: .center) { 43 | self.icon 44 | .foregroundColor(.accentColor) 45 | .frame(width: 20) 46 | .onTapGesture(perform: subreddit.open) 47 | Menu("r/\(subreddit.name ?? "???")") { 48 | Button(action: subreddit.open) { 49 | Text("Open") 50 | } 51 | Divider() 52 | Button(action: { 53 | store.send(.deleteFavoriteSubreddit(self.subreddit)) 54 | }) { 55 | Text("Delete") 56 | .foregroundColor(.red) 57 | } 58 | } 59 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 60 | .menuStyle(BorderlessButtonMenuStyle()) 61 | } 62 | .contentShape(Rectangle()) 63 | .onHover { 64 | self.isHovered = $0 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Popover/View/FavoriteSubredditsSectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteSubredditsSectionView.swift 3 | // Tweaks for Reddit Extension 4 | // 5.0 5 | // 10.16 6 | // 7 | // Created by Michael Rippe on 7/9/20. 8 | // Copyright © 2020 Michael Rippe. All rights reserved. 9 | // 10 | 11 | import Combine 12 | import SwiftUI 13 | import TFRCore 14 | 15 | // TODO 16 | // https://stackoverflow.com/questions/60454752/swiftui-background-color-of-list-mac-os 17 | extension NSTableView { 18 | open override func viewDidMoveToWindow() { 19 | super.viewDidMoveToWindow() 20 | 21 | backgroundColor = NSColor.clear 22 | enclosingScrollView?.drawsBackground = false 23 | } 24 | } 25 | 26 | struct Shake: GeometryEffect { 27 | var amount: CGFloat = 10 28 | var shakesPerUnit = 3 29 | var animatableData: CGFloat 30 | 31 | func effectValue(size: CGSize) -> ProjectionTransform { 32 | ProjectionTransform( 33 | CGAffineTransform( 34 | translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)), 35 | y: 0 36 | ) 37 | ) 38 | } 39 | } 40 | 41 | struct FavoriteSubredditsSectionView: View { 42 | 43 | @EnvironmentObject private var store: PopoverStore 44 | 45 | @State private var favoriteSubredditField = "" 46 | 47 | @State private var isShowingError: Int = 0 48 | 49 | var body: some View { 50 | VStack(alignment: .leading) { 51 | HStack { 52 | Menu("Sorting \(store.state.favoriteSubredditListSortingMethod.description)") { 53 | ForEach(FavoriteSubredditSortingMethod.allCases, id: \.self) { method in 54 | Button("Sort \(method.description)") { 55 | store.send(.setFavoriteSubredditSortingMethod(method: method)) 56 | } 57 | } 58 | } 59 | Menu("Showing \(store.state.favoriteSubredditListHeight.displayName)") { 60 | ForEach(FavoriteSubredditListHeight.allCases, id: \.self) { listHeight in 61 | Button("Show \(listHeight.displayName)") { 62 | store.send(.setFavoriteSubredditsListHeight(height: listHeight)) 63 | } 64 | } 65 | } 66 | } 67 | TextField("r/", text: $favoriteSubredditField, onCommit: { 68 | if favoriteSubredditField == "" { 69 | withAnimation(.default) { 70 | isShowingError += 1 71 | } 72 | return 73 | } 74 | store.send(.addFavoriteSubreddit(favoriteSubredditField)) 75 | favoriteSubredditField = "" 76 | }) 77 | .modifier(Shake(animatableData: CGFloat(isShowingError))) 78 | Group { 79 | if store.state.favoriteSubredditListSortingMethod == .alphabetical { 80 | List(store.state.favoriteSubreddits.sorted { $0.name! < $1.name! }, rowContent: FavoriteSubredditView.init(subreddit:)) 81 | } else { 82 | List { 83 | ForEach(store.state.favoriteSubreddits.sorted { $0.position < $1.position }) { 84 | FavoriteSubredditView(subreddit: $0) 85 | } 86 | .onMove { indices, newOffset in 87 | store.send(.moveFavoriteSubreddit(indices: indices, newOffset: newOffset)) 88 | } 89 | } 90 | } 91 | } 92 | .transition(.opacity.animation(.default)) 93 | .listStyle(PlainListStyle()) 94 | .frame(height: min( 95 | CGFloat(store.state.favoriteSubredditListHeight.heightInPixels), 96 | 25 * CGFloat(store.state.favoriteSubreddits.count) 97 | )) 98 | 99 | } 100 | } 101 | 102 | } 103 | 104 | struct FavoriteSubredditsSectionView_Previews: PreviewProvider { 105 | static var previews: some View { 106 | FavoriteSubredditsSectionView() 107 | .environmentObject(PopoverStore.preview) 108 | .frame(width: TweaksForReddit.popoverWidth) 109 | .padding() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Popover/View/FeaturesListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeaturesListView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 2/25/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import SwiftUI 11 | import TFRCore 12 | 13 | struct FeaturesListView: View { 14 | 15 | @EnvironmentObject private var store: PopoverStore 16 | 17 | private var features: [Feature] { 18 | store.state.features.lazy.filter { 19 | !$0.premium 20 | }.sorted { 21 | $0 < $1 22 | } 23 | } 24 | 25 | var body: some View { 26 | GroupBox(label: Text("Features")) { 27 | VStack(alignment: .leading) { 28 | ForEach(features, id: \.self) { feature in 29 | Toggle(feature.description, isOn: store.binding(for: feature)) 30 | .help(feature.help) 31 | .accessibilityLabel(feature.description) 32 | if feature == .customSubredditBar && store.binding(for: feature).wrappedValue { 33 | FavoriteSubredditsSectionView() 34 | } 35 | } 36 | } 37 | .padding(10) 38 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 39 | } 40 | } 41 | 42 | } 43 | 44 | struct FeaturesListView_Preview: PreviewProvider { 45 | static var previews: some View { 46 | FeaturesListView() 47 | .environmentObject(PopoverStore.preview) 48 | .frame(width: TweaksForReddit.popoverWidth) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Popover/View/HiddenPostsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HiddenPostsView.swift 3 | // HiddenPostsView 4 | // 5 | // Created by Michael Rippe on 9/12/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCore 11 | 12 | struct HiddenPostsView: View { 13 | 14 | @EnvironmentObject private var store: RedditStore 15 | 16 | var body: some View { 17 | VStack { 18 | if store.state.hiddenPosts == nil { 19 | VStack { 20 | Spacer() 21 | ProgressView() 22 | .progressViewStyle(CircularProgressViewStyle()) 23 | Text("Fetching...") 24 | Spacer() 25 | } 26 | } else { 27 | List(store.state.hiddenPosts!, id: \.self) { post in 28 | ZStack { 29 | if store.state.postsBeingUnhidden.contains(post) { 30 | ProgressView() 31 | .progressViewStyle(CircularProgressViewStyle()) 32 | .imageScale(.small) 33 | } 34 | PostView(post: post) 35 | } 36 | } 37 | } 38 | HStack { 39 | Button("\(Image(systemName: "arrow.clockwise"))") { 40 | store.send(.fetchHiddenPosts()) 41 | } 42 | .scaleEffect(0.8) 43 | .buttonStyle(RedditweaksButtonStyle()) 44 | .padding([.bottom, .leading], 5) 45 | Button("\(Image(systemName: "arrow.left"))") { 46 | if let first = store.state.hiddenPosts?.first { 47 | store.send(.fetchHiddenPosts(before: first)) 48 | } 49 | } 50 | .scaleEffect(0.8) 51 | .buttonStyle(RedditweaksButtonStyle()) 52 | .disabled(store.state.hiddenPosts == nil || store.state.hiddenPostsPage == 1) 53 | .padding(.bottom, 5) 54 | Spacer() 55 | Text("Page \(store.state.hiddenPostsPage)") 56 | Spacer() 57 | Button("\(Image(systemName: "arrow.right"))") { 58 | if let last = store.state.hiddenPosts?.last { 59 | store.send(.fetchHiddenPosts(after: last)) 60 | } 61 | } 62 | .scaleEffect(0.8) 63 | .buttonStyle(RedditweaksButtonStyle()) 64 | .disabled(store.state.hiddenPosts == nil) 65 | .padding(.bottom, 5) 66 | Button("Unhide all") { 67 | if let posts = store.state.hiddenPosts { 68 | store.send(.unhidePosts(posts)) 69 | } 70 | } 71 | .scaleEffect(0.8) 72 | .buttonStyle(RedditweaksButtonStyle()) 73 | .disabled(store.state.hiddenPosts == nil) 74 | .padding([.bottom, .trailing], 5) 75 | } 76 | } 77 | .frame(width: 400, height: 400) 78 | .transition(.opacity.animation(.easeInOut)) 79 | } 80 | } 81 | 82 | struct HiddenPostsView_Previews: PreviewProvider { 83 | private static let store = RedditStore( 84 | initialState: RedditState(hiddenPosts: nil), 85 | reducer: redditReducer, 86 | environment: .shared 87 | ) 88 | static var previews: some View { 89 | HiddenPostsView() 90 | .environmentObject(store) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Popover/View/PopoverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopoverView.swift 3 | // Tweaks for Reddit Extension 4 | // 5.0 5 | // 10.16 6 | // 7 | // Created by Michael Rippe on 7/6/20. 8 | // Copyright © 2020 Michael Rippe. All rights reserved. 9 | // 10 | 11 | import AppKit 12 | import Combine 13 | import StoreKit 14 | import SwiftUI 15 | import TFRCompose 16 | import TFRCore 17 | 18 | public struct PopoverView: View { 19 | 20 | @ObservedObject private var store: PopoverStore 21 | 22 | public init(store: PopoverStore) { 23 | self.store = store 24 | } 25 | 26 | public var body: some View { 27 | VStack(spacing: 5) { 28 | VersionView() 29 | .onTapGesture { 30 | store.send(.showWhatsNew(true)) 31 | } 32 | 33 | RedditInfoView() 34 | .frame(width: TweaksForReddit.popoverWidth, height: 150) 35 | .environmentObject( 36 | store.derived( 37 | deriveState: \.redditState, 38 | deriveAction: PopoverAction.reddit 39 | ) 40 | ) 41 | .popover( 42 | isPresented: $store.state.isShowingWhatsNew, 43 | attachmentAnchor: .rect(.bounds), 44 | arrowEdge: Edge.leading 45 | ) { 46 | WhatsNewView(isPresented: $store.state.isShowingWhatsNew) 47 | } 48 | 49 | if SKPaymentQueue.canMakePayments() { 50 | GroupBox(label: Text("In-App Purchases")) { 51 | HStack { 52 | Toggle("Live preview comments in markdown", isOn: store.binding(for: .liveCommentPreview)) 53 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 54 | .padding(10) 55 | .disabled(!NSUbiquitousKeyValueStore.default.bool(forKey: InAppPurchase.liveCommentPreview.productId)) 56 | } 57 | } 58 | } 59 | 60 | FeaturesListView() 61 | .environmentObject(store) 62 | 63 | HStack { 64 | Text("Find a bug? Got a suggestion?") 65 | Button("Contact us \(Image(systemName: "envelope.fill"))") { 66 | store.send(.openFeedbackEmail) 67 | } 68 | .buttonStyle(RedditweaksButtonStyle()) 69 | .scaleEffect(0.8) 70 | .contextMenu { 71 | Button("Copy debug info to clipboard") { 72 | store.send(.copyDebugInfo) 73 | } 74 | } 75 | } 76 | } 77 | .padding(10) 78 | .frame(width: TweaksForReddit.popoverWidth, alignment: .top) 79 | .onAppear { 80 | store.send(.load) 81 | } 82 | .onDisappear { 83 | store.send(.save) 84 | #if !DEBUG 85 | store.send(.askForReview) 86 | #endif 87 | } 88 | } 89 | 90 | } 91 | 92 | struct PopoverView_Previews: PreviewProvider { 93 | static var previews: some View { 94 | PopoverView(store: .preview) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Popover/View/PostView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostView.swift.swift 3 | // PostView.swift 4 | // 5 | // Created by Michael Rippe on 9/12/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCore 11 | 12 | struct PostView: View { 13 | 14 | @EnvironmentObject private var store: RedditStore 15 | 16 | let post: Post 17 | 18 | private struct XButtonStyle: ButtonStyle { 19 | @State private var isHovered = false 20 | func makeBody(configuration: Configuration) -> some View { 21 | configuration.label 22 | .buttonStyle(BorderlessButtonStyle()) 23 | .contentShape(Rectangle()) 24 | .onHover { isHovered = $0 } 25 | .foregroundColor(isHovered ? .red : Color(.placeholderTextColor)) 26 | .scaleEffect(isHovered ? 1.0 : 1.15) 27 | } 28 | } 29 | 30 | var body: some View { 31 | HStack { 32 | Button("\(Image(systemName: "chevron.left"))") { 33 | NSWorkspace.shared.open(URL(string: "https://www.reddit.com\(post.permalink)")!) 34 | } 35 | .buttonStyle(BorderlessButtonStyle()) 36 | .foregroundColor(Color(.placeholderTextColor)) 37 | VStack(alignment: .leading) { 38 | Text(post.title).lineLimit(2) 39 | Text("in r/") + Text(post.subreddit).bold() 40 | } 41 | Spacer() 42 | Button("\(Image(systemName: "xmark"))") { 43 | store.send(.unhidePosts([post])) 44 | } 45 | .buttonStyle(XButtonStyle()) 46 | } 47 | .opacity(store.state.postsBeingUnhidden.contains(post) ? 0.33 : 1.0) 48 | .blur(radius: store.state.postsBeingUnhidden.contains(post) ? 1.5 : 0.0) 49 | } 50 | } 51 | 52 | //struct PostView_swift_Previews: PreviewProvider { 53 | // static var previews: some View { 54 | // PostView(post: Post()) 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /Popover/View/SectionBackgroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionBackgroundView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 2/25/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SectionBackgroundView: View { 12 | 13 | @Environment(\.colorScheme) private var colorScheme 14 | 15 | var body: some View { 16 | RoundedRectangle(cornerRadius: 10, style: .continuous) 17 | .fill(Color(.textBackgroundColor)) 18 | .opacity(colorScheme == .light ? 0.3 : 0.7) 19 | .shadow(radius: 5) 20 | } 21 | 22 | } 23 | 24 | struct SectionBackgroundView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | SectionBackgroundView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Popover/View/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Tweaks for Reddit 4 | // 5 | // Created by Michael Rippe on 3/2/21. 6 | // Copyright © 2021 Michael Rippe. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCompose 11 | import TFRCore 12 | 13 | struct SettingsView: View { 14 | 15 | @EnvironmentObject private var store: Store 16 | 17 | var body: some View { 18 | VStack(alignment: .leading) { 19 | HStack { 20 | Text("Favorites display size") 21 | .frame(minWidth: 0, maxWidth: .infinity) 22 | Picker(selection: store.binding(for: \.favoriteSubredditListHeight, transform: PopoverAction.setFavoriteSubredditsListHeight(height:)), 23 | label: EmptyView() 24 | ) { 25 | ForEach(FavoriteSubredditListHeight.allCases, id: \.self) { height in 26 | Button(height.displayName) { 27 | store.send(.setFavoriteSubredditsListHeight(height: height)) 28 | } 29 | } 30 | } 31 | .pickerStyle(SegmentedPickerStyle()) 32 | } 33 | .contentShape(Rectangle()) 34 | .help("Determines how many subreddits are visible in the Favorite Subreddits list") 35 | } 36 | .frame(minWidth: 0, maxWidth: .infinity) 37 | .padding(5) 38 | } 39 | } 40 | 41 | struct SettingsView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | SettingsView() 44 | .frame(width: TweaksForReddit.popoverWidth) 45 | .environmentObject(PopoverStore.preview) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Popover/View/VersionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionView.swift 3 | // VersionView 4 | // 5 | // Created by Michael Rippe on 9/13/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCore 11 | 12 | struct VersionView: View { 13 | var body: some View { 14 | Text("Tweaks for Reddit v\(TweaksForReddit.version)") 15 | .bold() 16 | .foregroundColor(Color(.textColor)) 17 | .opacity(0.33) 18 | } 19 | } 20 | 21 | @available(macOS 12, *) 22 | struct VersionView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | Group { 25 | VersionView() 26 | VersionView() 27 | .preferredColorScheme(.dark) 28 | } 29 | .padding() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Popover/View/WhatsNewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WhatsNewView.swift 3 | // WhatsNewView 4 | // 5 | // Created by Michael Rippe on 9/2/21. 6 | // Copyright © 2021 bermudalocket. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TFRCore 11 | 12 | public struct WhatsNewView: View { 13 | 14 | @Binding var isPresented: Bool 15 | 16 | public init(isPresented: Binding) { 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 | [![xcodebuild](https://github.com/bermudalocket/Tweaks-for-Reddit/actions/workflows/build.yml/badge.svg?branch=main)](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 | --------------------------------------------------------------------------------