macOS: Add new introduction

This commit is contained in:
Leon Böttger committed 2025-12-20 12:12:12 +01:00
1 parent 8393a7495f
commit 3b99eda41c
13 files changed
+2291 -72

No files matched your search

+11 -3
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -393,6 +393,10 @@
EBE5FF4A2EDB8D4B0019B380 /* Config.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Config.storekit; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EB7A95582EF67C860021111B /* Introduction */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Introduction; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
699DEB9E2AB0573200115D22 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -657,6 +661,7 @@
EB7F54112D78E95C00989564 /* TutorialView.swift */,
EB7F54132D78E9E400989564 /* LicenseWindow.swift */,
EB869DB42E92AA33009AAAD4 /* QuickDropToastView.swift */,
EB7A95582EF67C860021111B /* Introduction */,
EBA1BB1C2EF1F5650068E46E /* ChangeDeviceNameModifier.swift */,
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */,
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */,
@@ -736,6 +741,9 @@
dependencies = (
699DEBAD2AB0573200115D22 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EB7A95582EF67C860021111B /* Introduction */,
);
name = QuickDrop;
packageProductDependencies = (
EB2F9BFD2DA05F48004010A2 /* BezelNotification */,
@@ -1324,7 +1332,7 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SHARED_BUILD = 97;
SHARED_BUILD = 98;
SHARED_VERSION = 2.9.1;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@@ -1387,7 +1395,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SHARED_BUILD = 97;
SHARED_BUILD = 98;
SHARED_VERSION = 2.9.1;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -52,7 +52,7 @@
"location" : "https://github.com/leonboe1/LUI.git",
"state" : {
"branch" : "main",
"revision" : "81066615c872a1a124d89064c53b93625933b282"
"revision" : "a14bf52e65926e1780dba1f14c076d4267ee7ea3"
}
},
{
+5 -4
View File
@@ -60,7 +60,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
menu.addItem(NSMenuItem.separator())
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openMainWindow), keyEquivalent: "")
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
menu.addItem(userManualItem)
@@ -79,7 +79,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
if !Settings.shared.appLaunchedBefore {
log("[AppDelegate] Opening Welcome Screen")
// open welcome screen
openWelcomeScreen()
openMainWindow(openIntroduction: true)
Settings.shared.appLaunchedBefore = true
// user installed the app after the IAP was implemented, set the user as eligible for IAP
@@ -149,7 +149,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
openWelcomeScreen()
openMainWindow()
statusItem?.isVisible = true
return true
}
@@ -206,7 +206,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
}
@objc func openWelcomeScreen() {
@objc func openMainWindow(openIntroduction: Bool = false) {
NSApp.setActivationPolicy(.regular)
@@ -220,6 +220,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
}
let welcomeView = WelcomeScreen(
openIntroduction: openIntroduction,
openPlusScreen: openPlusScreen,
openAppAdvertisementView: { self.openSheetView(type: .downloadAndroidApp) },
openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) },
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Plain.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,52 @@
{
"images" : [
{
"filename" : "13-Ventura-Light-thumb.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "IMG_0185.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 905 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,234 @@
//
// IntroductionSplashView.swift
// QuickDrop
//
// Created by Leon Böttger on 20.12.25.
//
import SwiftUI
import LUI
struct IntroductionSplashView: View {
@Environment(\.colorScheme) private var colorScheme
@State private var currentPage: IntroductionPage = .splash
let onFinish: (() -> Void)
var body: some View {
GeometryReader { geo in
ZStack {
ZStack {
if colorScheme == .dark {
Color.black.opacity(0.2)
}
else {
Color.white
}
}
.ignoresSafeArea()
VStack() {
// Top Image
let topHeight = geo.size.height * 0.35
ZStack {
Color.gray.opacity(colorScheme == .light ? 0.1 : 0.12)
currentPage.topHeader(with: topHeight)
}
.edgesIgnoringSafeArea(.top)
.frame(height: topHeight)
Spacer()
VStack {
// Headline Text
Text(currentPage.title.localized())
.font(.system(size: 32))
.fontWeight(.semibold)
.multilineTextAlignment(.center)
.minimumScaleFactor(0.5)
.lineLimit(1)
// Subheadline
Text(currentPage.subtitle.localized())
.font(.system(size: 15))
.lineSpacing(5)
.multilineTextAlignment(.center)
.padding(.top)
.minimumScaleFactor(0.5)
.opacity(0.8)
}
.padding(.horizontal, 40)
Spacer()
Spacer()
Divider()
// Buttons
HStack {
if let skipAction = currentPage.skipAction {
Button("introduction_skip") {
if let nextPage = skipAction() {
currentPage = nextPage
}
else {
onFinish()
}
}
.foregroundColor(.gray)
.buttonStyle(.borderless)
}
Spacer()
Button("introduction_continue") {
if let nextPage = currentPage.presentNextPage() {
currentPage = nextPage
}
else {
onFinish()
}
}
.buttonStyle(.bordered)
.controlSize(.large)
.keyboardShortcut(.defaultAction)
.padding(.horizontal)
.padding(.vertical, 10)
}
.padding(.horizontal)
.padding(.bottom, 10)
}
}
}
}
}
enum IntroductionPage: CaseIterable {
case splash
case noWifi
case localNetworkAccess
case enableShareExtension
case finished
var title: String {
switch self {
case .splash:
return "WelcomeToQuickDrop"
case .noWifi:
return "introduction_no_wifi"
case .localNetworkAccess:
return "introduction_local_network_access"
case .enableShareExtension:
return "introduction_enable_share_extension"
case .finished:
return "introduction_finished"
}
}
var subtitle: String {
switch self {
case .splash:
return "QuickDropWelcomeDescription"
case .noWifi:
return "introduction_no_wifi_description"
case .localNetworkAccess:
return "introduction_local_network_access_description"
case .enableShareExtension:
return "introduction_enable_share_extension_description"
case .finished:
return "introduction_finished_description"
}
}
var skipAction: (() -> IntroductionPage?)? {
switch self {
case .splash:
return nil
case .noWifi:
return nil//{presentNextPage()}
case .localNetworkAccess:
return nil
case .enableShareExtension:
return nil//{presentNextPage()}
case .finished:
return nil
}
}
func presentNextPage() -> IntroductionPage? {
switch self {
case .splash:
return NearbyConnectionManager.shared.isConnectedToLocalNetwork ? .localNetworkAccess : .noWifi
case .noWifi:
return .localNetworkAccess
case .localNetworkAccess:
DeviceToDeviceHeuristicScanner().scan(completion: { _ in })
return .enableShareExtension
case .enableShareExtension:
if let url = URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension") {
NSWorkspace.shared.open(url)
}
return .finished
case .finished:
return nil
}
}
func canContinue() -> Bool {
switch self {
case .noWifi:
return NearbyConnectionManager.shared.isConnectedToLocalNetwork
case .enableShareExtension:
return NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension")) != nil
default:
return true
}
}
func topHeader(with height: CGFloat) -> some View {
ZStack {
switch self {
case .splash:
Image(.quickDropMockup)
.resizable()
.scaledToFit()
.padding(.top, height * 0.33)
.padding(.bottom, height * 0.1)
case .noWifi:
imageView(for: "wifi.slash", with: height)
case .localNetworkAccess:
imageView(for: "network", with: height)
case .enableShareExtension:
imageView(for: "square.and.arrow.up", with: height)
case .finished:
imageView(for: "checkmark.seal.fill", with: height)
}
}
.foregroundColor(.blue)
}
private func imageView(for image: String, with height: CGFloat) -> some View {
Image(systemName: image)
.resizable()
.scaledToFit()
.frame(height: height * 0.5)
.padding(.top, height * 0.13)
}
}
#Preview {
IntroductionSplashView{}
.frame(width: 600, height: 500)
}
+1 -1
View File
@@ -99,5 +99,5 @@ struct LargeAppIconView<Content: View>: View {
#Preview {
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
WelcomeScreen(openIntroduction: false, openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
}
+95 -58
View File
@@ -10,6 +10,21 @@ import UniformTypeIdentifiers
struct WelcomeScreen: View {
internal init(
openIntroduction: Bool,
openPlusScreen: @escaping () -> Void,
openAppAdvertisementView: @escaping () -> Void,
openCableTransmissionView: @escaping () -> Void,
checkForNetworkIssues: @escaping () -> Void
) {
self._openedIntroduction = State(initialValue: openIntroduction)
self.openPlusScreen = openPlusScreen
self.openAppAdvertisementView = openAppAdvertisementView
self.openCableTransmissionView = openCableTransmissionView
self.checkForNetworkIssues = checkForNetworkIssues
}
@Environment(\.colorScheme) var colorScheme
let openPlusScreen: () -> Void
@@ -18,74 +33,96 @@ struct WelcomeScreen: View {
let checkForNetworkIssues: () -> Void
@State private var selection: Tabs? = Tabs.receive
@State private var openedIntroduction: Bool
var body: some View {
HStack(spacing: 0) {
List(selection: $selection) {
ForEach(Tabs.allCases.filter({$0 != .settings }), id: \.self) { tab in
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
.frame(height: 30)
ZStack {
if openedIntroduction {
Image(.introductionBackground)
.resizable()
.ignoresSafeArea(.all)
IntroductionSplashView {
openedIntroduction = false
}
Divider()
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
sendLoggingString()
}
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
openPrivacyPolicy()
}
ExternalLinkLabel(label: "AndroidApp", icon: getPhoneIcon()) {
openAppAdvertisementView()
}
ExternalLinkLabel(label: "TransmitUsingCable", icon: getCableIcon()) {
openCableTransmissionView()
}
Divider()
Label(Tabs.settings.title, systemImage: Tabs.settings.systemImage)
.tag(Tabs.settings)
.frame(height: 30)
.background(Color(NSColor.windowBackgroundColor))
.cornerRadius(25)
.padding(.vertical, 60)
.padding(.horizontal, 190)
}
.minimumScaleFactor(0.5)
.frame(width: 220)
.listStyle(SidebarListStyle())
Divider()
.opacity(colorScheme == .light ? 1 : 0.5)
.edgesIgnoringSafeArea(.vertical)
ZStack {
Color.defaultBackground.edgesIgnoringSafeArea(.vertical)
else {
switch selection {
case .receive:
TutorialView(title: "WelcomeToQuickDrop", text: "UserManualDescription", showsLicense: true, openPlus: openPlusScreen)
case .send:
TutorialView(title: "SendFiles", text: "SendFilesDescription", showsLicense: false, openPlus: openPlusScreen)
case .troubleshooting:
TutorialView(title: "Troubleshooting", text: "TroubleshootingDescription", showsLicense: false, openPlus: openPlusScreen)
.onAppear {
checkForNetworkIssues()
HStack(spacing: 0) {
List(selection: $selection) {
ForEach(Tabs.allCases.filter({$0 != .settings }), id: \.self) { tab in
Label(tab.title, systemImage: tab.systemImage)
.tag(tab)
.frame(height: 30)
}
default:
SettingsView()
Divider()
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
sendLoggingString()
}
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
openPrivacyPolicy()
}
ExternalLinkLabel(label: "AndroidApp", icon: getPhoneIcon()) {
openAppAdvertisementView()
}
ExternalLinkLabel(label: "TransmitUsingCable", icon: getCableIcon()) {
openCableTransmissionView()
}
Divider()
Label(Tabs.settings.title, systemImage: Tabs.settings.systemImage)
.tag(Tabs.settings)
.frame(height: 30)
}
.minimumScaleFactor(0.5)
.frame(width: 220)
.listStyle(SidebarListStyle())
Divider()
.opacity(colorScheme == .light ? 1 : 0.5)
.edgesIgnoringSafeArea(.vertical)
ZStack {
Color.defaultBackground.edgesIgnoringSafeArea(.vertical)
switch selection {
case .receive:
TutorialView(title: "ReceiveFiles", text: "UserManualDescription", showsLicense: true, openPlus: openPlusScreen)
case .send:
TutorialView(title: "SendFiles", text: "SendFilesDescription", showsLicense: false, openPlus: openPlusScreen)
case .troubleshooting:
TutorialView(title: "Troubleshooting", text: "TroubleshootingDescription", showsLicense: false, openPlus: openPlusScreen)
.onAppear {
checkForNetworkIssues()
}
default:
SettingsView()
}
}
}
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
handleDrop(providers: providers)
return true
}
}
}
.animation(.easeInOut, value: openedIntroduction)
.frame(width: 1000, height: 600)
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
handleDrop(providers: providers)
return true
}
}
func openPrivacyPolicy() {
@@ -202,5 +239,5 @@ enum Tabs: CaseIterable {
#Preview {
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
WelcomeScreen(openIntroduction: true, openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
}
+7 -5
View File
@@ -194,16 +194,18 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
func finish() {
if let error = error {
#if os(iOS)
errorVibration()
#else
#if os(macOS)
DispatchQueue.main.async {
self.hideQuickDropToast()
}
#endif
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "UnknownDevice".localized(), error: error)
if let name = device.name {
errorVibration()
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error)
}
} else {
#if os(iOS)