├── .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 |
--------------------------------------------------------------------------------