├── .github └── workflows │ └── nightly.yml ├── .gitignore ├── .swift-version ├── .swiftformat ├── ArtworkSource ├── README.md ├── iOS │ ├── Asobi-lg.ai │ ├── Asobi-med.ai │ ├── Asobi-sm.ai │ └── Asobi-xs.ai └── macOS │ ├── ActualSizes.txt │ ├── Asobi-1024.ai │ ├── Asobi-128.ai │ ├── Asobi-16.ai │ ├── Asobi-256.ai │ ├── Asobi-32.ai │ ├── Asobi-512.ai │ └── Asobi-64.ai ├── Asobi.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── Asobi.xcscheme ├── Asobi ├── Asobi.entitlements ├── AsobiApp.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Asobi-1024.png │ │ ├── Asobi-1026.png │ │ ├── Asobi-120.png │ │ ├── Asobi-121.png │ │ ├── Asobi-128.png │ │ ├── Asobi-152.png │ │ ├── Asobi-16.png │ │ ├── Asobi-167.png │ │ ├── Asobi-180.png │ │ ├── Asobi-20.png │ │ ├── Asobi-256.png │ │ ├── Asobi-257.png │ │ ├── Asobi-29.png │ │ ├── Asobi-32.png │ │ ├── Asobi-33.png │ │ ├── Asobi-40.png │ │ ├── Asobi-41.png │ │ ├── Asobi-42.png │ │ ├── Asobi-512.png │ │ ├── Asobi-513.png │ │ ├── Asobi-58.png │ │ ├── Asobi-59.png │ │ ├── Asobi-60.png │ │ ├── Asobi-64.png │ │ ├── Asobi-76.png │ │ ├── Asobi-80.png │ │ ├── Asobi-81.png │ │ ├── Asobi-87.png │ │ └── Contents.json │ ├── AppIcons │ │ ├── Contents.json │ │ ├── GradientsAppIcon.appiconset │ │ │ ├── 1024x1024@1x.png │ │ │ ├── 20x20@1x.png │ │ │ ├── 20x20@2x.png │ │ │ ├── 20x20@3x.png │ │ │ ├── 29x29@1x.png │ │ │ ├── 29x29@2x-1.png │ │ │ ├── 29x29@2x.png │ │ │ ├── 29x29@3x.png │ │ │ ├── 40x40@1x-1.png │ │ │ ├── 40x40@1x.png │ │ │ ├── 40x40@2x-1.png │ │ │ ├── 40x40@2x.png │ │ │ ├── 40x40@3x.png │ │ │ ├── 60x60@2x.png │ │ │ ├── 60x60@3x.png │ │ │ ├── 76x76@1x.png │ │ │ ├── 76x76@2x.png │ │ │ ├── 83.5x83.5@2x.png │ │ │ └── Contents.json │ │ ├── JustPinkAppIcon.appiconset │ │ │ ├── 1024x1024@1x.png │ │ │ ├── 20x20@1x.png │ │ │ ├── 20x20@2x.png │ │ │ ├── 20x20@3x.png │ │ │ ├── 29x29@1x.png │ │ │ ├── 29x29@2x.png │ │ │ ├── 29x29@3x.png │ │ │ ├── 40x40@1x.png │ │ │ ├── 40x40@2x.png │ │ │ ├── 40x40@3x.png │ │ │ ├── 60x60@2x.png │ │ │ ├── 60x60@3x.png │ │ │ ├── 76x76@1x.png │ │ │ ├── 76x76@2x.png │ │ │ ├── 83.5x83.5@2x.png │ │ │ └── Contents.json │ │ ├── OceanAppIcon.appiconset │ │ │ ├── 1024x1024@1x.png │ │ │ ├── 20x20@1x.png │ │ │ ├── 20x20@2x.png │ │ │ ├── 20x20@3x.png │ │ │ ├── 29x29@1x.png │ │ │ ├── 29x29@2x.png │ │ │ ├── 29x29@3x.png │ │ │ ├── 40x40@1x.png │ │ │ ├── 40x40@2x.png │ │ │ ├── 40x40@3x.png │ │ │ ├── 60x60@2x.png │ │ │ ├── 60x60@3x.png │ │ │ ├── 76x76@1x.png │ │ │ ├── 76x76@2x.png │ │ │ ├── 83.5x83.5@2x.png │ │ │ └── Contents.json │ │ └── SunsetAppIcon.appiconset │ │ │ ├── 1024x1024@1x.png │ │ │ ├── 20x20@1x.png │ │ │ ├── 20x20@2x.png │ │ │ ├── 20x20@3x.png │ │ │ ├── 29x29@1x.png │ │ │ ├── 29x29@2x.png │ │ │ ├── 29x29@3x.png │ │ │ ├── 40x40@1x.png │ │ │ ├── 40x40@2x.png │ │ │ ├── 40x40@3x.png │ │ │ ├── 60x60@2x.png │ │ │ ├── 60x60@3x.png │ │ │ ├── 76x76@1x.png │ │ │ ├── 76x76@2x.png │ │ │ ├── 83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppImages │ │ ├── AppImage.imageset │ │ │ ├── Asobi-180.png │ │ │ └── Contents.json │ │ ├── AppImageRounded.imageset │ │ │ ├── Contents.json │ │ │ └── RoundedAppImage.png │ │ ├── Contents.json │ │ ├── GradientsImage.imageset │ │ │ ├── 60x60@3x.png │ │ │ └── Contents.json │ │ ├── JustPinkImage.imageset │ │ │ ├── 60x60@3x.png │ │ │ └── Contents.json │ │ ├── OceanImage.imageset │ │ │ ├── 60x60@3x.png │ │ │ └── Contents.json │ │ └── SunsetImage.imageset │ │ │ ├── 60x60@3x.png │ │ │ └── Contents.json │ └── Contents.json ├── Classes │ └── Application.swift ├── DataManagement │ ├── AsobiDB.xcdatamodeld │ │ ├── .xccurrentversion │ │ ├── AsobiDB.xcdatamodel │ │ │ └── contents │ │ └── AsobiDB_v2.xcdatamodel │ │ │ └── contents │ ├── DataClasses │ │ ├── History+CoreDataClass.swift │ │ ├── History+CoreDataProperties.swift │ │ ├── HistoryEntry+CoreDataClass.swift │ │ └── HistoryEntry+CoreDataProperties.swift │ └── PersistenceController.swift ├── Extensions │ ├── CGFloat.swift │ ├── Collection.swift │ ├── Color.swift │ ├── DateFormatter.swift │ ├── Task.swift │ ├── TextField.swift │ ├── UIDevice.swift │ ├── UIHostingController.swift │ └── View.swift ├── Info.plist ├── JavaScript │ ├── FindInPage.js │ ├── JSLoader.swift │ ├── MacOSPaste.js │ └── ZoomUnlocker.js ├── LaunchScreen.storyboard ├── Models │ └── SettingsModels.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── RepresentableViews │ ├── AsobiRootViewController.swift │ ├── NotifyingContextMenu.swift │ ├── WebView.swift │ └── WillDisappearHandler.swift ├── ViewModels │ ├── DownloadManager.swift │ ├── NavigationViewModel.swift │ └── WebViewModel.swift ├── Views │ ├── AboutView.swift │ ├── AuthOverlayView.swift │ ├── CommonViews │ │ ├── AllowedURLSchemeView.swift │ │ ├── AppIconButtonView.swift │ │ ├── AppIconPickerView.swift │ │ ├── ContextMenuButton.swift │ │ ├── EmptyInstructionView.swift │ │ ├── GroupBoxStyle.swift │ │ ├── InlineHeader.swift │ │ ├── ListRowViews.swift │ │ ├── Modifiers │ │ │ ├── ApplyTheme.swift │ │ │ ├── ConditionalListStyle.swift │ │ │ ├── DisabledAppearance.swift │ │ │ ├── DynamicContextMenu.swift │ │ │ ├── InlinedList.swift │ │ │ ├── TextFieldClearMode.swift │ │ │ └── WillDisappearModifier.swift │ │ ├── NavView.swift │ │ ├── PaddedTextFieldStyle.swift │ │ └── PopupExceptionView.swift │ ├── ComponentViews │ │ ├── Library │ │ │ ├── BookmarkView.swift │ │ │ ├── EditBookmarkView.swift │ │ │ ├── HistoryActionView.swift │ │ │ ├── HistoryView.swift │ │ │ └── LibraryActionsView.swift │ │ ├── NavigationBar │ │ │ ├── FindInPageButtonView.swift │ │ │ ├── ForwardBackButtonView.swift │ │ │ ├── HomeButtonView.swift │ │ │ ├── LibraryButtonView.swift │ │ │ ├── RefreshButtonView.swift │ │ │ ├── SettingsButtonView.swift │ │ │ └── UrlBarButtonView.swift │ │ ├── Settings │ │ │ ├── SettingsAppearanceView.swift │ │ │ ├── SettingsBehaviorView.swift │ │ │ ├── SettingsDownloadsView.swift │ │ │ ├── SettingsPickerViews.swift │ │ │ ├── SettingsPrivacyView.swift │ │ │ ├── SettingsSyncView.swift │ │ │ └── SettingsWebsiteView.swift │ │ └── WebAlert │ │ │ ├── WebAlertPanel.swift │ │ │ ├── WebAuthPanel.swift │ │ │ ├── WebConfirmPanel.swift │ │ │ └── WebPromptPanel.swift │ ├── ContentView.swift │ ├── FindInPageView.swift │ ├── LibraryView.swift │ ├── MainView.swift │ ├── NavigationBarView.swift │ ├── SettingsView.swift │ └── UrlBarView.swift └── blocklist.json ├── AsobiTests ├── AsobiTests.swift └── Info.plist ├── AsobiUITests ├── AsobiUITests.swift └── Info.plist ├── LICENSE └── README.md /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload nightly ipa 2 | 3 | on: 4 | push: 5 | branches: [none] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup Xcode 13 | uses: maxim-lobanov/setup-xcode@v1 14 | with: 15 | xcode-version: latest-stable 16 | - name: Get commit SHA 17 | id: commitinfo 18 | run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 19 | - name: Build 20 | run: xcodebuild -scheme Asobi -configuration Release archive -archivePath build/Asobi.xcarchive CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO 21 | - name: Package ipa 22 | run: | 23 | mkdir Payload 24 | cp -r build/Asobi.xcarchive/Products/Applications/Asobi.app Payload 25 | zip -r Asobi-iOS_nightly-${{ steps.commitinfo.outputs.sha_short }}.ipa Payload 26 | - name: Upload artifacts 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: Asobi-iOS_nightly-${{ steps.commitinfo.outputs.sha_short }}.ipa 30 | path: Asobi-iOS_nightly-${{ steps.commitinfo.outputs.sha_short }}.ipa 31 | if-no-files-found: error 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/xcode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode 3 | 4 | ### Xcode ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Gcc Patch 30 | /*.gcno 31 | 32 | ### Xcode Patch ### 33 | *.xcodeproj/* 34 | !*.xcodeproj/project.pbxproj 35 | !*.xcodeproj/xcshareddata/ 36 | !*.xcworkspace/contents.xcworkspacedata 37 | **/xcshareddata/WorkspaceSettings.xcsettings 38 | 39 | # End of https://www.toptal.com/developers/gitignore/api/xcode 40 | 41 | *.DS_STORE 42 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.5 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --acronyms ID,URL,UUID 2 | --allman false 3 | --assetliterals visual-width 4 | --beforemarks 5 | --binarygrouping none 6 | --categorymark "MARK: %c" 7 | --classthreshold 0 8 | --closingparen balanced 9 | --closurevoid remove 10 | --commas inline 11 | --conflictmarkers reject 12 | --decimalgrouping none 13 | --elseposition same-line 14 | --emptybraces no-space 15 | --enumthreshold 0 16 | --exponentcase lowercase 17 | --exponentgrouping disabled 18 | --extensionacl on-extension 19 | --extensionlength 0 20 | --extensionmark "MARK: - %t + %c" 21 | --fractiongrouping disabled 22 | --fragment false 23 | --funcattributes preserve 24 | --groupedextension "MARK: %c" 25 | --guardelse auto 26 | --header ignore 27 | --hexgrouping none 28 | --hexliteralcase uppercase 29 | --ifdef no-indent 30 | --importgrouping alpha 31 | --indent 4 32 | --indentcase false 33 | --indentstrings false 34 | --lifecycle 35 | --lineaftermarks true 36 | --linebreaks lf 37 | --markcategories true 38 | --markextensions always 39 | --marktypes always 40 | --maxwidth none 41 | --modifierorder 42 | --nevertrailing 43 | --nospaceoperators 44 | --nowrapoperators 45 | --octalgrouping none 46 | --operatorfunc spaced 47 | --organizetypes actor,class,enum,struct 48 | --patternlet hoist 49 | --ranges spaced 50 | --redundanttype infer-locals-only 51 | --self remove 52 | --selfrequired 53 | --semicolons never 54 | --shortoptionals always 55 | --smarttabs enabled 56 | --stripunusedargs closure-only 57 | --structthreshold 0 58 | --tabwidth unspecified 59 | --trailingclosures 60 | --trimwhitespace always 61 | --typeattributes preserve 62 | --typemark "MARK: - %t" 63 | --varattributes preserve 64 | --voidtype void 65 | --wraparguments preserve 66 | --wrapcollections before-first 67 | --wrapconditions preserve 68 | --wrapparameters after-first 69 | --wrapreturntype preserve 70 | --wrapternary default 71 | --wraptypealiases preserve 72 | --xcodeindentation disabled 73 | --yodaswap always 74 | -------------------------------------------------------------------------------- /ArtworkSource/README.md: -------------------------------------------------------------------------------- 1 | # Artwork 2 | 3 | Copyright (c) 2021-present, Brian Dashore. 4 | 5 | These source files and images are part of the Asobi project. They are not allowed to be redistributed without explicit permission from the holder, Brian Dashore. 6 | 7 | The source files in the iOS folder are ONLY for use to create community contributed icons for the Asobi project. All other use is prohibited. 8 | -------------------------------------------------------------------------------- /ArtworkSource/iOS/Asobi-lg.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/iOS/Asobi-lg.ai -------------------------------------------------------------------------------- /ArtworkSource/iOS/Asobi-med.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/iOS/Asobi-med.ai -------------------------------------------------------------------------------- /ArtworkSource/iOS/Asobi-sm.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/iOS/Asobi-sm.ai -------------------------------------------------------------------------------- /ArtworkSource/iOS/Asobi-xs.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/iOS/Asobi-xs.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/ActualSizes.txt: -------------------------------------------------------------------------------- 1 | 1024px = 819.3182 px * 819.3182 px 2 | 512px = 412.4639 px * 412.4639 px 3 | 256px = 205.2593 px * 205.2593 px 4 | 128px = 103.9433 px * 103.9433 px 5 | 64px = 52.1637 px * 52.1637 px 6 | 32px = 28px * 28px 7 | 16px = 14px * 14px -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-1024.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-1024.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-128.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-128.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-16.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-16.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-256.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-256.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-32.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-32.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-512.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-512.ai -------------------------------------------------------------------------------- /ArtworkSource/macOS/Asobi-64.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/ArtworkSource/macOS/Asobi-64.ai -------------------------------------------------------------------------------- /Asobi.xcodeproj/xcshareddata/xcschemes/Asobi.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Asobi/Asobi.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | iCloud.me.kingbri.Asobi 10 | 11 | com.apple.developer.icloud-services 12 | 13 | CloudKit 14 | 15 | com.apple.security.app-sandbox 16 | 17 | com.apple.security.files.downloads.read-write 18 | 19 | com.apple.security.files.user-selected.read-write 20 | 21 | com.apple.security.network.client 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Asobi/AsobiApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsobiApp.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/2/21. 6 | // 7 | 8 | import Introspect 9 | import SwiftUI 10 | 11 | @main 12 | struct AsobiApp: App { 13 | let persistenceController = PersistenceController.shared 14 | 15 | // At top level for keyboard commands 16 | @StateObject var webModel: WebViewModel = .init() 17 | @StateObject var navModel: NavigationViewModel = .init() 18 | 19 | // At top level for scenePhase if needed 20 | @StateObject var rootViewController: AsobiRootViewController = .init(rootViewController: nil, style: .default) 21 | 22 | @AppStorage("browserModeEnabled") var browserModeEnabled = false 23 | @AppStorage("allowZoom") var allowZoom = true 24 | 25 | var body: some Scene { 26 | WindowGroup { 27 | MainView() 28 | .introspectViewController { viewController in 29 | let window = viewController.view.window 30 | guard let rootViewController = window?.rootViewController else { return } 31 | self.rootViewController.rootViewController = rootViewController 32 | self.rootViewController.ignoreDarkMode = true 33 | 34 | window?.rootViewController = self.rootViewController 35 | } 36 | .environmentObject(webModel) 37 | .environmentObject(navModel) 38 | .environmentObject(rootViewController) 39 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 40 | } 41 | .commands { 42 | CommandGroup(after: .textEditing) { 43 | Divider() 44 | 45 | Button("Find in page") { 46 | navModel.currentPillView = .findInPage 47 | }.keyboardShortcut("f", modifiers: [.command, .shift]) 48 | 49 | if browserModeEnabled { 50 | Button("Show URL bar") { 51 | navModel.currentPillView = .urlBar 52 | }.keyboardShortcut("u", modifiers: [.command, .shift]) 53 | } 54 | } 55 | 56 | CommandGroup(after: .windowSize) { 57 | Divider() 58 | 59 | if allowZoom { 60 | Button("Zoom in") { 61 | let currentZoomScale = webModel.webView.scrollView.zoomScale 62 | webModel.webView.scrollView.zoomScale = currentZoomScale + 0.1 63 | } 64 | .keyboardShortcut("+", modifiers: [.command, .shift]) 65 | 66 | Button("Zoom out") { 67 | let currentZoomScale = webModel.webView.scrollView.zoomScale 68 | webModel.webView.scrollView.zoomScale = currentZoomScale - 0.1 69 | } 70 | .keyboardShortcut("-", modifiers: [.command, .shift]) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-1024.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-1026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-1026.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-120.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-121.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-121.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-128.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-152.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-16.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-167.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-180.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-20.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-256.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-257.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-257.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-29.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-32.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-33.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-40.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-41.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-42.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-512.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-513.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-513.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-58.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-59.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-60.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-64.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-76.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-80.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-81.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-81.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcon.appiconset/Asobi-87.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Asobi-42.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Asobi-60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Asobi-59.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Asobi-87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Asobi-81.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "Asobi-121.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Asobi-120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "Asobi-180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Asobi-20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "Asobi-41.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Asobi-29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "Asobi-58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Asobi-40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "Asobi-80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Asobi-76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "Asobi-152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Asobi-167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "Asobi-1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "Asobi-16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "Asobi-33.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "Asobi-32.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "Asobi-64.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "Asobi-128.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "Asobi-257.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "Asobi-256.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "Asobi-513.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "Asobi-512.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "Asobi-1026.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/1024x1024@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/20x20@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/20x20@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/20x20@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@2x-1.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/29x29@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@1x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@1x-1.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@2x-1.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/40x40@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/60x60@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/76x76@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/76x76@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/83.5x83.5@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/GradientsAppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29x29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "29x29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "40x40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "40x40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "60x60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "60x60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20x20@1x.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "40x40@1x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29x29@1x.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "29x29@2x-1.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40x40@1x-1.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "40x40@2x-1.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76x76@1x.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "76x76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "83.5x83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024x1024@1x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/1024x1024@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/20x20@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/20x20@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/20x20@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/29x29@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/29x29@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/29x29@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/40x40@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/40x40@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/40x40@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/60x60@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/76x76@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/76x76@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/83.5x83.5@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/JustPinkAppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29x29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "29x29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "40x40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "40x40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "60x60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "60x60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20x20@1x.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "20x20@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29x29@1x.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "29x29@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40x40@1x.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "40x40@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76x76@1x.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "76x76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "83.5x83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024x1024@1x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/1024x1024@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/20x20@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/20x20@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/20x20@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/29x29@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/29x29@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/29x29@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/40x40@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/40x40@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/40x40@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/60x60@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/76x76@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/76x76@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/83.5x83.5@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/OceanAppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29x29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "29x29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "40x40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "40x40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "60x60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "60x60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20x20@1x.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "20x20@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29x29@1x.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "29x29@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40x40@1x.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "40x40@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76x76@1x.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "76x76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "83.5x83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024x1024@1x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/1024x1024@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/20x20@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/20x20@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/20x20@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/29x29@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/29x29@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/29x29@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/40x40@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/40x40@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/40x40@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/60x60@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/76x76@1x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/76x76@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/83.5x83.5@2x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppIcons/SunsetAppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29x29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "29x29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "40x40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "40x40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "60x60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "60x60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "20x20@1x.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "20x20@2x.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "29x29@1x.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "29x29@2x.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "40x40@1x.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "40x40@2x.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "76x76@1x.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "76x76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "83.5x83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "1024x1024@1x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/AppImage.imageset/Asobi-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppImages/AppImage.imageset/Asobi-180.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/AppImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Asobi-180.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/AppImageRounded.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "RoundedAppImage.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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/AppImageRounded.imageset/RoundedAppImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppImages/AppImageRounded.imageset/RoundedAppImage.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/GradientsImage.imageset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppImages/GradientsImage.imageset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/GradientsImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "60x60@3x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/JustPinkImage.imageset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppImages/JustPinkImage.imageset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/JustPinkImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "60x60@3x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/OceanImage.imageset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppImages/OceanImage.imageset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/OceanImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "60x60@3x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/SunsetImage.imageset/60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingbri1/Asobi/a8c3182fb40cba91893c2c840399b8570dd08cfd/Asobi/Assets.xcassets/AppImages/SunsetImage.imageset/60x60@3x.png -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/AppImages/SunsetImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "60x60@3x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Asobi/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Asobi/Classes/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Application { 11 | static let shared = Application() 12 | 13 | var appVersion: String { 14 | Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0" 15 | } 16 | 17 | var appBuild: String { 18 | Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "0" 19 | } 20 | 21 | // Debug = development, Nightly = actions, Release = stable 22 | var buildType: String { 23 | #if DEBUG 24 | return "Debug" 25 | #else 26 | return "Release" 27 | #endif 28 | } 29 | 30 | let osVersion: OperatingSystemVersion = ProcessInfo().operatingSystemVersion 31 | } 32 | -------------------------------------------------------------------------------- /Asobi/DataManagement/AsobiDB.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | AsobiDB_v2.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Asobi/DataManagement/AsobiDB.xcdatamodeld/AsobiDB.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Asobi/DataManagement/AsobiDB.xcdatamodeld/AsobiDB_v2.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Asobi/DataManagement/DataClasses/History+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // History+CoreDataClass.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/9/21. 6 | // 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | 12 | @objc(History) 13 | public class History: NSManagedObject {} 14 | -------------------------------------------------------------------------------- /Asobi/DataManagement/DataClasses/History+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // History+CoreDataProperties.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/9/21. 6 | // 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | 12 | public extension History { 13 | @nonobjc class func fetchRequest() -> NSFetchRequest { 14 | NSFetchRequest(entityName: "History") 15 | } 16 | 17 | @NSManaged var date: Date? 18 | @NSManaged var dateString: String? 19 | @NSManaged var entries: NSSet? 20 | 21 | var entryArray: [HistoryEntry] { 22 | let entrySet = entries as? Set ?? [] 23 | 24 | return entrySet.sorted { 25 | $0.timestamp > $1.timestamp 26 | } 27 | } 28 | } 29 | 30 | // MARK: Generated accessors for entries 31 | 32 | public extension History { 33 | @objc(addEntriesObject:) 34 | @NSManaged func addToEntries(_ value: HistoryEntry) 35 | 36 | @objc(removeEntriesObject:) 37 | @NSManaged func removeFromEntries(_ value: HistoryEntry) 38 | 39 | @objc(addEntries:) 40 | @NSManaged func addToEntries(_ values: NSSet) 41 | 42 | @objc(removeEntries:) 43 | @NSManaged func removeFromEntries(_ values: NSSet) 44 | } 45 | 46 | extension History: Identifiable {} 47 | -------------------------------------------------------------------------------- /Asobi/DataManagement/DataClasses/HistoryEntry+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryEntry+CoreDataClass.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/9/21. 6 | // 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | 12 | @objc(HistoryEntry) 13 | public class HistoryEntry: NSManagedObject {} 14 | -------------------------------------------------------------------------------- /Asobi/DataManagement/DataClasses/HistoryEntry+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryEntry+CoreDataProperties.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/9/21. 6 | // 7 | // 8 | 9 | import CoreData 10 | import Foundation 11 | 12 | public extension HistoryEntry { 13 | @nonobjc class func fetchRequest() -> NSFetchRequest { 14 | NSFetchRequest(entityName: "HistoryEntry") 15 | } 16 | 17 | @NSManaged var name: String? 18 | @NSManaged var timestamp: Double 19 | @NSManaged var url: String? 20 | @NSManaged var parentHistory: History? 21 | } 22 | 23 | extension HistoryEntry: Identifiable {} 24 | -------------------------------------------------------------------------------- /Asobi/Extensions/CGFloat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 12/23/21. 6 | // 7 | 8 | import CoreGraphics 9 | import Foundation 10 | 11 | extension CGFloat { 12 | func roundToPlaces(_ places: Int) -> Double { 13 | let multiplier = pow(10, Double(places)) 14 | return (self * multiplier).rounded() / multiplier 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Asobi/Extensions/Collection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 6/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Collection { 11 | // From https://stackoverflow.com/questions/25329186/safe-bounds-checked-array-lookup-in-swift-through-optional-bindings 12 | subscript(safe index: Index) -> Element? { 13 | indices.contains(index) ? self[index] : nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Asobi/Extensions/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 10/5/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Color { 12 | init(rgb: String) { 13 | let rgbValues = rgb.dropFirst(4).dropLast(1).components(separatedBy: ", ") 14 | 15 | let uiColor = UIColor( 16 | red: CGFloat((rgbValues[0] as NSString).floatValue / 255), 17 | green: CGFloat((rgbValues[1] as NSString).floatValue / 255), 18 | blue: CGFloat((rgbValues[2] as NSString).floatValue / 255), 19 | alpha: 1.0 20 | ) 21 | 22 | self.init(uiColor) 23 | } 24 | 25 | var isLight: Bool { 26 | let uiColor = UIColor(self) 27 | 28 | var white: CGFloat = 0 29 | uiColor.getWhite(&white, alpha: nil) 30 | return white >= 0.5 31 | } 32 | } 33 | 34 | // From zane-carter: https://gist.github.com/zane-carter/fc2bf8f5f5ac45196b4c9b01d54aca80 35 | extension Color: RawRepresentable { 36 | public init?(rawValue: String) { 37 | guard let data = Data(base64Encoded: rawValue) else { 38 | self = .black 39 | return 40 | } 41 | 42 | do { 43 | let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) ?? .red 44 | self = Color(color) 45 | } catch { 46 | self = .red 47 | } 48 | } 49 | 50 | public var rawValue: String { 51 | do { 52 | let data = try NSKeyedArchiver.archivedData(withRootObject: UIColor(self), requiringSecureCoding: false) as Data 53 | return data.base64EncodedString() 54 | } catch { 55 | return "" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Asobi/Extensions/DateFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/14/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | static let historyDateFormatter: DateFormatter = { 12 | let df = DateFormatter() 13 | df.dateFormat = "ddMMyyyy" 14 | 15 | return df 16 | }() 17 | } 18 | -------------------------------------------------------------------------------- /Asobi/Extensions/Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/12/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Task where Success == Never, Failure == Never { 11 | static func sleep(seconds: Double) async throws { 12 | let duration = UInt64(seconds * 1000000000) 13 | try await Task.sleep(nanoseconds: duration) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Asobi/Extensions/TextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextField.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 6/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension TextField { 11 | func clearButtonMode(_ clearButtonMode: UITextField.ViewMode) -> some View { 12 | modifier(TextFieldClearMode(clearButtonMode: clearButtonMode)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Asobi/Extensions/UIDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 10/9/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum DeviceType { 11 | case phone 12 | case pad 13 | case mac 14 | } 15 | 16 | extension UIDevice { 17 | var deviceType: DeviceType? { 18 | #if targetEnvironment(macCatalyst) 19 | return .mac 20 | #else 21 | switch UIDevice.current.userInterfaceIdiom { 22 | case .phone: 23 | return .phone 24 | case .pad: 25 | return .pad 26 | default: 27 | return nil 28 | } 29 | #endif 30 | } 31 | 32 | var hasNotch: Bool { 33 | if #available(iOS 11.0, *) { 34 | let keyWindow = UIApplication.shared.windows.filter(\.isKeyWindow).first 35 | return keyWindow?.safeAreaInsets.bottom ?? 0 > 0 36 | } 37 | return false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Asobi/Extensions/UIHostingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIHostingController.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/20/23. 6 | // 7 | // Initalizer to disable keyboard avoidance if necessary 8 | // From https://steipete.com/posts/disabling-keyboard-avoidance-in-swiftui-uihostingcontroller/ 9 | // 10 | 11 | import SwiftUI 12 | 13 | public extension UIHostingController { 14 | convenience init(rootView: Content, ignoresKeyboard: Bool) { 15 | self.init(rootView: rootView) 16 | 17 | if ignoresKeyboard { 18 | guard let viewClass = object_getClass(view) else { return } 19 | 20 | let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoresKeyboard") 21 | if let viewSubclass = NSClassFromString(viewSubclassName) { 22 | object_setClass(view, viewSubclass) 23 | } else { 24 | guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } 25 | guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } 26 | 27 | if let method = class_getInstanceMethod(viewClass, NSSelectorFromString("keyboardWillShowWithNotification:")) { 28 | let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in } 29 | class_addMethod(viewSubclass, NSSelectorFromString("keyboardWillShowWithNotification:"), 30 | imp_implementationWithBlock(keyboardWillShow), method_getTypeEncoding(method)) 31 | } 32 | objc_registerClassPair(viewSubclass) 33 | object_setClass(view, viewSubclass) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Asobi/Extensions/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 12/27/21. 6 | // 7 | 8 | import Combine 9 | import Introspect 10 | import SwiftUI 11 | 12 | extension View { 13 | // MARK: Custom combine publishers 14 | 15 | // Modified from https://stackoverflow.com/questions/65784294/how-to-detect-if-keyboard-is-present-in-swiftui 16 | // Uses keyboardWillHide to properly track when to adjust the height for the pill view buffer 17 | var keyboardPublisher: AnyPublisher { 18 | Publishers 19 | .Merge( 20 | NotificationCenter 21 | .default 22 | .publisher(for: UIResponder.keyboardWillShowNotification) 23 | .map { _ in true }, 24 | NotificationCenter 25 | .default 26 | .publisher(for: UIResponder.keyboardWillHideNotification) 27 | .map { _ in false } 28 | ) 29 | .eraseToAnyPublisher() 30 | } 31 | 32 | var scenePhasePublisher: AnyPublisher { 33 | Publishers 34 | .Merge3( 35 | NotificationCenter 36 | .default 37 | .publisher(for: UIApplication.willResignActiveNotification) 38 | .map { _ in .inactive }, 39 | NotificationCenter 40 | .default 41 | .publisher(for: UIApplication.didBecomeActiveNotification) 42 | .map { _ in .active }, 43 | NotificationCenter 44 | .default 45 | .publisher(for: UIApplication.didEnterBackgroundNotification) 46 | .map { _ in .background } 47 | ) 48 | .eraseToAnyPublisher() 49 | } 50 | 51 | // MARK: Modifiers 52 | 53 | func applyTheme(_ colorScheme: ColorScheme?) -> some View { 54 | modifier(ApplyTheme(colorScheme: colorScheme)) 55 | } 56 | 57 | func disabledAppearance(_ disabled: Bool = false) -> some View { 58 | modifier(DisabledAppearance(disabled: disabled)) 59 | } 60 | 61 | func onWillDisappear(_ perform: @escaping () -> Void) -> some View { 62 | modifier(WillDisappearModifier(callback: perform)) 63 | } 64 | 65 | func dynamicContextMenu(buttons: [ContextMenuButton], title: String? = nil, willEnd: (() -> Void)? = nil, willDisplay: (() -> Void)? = nil) -> some View { 66 | modifier(DynamicContextMenu(buttons: buttons, title: title, willDisplay: willDisplay, willEnd: willEnd)) 67 | } 68 | 69 | func inlinedList() -> some View { 70 | modifier(InlinedList()) 71 | } 72 | 73 | func conditionalListStyle() -> some View { 74 | modifier(ConditionalListStyle()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Asobi/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 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Editor 24 | CFBundleURLName 25 | me.kingbri.asobi 26 | CFBundleURLSchemes 27 | 28 | asobi 29 | 30 | 31 | 32 | CFBundleVersion 33 | $(CURRENT_PROJECT_VERSION) 34 | LSApplicationCategoryType 35 | public.app-category.utilities 36 | LSRequiresIPhoneOS 37 | 38 | LSSupportsOpeningDocumentsInPlace 39 | 40 | NSAppTransportSecurity 41 | 42 | NSAllowsArbitraryLoads 43 | 44 | 45 | NSFaceIDUsageDescription 46 | Asobi requires permission to access faceID because of your user defined setting 47 | NSPhotoLibraryAddUsageDescription 48 | Asobi requires your permission to save to your photo gallery 49 | UIApplicationSceneManifest 50 | 51 | UIApplicationSupportsMultipleScenes 52 | 53 | 54 | UIApplicationSupportsIndirectInputEvents 55 | 56 | UIBackgroundModes 57 | 58 | audio 59 | remote-notification 60 | 61 | UIFileSharingEnabled 62 | 63 | UILaunchScreen 64 | 65 | UILaunchStoryboardName 66 | LaunchScreen 67 | UIRequiredDeviceCapabilities 68 | 69 | armv7 70 | 71 | UISupportedInterfaceOrientations 72 | 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationLandscapeLeft 75 | UIInterfaceOrientationLandscapeRight 76 | 77 | UISupportedInterfaceOrientations~ipad 78 | 79 | UIInterfaceOrientationPortrait 80 | UIInterfaceOrientationPortraitUpsideDown 81 | UIInterfaceOrientationLandscapeLeft 82 | UIInterfaceOrientationLandscapeRight 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Asobi/JavaScript/FindInPage.js: -------------------------------------------------------------------------------- 1 | // Minimal find in page script to be run in the DOM 2 | // Includes scrolling to a specific query, highlights, and removing said highlights 3 | // Preserves event listeners and text casing 4 | // (c) 2022, Brian Dashore. 5 | 6 | let totalResultLength = 0 7 | let previousIndex = -1 8 | 9 | // Index functions like a normal JS array where 0 is the first element 10 | // Make sure to pass swift's index - 1 into this function 11 | function scrollToFindResult(index) { 12 | let element = document.getElementById(`findResult-${index}`) 13 | 14 | if (element === null) { 15 | return 16 | } 17 | 18 | if (previousIndex !== -1) { 19 | let previousElement = document.getElementById(`findResult-${previousIndex}`) 20 | if (previousElement === null) { 21 | return 22 | } 23 | 24 | if (previousElement.style) { 25 | previousElement.style.backgroundColor = "yellow" 26 | } 27 | } 28 | 29 | if (element.style) { 30 | element.style.backgroundColor = "orange" 31 | element.scrollIntoViewIfNeeded(true) 32 | } 33 | 34 | if (index < totalResultLength || index >= 0) { 35 | previousIndex = index 36 | } 37 | 38 | // Update the result object with the new index 39 | let resultObject = { 40 | currentIndex: index, 41 | totalResultLength, 42 | } 43 | 44 | window.webkit.messageHandlers.findListener.postMessage( 45 | JSON.stringify(resultObject) 46 | ) 47 | } 48 | 49 | function undoFindHighlights() { 50 | for (let i = 0; i < totalResultLength; i++) { 51 | let node = document.getElementById(`findResult-${i}`) 52 | 53 | if (node === null) { 54 | continue 55 | } 56 | 57 | let parent = node.parentNode 58 | 59 | while (node.firstChild) { 60 | parent.insertBefore(node.firstChild, node) 61 | } 62 | 63 | node.remove() 64 | parent.normalize() 65 | } 66 | 67 | // Reset the total results for the next search query 68 | totalResultLength = 0 69 | } 70 | 71 | function findAndHighlightQuery(query) { 72 | const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT) 73 | const textNodes = [] 74 | 75 | while (walker.nextNode()) { 76 | let currentNode = walker.currentNode 77 | let lowercaseQuery = query.toLowerCase() 78 | 79 | // Ignore all script tags 80 | if ( 81 | currentNode.textContent.toLowerCase().includes(lowercaseQuery) && 82 | !currentNode.parentElement.tagName.toLowerCase().includes("script") 83 | ) { 84 | textNodes.push(currentNode) 85 | } 86 | } 87 | 88 | // If there's nothing, return nothing 89 | if (textNodes.length === 0) { 90 | let resultObject = { 91 | currentIndex: 0, 92 | totalResultLength: 0, 93 | } 94 | 95 | window.webkit.messageHandlers.findListener.postMessage( 96 | JSON.stringify(resultObject) 97 | ) 98 | } 99 | 100 | for (let node of textNodes) { 101 | let splitContent = getModifiedHtml(query, node.wholeText) 102 | 103 | let div = document.createElement("div") 104 | node.parentNode.insertBefore(div, node) 105 | div.insertAdjacentHTML("afterend", splitContent) 106 | 107 | div.remove() 108 | node.remove() 109 | } 110 | 111 | // Returned to Swift 112 | // currentIndex: The index of the focused result 113 | // totalResultLength: The overall length of the result array 114 | let resultObject = { 115 | currentIndex: 0, 116 | totalResultLength, 117 | } 118 | 119 | window.webkit.messageHandlers.findListener.postMessage( 120 | JSON.stringify(resultObject) 121 | ) 122 | } 123 | 124 | function getModifiedHtml(query, originalTextContent) { 125 | let queryRegExp = new RegExp(query, "gi") 126 | let stringArray = [] 127 | let lastIndex = 0 128 | 129 | while ((match = queryRegExp.exec(originalTextContent))) { 130 | stringArray.push(originalTextContent.substring(lastIndex, match.index)) 131 | 132 | let overlay = document.createElement("span") 133 | overlay.style.backgroundColor = "yellow" 134 | overlay.style.color = "black" 135 | 136 | let alteredSubstring = originalTextContent.substring( 137 | match.index, 138 | queryRegExp.lastIndex 139 | ) 140 | 141 | overlay.textContent = alteredSubstring 142 | overlay.id = `findResult-${totalResultLength}` 143 | 144 | totalResultLength++ 145 | 146 | // Only trim the HTML to preserve space length 147 | stringArray.push(overlay.outerHTML.trim()) 148 | 149 | lastIndex = queryRegExp.lastIndex 150 | } 151 | 152 | stringArray.push( 153 | originalTextContent.substring(lastIndex, originalTextContent.length) 154 | ) 155 | 156 | // Filter out any empty elements 157 | stringArray = stringArray.filter((entry) => entry !== "") 158 | 159 | return stringArray.join("") 160 | } 161 | -------------------------------------------------------------------------------- /Asobi/JavaScript/JSLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSLoader.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/11/22. 6 | // 7 | 8 | import Foundation 9 | import WebKit 10 | 11 | private enum JSLoadError: Error { 12 | case FailedConversion(JSErrorStruct) 13 | case InvalidFile(JSErrorStruct) 14 | } 15 | 16 | struct JSErrorStruct { 17 | let scriptName: String 18 | let error: Error? 19 | } 20 | 21 | struct JSScript { 22 | let name: String 23 | let devices: [DeviceType] 24 | } 25 | 26 | class JavaScriptLoader { 27 | func loadScripts(scripts: [JSScript], _ webView: WKWebView) -> [String] { 28 | var failureArray: [String] = [] 29 | 30 | for script in scripts { 31 | do { 32 | try loadExternalScript(script: script, webView) 33 | } catch let JSLoadError.InvalidFile(error) { 34 | failureArray.append(error.scriptName) 35 | 36 | debugPrint("JS Loading Error: Invalid filename for \(error.scriptName).") 37 | } catch let JSLoadError.FailedConversion(error) { 38 | failureArray.append(error.scriptName) 39 | 40 | debugPrint("JS Loading Error: The file for \(error.scriptName) cannot be turned into a string: \(String(describing: error.error)).") 41 | } catch { 42 | break 43 | } 44 | } 45 | 46 | return failureArray 47 | } 48 | 49 | func loadExternalScript(script: JSScript, _ webView: WKWebView) throws { 50 | let currentDevice = UIDevice.current.deviceType ?? .phone 51 | 52 | if !script.devices.contains(currentDevice) { 53 | return 54 | } 55 | 56 | if let path = Bundle.main.path(forResource: script.name, ofType: "js") { 57 | do { 58 | let jsString = try String(contentsOfFile: path, encoding: .utf8) 59 | let tempScript = WKUserScript(source: jsString, injectionTime: .atDocumentEnd, forMainFrameOnly: false) 60 | webView.configuration.userContentController.addUserScript(tempScript) 61 | } catch { 62 | throw JSLoadError.FailedConversion(JSErrorStruct(scriptName: script.name, error: error)) 63 | } 64 | } else { 65 | throw JSLoadError.InvalidFile(JSErrorStruct(scriptName: script.name, error: nil)) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Asobi/JavaScript/MacOSPaste.js: -------------------------------------------------------------------------------- 1 | const inputs = document.querySelectorAll("input[type=text]") 2 | let alreadyPasted = false 3 | 4 | for (const input of inputs) { 5 | input.addEventListener("paste", (event) => { 6 | event.preventDefault() 7 | 8 | // Don't call paste event two times in a single paste command 9 | if (alreadyPasted) { 10 | alreadyPasted = false 11 | return 12 | } 13 | 14 | const paste = (event.clipboardData || window.clipboardData).getData("text") 15 | 16 | const beginningString = 17 | input.value.substring(0, input.selectionStart) + paste 18 | 19 | input.value = 20 | beginningString + 21 | input.value.substring(input.selectionEnd, input.value.length) 22 | 23 | alreadyPasted = true 24 | 25 | input.setSelectionRange(beginningString.length, beginningString.length) 26 | 27 | input.scrollLeft = input.scrollWidth 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /Asobi/JavaScript/ZoomUnlocker.js: -------------------------------------------------------------------------------- 1 | async function run() { 2 | let viewport = document.querySelector("meta[name=viewport]"); 3 | 4 | // Edit the existing viewport, otherwise create a new element 5 | const useZoom = await window.webkit.messageHandlers.zoomUnlocker.postMessage("hello") ?? 0 6 | 7 | if (viewport) { 8 | if (useZoom) { 9 | viewport.setAttribute('content', `width=device-width, initial-scale=1.0, user-scalable=1`) 10 | } else { 11 | viewport.setAttribute('content', `width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1`) 12 | } 13 | } else { 14 | let meta = document.createElement('meta'); 15 | meta.name = 'viewport' 16 | meta.content = 'width=device-width, initial-scale=1.0' 17 | document.head.appendChild(meta) 18 | } 19 | } 20 | 21 | run() 22 | -------------------------------------------------------------------------------- /Asobi/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Asobi/Models/SettingsModels.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsModels.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum StatusBarStyleType: String, CaseIterable { 11 | case theme 12 | case automatic 13 | case accent 14 | case custom 15 | } 16 | 17 | enum StatusBarBehaviorType: String, CaseIterable { 18 | case hide 19 | case partialHide 20 | case pin 21 | } 22 | 23 | enum DefaultSearchEngine: String, CaseIterable { 24 | case google 25 | case brave 26 | case bing 27 | case duckduckgo 28 | case startpage 29 | case custom 30 | } 31 | -------------------------------------------------------------------------------- /Asobi/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Asobi/RepresentableViews/AsobiRootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostingViewController.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/7/22. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | // Inspired by Thomas Rademaker's UIHosting controller 12 | // Article: https://barstool.engineering/set-the-ios-status-bar-style-in-swiftui-using-a-custom-view-modifier/ 13 | 14 | // ObservableObject to observe statusbar changes and set accordingly 15 | class AsobiRootViewController: UIViewController, ObservableObject { 16 | @AppStorage("statusBarPinType") var statusBarPinType: StatusBarBehaviorType = .partialHide 17 | 18 | var rootViewController: UIViewController? 19 | var style: UIStatusBarStyle = .lightContent { 20 | didSet { 21 | rootViewController?.setNeedsStatusBarAppearanceUpdate() 22 | } 23 | } 24 | 25 | var statusBarHidden: Bool = false { 26 | didSet { 27 | UIView.animate(withDuration: 0.3) { 28 | self.rootViewController?.setNeedsStatusBarAppearanceUpdate() 29 | } 30 | } 31 | } 32 | 33 | var grayHomeIndicator: Bool = false { 34 | didSet { 35 | rootViewController?.setNeedsUpdateOfScreenEdgesDeferringSystemGestures() 36 | } 37 | } 38 | 39 | var ignoreDarkMode: Bool = false 40 | 41 | init(rootViewController: UIViewController?, style: UIStatusBarStyle, ignoreDarkMode: Bool = false) { 42 | self.rootViewController = rootViewController 43 | self.style = style 44 | self.ignoreDarkMode = ignoreDarkMode 45 | 46 | super.init(nibName: nil, bundle: nil) 47 | 48 | if statusBarPinType == .hide { 49 | statusBarHidden = true 50 | } 51 | } 52 | 53 | required init?(coder: NSCoder) { 54 | super.init(coder: coder) 55 | } 56 | 57 | override func viewDidLoad() { 58 | super.viewDidLoad() 59 | guard let child = rootViewController else { return } 60 | addChild(child) 61 | view.addSubview(child.view) 62 | child.didMove(toParent: self) 63 | } 64 | 65 | override var preferredStatusBarStyle: UIStatusBarStyle { 66 | if ignoreDarkMode || traitCollection.userInterfaceStyle == .light { 67 | return style 68 | } else { 69 | if style == .darkContent { 70 | return .lightContent 71 | } else { 72 | return .darkContent 73 | } 74 | } 75 | } 76 | 77 | override var prefersStatusBarHidden: Bool { 78 | statusBarHidden 79 | } 80 | 81 | override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { 82 | .fade 83 | } 84 | 85 | override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { 86 | if grayHomeIndicator { 87 | return [.bottom] 88 | } else { 89 | return [] 90 | } 91 | } 92 | 93 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 94 | setNeedsStatusBarAppearanceUpdate() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Asobi/RepresentableViews/NotifyingContextMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotifyingContextMenu.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | // Inspired from https://stackoverflow.com/questions/72714335/swiftui-notification-when-contextmenu-is-dismissed-ios 8 | // 9 | 10 | import SwiftUI 11 | 12 | struct NotifyingContextMenu: UIViewRepresentable { 13 | @ViewBuilder var parentView: Content 14 | let actions: [UIAction] 15 | let title: String? 16 | let willDisplay: (() -> Void)? 17 | let willEnd: (() -> Void)? 18 | 19 | func makeCoordinator() -> Coordinator { 20 | Coordinator(self) 21 | } 22 | 23 | class Coordinator: NSObject, UIContextMenuInteractionDelegate { 24 | var contextMenu: UIContextMenuInteraction! 25 | 26 | let parent: NotifyingContextMenu 27 | init(_ parent: NotifyingContextMenu) { 28 | self.parent = parent 29 | } 30 | 31 | func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { 32 | UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [self] 33 | _ in 34 | 35 | UIMenu(title: parent.title ?? "", children: parent.actions) 36 | }) 37 | } 38 | 39 | func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { 40 | print(#function) 41 | parent.willDisplay?() 42 | } 43 | 44 | func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { 45 | print(#function) 46 | parent.willEnd?() 47 | } 48 | } 49 | 50 | func makeUIView(context: Context) -> UIView { 51 | // Add a hosting controller shim to access the ContextMenu properties 52 | let hostingWrapper = UIHostingController(rootView: parentView, ignoresKeyboard: true).view! 53 | context.coordinator.contextMenu = UIContextMenuInteraction(delegate: context.coordinator) 54 | hostingWrapper.addInteraction(context.coordinator.contextMenu) 55 | return hostingWrapper 56 | } 57 | 58 | func updateUIView(_ uiView: UIView, context: Context) {} 59 | } 60 | -------------------------------------------------------------------------------- /Asobi/RepresentableViews/WillDisappearHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WillDisappear.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 6/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // From https://stackoverflow.com/questions/59745663/is-there-a-swiftui-equivalent-for-viewwilldisappear-or-detect-when-a-view-is 11 | struct WillDisappearHandler: UIViewControllerRepresentable { 12 | func makeCoordinator() -> WillDisappearHandler.Coordinator { 13 | Coordinator(onWillDisappear: onWillDisappear) 14 | } 15 | 16 | let onWillDisappear: () -> Void 17 | 18 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController { 19 | context.coordinator 20 | } 21 | 22 | func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext) {} 23 | 24 | typealias UIViewControllerType = UIViewController 25 | 26 | class Coordinator: UIViewController { 27 | let onWillDisappear: () -> Void 28 | 29 | init(onWillDisappear: @escaping () -> Void) { 30 | self.onWillDisappear = onWillDisappear 31 | super.init(nibName: nil, bundle: nil) 32 | } 33 | 34 | @available(*, unavailable) 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override func viewWillDisappear(_ animated: Bool) { 40 | super.viewWillDisappear(animated) 41 | onWillDisappear() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Asobi/ViewModels/NavigationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationViewModel.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 10/25/21. 6 | // 7 | 8 | import LocalAuthentication 9 | import SwiftUI 10 | 11 | @MainActor 12 | class NavigationViewModel: ObservableObject { 13 | enum SheetType: Identifiable { 14 | var id: Int { 15 | hashValue 16 | } 17 | 18 | case settings 19 | case library 20 | case bookmarkEditing 21 | } 22 | 23 | enum AuthAlertType: Identifiable, Hashable { 24 | var id: Self { 25 | self 26 | } 27 | 28 | case cancelled 29 | case missing 30 | case error(localizedDescription: String) 31 | } 32 | 33 | enum PillViewType { 34 | case urlBar 35 | case findInPage 36 | } 37 | 38 | @AppStorage("persistNavigation") private var persistNavigation = false 39 | @AppStorage("autoHideNavigation") private var autoHideNavigation = false 40 | @AppStorage("forceSecurityCredentials") private var forceSecurityCredentials = false 41 | 42 | @Published var currentSheet: SheetType? 43 | @Published var showNavigationBar = true 44 | @Published var isUnlocked = true 45 | @Published var authErrorAlert: AuthAlertType? 46 | @Published var blurRadius: CGFloat = 0 47 | @Published var libraryMenuOpen = false 48 | @Published var isKeyboardShowing = false 49 | @Published var currentPillView: PillViewType? { 50 | didSet { 51 | // If the button is triggered twice, assume that the user wants to hide the view 52 | if oldValue == currentPillView { 53 | currentPillView = nil 54 | } 55 | } 56 | } 57 | 58 | private var autoHideTask: Task? 59 | 60 | init() { 61 | if forceSecurityCredentials { 62 | isUnlocked = false 63 | } 64 | 65 | // These two settings should never be enabled and run a check on view init 66 | if persistNavigation, autoHideNavigation { 67 | UserDefaults.standard.set(false, forKey: "persistNavigation") 68 | UserDefaults.standard.set(false, forKey: "autoHideNavigation") 69 | 70 | withAnimation { 71 | showNavigationBar = true 72 | } 73 | } 74 | } 75 | 76 | func toggleNavigationBar() { 77 | withAnimation(.easeInOut(duration: 0.3)) { 78 | showNavigationBar.toggle() 79 | } 80 | } 81 | 82 | func setNavigationBar(_ enabled: Bool) { 83 | withAnimation(.easeInOut(duration: 0.3)) { 84 | showNavigationBar = enabled 85 | } 86 | } 87 | 88 | func authenticateOnStartup() async { 89 | let context = LAContext() 90 | var error: NSError? 91 | 92 | // Can we authenticate? 93 | if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { 94 | let reason = "Authentication is required to access Asobi" 95 | 96 | do { 97 | let result = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) 98 | 99 | isUnlocked = result 100 | } catch { 101 | let error = error as NSError 102 | 103 | // The MainActor attribute doesn't fire here, so manually call it to run UI updates 104 | if error.code == -2 || error.code == -4 { 105 | await MainActor.run { 106 | authErrorAlert = .cancelled 107 | } 108 | } else { 109 | await MainActor.run { 110 | authErrorAlert = .error(localizedDescription: error.localizedDescription) 111 | } 112 | } 113 | } 114 | } else { 115 | // There's no authentication methods, so unlock anyway, show an error, and turn off the setting 116 | authErrorAlert = .missing 117 | 118 | isUnlocked = true 119 | UserDefaults.standard.set(false, forKey: "forceSecurityCredentials") 120 | } 121 | } 122 | 123 | // Checks if a user has an authentication method 124 | func authenticationPresent() -> Bool { 125 | let context = LAContext() 126 | var error: NSError? 127 | 128 | return context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) 129 | } 130 | 131 | // If auto hiding is enabled 132 | func autoHideNavigationBar() { 133 | if let autoHideTask = autoHideTask { 134 | autoHideTask.cancel() 135 | } 136 | 137 | autoHideTask = Task { 138 | while showNavigationBar { 139 | try? await Task.sleep(seconds: 3) 140 | 141 | // Immediately break out if the task is cancelled 142 | if Task.isCancelled { 143 | return 144 | } 145 | 146 | // If any of these conditions are met, send a show navbar command 147 | // rather than leaving it at the current state 148 | if persistNavigation || libraryMenuOpen || !autoHideNavigation || currentSheet != nil || currentPillView != nil { 149 | continue 150 | } else { 151 | setNavigationBar(false) 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Asobi/Views/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AboutView: View { 11 | @AppStorage("selectedIconKey") var selectedIconKey = "AppImage" 12 | 13 | var body: some View { 14 | List { 15 | Section { 16 | ListRowTextView(leftText: "Version", rightText: Application.shared.appVersion) 17 | ListRowTextView(leftText: "Build number", rightText: Application.shared.appBuild) 18 | ListRowTextView(leftText: "Build type", rightText: Application.shared.buildType) 19 | ListRowExternalLinkView(text: "App website", link: "https://kingbri.dev/asobi") 20 | ListRowExternalLinkView(text: "GitHub repository", link: "https://github.com/bdashore3/Asobi") 21 | ListRowExternalLinkView(text: "Discord support", link: "https://kingbri.dev/discord") 22 | } header: { 23 | VStack(alignment: .center) { 24 | Image(selectedIconKey) 25 | .resizable() 26 | .frame(width: 100, height: 100) 27 | .clipShape(RoundedRectangle(cornerRadius: 100 * 0.225, style: .continuous)) 28 | .padding(.top, 24) 29 | 30 | Text("Asobi is a free and open source browser application developed by Brian Dashore under the Apache-2.0 license.") 31 | .textCase(.none) 32 | .foregroundColor(.label) 33 | .font(.body) 34 | .padding(.top, 8) 35 | .padding(.bottom, 20) 36 | } 37 | .listRowInsets(EdgeInsets(top: 0, leading: 7, bottom: 0, trailing: 0)) 38 | } 39 | } 40 | .listStyle(.insetGrouped) 41 | .navigationTitle("About") 42 | } 43 | } 44 | 45 | #if DEBUG 46 | struct AboutView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | AboutView() 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Asobi/Views/AuthOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthOverlayView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 2/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AuthOverlayView: View { 11 | @EnvironmentObject var navModel: NavigationViewModel 12 | 13 | var body: some View { 14 | // Dummy ZStack for alert presentation 15 | Color.gray 16 | .opacity(0.3) 17 | .alert(item: $navModel.authErrorAlert) { alert in 18 | switch alert { 19 | case .cancelled: 20 | return Alert( 21 | title: Text("Authentication error"), 22 | message: Text("The person using Asobi turned on authentication in settings and auth was forcibly cancelled. \n\nYou can retry if this was a mistake."), 23 | primaryButton: .default(Text("Retry")) { 24 | Task { 25 | await navModel.authenticateOnStartup() 26 | } 27 | }, 28 | secondaryButton: .cancel() 29 | ) 30 | case .missing: 31 | return Alert( 32 | title: Text("Authentication error"), 33 | message: Text("It looks like your authentication was turned off, so Asobi automatically unlocked itself. \n\nPlease re-enable an iOS or macOS passcode and turn on the authentication toggle in Asobi's settings to re-enable this feature."), 34 | dismissButton: .default(Text("OK")) 35 | ) 36 | case let .error(localizedDescription: localizedDescription): 37 | return Alert( 38 | title: Text("Authentication error"), 39 | message: Text("Unhandled exception, the description is posted below: \n\n\(localizedDescription)"), 40 | dismissButton: .default(Text("OK")) 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | 47 | struct AuthOverlayView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | AuthOverlayView() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/AllowedURLSchemeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllowedURLSchemeView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AllowedURLSchemeView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | let backgroundContext = PersistenceController.shared.backgroundContext 14 | 15 | @FetchRequest( 16 | entity: AllowedURLScheme.entity(), 17 | sortDescriptors: [] 18 | ) var allowedSchemes: FetchedResults 19 | 20 | @State private var showErrorAlert = false 21 | @State private var errorMessage: String = "" 22 | @State private var newSchemeUrl: String = "" 23 | 24 | var body: some View { 25 | Form { 26 | Section( 27 | header: "Add a website", 28 | footer: "When adding a scheme, it must have :// included" 29 | ) { 30 | HStack { 31 | TextField("Enter URL or scheme", text: $newSchemeUrl) 32 | .clearButtonMode(.whileEditing) 33 | .disableAutocorrection(true) 34 | .keyboardType(.URL) 35 | .autocapitalization(.none) 36 | .lineLimit(1) 37 | 38 | Spacer() 39 | 40 | Button("Add") { 41 | guard let newScheme = URL(string: newSchemeUrl)?.scheme else { 42 | errorMessage = "Cannot add the scheme because the URL is invalid. Maybe you forgot :// after the scheme name?" 43 | showErrorAlert.toggle() 44 | 45 | return 46 | } 47 | 48 | if newScheme == "http" || newScheme == "https" { 49 | errorMessage = "Cannot add http or https schemes because they are allowed by default" 50 | showErrorAlert.toggle() 51 | 52 | return 53 | } 54 | 55 | let schemeRequest = AllowedURLScheme.fetchRequest() 56 | schemeRequest.predicate = NSPredicate(format: "scheme == %@", newScheme) 57 | schemeRequest.fetchLimit = 1 58 | 59 | guard let count = try? backgroundContext.count(for: schemeRequest) else { 60 | errorMessage = "Cannot add this scheme because it already exists" 61 | showErrorAlert.toggle() 62 | 63 | return 64 | } 65 | 66 | if count < 1 { 67 | let newAllowedScheme = AllowedURLScheme(context: backgroundContext) 68 | newAllowedScheme.scheme = newScheme 69 | 70 | PersistenceController.shared.save(backgroundContext) 71 | } else { 72 | errorMessage = "Cannot add this scheme because it already exists" 73 | showErrorAlert.toggle() 74 | } 75 | } 76 | } 77 | } 78 | 79 | Section(header: "Allowed schemes") { 80 | if allowedSchemes.isEmpty { 81 | Text("There are no allowed URL schemes (https and http are always allowed)") 82 | } else { 83 | ForEach(allowedSchemes, id: \.self) { allowedScheme in 84 | Text(allowedScheme.scheme ?? "No scheme present") 85 | } 86 | .onDelete(perform: removeItem) 87 | } 88 | } 89 | } 90 | .alert(isPresented: $showErrorAlert) { 91 | Alert( 92 | title: Text("Error"), 93 | message: Text(errorMessage), 94 | dismissButton: .cancel(Text("OK")) 95 | ) 96 | } 97 | .toolbar { 98 | ToolbarItem(placement: .navigationBarTrailing) { 99 | EditButton() 100 | } 101 | } 102 | .navigationTitle("Allowed URL Schemes") 103 | .navigationBarTitleDisplayMode(.inline) 104 | } 105 | 106 | func removeItem(at offsets: IndexSet) { 107 | for index in offsets { 108 | if let allowedScheme = allowedSchemes[safe: index] { 109 | PersistenceController.shared.delete(allowedScheme, context: backgroundContext) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/AppIconButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIconButtonView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 12/26/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppIconButtonView: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | @AppStorage("selectedIconKey") var selectedIconKey = "AppImage" 14 | @AppStorage("navigationAccent") var navigationAccent: Color = .red 15 | 16 | @State private var showErrorAlert = false 17 | @State private var errorAlertText = "" 18 | 19 | let imageKey: String 20 | let iconKey: String? 21 | let iconName: String 22 | let author: String 23 | 24 | var body: some View { 25 | VStack { 26 | Button(action: { 27 | Task { 28 | do { 29 | try await UIApplication.shared.setAlternateIconName(iconKey) 30 | selectedIconKey = imageKey 31 | } catch { 32 | errorAlertText = error.localizedDescription 33 | showErrorAlert.toggle() 34 | } 35 | } 36 | }, label: { 37 | Image(imageKey) 38 | .resizable() 39 | .frame(width: 60, height: 60) 40 | .cornerRadius(10) 41 | }) 42 | 43 | VStack { 44 | Text(iconName) 45 | Text("-\(author)") 46 | } 47 | .foregroundColor(selectedIconKey == imageKey ? navigationAccent : (colorScheme == .light ? .black : .white)) 48 | .font(.caption2, weight: selectedIconKey == imageKey ? .bold : .regular) 49 | } 50 | .alert(isPresented: $showErrorAlert) { 51 | Alert( 52 | title: Text("Error!"), 53 | message: Text("App icon error: \(errorAlertText)"), 54 | dismissButton: .cancel(Text("OK")) 55 | ) 56 | } 57 | } 58 | } 59 | 60 | struct AppIconButtonView_Previews: PreviewProvider { 61 | static var previews: some View { 62 | AppIconButtonView(imageKey: "AppImage", iconKey: nil, iconName: "Default", author: "kingbri") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/AppIconPickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIconPickerView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 12/26/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppIconPickerView: View { 11 | var body: some View { 12 | ScrollView(.horizontal, showsIndicators: false) { 13 | LazyHGrid(rows: [GridItem()], spacing: 25) { 14 | AppIconButtonView(imageKey: "AppImage", iconKey: nil, iconName: "Default", author: "kingbri") 15 | AppIconButtonView(imageKey: "GradientsImage", iconKey: "GradientsAppIcon", iconName: "Gradients", author: "Idiocy Max") 16 | AppIconButtonView(imageKey: "SunsetImage", iconKey: "SunsetAppIcon", iconName: "Sunset", author: "kingbri") 17 | AppIconButtonView(imageKey: "OceanImage", iconKey: "OceanAppIcon", iconName: "Ocean", author: "kingbri") 18 | AppIconButtonView(imageKey: "JustPinkImage", iconKey: "JustPinkAppIcon", iconName: "Just Pink", author: "kingbri") 19 | } 20 | .padding(.vertical, 10) 21 | } 22 | } 23 | } 24 | 25 | struct AppIconPickerView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | AppIconPickerView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/ContextMenuButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextMenuButton.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | // Common button for a dynamic context menu 8 | // 9 | 10 | import SwiftUI 11 | 12 | struct ContextMenuButton: Identifiable { 13 | let id: UUID 14 | let text: String 15 | let systemImage: String? 16 | let action: () -> Void 17 | 18 | init(_ text: String, systemImage: String? = nil, action: @escaping () -> Void) { 19 | id = UUID() 20 | self.text = text 21 | self.systemImage = systemImage 22 | self.action = action 23 | } 24 | 25 | @ViewBuilder func toSwiftUIButton() -> some View { 26 | Button { 27 | action() 28 | } label: { 29 | Label(text, systemImage: systemImage ?? "") 30 | } 31 | } 32 | 33 | func toUIAction() -> UIAction { 34 | UIAction(title: text, image: UIImage(systemName: systemImage ?? ""), handler: { _ in 35 | action() 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/EmptyInstructionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyInstructionView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmptyInstructionView: View { 11 | let title: String 12 | let message: String 13 | 14 | var body: some View { 15 | VStack(spacing: 5) { 16 | Text(title) 17 | .font(.system(size: 25, weight: .semibold)) 18 | 19 | Text(message) 20 | .padding(.horizontal, 50) 21 | } 22 | .multilineTextAlignment(.center) 23 | .foregroundColor(.secondaryLabel) 24 | .frame(maxWidth: .infinity, maxHeight: .infinity) 25 | .ignoresSafeArea() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/GroupBoxStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GroupBoxStyle.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 7/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoadingGroupBoxStyle: GroupBoxStyle { 11 | func makeBody(configuration: Configuration) -> some View { 12 | VStack { 13 | configuration.label 14 | configuration.content 15 | } 16 | .padding(10) 17 | .background(.systemGroupedBackground) 18 | .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/InlineHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineHeader.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/10/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InlineHeader: View { 11 | let title: String 12 | 13 | init(_ title: String) { 14 | self.title = title 15 | } 16 | 17 | var body: some View { 18 | if #available(iOS 16, *) { 19 | Text(title) 20 | } else if #available(iOS 15, *) { 21 | Text(title) 22 | .listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 0)) 23 | } else { 24 | Text(title) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/ListRowViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListRowView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // These views were imported from FileBridge 11 | 12 | // View alias for a list row with an external link 13 | struct ListRowLinkView: View { 14 | @Environment(\.colorScheme) var colorScheme: ColorScheme 15 | 16 | @EnvironmentObject var webModel: WebViewModel 17 | @EnvironmentObject var navModel: NavigationViewModel 18 | 19 | let text: String 20 | let link: String 21 | var subText: String? 22 | @State var useStatefulBookmarks: Bool = false 23 | 24 | var body: some View { 25 | ZStack { 26 | Color.clear 27 | HStack { 28 | VStack(alignment: .leading) { 29 | Text(text) 30 | .font(subText != nil ? .subheadline : .body) 31 | .foregroundColor(colorScheme == .light ? Color.black : Color.white) 32 | .lineLimit(1) 33 | 34 | if let subText = subText { 35 | Text(subText) 36 | .font(.footnote) 37 | .foregroundColor(.gray) 38 | .lineLimit(1) 39 | } 40 | } 41 | 42 | Spacer() 43 | 44 | Image(systemName: "chevron.right") 45 | .foregroundColor(.gray) 46 | } 47 | } 48 | .contentShape(Rectangle()) 49 | .onTapGesture { 50 | var loadLink = link 51 | 52 | if useStatefulBookmarks { 53 | let cutUrl = link.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "http://", with: "") 54 | 55 | let historyRequest = HistoryEntry.fetchRequest() 56 | historyRequest.predicate = NSPredicate(format: "url CONTAINS %@", cutUrl) 57 | historyRequest.sortDescriptors = [NSSortDescriptor(keyPath: \HistoryEntry.timestamp, ascending: false)] 58 | historyRequest.fetchLimit = 1 59 | 60 | if let entry = try? PersistenceController.shared.backgroundContext.fetch(historyRequest).first, let url = entry.url { 61 | if entry.parentHistory == nil { 62 | webModel.toastDescription = "This history entry is a zombie! You may want to repair your history in Library > Actions" 63 | } 64 | 65 | loadLink = url 66 | } 67 | } 68 | 69 | webModel.loadUrl(loadLink) 70 | 71 | navModel.currentSheet = nil 72 | } 73 | } 74 | } 75 | 76 | struct ListRowExternalLinkView: View { 77 | let text: String 78 | let link: String 79 | 80 | var body: some View { 81 | HStack { 82 | Link(text, destination: URL(string: link)!) 83 | .foregroundColor(.primary) 84 | 85 | Spacer() 86 | 87 | Image(systemName: "arrow.up.forward.app.fill") 88 | .foregroundColor(.gray) 89 | } 90 | } 91 | } 92 | 93 | struct ListRowTextView: View { 94 | let leftText: String 95 | var rightText: String? 96 | var rightSymbol: String? 97 | 98 | var body: some View { 99 | HStack { 100 | Text(leftText) 101 | 102 | Spacer() 103 | 104 | if let rightText = rightText { 105 | Text(rightText) 106 | } else { 107 | Image(systemName: rightSymbol!) 108 | .foregroundColor(.gray) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/ApplyTheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplyTheme.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | 8 | import Introspect 9 | import SwiftUI 10 | 11 | struct ApplyTheme: ViewModifier { 12 | let colorScheme: ColorScheme? 13 | 14 | func body(content: Content) -> some View { 15 | content 16 | .introspectViewController { UIViewController in 17 | switch colorScheme { 18 | case .dark: 19 | UIViewController.overrideUserInterfaceStyle = .dark 20 | case .light: 21 | UIViewController.overrideUserInterfaceStyle = .light 22 | default: 23 | UIViewController.overrideUserInterfaceStyle = .unspecified 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/ConditionalListStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConditionalListStyle.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | // Conditionally switches a list style between grouped and inset grouped for Mac vs iOS 8 | // 9 | 10 | import SwiftUI 11 | 12 | struct ConditionalListStyle: ViewModifier { 13 | func body(content: Content) -> some View { 14 | if UIDevice.current.deviceType == .mac { 15 | content 16 | .listStyle(.insetGrouped) 17 | } else { 18 | content 19 | .listStyle(.grouped) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/DisabledAppearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisabledAppearance.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DisabledAppearance: ViewModifier { 11 | let disabled: Bool 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .disabled(disabled) 16 | .opacity(disabled ? 0.5 : 1) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/DynamicContextMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicContextMenu.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | // Switches between native SwiftUI and Notifying context menus depending on the OS 8 | // 9 | 10 | import SwiftUI 11 | 12 | struct DynamicContextMenu: ViewModifier { 13 | let buttons: [ContextMenuButton] 14 | let title: String? 15 | let willDisplay: (() -> Void)? 16 | let willEnd: (() -> Void)? 17 | 18 | func body(content: Content) -> some View { 19 | if UIDevice.current.deviceType == .mac { 20 | content 21 | .contextMenu { 22 | ForEach(buttons) { button in 23 | button.toSwiftUIButton() 24 | } 25 | } 26 | } else { 27 | NotifyingContextMenu( 28 | parentView: { content }, 29 | actions: buttons.map { $0.toUIAction() }, 30 | title: title, 31 | willDisplay: willDisplay, 32 | willEnd: willEnd 33 | ) 34 | .ignoresSafeArea(.keyboard) 35 | .fixedSize() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/InlinedList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlinedList.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | // Removes the top padding on unsectioned lists 8 | // If a list is sectioned, see InlineHeader 9 | // 10 | 11 | import Introspect 12 | import SwiftUI 13 | 14 | struct InlinedList: ViewModifier { 15 | func body(content: Content) -> some View { 16 | if #available(iOS 16, *) { 17 | content 18 | .introspectCollectionView { collectionView in 19 | collectionView.contentInset.top = -20 20 | } 21 | } else { 22 | content 23 | .introspectTableView { tableView in 24 | tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20)) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/TextFieldClearMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldClearMode.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TextFieldClearMode: ViewModifier { 11 | let clearButtonMode: UITextField.ViewMode 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .introspectTextField { textField in 16 | textField.clearButtonMode = clearButtonMode 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/Modifiers/WillDisappearModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WillDisappearModifier.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WillDisappearModifier: ViewModifier { 11 | let callback: () -> Void 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .background(WillDisappearHandler(onWillDisappear: callback)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/NavView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 6/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Hybrid view to decide whether to use a NavigationStack or legacy NavigationView 11 | // Thanks Mantton! 12 | struct NavView: View { 13 | let content: () -> Content 14 | init(@ViewBuilder _ content: @escaping () -> Content) { 15 | self.content = content 16 | } 17 | 18 | var body: some View { 19 | if #available(iOS 16, *) { 20 | NavigationStack { 21 | content() 22 | } 23 | } else { 24 | NavigationView { 25 | content() 26 | } 27 | .navigationViewStyle(.stack) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/PaddedTextFieldStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaddedTextFieldStyle.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | // A non-rounded TextField with padding 8 | // 9 | 10 | import SwiftUI 11 | 12 | struct PaddedTextFieldStyle: TextFieldStyle { 13 | @Environment(\.colorScheme) var colorScheme: ColorScheme 14 | let isRounded: Bool 15 | 16 | public func _body(configuration: TextField) -> some View { 17 | configuration 18 | .padding(5) 19 | .background( 20 | colorScheme == .light ? .secondarySystemGroupedBackground : .tertiarySystemGroupedBackground 21 | ) 22 | .cornerRadius(isRounded ? 5 : 0) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Asobi/Views/CommonViews/PopupExceptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopupExceptionView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 5/20/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PopupExceptionView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | let backgroundContext = PersistenceController.shared.backgroundContext 14 | 15 | @FetchRequest( 16 | entity: AllowedPopup.entity(), 17 | sortDescriptors: [] 18 | ) var allowedPopups: FetchedResults 19 | 20 | @State private var newAllowedSiteUrl = "" 21 | 22 | var body: some View { 23 | Form { 24 | Section(header: "Add a website") { 25 | HStack { 26 | TextField("Enter URL", text: $newAllowedSiteUrl) 27 | .clearButtonMode(.whileEditing) 28 | .disableAutocorrection(true) 29 | .keyboardType(.URL) 30 | .autocapitalization(.none) 31 | .lineLimit(1) 32 | 33 | Spacer() 34 | 35 | Button("Add") { 36 | let popupRequest = AllowedPopup.fetchRequest() 37 | popupRequest.predicate = NSPredicate(format: "url == %@", newAllowedSiteUrl) 38 | popupRequest.fetchLimit = 1 39 | 40 | guard let count = try? backgroundContext.count(for: popupRequest) else { 41 | return 42 | } 43 | 44 | if count < 1 { 45 | let newAllowedSite = AllowedPopup(context: backgroundContext) 46 | newAllowedSite.url = newAllowedSiteUrl 47 | 48 | PersistenceController.shared.save(backgroundContext) 49 | } 50 | } 51 | } 52 | } 53 | 54 | Section(header: "Allowed websites") { 55 | if allowedPopups.isEmpty { 56 | Text("There are no allowed popup websites") 57 | } else { 58 | ForEach(allowedPopups, id: \.self) { allowedPopup in 59 | Text(allowedPopup.url ?? "No URL found") 60 | } 61 | .onDelete(perform: removeItem) 62 | } 63 | } 64 | } 65 | .toolbar { 66 | ToolbarItem(placement: .navigationBarTrailing) { 67 | EditButton() 68 | } 69 | } 70 | .onAppear { 71 | if let url = webModel.webView.url { 72 | newAllowedSiteUrl = url.absoluteString 73 | } 74 | } 75 | .navigationTitle("Popup Exceptions") 76 | .navigationBarTitleDisplayMode(.inline) 77 | } 78 | 79 | func removeItem(at offsets: IndexSet) { 80 | for index in offsets { 81 | if let allowedPopup = allowedPopups[safe: index] { 82 | PersistenceController.shared.delete(allowedPopup, context: backgroundContext) 83 | } 84 | } 85 | } 86 | } 87 | 88 | struct PopupExceptionView_Previews: PreviewProvider { 89 | static var previews: some View { 90 | PopupExceptionView() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Library/BookmarkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 10/22/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BookmarkView: View { 11 | @AppStorage("defaultUrl") var defaultUrl = "" 12 | @AppStorage("useStatefulBookmarks") var useStatefulBookmarks = false 13 | 14 | @EnvironmentObject var webModel: WebViewModel 15 | @EnvironmentObject var navModel: NavigationViewModel 16 | 17 | let backgroundContext = PersistenceController.shared.backgroundContext 18 | 19 | var bookmarks: FetchedResults 20 | 21 | @Binding var currentBookmark: Bookmark? 22 | @Binding var showEditing: Bool 23 | 24 | var body: some View { 25 | List { 26 | if !bookmarks.isEmpty { 27 | ForEach(bookmarks, id: \.self) { bookmark in 28 | // Check for iOS 15 and ONLY iOS 15 29 | if #available(iOS 15.0, *), UIDevice.current.deviceType != .mac { 30 | ListRowLinkView(text: bookmark.name ?? "Unknown", link: bookmark.url ?? "", useStatefulBookmarks: useStatefulBookmarks) 31 | .swipeActions(edge: .trailing, allowsFullSwipe: false) { 32 | if #available(iOS 16, *) { 33 | NavigationLink("Edit", destination: EditBookmarkView(bookmark: bookmark)) 34 | .tint(.blue) 35 | } else { 36 | Button("Edit") { 37 | currentBookmark = bookmark 38 | 39 | showEditing = true 40 | } 41 | .tint(.blue) 42 | } 43 | 44 | Button("Delete") { 45 | PersistenceController.shared.delete(bookmark, context: backgroundContext) 46 | } 47 | .tint(.red) 48 | } 49 | .swipeActions(edge: .leading, allowsFullSwipe: false) { 50 | Button("Set as default") { 51 | if let bookmarkUrl = bookmark.url { 52 | defaultUrl = bookmarkUrl 53 | } 54 | } 55 | .tint(.green) 56 | } 57 | } else { 58 | ListRowLinkView(text: bookmark.name ?? "Unknown", link: bookmark.url ?? "", useStatefulBookmarks: useStatefulBookmarks) 59 | .contextMenu { 60 | Button { 61 | currentBookmark = bookmark 62 | 63 | showEditing = true 64 | } label: { 65 | Label("Edit bookmark", systemImage: "pencil") 66 | } 67 | 68 | Button { 69 | PersistenceController.shared.delete(bookmark, context: backgroundContext) 70 | } label: { 71 | Label("Delete bookmark", systemImage: "trash") 72 | } 73 | 74 | Button { 75 | if let bookmarkUrl = bookmark.url { 76 | defaultUrl = bookmarkUrl 77 | } 78 | } label: { 79 | Label("Set as default URL", systemImage: "archivebox") 80 | } 81 | } 82 | } 83 | } 84 | .onMove(perform: moveItem) 85 | .onDelete(perform: removeItem) 86 | } 87 | } 88 | .inlinedList() 89 | .conditionalListStyle() 90 | } 91 | 92 | func removeItem(at offsets: IndexSet) { 93 | for index in offsets { 94 | if let bookmark = bookmarks[safe: index] { 95 | PersistenceController.shared.delete(bookmark, context: backgroundContext) 96 | } 97 | } 98 | } 99 | 100 | func moveItem(from source: IndexSet, to destination: Int) { 101 | var changedBookmarks = bookmarks.map { $0 } 102 | 103 | changedBookmarks.move(fromOffsets: source, toOffset: destination) 104 | 105 | for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) { 106 | changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex) 107 | } 108 | 109 | PersistenceController.shared.save() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Library/EditBookmarkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddBookmarkView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 10/16/21. 6 | // 7 | 8 | import CoreData 9 | import Introspect 10 | import SwiftUI 11 | 12 | struct EditBookmarkView: View { 13 | @Environment(\.presentationMode) var presentationMode 14 | 15 | @EnvironmentObject var webModel: WebViewModel 16 | @EnvironmentObject var navModel: NavigationViewModel 17 | 18 | let backgroundContext = PersistenceController.shared.backgroundContext 19 | 20 | @State var bookmark: Bookmark? = nil 21 | 22 | @State private var bookmarkName = "" 23 | @State private var bookmarkUrl = "" 24 | @State private var showUrlError = false 25 | 26 | var body: some View { 27 | Form { 28 | // Because onAppear doesn't work properly with sheet + form 29 | Section { 30 | TextField("Enter title", text: $bookmarkName) 31 | .clearButtonMode(.whileEditing) 32 | 33 | TextField("Enter URL", text: $bookmarkUrl) 34 | .clearButtonMode(.whileEditing) 35 | .disableAutocorrection(true) 36 | .keyboardType(.URL) 37 | .autocapitalization(.none) 38 | } 39 | .onAppear { 40 | bookmarkName = bookmark?.name ?? webModel.webView.title ?? "" 41 | bookmarkUrl = bookmark?.url ?? webModel.webView.url?.absoluteString ?? "" 42 | 43 | if !(bookmarkUrl.hasPrefix("http://") || bookmarkUrl.hasPrefix("https://")) { 44 | bookmarkUrl = "https://\(bookmarkUrl)" 45 | } 46 | } 47 | } 48 | .alert(isPresented: $showUrlError) { 49 | Alert( 50 | title: Text("Empty fields"), 51 | message: Text("The bookmark title and URL cannot be empty. Please input the valid fields"), 52 | dismissButton: .default(Text("OK")) 53 | ) 54 | } 55 | .navigationBarTitle("Editing Bookmark", displayMode: .inline) 56 | .toolbar { 57 | ToolbarItem(placement: .navigationBarLeading) { 58 | if navModel.currentSheet != .library { 59 | Button("Cancel") { 60 | presentationMode.wrappedValue.dismiss() 61 | } 62 | .keyboardShortcut(.cancelAction) 63 | } 64 | } 65 | ToolbarItem(placement: .navigationBarTrailing) { 66 | Button("Save") { 67 | if bookmarkUrl == "" || bookmarkName == "" { 68 | showUrlError.toggle() 69 | 70 | return 71 | } 72 | 73 | if let unwrappedBookmark = bookmark { 74 | // Update an existing bookmark 75 | unwrappedBookmark.name = bookmarkName.trimmingCharacters(in: .whitespaces) 76 | unwrappedBookmark.url = bookmarkUrl.trimmingCharacters(in: .whitespaces) 77 | } else { 78 | // If called from a context menu, we need to upsert the bookmark 79 | let tempName = bookmarkName.trimmingCharacters(in: .whitespaces) 80 | 81 | let bookmarkRequest = Bookmark.fetchRequest() 82 | bookmarkRequest.predicate = NSPredicate(format: "name == %@", tempName) 83 | bookmarkRequest.fetchLimit = 1 84 | 85 | if let existingBookmark = try? backgroundContext.fetch(bookmarkRequest).first { 86 | PersistenceController.shared.delete(existingBookmark, context: backgroundContext) 87 | } 88 | 89 | // Set a new bookmark 90 | let bookmark = Bookmark(context: backgroundContext) 91 | bookmark.name = tempName 92 | bookmark.url = bookmarkUrl.trimmingCharacters(in: .whitespaces) 93 | } 94 | 95 | PersistenceController.shared.save(backgroundContext) 96 | 97 | presentationMode.wrappedValue.dismiss() 98 | 99 | bookmark = nil 100 | } 101 | .keyboardShortcut(.defaultAction) 102 | } 103 | } 104 | .blur(radius: UIDevice.current.deviceType == .mac ? 0 : navModel.blurRadius) 105 | } 106 | } 107 | 108 | struct EditBookmarkView_Previews: PreviewProvider { 109 | static var previews: some View { 110 | EditBookmarkView(bookmark: nil) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Library/HistoryActionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryActionView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/13/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HistoryActionView: View { 11 | enum HistoryAlertType: Identifiable { 12 | var id: Int { 13 | hashValue 14 | } 15 | 16 | case warn 17 | case error 18 | case success 19 | } 20 | 21 | @State var labelText: String 22 | 23 | @State private var currentHistoryAlert: HistoryAlertType? 24 | @State private var showWarnAlert = false 25 | @State private var showErrorAlert = false 26 | @State private var showSuccessAlert = false 27 | 28 | @State private var showActionSheet = false 29 | @State private var historyDeleteRange: HistoryDeleteRange = .day 30 | @State private var successAlertRange: String = "" 31 | @State private var errorMessage: String? 32 | 33 | var body: some View { 34 | Button { 35 | showActionSheet.toggle() 36 | } label: { 37 | Text(labelText) 38 | .foregroundColor(.red) 39 | } 40 | .actionSheet(isPresented: $showActionSheet) { 41 | ActionSheet( 42 | title: Text("Clear browsing data"), 43 | message: Text("This will delete your browsing history! Be careful."), 44 | buttons: [ 45 | .destructive(Text("Past day")) { 46 | historyDeleteRange = .day 47 | successAlertRange = "day" 48 | showWarnAlert.toggle() 49 | }, 50 | .destructive(Text("Past week")) { 51 | historyDeleteRange = .week 52 | successAlertRange = "week" 53 | showWarnAlert.toggle() 54 | }, 55 | .destructive(Text("Past 4 weeks")) { 56 | historyDeleteRange = .month 57 | successAlertRange = "4 weeks" 58 | showWarnAlert.toggle() 59 | }, 60 | .destructive(Text("All time")) { 61 | historyDeleteRange = .allTime 62 | showWarnAlert.toggle() 63 | }, 64 | .cancel() 65 | ] 66 | ) 67 | } 68 | .alert(item: $currentHistoryAlert) { alert in 69 | switch alert { 70 | case .warn: 71 | return Alert( 72 | title: Text("Are you sure?"), 73 | message: Text("Deleting browser history is an irreversible action!"), 74 | primaryButton: .destructive(Text("Yes")) { 75 | do { 76 | try PersistenceController.shared.batchDeleteHistory(range: historyDeleteRange) 77 | currentHistoryAlert = .success 78 | } catch { 79 | errorMessage = error.localizedDescription 80 | currentHistoryAlert = .error 81 | } 82 | }, 83 | secondaryButton: .cancel() 84 | ) 85 | case .error: 86 | return Alert( 87 | title: Text("Error when clearing data!"), 88 | message: Text(errorMessage ?? "This alert popped up by accident, send feedback to the dev."), 89 | dismissButton: .default(Text("OK")) 90 | ) 91 | case .success: 92 | return Alert( 93 | title: Text("Success!"), 94 | message: Text("Your browsing data \(successAlertRange.isEmpty ? "" : "from the past \(successAlertRange)") has been cleared"), 95 | dismissButton: .default(Text("OK")) { 96 | successAlertRange = "" 97 | } 98 | ) 99 | } 100 | } 101 | } 102 | } 103 | 104 | struct HistoryActionView_Previews: PreviewProvider { 105 | static var previews: some View { 106 | HistoryActionView(labelText: "Clear") 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Library/HistoryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HistoryView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 11/9/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HistoryView: View { 11 | private var formatter: DateFormatter = .init() 12 | 13 | let backgroundContext = PersistenceController.shared.backgroundContext 14 | 15 | var history: FetchedResults 16 | 17 | init(history: FetchedResults) { 18 | self.history = history 19 | formatter.dateStyle = .medium 20 | formatter.timeStyle = .none 21 | } 22 | 23 | var body: some View { 24 | List { 25 | if !history.isEmpty { 26 | ForEach(groupedEntries(history), id: \.self) { (section: [History]) in 27 | Section(header: Text(formatter.string(from: section[0].date ?? Date()))) { 28 | ForEach(section, id: \.self) { history in 29 | ForEach(history.entryArray) { entry in 30 | ListRowLinkView(text: entry.name ?? "Unknown", link: entry.url ?? "", subText: entry.url) 31 | } 32 | .onDelete { offsets in 33 | self.removeEntry(at: offsets, from: history) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | .conditionalListStyle() 41 | } 42 | 43 | func groupedEntries(_ result: FetchedResults) -> [[History]] { 44 | Dictionary(grouping: result) { (element: History) in 45 | element.dateString ?? "" 46 | }.values.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() } 47 | } 48 | 49 | func removeEntry(at offsets: IndexSet, from history: History) { 50 | for index in offsets { 51 | if let entry = history.entryArray[safe: index] { 52 | history.removeFromEntries(entry) 53 | PersistenceController.shared.delete(entry, context: backgroundContext) 54 | } 55 | 56 | if history.entryArray.isEmpty { 57 | PersistenceController.shared.delete(history, context: backgroundContext) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/FindInPageButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FindInPageButtonView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FindInPageButtonView: View { 11 | @EnvironmentObject var navModel: NavigationViewModel 12 | 13 | var body: some View { 14 | Button(action: { 15 | navModel.currentPillView = .findInPage 16 | }, label: { 17 | Image(systemName: "text.magnifyingglass") 18 | .padding(.horizontal, 4) 19 | }) 20 | .keyboardShortcut("f", modifiers: [.command, .shift]) 21 | } 22 | } 23 | 24 | struct FindInPageButtonView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | FindInPageButtonView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/ForwardBackButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForwardBackView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ForwardBackButtonView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | var body: some View { 14 | Button(action: { 15 | webModel.goBack() 16 | }, label: { 17 | Image(systemName: "arrow.left") 18 | .padding(4) 19 | }) 20 | .disabled(!webModel.canGoBack) 21 | 22 | Spacer() 23 | 24 | Button(action: { 25 | webModel.goForward() 26 | }, label: { 27 | Image(systemName: "arrow.right") 28 | .padding(.horizontal, 4) 29 | }) 30 | .disabled(!webModel.canGoForward) 31 | } 32 | } 33 | 34 | #if DEBUG 35 | struct ForwardBackButtonView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | ForwardBackButtonView() 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/HomeButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeButtonView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | var body: some View { 14 | Button(action: { 15 | webModel.goHome() 16 | }, label: { 17 | Image(systemName: "house") 18 | .padding(.horizontal, 4) 19 | }) 20 | } 21 | } 22 | 23 | #if DEBUG 24 | struct HomeButtonView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | HomeButtonView() 27 | } 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/LibraryButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutButtonView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LibraryButtonView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | @EnvironmentObject var navModel: NavigationViewModel 13 | 14 | @AppStorage("browserModeEnabled") var browserModeEnabled = false 15 | 16 | var body: some View { 17 | Button(action: { 18 | navModel.currentSheet = .library 19 | }, label: { 20 | Image(systemName: "book") 21 | .padding(.horizontal, 4) 22 | }) 23 | .dynamicContextMenu( 24 | buttons: getContextMenuButtons(), 25 | willEnd: { 26 | navModel.libraryMenuOpen = false 27 | }, 28 | willDisplay: { 29 | navModel.libraryMenuOpen = true 30 | } 31 | ) 32 | } 33 | 34 | func getContextMenuButtons() -> [ContextMenuButton] { 35 | var buttons = [ 36 | ContextMenuButton("Copy current URL", systemImage: "doc.on.doc") { 37 | UIPasteboard.general.string = webModel.webView.url?.absoluteString 38 | }, 39 | ContextMenuButton("Add bookmark", systemImage: "plus.circle") { 40 | navModel.currentSheet = .bookmarkEditing 41 | } 42 | ] 43 | 44 | if webModel.findInPageEnabled { 45 | buttons.append( 46 | ContextMenuButton("Find in page", systemImage: "text.magnifyingglass") { 47 | navModel.currentPillView = .findInPage 48 | } 49 | ) 50 | } 51 | 52 | if browserModeEnabled { 53 | buttons.append( 54 | ContextMenuButton("Go to homepage", systemImage: "house") { 55 | webModel.goHome() 56 | } 57 | ) 58 | } 59 | 60 | if UIDevice.current.deviceType == .phone { 61 | buttons.append( 62 | ContextMenuButton("Refresh page", systemImage: "arrow.clockwise") { 63 | webModel.webView.reload() 64 | } 65 | ) 66 | } 67 | 68 | return buttons 69 | } 70 | } 71 | 72 | #if DEBUG 73 | struct LibraryButtonView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | LibraryButtonView() 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/RefreshButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshButtonView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 10/6/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RefreshButtonView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | var body: some View { 14 | Button(action: { 15 | webModel.webView.reload() 16 | webModel.showLoadingProgress = true 17 | }, label: { 18 | Image(systemName: "arrow.clockwise") 19 | .padding(.horizontal, 4) 20 | }) 21 | .keyboardShortcut("r") 22 | } 23 | } 24 | 25 | struct RefreshButtonView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | RefreshButtonView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/SettingsButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsButtonView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsButtonView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | @EnvironmentObject var navModel: NavigationViewModel 13 | 14 | var body: some View { 15 | Button(action: { 16 | navModel.currentSheet = .settings 17 | }, label: { 18 | Image(systemName: "gear") 19 | .padding(.horizontal, 4) 20 | }) 21 | } 22 | } 23 | 24 | #if DEBUG 25 | struct SettingsButtonView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | SettingsButtonView() 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/NavigationBar/UrlBarButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlBarButtonView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 6/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UrlBarButtonView: View { 11 | @EnvironmentObject var navModel: NavigationViewModel 12 | 13 | var body: some View { 14 | Button(action: { 15 | navModel.currentPillView = .urlBar 16 | }, label: { 17 | Image(systemName: "link") 18 | .padding(.horizontal, 4) 19 | }) 20 | .keyboardShortcut("u", modifiers: [.command, .shift]) 21 | } 22 | } 23 | 24 | struct UrlBarButtonView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | UrlBarButtonView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Settings/SettingsAppearanceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsAppearanceView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsAppearanceView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | @AppStorage("leftHandMode") var leftHandMode = false 14 | @AppStorage("useDarkTheme") var useDarkTheme = false 15 | 16 | @AppStorage("followSystemTheme") var followSystemTheme = true 17 | 18 | @AppStorage("navigationAccent") var navigationAccent: Color = .red 19 | @AppStorage("statusBarStyleType") var statusBarStyleType: StatusBarStyleType = .automatic 20 | @AppStorage("statusBarAccent") var statusBarAccent: Color = .clear 21 | 22 | var body: some View { 23 | // MARK: Appearance settings 24 | 25 | // The combination of toggles and a ColorPicker cause keyboard shortcuts to stop working 26 | // Reported this bug to Apple 27 | Section(header: Text("Appearance")) { 28 | Toggle(isOn: $leftHandMode) { 29 | Text("Left handed mode") 30 | } 31 | 32 | Toggle(isOn: $useDarkTheme) { 33 | Text("Use dark theme") 34 | } 35 | .disabledAppearance(followSystemTheme) 36 | 37 | Toggle(isOn: $followSystemTheme) { 38 | Text("Follow system theme") 39 | } 40 | 41 | ColorPicker("Accent color", selection: $navigationAccent, supportsOpacity: false) 42 | .onChange(of: navigationAccent) { _ in 43 | if statusBarStyleType == .accent { 44 | webModel.setStatusbarColor() 45 | } 46 | } 47 | 48 | if UIDevice.current.deviceType != .mac { 49 | NavigationLink( 50 | destination: StatusBarStylePicker(), 51 | label: { 52 | HStack { 53 | Text("Status bar style") 54 | Spacer() 55 | Group { 56 | switch statusBarStyleType { 57 | case .automatic: 58 | Text("Automatic") 59 | case .theme: 60 | Text("Theme") 61 | case .accent: 62 | Text("Accent") 63 | case .custom: 64 | Text("Custom") 65 | } 66 | } 67 | .foregroundColor(.gray) 68 | } 69 | } 70 | ) 71 | .onChange(of: statusBarStyleType) { _ in 72 | webModel.setStatusbarColor() 73 | } 74 | 75 | ColorPicker("Status bar color", selection: $statusBarAccent, supportsOpacity: true) 76 | .onChange(of: statusBarAccent) { _ in 77 | webModel.setStatusbarColor() 78 | } 79 | .disabledAppearance(statusBarStyleType != .custom) 80 | } 81 | } 82 | } 83 | } 84 | 85 | struct SettingsAppearanceView_Previews: PreviewProvider { 86 | static var previews: some View { 87 | SettingsAppearanceView() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Settings/SettingsDownloadsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsDownloadsView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsDownloadsView: View { 11 | @EnvironmentObject var navModel: NavigationViewModel 12 | @EnvironmentObject var downloadManager: DownloadManager 13 | 14 | @AppStorage("overwriteDownloadedFiles") var overwriteDownloadedFiles = true 15 | 16 | @AppStorage("defaultDownloadDirectory") var defaultDownloadDirectory = "" 17 | @AppStorage("downloadDirectoryBookmark") var downloadDirectoryBookmark: Data? 18 | 19 | @State private var showDownloadResetAlert: Bool = false 20 | @State private var backgroundColor: Color = .clear 21 | 22 | var body: some View { 23 | // MARK: Downloads directory (for iDevices) 24 | 25 | if UIDevice.current.deviceType != .mac { 26 | Section( 27 | header: Text("Download options"), 28 | footer: Text("If a downloaded file has the same name as a local file, the local file will be overwritten if the toggle is on.") 29 | ) { 30 | HStack { 31 | Text("Downloads") 32 | 33 | Spacer() 34 | 35 | Group { 36 | Text(defaultDownloadDirectory.isEmpty ? "Downloads" : defaultDownloadDirectory) 37 | Image(systemName: "chevron.right") 38 | } 39 | .foregroundColor(.gray) 40 | } 41 | .lineLimit(0) 42 | .background(backgroundColor) 43 | .contentShape(Rectangle()) 44 | .onTapGesture { 45 | Task { 46 | navModel.currentSheet = nil 47 | 48 | try await Task.sleep(seconds: 0.5) 49 | 50 | downloadManager.showDefaultDirectoryPicker.toggle() 51 | } 52 | } 53 | 54 | Toggle(isOn: $overwriteDownloadedFiles) { 55 | Text("Overwrite files on download") 56 | } 57 | 58 | Button("Reset download directory") { 59 | downloadDirectoryBookmark = nil 60 | defaultDownloadDirectory = "" 61 | 62 | showDownloadResetAlert.toggle() 63 | } 64 | .foregroundColor(.red) 65 | .alert(isPresented: $showDownloadResetAlert) { 66 | Alert( 67 | title: Text("Success"), 68 | message: Text("The downloads directory has been reset to Asobi's documents folder"), 69 | dismissButton: .default(Text("OK")) 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | struct SettingsDownloadsView_Previews: PreviewProvider { 78 | static var previews: some View { 79 | SettingsDownloadsView() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Settings/SettingsPickerViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarBehaviorPicker.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/7/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StatusBarStylePicker: View { 11 | @AppStorage("statusBarStyleType") var statusBarStyleType: StatusBarStyleType = .automatic 12 | 13 | var body: some View { 14 | List { 15 | ForEach(StatusBarStyleType.allCases, id: \.self) { style in 16 | Button { 17 | statusBarStyleType = style 18 | } label: { 19 | HStack { 20 | Text(fetchPickerChoiceName(style)) 21 | Spacer() 22 | if style == statusBarStyleType { 23 | Image(systemName: "checkmark") 24 | .foregroundColor(.blue) 25 | } 26 | } 27 | } 28 | .accentColor(.primary) 29 | } 30 | } 31 | .listStyle(.insetGrouped) 32 | .inlinedList() 33 | .navigationTitle("Status Bar Style") 34 | .navigationBarTitleDisplayMode(.inline) 35 | } 36 | 37 | func fetchPickerChoiceName(_ style: StatusBarStyleType) -> String { 38 | switch style { 39 | case .automatic: 40 | return "Automatic tint" 41 | case .theme: 42 | return "Follow theme" 43 | case .accent: 44 | return "App accent color" 45 | case .custom: 46 | return "Custom color" 47 | } 48 | } 49 | } 50 | 51 | struct StatusBarBehaviorPicker: View { 52 | @AppStorage("statusBarPinType") var statusBarPinType: StatusBarBehaviorType = .partialHide 53 | 54 | var body: some View { 55 | List { 56 | ForEach(StatusBarBehaviorType.allCases, id: \.self) { behavior in 57 | Button { 58 | statusBarPinType = behavior 59 | } label: { 60 | HStack { 61 | Text(fetchPickerChoiceName(behavior)) 62 | Spacer() 63 | if behavior == statusBarPinType { 64 | Image(systemName: "checkmark") 65 | .foregroundColor(.blue) 66 | } 67 | } 68 | } 69 | .accentColor(.primary) 70 | } 71 | } 72 | .listStyle(.insetGrouped) 73 | .inlinedList() 74 | .navigationTitle("Status Bar Behavior") 75 | .navigationBarTitleDisplayMode(.inline) 76 | } 77 | 78 | func fetchPickerChoiceName(_ behavior: StatusBarBehaviorType) -> String { 79 | switch behavior { 80 | case .hide: 81 | return "Hidden" 82 | case .partialHide: 83 | return "Partially hidden" 84 | case .pin: 85 | return "Pinned" 86 | } 87 | } 88 | } 89 | 90 | struct BrowserSearchEnginePicker: View { 91 | @AppStorage("defaultSearchEngine") var defaultSearchEngine: DefaultSearchEngine = .google 92 | @AppStorage("customDefaultSearchEngine") var customSearchEngine = "" 93 | 94 | var body: some View { 95 | List { 96 | ForEach(DefaultSearchEngine.allCases, id: \.self) { engine in 97 | Button { 98 | defaultSearchEngine = engine 99 | } label: { 100 | HStack { 101 | Text(fetchPickerChoiceName(engine)) 102 | Spacer() 103 | if engine == defaultSearchEngine { 104 | Image(systemName: "checkmark") 105 | .foregroundColor(.blue) 106 | } 107 | } 108 | } 109 | .accentColor(.primary) 110 | } 111 | 112 | if defaultSearchEngine == .custom { 113 | Section(header: Text("Custom search engine")) { 114 | TextField("https://domain.com/search?q=%s", text: $customSearchEngine) 115 | .keyboardType(.URL) 116 | .autocorrectionDisabled(true) 117 | .autocapitalization(.none) 118 | } 119 | } 120 | } 121 | .listStyle(.insetGrouped) 122 | .inlinedList() 123 | .navigationTitle("Default Search Engine") 124 | .navigationBarTitleDisplayMode(.inline) 125 | } 126 | 127 | func fetchPickerChoiceName(_ engine: DefaultSearchEngine) -> String { 128 | switch engine { 129 | case .google: 130 | return "Google" 131 | case .brave: 132 | return "Brave" 133 | case .bing: 134 | return "Bing" 135 | case .duckduckgo: 136 | return "DuckDuckGo" 137 | case .startpage: 138 | return "Startpage" 139 | case .custom: 140 | return "Custom" 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Settings/SettingsPrivacyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsPrivacyView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/9/22. 6 | // 7 | 8 | import LocalAuthentication 9 | import SwiftUI 10 | 11 | struct SettingsPrivacyView: View { 12 | @EnvironmentObject var webModel: WebViewModel 13 | @EnvironmentObject var navModel: NavigationViewModel 14 | 15 | @AppStorage("incognitoMode") var incognitoMode = false 16 | @AppStorage("blockAds") var blockAds = false 17 | @AppStorage("blockPopups") var blockPopups = false 18 | @AppStorage("blurInRecents") var blurInRecents = false 19 | @AppStorage("forceSecurityCredentials") var forceSecurityCredentials = false 20 | 21 | @AppStorage("httpsOnlyMode") var httpsOnlyMode = true 22 | 23 | @State private var showAdblockAlert: Bool = false 24 | @State private var alreadyAuthenticated: Bool = false 25 | @State private var presentAlert: Bool = false 26 | 27 | var body: some View { 28 | // MARK: Privacy settings 29 | 30 | Section( 31 | header: Text("Privacy and security"), 32 | footer: Text("The adblocker blocks in-page ads and the popup blocker blocks popups. Make sure to enable what you need.") 33 | ) { 34 | Toggle(isOn: $incognitoMode) { 35 | Text("Incognito mode") 36 | } 37 | 38 | Toggle(isOn: $httpsOnlyMode) { 39 | Text("Https only mode") 40 | } 41 | 42 | Toggle(isOn: $blockAds) { 43 | Text("Block ads") 44 | } 45 | .onChange(of: blockAds) { changed in 46 | if changed { 47 | Task { 48 | await webModel.enableBlocker() 49 | } 50 | } else { 51 | webModel.disableBlocker() 52 | } 53 | 54 | showAdblockAlert.toggle() 55 | } 56 | .alert(isPresented: $showAdblockAlert) { 57 | Alert( 58 | title: Text(blockAds ? "Adblock enabled" : "Adblock disabled"), 59 | message: Text("The page will refresh when you exit settings"), 60 | dismissButton: .cancel(Text("OK")) 61 | ) 62 | } 63 | 64 | Toggle(isOn: $blockPopups) { 65 | Text("Block popups") 66 | } 67 | 68 | if blockPopups { 69 | NavigationLink("Popup exceptions", destination: PopupExceptionView()) 70 | } 71 | 72 | NavigationLink("Allowed URL schemes", destination: AllowedURLSchemeView()) 73 | 74 | if UIDevice.current.deviceType != .mac { 75 | Toggle(isOn: $blurInRecents) { 76 | Text("Blur in recents menu") 77 | } 78 | } 79 | 80 | if navModel.authenticationPresent() { 81 | Toggle(isOn: $forceSecurityCredentials) { 82 | Text("Force authentication") 83 | } 84 | .onChange(of: forceSecurityCredentials) { changed in 85 | // To prevent looping of authentication prompts 86 | if alreadyAuthenticated { 87 | alreadyAuthenticated = false 88 | return 89 | } 90 | 91 | let context = LAContext() 92 | 93 | Task { 94 | do { 95 | let result = try await context.evaluatePolicy( 96 | .deviceOwnerAuthentication, 97 | localizedReason: "Authentication is required to change this setting" 98 | ) 99 | 100 | forceSecurityCredentials = result ? changed : !changed 101 | } catch { 102 | // Ignore and log the error 103 | debugPrint("Settings authentication error!: \(error)") 104 | 105 | await MainActor.run { 106 | alreadyAuthenticated = true 107 | 108 | forceSecurityCredentials = !changed 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | struct SettingsPrivacyView_Previews: PreviewProvider { 119 | static var previews: some View { 120 | SettingsPrivacyView() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Settings/SettingsSyncView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsSyncView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 5/14/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsSyncView: View { 11 | @AppStorage("iCloudEnabled") var iCloudEnabled = false 12 | 13 | @State private var showiCloudAlert = false 14 | 15 | var body: some View { 16 | Section( 17 | header: Text("Sync options"), 18 | footer: Text("iCloud syncing may result in duplicates of history or bookmarks.") 19 | ) { 20 | Toggle(isOn: $iCloudEnabled) { 21 | Text("iCloud sync") 22 | } 23 | .onChange(of: iCloudEnabled) { _ in 24 | showiCloudAlert.toggle() 25 | } 26 | .alert(isPresented: $showiCloudAlert) { 27 | Alert( 28 | title: Text(iCloudEnabled ? "Syncing enabled" : "Syncing disabled"), 29 | message: Text("Changing this setting requires an app restart"), 30 | dismissButton: .cancel(Text("OK")) 31 | ) 32 | } 33 | } 34 | } 35 | } 36 | 37 | struct SettingsSyncView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | SettingsSyncView() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/Settings/SettingsWebsiteView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsWebsiteView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 4/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsWebsiteView: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | @AppStorage("changeUserAgent") var changeUserAgent = false 14 | @AppStorage("loadLastHistory") var loadLastHistory = false 15 | @AppStorage("browserModeEnabled") var browserModeEnabled = false 16 | 17 | @AppStorage("defaultUrl") var defaultUrl = "" 18 | 19 | @AppStorage("defaultSearchEngine") var defaultSearchEngine: DefaultSearchEngine = .google 20 | 21 | @State private var showUrlChangeAlert: Bool = false 22 | @State private var showUrlBarAlert: Bool = false 23 | 24 | var body: some View { 25 | // MARK: Website settings (settings that can alter website content) 26 | 27 | Section(header: Text("Website settings")) { 28 | Toggle(isOn: $changeUserAgent) { 29 | Text("Request \(UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac ? "mobile" : "desktop") website") 30 | } 31 | .onChange(of: changeUserAgent) { changed in 32 | webModel.setUserAgent(changeUserAgent: changed) 33 | webModel.webView.reload() 34 | } 35 | } 36 | 37 | // MARK: Default URL setting 38 | 39 | Section( 40 | header: Text("Default URL"), 41 | footer: VStack(alignment: .leading, spacing: 8) { 42 | Text("Sets the default URL when the app is launched. Https will be automatically added if you don't provide it.") 43 | Text("The load most recent URL option loads the last URL from history on app launch.") 44 | } 45 | ) { 46 | // Auto capitalization modifier will be deprecated at some point 47 | TextField("https://...", text: $defaultUrl, onEditingChanged: { begin in 48 | if !begin, UIDevice.current.deviceType != .mac { 49 | showUrlChangeAlert.toggle() 50 | webModel.loadUrl() 51 | } 52 | }, onCommit: { 53 | if UIDevice.current.deviceType == .mac { 54 | showUrlChangeAlert.toggle() 55 | webModel.loadUrl() 56 | } 57 | }) 58 | .clearButtonMode(.whileEditing) 59 | .textCase(.lowercase) 60 | .disableAutocorrection(true) 61 | .keyboardType(.URL) 62 | .autocapitalization(.none) 63 | .alert(isPresented: $showUrlChangeAlert) { 64 | Alert( 65 | title: Text("The default URL was changed"), 66 | message: Text("Your page should have refreshed to the new URL"), 67 | dismissButton: .cancel(Text("OK!")) 68 | ) 69 | } 70 | .disabledAppearance(loadLastHistory) 71 | 72 | Toggle(isOn: $loadLastHistory) { 73 | Text("Load most recent URL") 74 | } 75 | } 76 | 77 | Section( 78 | header: Text("Browser mode"), 79 | footer: Text("Browser mode changes Asobi to work more like a mobile browser as opposed to a PWA companion.") 80 | ) { 81 | Toggle(isOn: $browserModeEnabled) { 82 | Text("Enable browser mode") 83 | } 84 | .onChange(of: browserModeEnabled) { changed in 85 | if changed { 86 | showUrlBarAlert.toggle() 87 | } 88 | } 89 | .alert(isPresented: $showUrlBarAlert) { 90 | Alert( 91 | title: Text("Browser mode enabled"), 92 | message: Text("The navigation bar should have a link icon now. \n\n" + 93 | "The homepage button is located in the library context menu. \n\n" + 94 | "If this interferes with PWAs, please disable the setting."), 95 | dismissButton: .default(Text("OK")) 96 | ) 97 | } 98 | 99 | if browserModeEnabled { 100 | NavigationLink( 101 | destination: BrowserSearchEnginePicker(), 102 | label: { 103 | HStack { 104 | Text("Search engine") 105 | Spacer() 106 | Group { 107 | switch defaultSearchEngine { 108 | case .google: 109 | Text("Google") 110 | case .brave: 111 | Text("Brave") 112 | case .bing: 113 | Text("Bing") 114 | case .duckduckgo: 115 | Text("DuckDuckGo") 116 | case .startpage: 117 | Text("Startpage") 118 | case .custom: 119 | Text("Custom") 120 | } 121 | } 122 | .foregroundColor(.gray) 123 | } 124 | } 125 | ) 126 | } 127 | } 128 | } 129 | } 130 | 131 | struct SettingsWebsiteView_Previews: PreviewProvider { 132 | static var previews: some View { 133 | SettingsWebsiteView() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/WebAlert/WebAlertPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertPanel.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebAlertPanel: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | var body: some View { 14 | GroupBox { 15 | VStack(spacing: 20) { 16 | Text(webModel.webAlertMessage) 17 | .frame(maxWidth: .infinity, alignment: .leading) 18 | 19 | Button("Close") { 20 | webModel.webAlertAction?() 21 | webModel.presentAlert(nil) 22 | } 23 | .frame(maxWidth: .infinity, alignment: .trailing) 24 | } 25 | } 26 | .cornerRadius(10) 27 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? 350 : 600) 28 | } 29 | } 30 | 31 | struct WebAlertPanel_Previews: PreviewProvider { 32 | static var previews: some View { 33 | WebAlertPanel() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/WebAlert/WebAuthPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebAuthPanel.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebAuthPanel: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | @State private var usernameText: String = "" 14 | @State private var passwordText: String = "" 15 | 16 | var body: some View { 17 | GroupBox { 18 | VStack(spacing: 20) { 19 | Text(webModel.webAlertMessage) 20 | .frame(maxWidth: .infinity, alignment: .leading) 21 | 22 | VStack(spacing: 0) { 23 | TextField("User Name", text: $usernameText) 24 | Divider() 25 | SecureField("Password", text: $passwordText) 26 | } 27 | .textFieldStyle(PaddedTextFieldStyle(isRounded: false)) 28 | .autocapitalization(.none) 29 | .autocorrectionDisabled(true) 30 | .cornerRadius(5) 31 | 32 | HStack(spacing: 30) { 33 | Button("Cancel") { 34 | webModel.webAuthAction?(.cancelAuthenticationChallenge, nil) 35 | webModel.presentAlert(nil) 36 | } 37 | 38 | Button { 39 | let credentials = URLCredential(user: usernameText, password: passwordText, persistence: .forSession) 40 | webModel.webAuthAction?(.useCredential, credentials) 41 | webModel.presentAlert(nil) 42 | } label: { 43 | Text("OK") 44 | .bold() 45 | } 46 | } 47 | .frame(maxWidth: .infinity, alignment: .trailing) 48 | } 49 | } 50 | .cornerRadius(10) 51 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? 350 : 600) 52 | } 53 | } 54 | 55 | struct WebAuthPanel_Previews: PreviewProvider { 56 | static var previews: some View { 57 | WebAuthPanel() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/WebAlert/WebConfirmPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebConfirmPanel'.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebConfirmPanel: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | var body: some View { 14 | GroupBox { 15 | VStack(spacing: 20) { 16 | Text(webModel.webAlertMessage) 17 | .frame(maxWidth: .infinity, alignment: .leading) 18 | 19 | HStack(spacing: 30) { 20 | Button("Cancel") { 21 | webModel.webConfirmAction?(false) 22 | webModel.presentAlert(nil) 23 | } 24 | 25 | Button { 26 | webModel.webConfirmAction?(true) 27 | webModel.presentAlert(nil) 28 | } label: { 29 | Text("OK") 30 | .bold() 31 | } 32 | } 33 | .frame(maxWidth: .infinity, alignment: .trailing) 34 | } 35 | } 36 | .cornerRadius(10) 37 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? 350 : 600) 38 | } 39 | } 40 | 41 | struct WebConfirmPanel_Previews: PreviewProvider { 42 | static var previews: some View { 43 | WebConfirmPanel() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Asobi/Views/ComponentViews/WebAlert/WebPromptPanel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebPromptPanel.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/21/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebPromptPanel: View { 11 | @EnvironmentObject var webModel: WebViewModel 12 | 13 | @State private var inputText: String = "" 14 | 15 | var body: some View { 16 | GroupBox { 17 | VStack(spacing: 20) { 18 | Text(webModel.webAlertMessage) 19 | .frame(maxWidth: .infinity, alignment: .leading) 20 | 21 | TextField("", text: $inputText) 22 | .textFieldStyle(PaddedTextFieldStyle(isRounded: true)) 23 | .autocapitalization(.none) 24 | .autocorrectionDisabled(true) 25 | 26 | HStack(spacing: 30) { 27 | Button("Cancel") { 28 | webModel.webPromptAction?(nil) 29 | webModel.presentAlert(nil) 30 | } 31 | 32 | Button { 33 | webModel.webPromptAction?(inputText) 34 | webModel.presentAlert(nil) 35 | } label: { 36 | Text("OK") 37 | .bold() 38 | } 39 | } 40 | .frame(maxWidth: .infinity, alignment: .trailing) 41 | } 42 | } 43 | .cornerRadius(10) 44 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? 350 : 600) 45 | } 46 | } 47 | 48 | struct WebPromptPanel_Previews: PreviewProvider { 49 | static var previews: some View { 50 | WebPromptPanel() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Asobi/Views/FindInPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FindInPageView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 1/4/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FindInPageView: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | @EnvironmentObject var webModel: WebViewModel 14 | @EnvironmentObject var navModel: NavigationViewModel 15 | 16 | @AppStorage("navigationAccent") var navigationAccent: Color = .red 17 | 18 | var body: some View { 19 | HStack { 20 | TextField( 21 | "Enter query", 22 | text: $webModel.findQuery, 23 | onCommit: { 24 | webModel.executeFindInPage() 25 | } 26 | ) 27 | 28 | if webModel.totalFindResults == 0 { 29 | Text("No results") 30 | .foregroundColor(.gray) 31 | } else if webModel.currentFindResult != -1, webModel.totalFindResults != -1 { 32 | Text("\(webModel.currentFindResult)/\(webModel.totalFindResults)") 33 | .foregroundColor(.gray) 34 | } 35 | 36 | Button(action: { 37 | webModel.executeFindInPage() 38 | }, label: { 39 | Image(systemName: "magnifyingglass") 40 | .padding(.horizontal, 4) 41 | }) 42 | .keyboardShortcut(.defaultAction) 43 | 44 | Button(action: { 45 | webModel.moveFindInPageResult(isIncrementing: false) 46 | }, label: { 47 | Image(systemName: "chevron.up") 48 | .padding(.horizontal, 4) 49 | }) 50 | 51 | Button(action: { 52 | webModel.moveFindInPageResult(isIncrementing: true) 53 | }, label: { 54 | Image(systemName: "chevron.down") 55 | .padding(.horizontal, 4) 56 | }) 57 | 58 | Button(action: { 59 | webModel.resetFindInPage() 60 | navModel.currentPillView = nil 61 | }, label: { 62 | Image(systemName: "xmark") 63 | .padding(.horizontal, 4) 64 | }) 65 | .keyboardShortcut(.cancelAction) 66 | } 67 | .padding(10) 68 | .accentColor(navigationAccent) 69 | .background(colorScheme == .light ? .white : .black) 70 | .cornerRadius(10) 71 | .transition(AnyTransition.move(edge: .bottom)) 72 | .animation(.easeInOut(duration: 0.3)) 73 | .padding(.horizontal, 4) 74 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? .infinity : 700) 75 | } 76 | } 77 | 78 | struct FindInPageView_Previews: PreviewProvider { 79 | static var previews: some View { 80 | FindInPageView() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Asobi/Views/NavigationBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationBarView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/4/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NavigationBarView: View { 11 | @Environment(\.horizontalSizeClass) var horizontalSizeClass 12 | @Environment(\.colorScheme) var colorScheme: ColorScheme 13 | 14 | @AppStorage("leftHandMode") var leftHandMode = false 15 | @AppStorage("browserModeEnabled") var browserModeEnabled = false 16 | 17 | @AppStorage("navigationAccent") var navigationAccent: Color = .red 18 | 19 | var body: some View { 20 | VStack { 21 | // Sets button position depending on hand mode setting 22 | HStack { 23 | if leftHandMode { 24 | ForwardBackButtonView() 25 | Spacer() 26 | SettingsButtonView() 27 | Spacer() 28 | LibraryButtonView() 29 | Spacer() 30 | 31 | if browserModeEnabled { 32 | UrlBarButtonView() 33 | } else { 34 | HomeButtonView() 35 | } 36 | 37 | if UIDevice.current.deviceType != .phone { 38 | Spacer() 39 | RefreshButtonView() 40 | Spacer() 41 | FindInPageButtonView() 42 | } 43 | } else { 44 | if UIDevice.current.deviceType != .phone { 45 | FindInPageButtonView() 46 | Spacer() 47 | RefreshButtonView() 48 | Spacer() 49 | } 50 | 51 | if browserModeEnabled { 52 | UrlBarButtonView() 53 | } else { 54 | HomeButtonView() 55 | } 56 | 57 | Spacer() 58 | LibraryButtonView() 59 | Spacer() 60 | SettingsButtonView() 61 | Spacer() 62 | ForwardBackButtonView() 63 | } 64 | } 65 | .padding() 66 | .accentColor(navigationAccent) 67 | 68 | if UIDevice.current.deviceType == .phone, UIDevice.current.hasNotch { 69 | Spacer() 70 | .frame(height: 20) 71 | } 72 | } 73 | .background(colorScheme == .light ? .white : .black) 74 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? .infinity : 600) 75 | .cornerRadius(UIDevice.current.deviceType == .phone ? 0 : 10) 76 | } 77 | } 78 | 79 | struct NavigationBarView_Previews: PreviewProvider { 80 | static var previews: some View { 81 | NavigationBarView() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Asobi/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 8/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | @EnvironmentObject var navModel: NavigationViewModel 12 | 13 | @AppStorage("useDarkTheme") var useDarkTheme = false 14 | @AppStorage("followSystemTheme") var followSystemTheme = true 15 | @AppStorage("navigationAccent") var navigationAccent: Color = .red 16 | 17 | // Core settings. All prefs saved in UserDefaults 18 | var body: some View { 19 | NavView { 20 | Form { 21 | SettingsAppearanceView() 22 | SettingsBehaviorView() 23 | SettingsPrivacyView() 24 | SettingsDownloadsView() 25 | SettingsWebsiteView() 26 | SettingsSyncView() 27 | 28 | // MARK: App icon picker (iDevices only) 29 | 30 | if UIDevice.current.deviceType != .mac { 31 | Section(header: Text("App Icon")) { 32 | AppIconPickerView() 33 | } 34 | } 35 | 36 | // MARK: Credentials and problems 37 | 38 | Section { 39 | ListRowExternalLinkView(text: "Guides", link: "https://github.com/bdashore3/Asobi/wiki") 40 | 41 | ListRowExternalLinkView(text: "Report issues", link: "https://github.com/bdashore3/Asobi/issues") 42 | 43 | NavigationLink(destination: AboutView()) { 44 | Text("About") 45 | } 46 | } 47 | } 48 | .toggleStyle(SwitchToggleStyle(tint: navigationAccent)) 49 | .navigationBarTitle("Settings") 50 | .toolbar { 51 | ToolbarItem(placement: .navigationBarTrailing) { 52 | Button("Done") { 53 | navModel.currentSheet = nil 54 | } 55 | .keyboardShortcut(.cancelAction) 56 | } 57 | } 58 | } 59 | .blur(radius: navModel.blurRadius) 60 | .applyTheme(followSystemTheme ? nil : (useDarkTheme ? .dark : .light)) 61 | } 62 | } 63 | 64 | #if DEBUG 65 | struct SettingsView_Previews: PreviewProvider { 66 | static var previews: some View { 67 | SettingsView() 68 | } 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /Asobi/Views/UrlBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlBarView.swift 3 | // Asobi 4 | // 5 | // Created by Brian Dashore on 6/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UrlBarView: View { 11 | @Environment(\.colorScheme) var colorScheme 12 | 13 | @EnvironmentObject var webModel: WebViewModel 14 | @EnvironmentObject var navModel: NavigationViewModel 15 | 16 | @AppStorage("useUrlBar") var useUrlBar = false 17 | @AppStorage("navigationAccent") var navigationAccent: Color = .red 18 | 19 | @State private var currentUrl: String = "" 20 | @State private var showCloseButton = true 21 | 22 | var body: some View { 23 | HStack { 24 | TextField( 25 | "https://...", 26 | text: $currentUrl, 27 | onEditingChanged: { changed in 28 | showCloseButton = !changed 29 | }, 30 | onCommit: { 31 | webModel.parseUrlBarQuery(currentUrl) 32 | } 33 | ) 34 | .clearButtonMode(.whileEditing) 35 | .textCase(.lowercase) 36 | .disableAutocorrection(true) 37 | .keyboardType(.URL) 38 | .autocapitalization(.none) 39 | 40 | Button { 41 | webModel.parseUrlBarQuery(currentUrl, forceSearch: true) 42 | } label: { 43 | Image(systemName: "magnifyingglass") 44 | } 45 | 46 | if showCloseButton { 47 | Button { 48 | navModel.currentPillView = nil 49 | } label: { 50 | Image(systemName: "xmark") 51 | .padding(.horizontal, 4) 52 | } 53 | .keyboardShortcut(.cancelAction) 54 | } 55 | } 56 | .onAppear { 57 | currentUrl = webModel.webView.url?.absoluteString ?? "" 58 | } 59 | .onChange(of: webModel.webView.url) { url in 60 | currentUrl = url?.absoluteString ?? "" 61 | } 62 | .padding(10) 63 | .accentColor(navigationAccent) 64 | .background(colorScheme == .light ? .white : .black) 65 | .cornerRadius(10) 66 | .transition(AnyTransition.move(edge: .bottom)) 67 | .animation(.easeInOut(duration: 0.3)) 68 | .padding(.horizontal, 4) 69 | .frame(maxWidth: UIDevice.current.deviceType == .phone ? .infinity : 700) 70 | } 71 | } 72 | 73 | struct UrlBarView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | UrlBarView() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /AsobiTests/AsobiTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsobiTests.swift 3 | // AsobiTests 4 | // 5 | // Created by Brian Dashore on 8/2/21. 6 | // 7 | 8 | import XCTest 9 | @testable import Asobi 10 | 11 | class AsobiTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /AsobiTests/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 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /AsobiUITests/AsobiUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsobiUITests.swift 3 | // AsobiUITests 4 | // 5 | // Created by Brian Dashore on 8/2/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class AsobiUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /AsobiUITests/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 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asobi 2 | 3 | An immersive browser application for iOS, iPadOS, and MacCatalyst. 4 | 5 | ## Download on the App Store for iOS/iPadOS and MacOS 6 | 7 | [https://apps.apple.com/us/app/asobi/id1589812837](https://apps.apple.com/us/app/asobi/id1589812837) 8 | 9 | ## Why did I make this? 10 | 11 | I really like to watch and read various forms of media. However, most web browsers have a TON of clutter surrounding the webpage I want to use. With desktop browsers, you can go full screen and not have to worry. However, mobile browsers don't have that luxury for all websites. 12 | 13 | Some browsers don't even allow you to hide the URL bar! 14 | 15 | Asobi removes all of this clutter by giving you a WebView with a minimalist navigation bar (and swipe gestures). By doing these things, you can have a distraction free browsing experience. 16 | 17 | ## Why not use Safari web clips? 18 | 19 | Well, safari web clips are great. They can easily save/load PWAs in an immersive feeling. However, these snippets don't allow for image saving, proper navigation, and sometimes are annoying to fiddle around with. 20 | 21 | ## Initial Setup 22 | 23 | When you open the app for the first time, you will see DuckDuckGo (or whatever the fallback webpage to load is). This is because you haven't set a default URL to navigate to on app load! 24 | 25 | To set a default URL: 26 | 27 | 1. Open settings 28 | 2. Scroll to the bottom option that says `Default URL` 29 | 3. Enter your favorite URL in the textbox and the site should load immediately, when you press the home button, or when you close/reopen the app 30 | 31 | ## Planned features 32 | 33 | Here are features that I have planned for future releases (these are also in the issues) 34 | 35 | - RPC on mac: Used for Discord to show that you're using Asobi 36 | - Find in page: Adds the ability to search for content in a webpage 37 | 38 | ## Building from source 39 | 40 | If you have Xcode, you can build Asobi from source and run it on your device. 41 | 42 | There are two branches in the repository: 43 | 44 | - default: The stable branch. Is parallel to the App Store version of Asobi. 45 | - next: The development branch. May contain breaking changes and is frequently force pushed to. 46 | 47 | Xcode builds are ran at your own risk and are not guaranteed to get support in the event of an app crash! 48 | 49 | ## Contribution 50 | 51 | If you have issues with the app: 52 | 53 | - Describe the issue in detail 54 | - If you have a feature request, please indicate it as so. Planned features are in a different section of the README, so be sure to read those before submitting. 55 | 56 | If you want to make custom icons for the app: 57 | 58 | - Please join [the discord](https://discord.gg/pswt7by) for more info 59 | 60 | # Developers and Permissions 61 | 62 | I try to make comments/commits as detailed as possible, but if you don't understand something, please contact me via Discord or Twitter! I'm always happy to talk! 63 | 64 | Creator/Developer: Brian Dashore 65 | 66 | Developer Website: [kingbri.dev](https://kingbri.dev) 67 | 68 | Developer Discord: kingbri#6666 69 | 70 | Join the support discord here (get the king-updates role to access the channel): [https://discord.gg/pswt7by](https://discord.gg/pswt7by) 71 | --------------------------------------------------------------------------------