Support native macOS from https://invent.kde.org/ruixuantu/kdeconnect-mac
@@ -113,6 +113,8 @@ unused_import:
|
||||
require_explicit_imports: true
|
||||
weak_delegate:
|
||||
severity: error
|
||||
empty_count:
|
||||
severity: warning
|
||||
|
||||
excluded:
|
||||
- KDE Connect/fastlane
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -30,6 +30,11 @@
|
||||
5EA4114627960D0B0044C559 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5EA4114527960D0B0044C559 /* Assets.xcassets */; };
|
||||
5EF3BDAB27995FCE005C2E3A /* iOS14CompatibleTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF3BDAA27995FCE005C2E3A /* iOS14CompatibleTextView.swift */; };
|
||||
5EFA051C279432CA009C91D2 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFA051B279432CA009C91D2 /* SettingsAboutView.swift */; };
|
||||
5EFFF3082D1B6F3000A3EFCA /* InternalBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFFF3032D1B6F3000A3EFCA /* InternalBattery.swift */; };
|
||||
5EFFF3092D1B6F3000A3EFCA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFFF3022D1B6F3000A3EFCA /* AppDelegate.swift */; };
|
||||
5EFFF30A2D1B6F3000A3EFCA /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFFF3062D1B6F3000A3EFCA /* NotificationManager.swift */; };
|
||||
5EFFF30B2D1B6F3000A3EFCA /* InternalBatteryFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFFF3042D1B6F3000A3EFCA /* InternalBatteryFinder.swift */; };
|
||||
5EFFF30C2D1B6F3000A3EFCA /* InternalBatteryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFFF3052D1B6F3000A3EFCA /* InternalBatteryMonitor.swift */; };
|
||||
744FF3AE2C06C5EB001B6234 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 744FF3AD2C06C5EB001B6234 /* DeviceInfo.swift */; };
|
||||
749868B12C070320003B37FA /* LoopbackLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 749868AD2C070320003B37FA /* LoopbackLink.m */; };
|
||||
749868B22C070320003B37FA /* LoopbackLinkProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 749868B02C070320003B37FA /* LoopbackLinkProvider.m */; };
|
||||
@@ -147,6 +152,11 @@
|
||||
5EA4114527960D0B0044C559 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
5EF3BDAA27995FCE005C2E3A /* iOS14CompatibleTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOS14CompatibleTextView.swift; sourceTree = "<group>"; };
|
||||
5EFA051B279432CA009C91D2 /* SettingsAboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = "<group>"; };
|
||||
5EFFF3022D1B6F3000A3EFCA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
5EFFF3032D1B6F3000A3EFCA /* InternalBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalBattery.swift; sourceTree = "<group>"; };
|
||||
5EFFF3042D1B6F3000A3EFCA /* InternalBatteryFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalBatteryFinder.swift; sourceTree = "<group>"; };
|
||||
5EFFF3052D1B6F3000A3EFCA /* InternalBatteryMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalBatteryMonitor.swift; sourceTree = "<group>"; };
|
||||
5EFFF3062D1B6F3000A3EFCA /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
744FF3AD2C06C5EB001B6234 /* DeviceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
|
||||
749868AD2C070320003B37FA /* LoopbackLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoopbackLink.m; sourceTree = "<group>"; };
|
||||
749868AE2C070320003B37FA /* LoopbackLinkProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoopbackLinkProvider.h; sourceTree = "<group>"; };
|
||||
@@ -225,6 +235,12 @@
|
||||
DB8E559D2815961200101059 /* iOS14+FocusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "iOS14+FocusState.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
5E1BB96C2D1B92AC0024F3FF /* Mac */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Mac; sourceTree = "<group>"; };
|
||||
5E6C9AB82D1B807400F48F25 /* Mac */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Mac; sourceTree = "<group>"; };
|
||||
5E997BBF2D1B7E300052D2C9 /* Mac */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Mac; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
A0A04429267BF38700CC21DD /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
@@ -308,6 +324,18 @@
|
||||
path = Texts;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5EFFF3072D1B6F3000A3EFCA /* Mac */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5EFFF3022D1B6F3000A3EFCA /* AppDelegate.swift */,
|
||||
5EFFF3032D1B6F3000A3EFCA /* InternalBattery.swift */,
|
||||
5EFFF3042D1B6F3000A3EFCA /* InternalBatteryFinder.swift */,
|
||||
5EFFF3052D1B6F3000A3EFCA /* InternalBatteryMonitor.swift */,
|
||||
5EFFF3062D1B6F3000A3EFCA /* NotificationManager.swift */,
|
||||
);
|
||||
path = Mac;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
749868AC2C0702DC003B37FA /* LoopbackBackend */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -421,6 +449,7 @@
|
||||
53A0A1DD283ED78700C7C473 /* LocalizedStringKey+Extensions.swift */,
|
||||
D20ABB0A29A4A04E006F277B /* FileTransferItem.swift */,
|
||||
D27D727E29B051D8002C00B7 /* NetworkChangeMonitor.swift */,
|
||||
5EFFF3072D1B6F3000A3EFCA /* Mac */,
|
||||
);
|
||||
path = "Swift Backend";
|
||||
sourceTree = "<group>";
|
||||
@@ -452,6 +481,7 @@
|
||||
A0D8CBCB26EF081D00791D07 /* Top Level */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5E997BBF2D1B7E300052D2C9 /* Mac */,
|
||||
A0A0442F267BF38700CC21DD /* KDE_Connect_App.swift */,
|
||||
A0A04431267BF38700CC21DD /* MainTabView.swift */,
|
||||
);
|
||||
@@ -461,6 +491,7 @@
|
||||
A0D8CBCC26EF082F00791D07 /* Devices */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5E6C9AB82D1B807400F48F25 /* Mac */,
|
||||
A0A0445A267BF40400CC21DD /* DevicesView.swift */,
|
||||
D2A8720D282C95D700DE980E /* DeviceDiscoveryHelp.swift */,
|
||||
A0A0445C267BF41200CC21DD /* DevicesDetailView.swift */,
|
||||
@@ -525,6 +556,7 @@
|
||||
A0D8CBD326EF090800791D07 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5E1BB96C2D1B92AC0024F3FF /* Mac */,
|
||||
A0A0445E267BF41B00CC21DD /* SettingsView.swift */,
|
||||
A0B8727C267BFBC500F0EB72 /* SettingsDeviceNameView.swift */,
|
||||
A0B87284267E73E600F0EB72 /* SettingsChosenThemeView.swift */,
|
||||
@@ -647,6 +679,11 @@
|
||||
dependencies = (
|
||||
D2AB56782988E4B9008A2217 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
5E1BB96C2D1B92AC0024F3FF /* Mac */,
|
||||
5E6C9AB82D1B807400F48F25 /* Mac */,
|
||||
5E997BBF2D1B7E300052D2C9 /* Mac */,
|
||||
);
|
||||
name = "KDE Connect";
|
||||
packageProductDependencies = (
|
||||
1491F15926AC8BD6008C1065 /* OpenSSL */,
|
||||
@@ -798,6 +835,11 @@
|
||||
A0FB449A26BA796400733914 /* Backend.swift in Sources */,
|
||||
A0D76FCE26E6F562009D9B03 /* RemoteInput.swift in Sources */,
|
||||
D20FCC41282F4D5500A6E16B /* Contributors.swift in Sources */,
|
||||
5EFFF3082D1B6F3000A3EFCA /* InternalBattery.swift in Sources */,
|
||||
5EFFF3092D1B6F3000A3EFCA /* AppDelegate.swift in Sources */,
|
||||
5EFFF30A2D1B6F3000A3EFCA /* NotificationManager.swift in Sources */,
|
||||
5EFFF30B2D1B6F3000A3EFCA /* InternalBatteryFinder.swift in Sources */,
|
||||
5EFFF30C2D1B6F3000A3EFCA /* InternalBatteryMonitor.swift in Sources */,
|
||||
D28C94CC27D1CAD2002EBC2D /* OSLogView.swift in Sources */,
|
||||
3D5C169E2A49934A005F423D /* MdnsDiscovery.swift in Sources */,
|
||||
1499A8E82698BFF300FDF493 /* KeychainItemWrapper.m in Sources */,
|
||||
@@ -1030,7 +1072,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"KDE Connect/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 5433B4KXM8;
|
||||
DEVELOPMENT_TEAM = 3ZWV7C62H6;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = "KDE Connect/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
@@ -1038,11 +1080,15 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.kde.kdeconnect;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = asia.turx.kdeconnect;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = NO;
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "KDE Connect/ObjC Backend/Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -1070,10 +1116,14 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.kde.kdeconnect;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = NO;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "KDE Connect/ObjC Backend/Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-Classic-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-Classic-1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-RoundedRectangle-1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Apollo Zhu on 3/4/22.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
@@ -146,3 +148,5 @@ struct NetworkPacketComposer_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "BaseLink.h"
|
||||
#import "NetworkPacket.h"
|
||||
#if !TARGET_OS_OSX
|
||||
#import "UIKit/UIKit.h"
|
||||
#endif
|
||||
//#import "deviceDelegate.h"
|
||||
//#import "BackgroundService.h"
|
||||
@class BaseLink;
|
||||
@@ -74,7 +76,9 @@ typedef NS_ENUM(NSUInteger, PairStatus)
|
||||
// data only and are therefore NOT stored persistently
|
||||
// Remote Input
|
||||
@property(nonatomic) float _cursorSensitivity;
|
||||
#if !TARGET_OS_OSX
|
||||
@property(nonatomic) UIImpactFeedbackStyle hapticStyle;
|
||||
#endif
|
||||
// Presenter
|
||||
@property(nonatomic) float _pointerSensitivity;
|
||||
|
||||
|
||||
@@ -76,7 +76,9 @@ static const NSTimeInterval kPairingTimeout = 30.0;
|
||||
_pluginsEnableStatus = [NSMutableDictionary dictionary];
|
||||
self.deviceDelegate = deviceDelegate;
|
||||
_cursorSensitivity = 3.0;
|
||||
#if !TARGET_OS_OSX
|
||||
_hapticStyle = 0;
|
||||
#endif
|
||||
_pointerSensitivity = 3.0;
|
||||
[self addLink:link];
|
||||
}
|
||||
@@ -501,7 +503,9 @@ static const NSTimeInterval kPairingTimeout = 30.0;
|
||||
[coder encodeInteger:_pairStatus forKey:@"_pairStatus"];
|
||||
[coder encodeObject:_pluginsEnableStatus forKey:@"_pluginsEnableStatus"];
|
||||
[coder encodeFloat:_cursorSensitivity forKey:@"_cursorSensitivity"];
|
||||
#if !TARGET_OS_OSX
|
||||
[coder encodeInteger:_hapticStyle forKey:@"_hapticStyle"];
|
||||
#endif
|
||||
[coder encodeFloat:_pointerSensitivity forKey:@"_pointerSensitivity"];
|
||||
}
|
||||
|
||||
@@ -525,7 +529,9 @@ static const NSTimeInterval kPairingTimeout = 30.0;
|
||||
_pairStatus = [coder decodeIntegerForKey:@"_pairStatus"];
|
||||
_pluginsEnableStatus = (NSMutableDictionary*)[(NSDictionary*)[coder decodeDictionaryWithKeysOfClass:[NSString class] objectsOfClass:[NSNumber class] forKey:@"_pluginsEnableStatus"] mutableCopy];
|
||||
_cursorSensitivity = [coder decodeFloatForKey:@"_cursorSensitivity"];
|
||||
#if !TARGET_OS_OSX
|
||||
_hapticStyle = [coder decodeIntegerForKey:@"_hapticStyle"];
|
||||
#endif
|
||||
_pointerSensitivity = [coder decodeFloatForKey:@"_pointerSensitivity"];
|
||||
|
||||
// To be set later in backgroundServices
|
||||
|
||||
@@ -104,6 +104,9 @@ FOUNDATION_EXPORT NetworkPacketType const NetworkPacketTypeRunCommand;
|
||||
- (NetworkPacket *) initWithType:(NetworkPacketType)type;
|
||||
+ (NetworkPacket *) createIdentityPacketWithTCPPort:(uint16_t)tcpPort;
|
||||
+ (NetworkPacket *) createPairPacket;
|
||||
#if TARGET_OS_OSX
|
||||
+ (NSString *) getMacUUID;
|
||||
#endif
|
||||
|
||||
- (BOOL)bodyHasKey:(nonnull NSString *)key;
|
||||
- (void)setBool:(BOOL)value forKey:(NSString *)key;
|
||||
|
||||
@@ -29,7 +29,12 @@
|
||||
#import "NetworkPacket.h"
|
||||
#import "Device.h"
|
||||
#import "KDE_Connect-Swift.h"
|
||||
#if !TARGET_OS_OSX
|
||||
@import UIKit;
|
||||
#else
|
||||
@import SystemConfiguration;
|
||||
#import <IOKit/IOKitLib.h>
|
||||
#endif
|
||||
|
||||
@import os.log;
|
||||
|
||||
@@ -76,6 +81,20 @@
|
||||
return np;
|
||||
}
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
// https://stackoverflow.com/questions/11113735/how-to-identify-a-mac-system-uniquely
|
||||
+ (NSString *) getMacUUID {
|
||||
io_service_t platformExpert = IOServiceGetMatchingService(kIOMainPortDefault,IOServiceMatching("IOPlatformExpertDevice"));
|
||||
if (!platformExpert) return nil;
|
||||
|
||||
CFTypeRef serialNumberAsCFString = IORegistryEntryCreateCFProperty(platformExpert,CFSTR(kIOPlatformUUIDKey),kCFAllocatorDefault, 0);
|
||||
if (!serialNumberAsCFString) return nil;
|
||||
|
||||
IOObjectRelease(platformExpert);
|
||||
return (__bridge NSString *)(serialNumberAsCFString);
|
||||
}
|
||||
#endif
|
||||
|
||||
//
|
||||
- (BOOL) bodyHasKey:(NSString*)key
|
||||
{
|
||||
|
||||
@@ -373,9 +373,11 @@
|
||||
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
|
||||
{
|
||||
os_log_with_type(logger, self.debugLogLevel, "TCP server: didAcceptNewSocket");
|
||||
#if !TARGET_OS_OSX
|
||||
[newSocket performBlock:^{
|
||||
[newSocket enableBackgroundingOnSocket];
|
||||
}];
|
||||
#endif
|
||||
@synchronized(_pendingSockets) {
|
||||
[_pendingSockets addObject:newSocket];
|
||||
}
|
||||
@@ -397,6 +399,7 @@
|
||||
//create LanLink and inform the background
|
||||
NSUInteger index=[_pendingSockets indexOfObject:sock];
|
||||
NetworkPacket* np=[_pendingNPs objectAtIndex:index];
|
||||
#if !TARGET_OS_OSX
|
||||
NSString* deviceId=[np objectForKey:@"deviceId"];
|
||||
BaseLink *link = self.connectedLinks[deviceId];
|
||||
|
||||
@@ -406,6 +409,7 @@
|
||||
[sock enableBackgroundingOnSocket];
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
NSArray *myCerts = [[NSArray alloc] initWithObjects: (__bridge id)_identity, nil];
|
||||
NSDictionary *tlsSettings = [[NSDictionary alloc] initWithObjectsAndKeys:
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
// TODO: We might be able to do something with the background activities plugin where it sends out its battery status every once in a while??? But maybe iOS will not unfreeze the entire app for us??? I really don't know...background activity is something that we'll have to figure out later on
|
||||
@objc class Battery: NSObject, ObservablePlugin {
|
||||
@@ -24,6 +28,10 @@ import UIKit
|
||||
@objc var remoteIsCharging: Bool = false
|
||||
@Published
|
||||
@objc var remoteThresholdEvent: Int = 0
|
||||
#if os(macOS)
|
||||
var batteryObserver: BatteryObserver? = nil
|
||||
#endif
|
||||
|
||||
private let logger = Logger()
|
||||
|
||||
@objc init(controlDevice: Device) {
|
||||
@@ -35,6 +43,7 @@ import UIKit
|
||||
}
|
||||
|
||||
@objc func startBatteryMonitoring() {
|
||||
#if !os(macOS)
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
|
||||
// Tip: to add an observer with a function/selector in another class that is not self,
|
||||
@@ -42,6 +51,9 @@ import UIKit
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.batteryStateDidChange(notification:)), name: UIDevice.batteryStateDidChangeNotification, object: UIDevice.current)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.batteryLevelDidChange(notification:)), name: UIDevice.batteryLevelDidChangeNotification, object: UIDevice.current)
|
||||
#else
|
||||
self.batteryObserver = BatteryObserver(batteryInfoDidChange)
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc func onDevicePacketReceived(np: NetworkPacket) {
|
||||
@@ -62,9 +74,10 @@ import UIKit
|
||||
}
|
||||
|
||||
@objc func sendBatteryStatusOut() {
|
||||
let np: NetworkPacket = NetworkPacket(type: .battery)
|
||||
#if !os(macOS)
|
||||
let batteryLevel: Int = Int(UIDevice.current.batteryLevel * 100)
|
||||
let batteryStatus = UIDevice.current.batteryState
|
||||
let np: NetworkPacket = NetworkPacket(type: .battery)
|
||||
if (batteryStatus != .unknown) {
|
||||
let batteryThresholdEvent: Int = (batteryLevel < 10) ? 1 : 0
|
||||
np.setInteger(batteryLevel, forKey: "currentCharge")
|
||||
@@ -79,6 +92,26 @@ import UIKit
|
||||
np.setInteger(0, forKey: "thresholdEvent")
|
||||
logger.notice("Battery status reported as unknown, reporting 0 for all values")
|
||||
}
|
||||
#else
|
||||
let internalFinder = InternalFinder()
|
||||
if (internalFinder.batteryPresent) {
|
||||
let internalBattery = internalFinder.getInternalBattery()
|
||||
let batteryLevel = Int(internalBattery?.charge ?? 0)
|
||||
let acPowered: Bool = internalBattery?.acPowered ?? false
|
||||
let batteryThresholdEvent: Int = (batteryLevel < 10) ? 1 : 0
|
||||
np.setInteger(Int(batteryLevel), forKey: "currentCharge")
|
||||
np.setBool((internalBattery?.acPowered ?? false), forKey: "isCharging")
|
||||
np.setInteger(batteryThresholdEvent, forKey: "thresholdEvent")
|
||||
print("Battery status accessed successfully, sending out:")
|
||||
print("BatteryLevel=\(batteryLevel)")
|
||||
print("BatteryisCharging=\(acPowered)")
|
||||
} else {
|
||||
np.setInteger(0, forKey: "currentCharge")
|
||||
np.setBool(false, forKey: "isCharging")
|
||||
np.setInteger(0, forKey: "thresholdEvent")
|
||||
print("Battery status reported as unknown, reporting 0 for all values")
|
||||
}
|
||||
#endif
|
||||
guard let controlDevice = controlDevice else {
|
||||
logger.fault("Sending battery status with leaked instance, \(CFGetRetainCount(self)) references remaining")
|
||||
return
|
||||
@@ -111,7 +144,7 @@ import UIKit
|
||||
}
|
||||
}
|
||||
|
||||
var statusColor: Color? {
|
||||
var statusColor: Color {
|
||||
if remoteThresholdEvent == 1 || remoteChargeLevel < 10 {
|
||||
return .red
|
||||
} else if remoteIsCharging {
|
||||
@@ -119,13 +152,18 @@ import UIKit
|
||||
} else if remoteChargeLevel < 40 {
|
||||
return .yellow
|
||||
} else {
|
||||
return nil
|
||||
#if !os(macOS)
|
||||
return .primary
|
||||
#else
|
||||
return .blue
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for setting up and responding to the device's own events when battery
|
||||
// status changes
|
||||
|
||||
#if !os(macOS)
|
||||
// When the state of the battery changes: plugged, unplugged, full charge, unknown
|
||||
@objc func batteryStateDidChange(notification: Notification) {
|
||||
sendBatteryStatusOut()
|
||||
@@ -135,6 +173,11 @@ import UIKit
|
||||
@objc func batteryLevelDidChange(notification: Notification) {
|
||||
sendBatteryStatusOut()
|
||||
}
|
||||
#else
|
||||
func batteryInfoDidChange(info: BatteryInfo) {
|
||||
sendBatteryStatusOut()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Global functions for Battery handling
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
// Created by Lucas Wang on 2021-09-05.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
@objc class Clipboard: NSObject, Plugin {
|
||||
static var lastLocalClipboardUpdateTimestamp: Int = 0
|
||||
@@ -27,7 +31,11 @@ import UIKit
|
||||
if (np.type == .clipboard || np.type == .clipboardConnect) {
|
||||
if (np.object(forKey: "content") != nil) {
|
||||
if (np.type == .clipboard) {
|
||||
#if !os(macOS)
|
||||
UIPasteboard.general.string = np.object(forKey: "content") as? String
|
||||
#else
|
||||
NSPasteboard.general.setString(np.object(forKey: "content") as? String ?? "", forType: .string)
|
||||
#endif
|
||||
Self.lastLocalClipboardUpdateTimestamp = Int(Date().millisecondsSince1970)
|
||||
logger.debug("Local clipboard synced with remote packet, timestamp updated")
|
||||
} else if (np.type == .clipboardConnect) {
|
||||
@@ -35,7 +43,11 @@ import UIKit
|
||||
if (packetTimeStamp == 0 || packetTimeStamp < Self.lastLocalClipboardUpdateTimestamp) {
|
||||
logger.info("Invalid timestamp from \(np.type.rawValue, privacy: .public), doing nothing")
|
||||
} else {
|
||||
#if !os(macOS)
|
||||
UIPasteboard.general.string = np.object(forKey: "content") as? String
|
||||
#else
|
||||
NSPasteboard.general.setString(np.object(forKey: "content") as? String ?? "", forType: .string)
|
||||
#endif
|
||||
Self.lastLocalClipboardUpdateTimestamp = Int(Date().millisecondsSince1970)
|
||||
logger.debug("Local clipboard synced with remote packet, timestamp updated")
|
||||
}
|
||||
@@ -48,6 +60,7 @@ import UIKit
|
||||
|
||||
// FIXME: unused function
|
||||
func connectClipboardContent() {
|
||||
#if !os(macOS)
|
||||
if let clipboardContent = UIPasteboard.general.string {
|
||||
let np = NetworkPacket(type: .clipboardConnect)
|
||||
np.setObject(clipboardContent, forKey: "content")
|
||||
@@ -56,9 +69,20 @@ import UIKit
|
||||
} else {
|
||||
logger.info("Attempt to connect local clipboard content with remote device returned nil")
|
||||
}
|
||||
#else
|
||||
if let clipboardContent = NSPasteboard.general.string(forType: .string) {
|
||||
let np = NetworkPacket(type: .clipboardConnect)
|
||||
np.setObject(clipboardContent, forKey: "content")
|
||||
np.setInteger(Self.lastLocalClipboardUpdateTimestamp, forKey: "timestamp")
|
||||
controlDevice.send(np, tag: Int(PACKET_TAG_CLIPBOARD))
|
||||
} else {
|
||||
print("Attempt to connect local clipboard content with remote device returned nil")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func sendClipboardContentOut() {
|
||||
#if !os(macOS)
|
||||
if let clipboardContent = UIPasteboard.general.string {
|
||||
let np = NetworkPacket(type: .clipboard)
|
||||
np.setObject(clipboardContent, forKey: "content")
|
||||
@@ -66,5 +90,14 @@ import UIKit
|
||||
} else {
|
||||
logger.info("Attempt to grab and update local clipboard content returned nil")
|
||||
}
|
||||
#else
|
||||
if let clipboardContent = NSPasteboard.general.string(forType: .string) {
|
||||
let np = NetworkPacket(type: .clipboard)
|
||||
np.setObject(clipboardContent, forKey: "content")
|
||||
controlDevice.send(np, tag: Int(PACKET_TAG_CLIPBOARD))
|
||||
} else {
|
||||
print("Attempt to grab and update local clipboard content returned nil")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Lucas Wang on 2021-09-13.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import UIKit.UIDevice
|
||||
|
||||
@@ -275,3 +277,5 @@ struct PresenterView_Previews: PreviewProvider {
|
||||
PresenterView(detailsDeviceId: "Hi")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
@@ -64,3 +66,5 @@ fileprivate struct _KeyboardListenerPlaceholderView: UIViewRepresentable {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Lucas Wang on 2021-09-06.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RemoteInputView: View {
|
||||
@@ -276,3 +278,5 @@ struct MousePadView_Previews: PreviewProvider {
|
||||
RemoteInputView(detailsDeviceId: "HI")
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -27,7 +27,9 @@ struct RunCommandView: View {
|
||||
ForEach(runCommandPlugin.commandEntries) { entry in
|
||||
Button {
|
||||
runCommandPlugin.runCommand(cmdKey: entry.key)
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.success)
|
||||
#endif
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
Text(entry.name)
|
||||
@@ -42,9 +44,11 @@ struct RunCommandView: View {
|
||||
}
|
||||
.environment(\.defaultMinListRowHeight, 50) // TODO: make this dynamic with GeometryReader???
|
||||
.navigationTitle("Run Command")
|
||||
#if !os(macOS)
|
||||
.navigationBarItems(trailing: Button(action: runCommandPlugin.sendSetupPacket) {
|
||||
Image(systemName: "command") // is there a better choice for this? This is a nice reference though I think
|
||||
})
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
import OrderedCollections
|
||||
import Photos
|
||||
|
||||
@@ -79,7 +83,9 @@ extension Notification.Name {
|
||||
guard let payloadPath = np.payloadPath else {
|
||||
logger.fault("File \(filename, privacy: .public) missing actual file contents")
|
||||
// FIXME: show error to UI
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.error)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
Task {
|
||||
@@ -87,7 +93,9 @@ extension Notification.Name {
|
||||
try await save(payloadPath, as: filename, for: np)
|
||||
// connectedDevicesViewModel.showFileReceivedAlert()
|
||||
logger.debug("File \(filename, privacy: .private(mask: .hash)) saved successfully")
|
||||
#if !os(macOS)
|
||||
await notificationHapticsGenerator.notificationOccurred(.success)
|
||||
#endif
|
||||
} catch {
|
||||
logger.fault("File \(filename, privacy: .public) failed to save due to \(error.localizedDescription, privacy: .public)")
|
||||
await MainActor.run {
|
||||
@@ -97,7 +105,9 @@ extension Notification.Name {
|
||||
error: error,
|
||||
countOtherFailedFilesInTheSameTransfer: 0
|
||||
))
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.error)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
@@ -107,18 +117,28 @@ extension Notification.Name {
|
||||
}
|
||||
} else if let sharedText = np._Body["text"] as? String {
|
||||
// Text sharing: copy to clipboard
|
||||
#if !os(macOS)
|
||||
UIPasteboard.general.string = sharedText
|
||||
#else
|
||||
NSPasteboard.general.setString(sharedText, forType: .string)
|
||||
#endif
|
||||
} else if let sharedURLText = np._Body["url"] as? String {
|
||||
// TODO: avoid to handle URL open in the share extension
|
||||
// URL sharing: open it through URL scheme
|
||||
if let sharedURL = URL(string: sharedURLText) {
|
||||
DispatchQueue.main.async {
|
||||
#if !os(macOS)
|
||||
UIApplication.shared.open(sharedURL)
|
||||
#else
|
||||
NSWorkspace.shared.open(sharedURL)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.fault("Nil received when trying to parse filename")
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.error)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +186,9 @@ extension Notification.Name {
|
||||
let filename = url.lastPathComponent
|
||||
guard let fileSize = attributes.fileSize else {
|
||||
logger.fault("Unable to read size of \(filename, privacy: .public), skipping")
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.error)
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -250,7 +272,9 @@ extension Notification.Name {
|
||||
if noConcurrentJobs {
|
||||
// Only 1 concurrent receiving job,
|
||||
// cancellation cancels everything
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.error)
|
||||
#endif
|
||||
self.numFilesReceived = 0
|
||||
self.totalNumOfFilesToReceive = 0
|
||||
} else {
|
||||
@@ -281,7 +305,9 @@ extension Notification.Name {
|
||||
|
||||
if self.currentFilesSending.removeValue(forKey: path) != nil {
|
||||
self.numFilesSuccessfullySent += 1
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.success)
|
||||
#endif
|
||||
self.sendSinglePayload()
|
||||
} else {
|
||||
self.logger.fault("Sent \(np) not currently sending")
|
||||
@@ -310,7 +336,9 @@ extension Notification.Name {
|
||||
} else {
|
||||
logger.fault("Cannot find info for \(np) after failed to send with \(error)")
|
||||
}
|
||||
#if !os(macOS)
|
||||
notificationHapticsGenerator.notificationOccurred(.error)
|
||||
#endif
|
||||
self.resetTransferData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,10 @@ let backgroundService: BackgroundService = {
|
||||
)
|
||||
}()
|
||||
|
||||
#if !os(macOS)
|
||||
// Device motion manager
|
||||
let motionManager: CMMotionManager = CMMotionManager()
|
||||
#endif
|
||||
|
||||
// Given the deviceId, saves/overwrites the device object from _device into _settings by encoding it and then into UserDefaults
|
||||
func saveDeviceToUserDefaults(deviceId: String) {
|
||||
|
||||
@@ -59,6 +59,9 @@ public extension DeviceType {
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
return macDeviceType
|
||||
#else
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .unspecified:
|
||||
return .unknown
|
||||
@@ -80,5 +83,6 @@ public extension DeviceType {
|
||||
@unknown default:
|
||||
return .unknown
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Claudio Cambra on 25/5/22.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -30,3 +32,5 @@ extension UIImpactFeedbackGenerator.FeedbackStyle: CaseIterable {
|
||||
// UIImpactFeedbackGenerator.FeedbackStyle.init(rawValue: Int)
|
||||
|
||||
let notificationHapticsGenerator: UINotificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||
|
||||
#endif
|
||||
|
||||
@@ -58,11 +58,17 @@ class KdeConnectSettings: NSObject, ObservableObject {
|
||||
if let savedUUID = wrapper.object(forKey: kSecValueData as String) as? String, !savedUUID.isEmpty {
|
||||
cachedUuid = savedUUID
|
||||
} else {
|
||||
#if !os(macOS)
|
||||
// identifierForVendor can be nil if called when a device has restarted but not been unlocked yet
|
||||
if let identifierForVendor = UIDevice.current.identifierForVendor {
|
||||
cachedUuid = identifierForVendor.uuidString.replacingOccurrences(of: "-", with: "")
|
||||
wrapper.setObject(cachedUuid, forKey: kSecValueData as String)
|
||||
}
|
||||
#else
|
||||
let identifierForVendor = NetworkPacket.getMacUUID()
|
||||
cachedUuid = identifierForVendor.replacingOccurrences(of: "-", with: "")
|
||||
wrapper.setObject(cachedUuid, forKey: kSecValueData as String)
|
||||
#endif
|
||||
}
|
||||
let logger = OSLog(subsystem: NSStringFromClass(self), category: "UUIDManager")
|
||||
os_log("Get UUID %{mask.hash}@", log: logger, type: .info, cachedUuid ?? "")
|
||||
@@ -127,7 +133,12 @@ class KdeConnectSettings: NSObject, ObservableObject {
|
||||
"savePhotosToPhotosLibrary": !DeviceType.isMac,
|
||||
"saveVideosToPhotosLibrary": !DeviceType.isMac,
|
||||
])
|
||||
self.deviceName = UserDefaults.standard.string(forKey: "deviceName") ?? UIDevice.current.name
|
||||
#if !os(macOS)
|
||||
let fallbackName = UIDevice.current.name
|
||||
#else
|
||||
let fallbackName = Host.current().localizedName ?? "Unknown Hostname"
|
||||
#endif
|
||||
self.deviceName = UserDefaults.standard.string(forKey: "deviceName") ?? fallbackName
|
||||
self.chosenTheme = UserDefaults.standard.string(forKey: "chosenTheme").flatMap(ColorScheme.init)
|
||||
self.directIPs = UserDefaults.standard.stringArray(forKey: "directIPs") ?? []
|
||||
self.disableUdpBroadcastDiscovery = UserDefaults.standard.bool(forKey: "disableUdpBroadcastDiscovery")
|
||||
|
||||
103
KDE Connect/KDE Connect/Swift Backend/Mac/AppDelegate.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/13.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var menu: NSMenu? = nil
|
||||
private var safe: Bool = true
|
||||
|
||||
private var allowedMenus: [String] = ["KDE Connect", "Devices"]
|
||||
|
||||
private var needMenuUpdate: Bool = false {
|
||||
didSet {
|
||||
if self.needMenuUpdate == true {
|
||||
if safe {
|
||||
safe = false
|
||||
self.menu?.items.removeAll(where: { !self.allowedMenus.contains($0.title) })
|
||||
safe = true
|
||||
}
|
||||
self.needMenuUpdate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestMenuUpdate() {
|
||||
if menu?.items.count != self.allowedMenus.count {
|
||||
self.needMenuUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillUpdate(_ notification: Notification) {
|
||||
if let menu = NSApplication.shared.mainMenu {
|
||||
self.menu = menu
|
||||
self.requestMenuUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
let deviceId = userInfo["DEVICE_ID"] as? String ?? nil
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case "PAIR_ACCEPT_ACTION":
|
||||
backgroundService.pairDevice(deviceId)
|
||||
case "PAIR_DECLINE_ACTION":
|
||||
backgroundService.unpairDevice(deviceId)
|
||||
case "FMD_FOUND_ACTION":
|
||||
MainView.updateFindMyPhoneTimer(isRunning: false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
let userInfo = notification.request.content.userInfo
|
||||
let deviceId = userInfo["DEVICE_ID"] as? String ?? nil
|
||||
|
||||
switch notification.request.identifier {
|
||||
case "PAIR_ACCEPT_ACTION":
|
||||
backgroundService.pairDevice(deviceId)
|
||||
case "PAIR_DECLINE_ACTION":
|
||||
backgroundService.unpairDevice(deviceId)
|
||||
case "FMD_FOUND_ACTION":
|
||||
MainView.updateFindMyPhoneTimer(isRunning: false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
completionHandler([.list, .banner, .sound, .badge])
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// InternalBattery.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
// https://stackoverflow.com/questions/57145091/swift-macos-batterylevel-property
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
public class InternalBattery {
|
||||
public var name: String?
|
||||
|
||||
public var timeToFull: Int?
|
||||
public var timeToEmpty: Int?
|
||||
|
||||
public var manufacturer: String?
|
||||
public var manufactureDate: Date?
|
||||
|
||||
public var currentCapacity: Int?
|
||||
public var maxCapacity: Int?
|
||||
public var designCapacity: Int?
|
||||
|
||||
public var cycleCount: Int?
|
||||
public var designCycleCount: Int?
|
||||
|
||||
public var acPowered: Bool?
|
||||
public var isCharging: Bool?
|
||||
public var isCharged: Bool?
|
||||
public var amperage: Int?
|
||||
public var voltage: Double?
|
||||
public var watts: Double?
|
||||
public var temperature: Double?
|
||||
|
||||
public var charge: Double? {
|
||||
get {
|
||||
if let current = self.currentCapacity,
|
||||
let max = self.maxCapacity {
|
||||
return (Double(current) / Double(max)) * 100.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var health: Double? {
|
||||
get {
|
||||
if let design = self.designCapacity,
|
||||
let current = self.maxCapacity {
|
||||
return (Double(current) / Double(design)) * 100.0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var timeLeft: String {
|
||||
get {
|
||||
if let isCharging = self.isCharging {
|
||||
if let minutes = isCharging ? self.timeToFull : self.timeToEmpty {
|
||||
if minutes <= 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
return String(format: "%.2d:%.2d", minutes / 60, minutes % 60)
|
||||
}
|
||||
}
|
||||
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
||||
public var timeRemaining: Int? {
|
||||
get {
|
||||
if let isCharging = self.isCharging {
|
||||
return isCharging ? self.timeToFull : self.timeToEmpty
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// InternalFinder.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
// https://stackoverflow.com/questions/57145091/swift-macos-batterylevel-property
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import Foundation
|
||||
import IOKit.ps
|
||||
|
||||
public class InternalFinder {
|
||||
private var serviceInternal: io_connect_t = 0 // io_object_t
|
||||
private var internalChecked: Bool = false
|
||||
private var hasInternalBattery: Bool = false
|
||||
|
||||
public init() { }
|
||||
|
||||
public var batteryPresent: Bool {
|
||||
get {
|
||||
if !self.internalChecked {
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
|
||||
self.hasInternalBattery = sources.count > 0
|
||||
self.internalChecked = true
|
||||
}
|
||||
|
||||
return self.hasInternalBattery
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func open() {
|
||||
self.serviceInternal = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"))
|
||||
}
|
||||
|
||||
fileprivate func close() {
|
||||
IOServiceClose(self.serviceInternal)
|
||||
IOObjectRelease(self.serviceInternal)
|
||||
|
||||
self.serviceInternal = 0
|
||||
}
|
||||
|
||||
public func getInternalBattery() -> InternalBattery? {
|
||||
self.open()
|
||||
|
||||
if self.serviceInternal == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let battery = self.getBatteryData()
|
||||
|
||||
self.close()
|
||||
|
||||
return battery
|
||||
}
|
||||
|
||||
fileprivate func getBatteryData() -> InternalBattery {
|
||||
let battery = InternalBattery()
|
||||
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
|
||||
for ps in sources {
|
||||
// Fetch the information for a given power source out of our snapshot
|
||||
let info = IOPSGetPowerSourceDescription(snapshot, ps).takeUnretainedValue() as! Dictionary<String, Any>
|
||||
|
||||
// Pull out the name and capacity
|
||||
battery.name = info[kIOPSNameKey] as? String
|
||||
|
||||
battery.timeToEmpty = info[kIOPSTimeToEmptyKey] as? Int
|
||||
battery.timeToFull = info[kIOPSTimeToFullChargeKey] as? Int
|
||||
}
|
||||
|
||||
// Capacities
|
||||
battery.currentCapacity = self.getIntValue("CurrentCapacity" as CFString)
|
||||
battery.maxCapacity = self.getIntValue("MaxCapacity" as CFString)
|
||||
battery.designCapacity = self.getIntValue("DesignCapacity" as CFString)
|
||||
|
||||
// Battery Cycles
|
||||
battery.cycleCount = self.getIntValue("CycleCount" as CFString)
|
||||
battery.designCycleCount = self.getIntValue("DesignCycleCount9C" as CFString)
|
||||
|
||||
// Plug
|
||||
battery.acPowered = self.getBoolValue("ExternalConnected" as CFString)
|
||||
battery.isCharging = self.getBoolValue("IsCharging" as CFString)
|
||||
battery.isCharged = self.getBoolValue("FullyCharged" as CFString)
|
||||
|
||||
// Power
|
||||
battery.amperage = self.getIntValue("Amperage" as CFString)
|
||||
battery.voltage = self.getVoltage()
|
||||
|
||||
// Various
|
||||
battery.temperature = self.getTemperature()
|
||||
|
||||
// Manufaction
|
||||
battery.manufacturer = self.getStringValue("Manufacturer" as CFString)
|
||||
battery.manufactureDate = self.getManufactureDate()
|
||||
|
||||
if let amperage = battery.amperage,
|
||||
let volts = battery.voltage, let isCharging = battery.isCharging {
|
||||
let factor: CGFloat = isCharging ? 1 : -1
|
||||
let watts: CGFloat = (CGFloat(amperage) * CGFloat(volts)) / 1000.0 * factor
|
||||
|
||||
battery.watts = Double(watts)
|
||||
}
|
||||
|
||||
return battery
|
||||
}
|
||||
|
||||
fileprivate func getIntValue(_ identifier: CFString) -> Int? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Int
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getStringValue(_ identifier: CFString) -> String? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? String
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getBoolValue(_ forIdentifier: CFString) -> Bool? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, forIdentifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Bool
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getTemperature() -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, "Temperature" as CFString, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as! Double / 100.0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getDoubleValue(_ identifier: CFString) -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Double
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getVoltage() -> Double? {
|
||||
if let value = getDoubleValue("Voltage" as CFString) {
|
||||
return value / 1000.0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func getManufactureDate() -> Date? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, "ManufactureDate" as CFString, kCFAllocatorDefault, 0) {
|
||||
let date = value.takeRetainedValue() as! Int
|
||||
|
||||
let day = date & 31
|
||||
let month = (date >> 5) & 15
|
||||
let year = ((date >> 9) & 127) + 1980
|
||||
|
||||
var components = DateComponents()
|
||||
components.calendar = Calendar.current
|
||||
components.day = day
|
||||
components.month = month
|
||||
components.year = year
|
||||
|
||||
return components.date
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// InternalBatteryMonitor.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2023/08/29.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import Foundation
|
||||
|
||||
// https://stackoverflow.com/questions/51275093/is-there-a-battery-level-did-change-notification-equivalent-for-kiopscurrentcapa
|
||||
|
||||
import Cocoa
|
||||
import IOKit
|
||||
|
||||
// Swift doesn't support nested protocol(?!)
|
||||
protocol BatteryInfoObserverProtocol: AnyObject {
|
||||
func batteryInfo(didChange info: BatteryInfo)
|
||||
}
|
||||
|
||||
class BatteryInfo {
|
||||
typealias ObserverProtocol = BatteryInfoObserverProtocol
|
||||
struct Observation {
|
||||
weak var observer: ObserverProtocol?
|
||||
}
|
||||
|
||||
static let shared = BatteryInfo()
|
||||
private init() {}
|
||||
|
||||
private var notificationSource: CFRunLoopSource?
|
||||
var observers = [ObjectIdentifier: Observation]()
|
||||
|
||||
private func startNotificationSource() {
|
||||
if notificationSource != nil {
|
||||
stopNotificationSource()
|
||||
}
|
||||
notificationSource = IOPSNotificationCreateRunLoopSource({ _ in
|
||||
BatteryInfo.shared.observers.forEach { (_, value) in
|
||||
value.observer?.batteryInfo(didChange: BatteryInfo.shared)
|
||||
}
|
||||
}, nil).takeRetainedValue() as CFRunLoopSource
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), notificationSource, .defaultMode)
|
||||
}
|
||||
private func stopNotificationSource() {
|
||||
guard let loop = notificationSource else { return }
|
||||
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), loop, .defaultMode)
|
||||
}
|
||||
|
||||
func addObserver(_ observer: ObserverProtocol) {
|
||||
if observers.count == 0 {
|
||||
startNotificationSource()
|
||||
}
|
||||
observers[ObjectIdentifier(observer)] = Observation(observer: observer)
|
||||
}
|
||||
func removeObserver(_ observer: ObserverProtocol) {
|
||||
observers.removeValue(forKey: ObjectIdentifier(observer))
|
||||
if observers.count == 0 {
|
||||
stopNotificationSource()
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for retrieving different properties in the battery description...
|
||||
}
|
||||
|
||||
class BatteryObserver: BatteryInfo.ObserverProtocol {
|
||||
var batteryInfoClosure: (_ info: BatteryInfo) -> ()
|
||||
|
||||
func batteryInfo(didChange info: BatteryInfo) {
|
||||
self.batteryInfoClosure(info)
|
||||
}
|
||||
|
||||
init(_ callback: @escaping (_ info: BatteryInfo) -> ()) {
|
||||
self.batteryInfoClosure = callback
|
||||
BatteryInfo.shared.addObserver(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
BatteryInfo.shared.removeObserver(self)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// NotificationManager.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/13.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
class NotificationManager: ObservableObject {
|
||||
var categories: Set<UNNotificationCategory>
|
||||
|
||||
init() {
|
||||
let acceptAction = UNNotificationAction(identifier: "PAIR_ACCEPT_ACTION", title: "Accept", options: [])
|
||||
let declineAction = UNNotificationAction(identifier: "PAIR_DECLINE_ACTION", title: "Decline", options: [])
|
||||
let foundAction = UNNotificationAction(identifier: "FMD_FOUND_ACTION", title: "Found", options: [])
|
||||
let normalCategory = UNNotificationCategory(identifier: "NORMAL", actions: [], intentIdentifiers: [])
|
||||
let pairRequestCategory = UNNotificationCategory(identifier: "PAIR_REQUEST", actions: [ acceptAction, declineAction ], intentIdentifiers: [], options: .customDismissAction)
|
||||
let findMyDeviceCategory = UNNotificationCategory(identifier: "FIND_MY_DEVICE", actions: [ foundAction ], intentIdentifiers: [], options: .customDismissAction)
|
||||
categories = [ normalCategory, pairRequestCategory, findMyDeviceCategory ]
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.setNotificationCategories(categories)
|
||||
}
|
||||
|
||||
func pairRequestPost(title: String, body: String, deviceId: String) {
|
||||
post(title: title, body: body, userInfo: [ "DEVICE_ID": deviceId ], categoryIdentifier: "PAIR_REQUEST")
|
||||
}
|
||||
|
||||
func post(title: String, body: String, userInfo: [AnyHashable: Any] = [:], categoryIdentifier: String = "NORMAL", interruptionLevel: UNNotificationInterruptionLevel = .timeSensitive) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = body
|
||||
content.userInfo = userInfo
|
||||
content.categoryIdentifier = categoryIdentifier
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
|
||||
|
||||
let uuidString = UUID().uuidString
|
||||
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)
|
||||
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.add(request)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Lucas Wang on 2021-09-06.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@@ -69,3 +71,5 @@ struct TwoFingerTapView: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Apollo Zhu on 4/24/22.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
@@ -109,3 +111,5 @@ fileprivate struct Focuser<Value: Hashable>: ViewModifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Ruixuan Tu on 2022-01-20.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@@ -46,3 +48,5 @@ struct iOS14CompatibleTextView_Previews: PreviewProvider {
|
||||
iOS14CompatibleTextView(NSAttributedString(string: "Preview"))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Lucas Wang on 2021-09-03.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ConfigureDeviceByIPView: View {
|
||||
@@ -82,3 +84,5 @@ struct ConfigureDeviceByIPView: View {
|
||||
// ConfigureDeviceByIPView()
|
||||
// }
|
||||
// }
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Lucas Wang on 2021-06-17.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import MediaPicker
|
||||
@@ -229,3 +231,5 @@ struct DevicesDetailView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Lucas Wang on 2021-06-17.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import AVFoundation
|
||||
@@ -382,3 +384,5 @@ struct DevicesView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// Created by Apollo Zhu on 11/3/22.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import OrderedCollections
|
||||
import struct CocoaAsyncSocket.GCDAsyncSocketError
|
||||
@@ -269,3 +271,5 @@ struct FileTransferStatusSection_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
219
KDE Connect/KDE Connect/Views/Devices/Mac/DeviceItemView.swift
Normal file
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// DeviceIcon.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/11.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import MediaPicker
|
||||
|
||||
struct DeviceItemView: View {
|
||||
let deviceId: String
|
||||
let parent: DevicesView?
|
||||
@Binding var deviceName: String
|
||||
let emoji: String
|
||||
let connState: DevicesView.ConnectionState
|
||||
let mockBatteryLevel: Int?
|
||||
@State var backgroundColor: Color
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(deviceId: String, parent: DevicesView? = nil, deviceName: Binding<String>, emoji: String, connState: DevicesView.ConnectionState, mockBatteryLevel: Int? = nil) {
|
||||
self.deviceId = deviceId
|
||||
self.parent = parent
|
||||
self._deviceName = deviceName
|
||||
self.emoji = emoji
|
||||
self.connState = connState
|
||||
self.mockBatteryLevel = mockBatteryLevel
|
||||
switch (connState) {
|
||||
case .connected:
|
||||
self._backgroundColor = State(initialValue: .green)
|
||||
case .saved:
|
||||
self._backgroundColor = State(initialValue: .gray)
|
||||
case .visible:
|
||||
self._backgroundColor = State(initialValue: .cyan)
|
||||
case .local:
|
||||
self._backgroundColor = State(initialValue: .cyan)
|
||||
}
|
||||
}
|
||||
|
||||
func getBackgroundColor(_ connState: DevicesView.ConnectionState) -> Color {
|
||||
switch (connState) {
|
||||
case .connected:
|
||||
return .green
|
||||
case .saved:
|
||||
return .gray
|
||||
case .visible:
|
||||
return .cyan
|
||||
case .local:
|
||||
return .cyan
|
||||
}
|
||||
}
|
||||
|
||||
func isPluginAvailable(_ plugin: NetworkPacket.`Type`) -> Bool {
|
||||
if let pluginsEnableStatus = backgroundService.devices[deviceId]?.pluginsEnableStatus {
|
||||
if pluginsEnableStatus[plugin] != nil {
|
||||
return (backgroundService.devices[deviceId]?.isPaired() ?? false) && (backgroundService.devices[deviceId]?.isReachable() ?? false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isPaired() -> Bool {
|
||||
backgroundService.devices[self.deviceId]?.isPaired() ?? false
|
||||
}
|
||||
|
||||
func isReachable() -> Bool {
|
||||
backgroundService.devices[self.deviceId]?.isReachable() ?? false
|
||||
}
|
||||
|
||||
@State private var showingPhotosPicker: Bool = false
|
||||
@State private var showingFilePicker: Bool = false
|
||||
@State var chosenFileURLs: [URL] = []
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(parent?.clickedDeviceId == self.deviceId ? Color.accentColor : self.backgroundColor)
|
||||
if self.connState == .connected && self.isPluginAvailable(.batteryRequest) {
|
||||
BatteryStatus(device: backgroundService._devices[self.deviceId]!) { battery in
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(battery.remoteChargeLevel) / 100)
|
||||
.rotation(.degrees(-90))
|
||||
.stroke(battery.statusColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
|
||||
.brightness(colorScheme == .light ? -0.1 : 0.1)
|
||||
Text(String(battery.remoteChargeLevel) + "%")
|
||||
.frame(maxWidth: 64, maxHeight: 64, alignment: .top)
|
||||
.padding(.top, 2)
|
||||
.font(.system(.footnote, design: .rounded).weight(.light))
|
||||
.foregroundColor(.black)
|
||||
}.onAppear {
|
||||
(backgroundService._devices[self.deviceId]!._plugins[.batteryRequest] as! Battery)
|
||||
.sendBatteryStatusOut()
|
||||
}
|
||||
} else if self.mockBatteryLevel != nil {
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(self.mockBatteryLevel!) / 100)
|
||||
.rotation(.degrees(-90))
|
||||
.stroke(.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round))
|
||||
.brightness(colorScheme == .light ? -0.1 : 0.1)
|
||||
Text(String(self.mockBatteryLevel!) + "%")
|
||||
.frame(maxWidth: 64, maxHeight: 64, alignment: .top)
|
||||
.padding(.top, 2)
|
||||
.font(.system(.footnote, design: .rounded).weight(.light))
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
Text(emoji)
|
||||
.font(.system(size: 32))
|
||||
.shadow(radius: 1)
|
||||
}
|
||||
.frame(width: 64, height: 64)
|
||||
HStack {
|
||||
Text(deviceName)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(parent?.clickedDeviceId == self.deviceId ? .white : Color.primary)
|
||||
.padding(.horizontal, 8)
|
||||
}.background(RoundedRectangle(cornerRadius: 8)
|
||||
.fill(parent?.clickedDeviceId == self.deviceId ? .accentColor : Color.blue.opacity(0)))
|
||||
}.onChange(of: self.connState) { newValue in
|
||||
withAnimation {
|
||||
self.backgroundColor = getBackgroundColor(newValue)
|
||||
}
|
||||
}.onTapGesture {
|
||||
parent?.clickedDeviceId = self.deviceId
|
||||
}.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
||||
// Ref: https://stackoverflow.com/questions/60831260/swiftui-drag-and-drop-files
|
||||
if isPluginAvailable(.share) {
|
||||
var droppedFileURLs: [URL] = []
|
||||
providers.forEach { provider in
|
||||
provider.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { (data, error) in
|
||||
if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) {
|
||||
droppedFileURLs.append(url)
|
||||
print("File drppped: ", url)
|
||||
}
|
||||
})
|
||||
}
|
||||
while droppedFileURLs.count != providers.count {
|
||||
continue // block thread until all providers are proceeded
|
||||
}
|
||||
(backgroundService._devices[self.deviceId]!._plugins[.share] as! Share).prepAndInitFileSend(fileURLs: droppedFileURLs)
|
||||
return true
|
||||
} else {
|
||||
self.backgroundColor = .red
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
withAnimation {
|
||||
self.backgroundColor = getBackgroundColor(self.connState)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}.contextMenu {
|
||||
if parent?.clickedDeviceId == self.deviceId {
|
||||
if self.connState == .connected || self.connState == .saved {
|
||||
Button("Unpair") {
|
||||
if self.isPaired() {
|
||||
backgroundService.unpairDevice(self.deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
if self.isReachable() {
|
||||
if self.isPluginAvailable(.ping) {
|
||||
Button("Ping") {
|
||||
(backgroundService.devices[self.deviceId]!.plugins[.ping] as! Ping).sendPing()
|
||||
}
|
||||
}
|
||||
|
||||
if self.isPluginAvailable(.clipboard) {
|
||||
Button("Push Local Clipboard") {
|
||||
(backgroundService.devices[self.deviceId]!.plugins[.clipboard] as! Clipboard).sendClipboardContentOut()
|
||||
}
|
||||
}
|
||||
|
||||
if self.isPluginAvailable(.share) {
|
||||
// TODO: fix media sharing
|
||||
// Button("Send Photos and Videos") {
|
||||
// showingPhotosPicker = true
|
||||
// }
|
||||
Button("Send Files") {
|
||||
showingFilePicker = true
|
||||
}
|
||||
}
|
||||
} else if self.connState == .connected {
|
||||
Button("Plugins if reachable") {}.disabled(true)
|
||||
}
|
||||
} else {
|
||||
Button("Pair") {
|
||||
backgroundService.pairDevice(self.deviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.mediaImporter(isPresented: $showingPhotosPicker, allowedMediaTypes: .all, allowsMultipleSelection: true) { result in
|
||||
if case .success(let chosenMediaURLs) = result, !chosenMediaURLs.isEmpty {
|
||||
(backgroundService._devices[self.deviceId]!._plugins[.share] as! Share).prepAndInitFileSend(fileURLs: chosenMediaURLs)
|
||||
} else {
|
||||
print("Media Picker Result: \(result)")
|
||||
}
|
||||
}.fileImporter(isPresented: $showingFilePicker, allowedContentTypes: allUTTypes, allowsMultipleSelection: true) { result in
|
||||
do {
|
||||
chosenFileURLs = try result.get()
|
||||
} catch {
|
||||
print("Document Picker Error")
|
||||
}
|
||||
if (chosenFileURLs.count > 0) {
|
||||
(backgroundService._devices[self.deviceId]!._plugins[.share] as! Share).prepAndInitFileSend(fileURLs: chosenFileURLs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceIcon_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DeviceItemView(deviceId: "0", parent: nil, deviceName: .constant("TURX's MacBook Pro"), emoji: "\u{1F5A5}", connState: .local)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
154
KDE Connect/KDE Connect/Views/Devices/Mac/MacDevicesView.swift
Normal file
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/11.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct DevicesView: View {
|
||||
var connectedDevicesIds: [String] {
|
||||
viewModel.connectedDevices.keys.sorted()
|
||||
}
|
||||
|
||||
var visibleDevicesIds: [String] {
|
||||
viewModel.visibleDevices.keys.sorted()
|
||||
}
|
||||
|
||||
var savedDevicesIds: [String] {
|
||||
viewModel.savedDevices.keys.sorted()
|
||||
}
|
||||
|
||||
@ObservedObject var viewModel: ConnectedDevicesViewModel = connectedDevicesViewModel
|
||||
|
||||
@State public var clickedDeviceId: String
|
||||
@State private var counter: Int
|
||||
|
||||
enum GenMode {
|
||||
case normal
|
||||
case empty
|
||||
case demo
|
||||
case hundred
|
||||
}
|
||||
let genMode: GenMode
|
||||
|
||||
enum ConnectionState {
|
||||
case connected
|
||||
case saved
|
||||
case visible
|
||||
case local
|
||||
}
|
||||
|
||||
init(genMode: GenMode = .normal) {
|
||||
self.clickedDeviceId = "-1"
|
||||
self.counter = 0
|
||||
self.genMode = genMode
|
||||
}
|
||||
|
||||
static func getEmojiFromDeviceType(_ deviceType: DeviceType) -> String {
|
||||
switch (deviceType) {
|
||||
case .desktop:
|
||||
return "\u{1F5A5}"
|
||||
case .laptop:
|
||||
return "\u{1F4BB}"
|
||||
case .phone:
|
||||
return "\u{1F4F1}"
|
||||
case .tablet:
|
||||
return "\u{1F3AC}"
|
||||
case .appletv:
|
||||
return "\u{1F4FA}"
|
||||
case .unknown:
|
||||
return "\u{2754}"
|
||||
@unknown default:
|
||||
return "\u{2753}"
|
||||
}
|
||||
}
|
||||
|
||||
func getDeviceIcons() -> [DeviceItemView] {
|
||||
switch (self.genMode) {
|
||||
case .empty:
|
||||
return []
|
||||
case .demo:
|
||||
return [
|
||||
DeviceItemView(deviceId: "1", parent: self, deviceName: .constant("My iPhone"), emoji: DevicesView.getEmojiFromDeviceType(.phone), connState: .connected, mockBatteryLevel: 67),
|
||||
DeviceItemView(deviceId: "2", parent: self, deviceName: .constant("My iMac"), emoji: DevicesView.getEmojiFromDeviceType(.desktop), connState: .connected),
|
||||
DeviceItemView(deviceId: "3", parent: self, deviceName: .constant("My MacBook"), emoji: DevicesView.getEmojiFromDeviceType(.laptop), connState: .saved),
|
||||
DeviceItemView(deviceId: "4", parent: self, deviceName: .constant("My iPad"), emoji: DevicesView.getEmojiFromDeviceType(.tablet), connState: .visible),
|
||||
DeviceItemView(deviceId: "5", parent:self, deviceName: .constant("My Apple TV"), emoji: DevicesView.getEmojiFromDeviceType(.appletv), connState: .visible),
|
||||
DeviceItemView(deviceId: "6", deviceName: .constant("Unknown device"), emoji: DevicesView.getEmojiFromDeviceType(.unknown), connState: .visible)
|
||||
]
|
||||
case .hundred:
|
||||
var deviceIcons = [DeviceItemView]()
|
||||
for demoDeviceId in 1...100 {
|
||||
deviceIcons.append(DeviceItemView(deviceId: String(demoDeviceId), parent: self, deviceName: .constant(String(demoDeviceId)), emoji: "\u{1F4F1}", connState: .saved))
|
||||
}
|
||||
return deviceIcons
|
||||
case .normal:
|
||||
var deviceIcons = [DeviceItemView]()
|
||||
for key in connectedDevicesIds {
|
||||
deviceIcons.append(DeviceItemView(
|
||||
deviceId: key,
|
||||
parent: self,
|
||||
deviceName: .constant(viewModel.connectedDevices[key] ?? "Unknown device"),
|
||||
emoji: DevicesView.getEmojiFromDeviceType(backgroundService._devices[key]?._deviceInfo.type ?? .unknown),
|
||||
connState: .connected
|
||||
))
|
||||
}
|
||||
for key in savedDevicesIds {
|
||||
deviceIcons.append(DeviceItemView(
|
||||
deviceId: key,
|
||||
parent: self,
|
||||
deviceName: .constant(viewModel.savedDevices[key] ?? "Unknown device"),
|
||||
emoji: DevicesView.getEmojiFromDeviceType(backgroundService._devices[key]?._deviceInfo.type ?? .unknown),
|
||||
connState: .saved
|
||||
))
|
||||
}
|
||||
for key in visibleDevicesIds {
|
||||
deviceIcons.append(DeviceItemView(
|
||||
deviceId: key,
|
||||
parent: self,
|
||||
deviceName: .constant(viewModel.visibleDevices[key] ?? "Unknown device"),
|
||||
emoji: DevicesView.getEmojiFromDeviceType(backgroundService._devices[key]?._deviceInfo.type ?? .unknown),
|
||||
connState: .visible
|
||||
))
|
||||
}
|
||||
return deviceIcons
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if getDeviceIcons().isEmpty {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text("No device discovered in the current network.")
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
ScrollView(.vertical) {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 128))]) {
|
||||
ForEach(getDeviceIcons(), id: \.deviceId) {deviceIcon in
|
||||
deviceIcon
|
||||
.padding(.all)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.all)
|
||||
.onTapGesture {
|
||||
self.clickedDeviceId = "-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DevicesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DevicesView(genMode: .demo)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Apollo Zhu on 4/29/23.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct FileTransferStatusOverview: View {
|
||||
@@ -143,3 +145,5 @@ struct FileTransferStatusOverview_Previews: PreviewProvider {
|
||||
.environmentObject(connectedDevicesViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// Created by Apollo Zhu on 4/12/23.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import Photos
|
||||
|
||||
@@ -191,3 +193,5 @@ struct FilesTab_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// Created by Apollo Zhu on 4/12/23.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OpenReceivedDocumentsFolderButton: View {
|
||||
@@ -72,3 +74,5 @@ struct OpenDocumentsFolderInFilesButton_Previews: PreviewProvider {
|
||||
OpenReceivedDocumentsFolderButton()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Created by Apollo Zhu on 2/24/22.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum AppIcon: RawRepresentable, CaseIterable {
|
||||
@@ -109,3 +111,5 @@ struct AppIconPicker_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// AdvancedSettingsView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AdvancedSettingsView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Button {
|
||||
backgroundService.stopDiscovery()
|
||||
CertificateService.shared.deleteAllItemsFromKeychain()
|
||||
UserDefaults.standard.removeObject(forKey: "savedDevices")
|
||||
exit(0)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "delete.right")
|
||||
VStack(alignment: .leading) {
|
||||
Text("Erase saved devices cache")
|
||||
.font(.headline)
|
||||
Text("If there are phantom devices or something just doesn't behave correctly, erasing all saved devices data might fix it. Requires the app to be fully restarted.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
}.buttonStyle(.plain)
|
||||
Spacer()
|
||||
Button {
|
||||
backgroundService.stopDiscovery()
|
||||
CertificateService.shared.deleteHostCertificateFromKeychain()
|
||||
exit(0)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "delete.right")
|
||||
VStack(alignment: .leading) {
|
||||
Text("Delete host certificate")
|
||||
.font(.headline)
|
||||
Text("Delete the host's certificate and re-generate it upon restart. Requires the app to be fully restarted. NOTE: this will make the device unable to connect with previously connected devices. You must unpair this device from the other remote devices and pair them again.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
}.buttonStyle(.plain)
|
||||
Spacer()
|
||||
}.padding(.all)
|
||||
}
|
||||
}
|
||||
|
||||
struct AdvancedSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AdvancedSettingsView()
|
||||
}
|
||||
}
|
||||
112
KDE Connect/KDE Connect/Views/Settings/Mac/AppSettingsView.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// AppSettingsView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum AppIcon: RawRepresentable, CaseIterable {
|
||||
case `default`
|
||||
case classic
|
||||
case roundedRectangle
|
||||
|
||||
init?(rawValue: String?) {
|
||||
switch rawValue {
|
||||
case "Mac-AppIcon-Classic":
|
||||
self = .classic
|
||||
case "Mac-AppIcon-RoundedRectangle":
|
||||
self = .roundedRectangle
|
||||
default:
|
||||
self = .default
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: String? {
|
||||
switch self {
|
||||
case .default:
|
||||
return "Mac-AppIcon"
|
||||
case .classic:
|
||||
return "Mac-AppIcon-Classic"
|
||||
case .roundedRectangle:
|
||||
return "Mac-AppIcon-RoundedRectangle"
|
||||
}
|
||||
}
|
||||
|
||||
var name: Text {
|
||||
switch self {
|
||||
case .default:
|
||||
return Text("Default")
|
||||
case .classic:
|
||||
return Text("Classic")
|
||||
case .roundedRectangle:
|
||||
return Text("Rounded Rectangle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppSettingsView: View {
|
||||
@EnvironmentObject var settings: KdeConnectSettings
|
||||
@Binding var chosenTheme: ColorScheme?
|
||||
private let themes: [ColorScheme?] = [nil, .light, .dark]
|
||||
@Binding var appIcon: AppIcon
|
||||
@State private var appIconName: String
|
||||
|
||||
init(chosenTheme: Binding<ColorScheme?>, appIcon: Binding<AppIcon>) {
|
||||
self._chosenTheme = chosenTheme
|
||||
self._appIcon = appIcon
|
||||
self.appIconName = appIcon.wrappedValue.rawValue ?? "AppIcon"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Picker(selection: $chosenTheme, label: Text("App Theme:")) {
|
||||
ForEach(themes, id: \.self) { theme in
|
||||
switch theme {
|
||||
case .light:
|
||||
Text("Light")
|
||||
case .dark:
|
||||
Text("Dark")
|
||||
default:
|
||||
Text("Default")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
.horizontalRadioGroupLayout()
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Picker(selection: $appIconName, label: Text("App Icon:")) {
|
||||
Text("Default").tag("Mac-AppIcon")
|
||||
Text("Classic").tag("Mac-AppIcon-Classic")
|
||||
Text("Rounded Rectangle").tag("Mac-AppIcon-RoundedRectangle")
|
||||
}
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
.horizontalRadioGroupLayout()
|
||||
.onChange(of: appIconName) { iconName in
|
||||
settings.appIcon = AppIcon(rawValue: (iconName == "Mac-AppIcon" ? nil : iconName))!
|
||||
NSApplication.shared.applicationIconImage = NSImage(named: appIconName)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Text("Preview: ")
|
||||
Image(nsImage: NSImage(named: appIconName)!)
|
||||
Spacer()
|
||||
}
|
||||
}.padding(.all)
|
||||
}
|
||||
}
|
||||
|
||||
struct AppSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AppSettingsView(chosenTheme: .constant(nil), appIcon: .constant(AppIcon(rawValue: nil)!))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// DeviceSettingsView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DeviceSettingsView: View {
|
||||
@Binding var deviceName: String
|
||||
// @State private var deviceType: String = "Laptop"
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("Device Name:")
|
||||
TextField("", text: self.$deviceName)
|
||||
.onChange(of: self.deviceName) { _ in
|
||||
MainView.mainViewSingleton?.refreshDiscoveryAndList()
|
||||
}
|
||||
}
|
||||
// HStack {
|
||||
// Picker(selection: $deviceType, label: Text("Device Type:")) {
|
||||
// Text("Laptop").tag("Laptop")
|
||||
// Text("PC").tag("PC")
|
||||
// }
|
||||
// .pickerStyle(RadioGroupPickerStyle())
|
||||
// .horizontalRadioGroupLayout()
|
||||
// Spacer()
|
||||
// }
|
||||
}.padding(.all)
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DeviceSettingsView(deviceName: .constant("My Laptop"))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// PrefPane.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject private var kdeConnectSettings: KdeConnectSettings = KdeConnectSettings.shared
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
DeviceSettingsView(deviceName: $kdeConnectSettings.deviceName)
|
||||
.tabItem {
|
||||
Label("Device", systemImage: "display")
|
||||
}
|
||||
PeerSettingsView(directIPs: $kdeConnectSettings.directIPs)
|
||||
.tabItem {
|
||||
Label("Peer", systemImage: "laptopcomputer.and.iphone")
|
||||
}
|
||||
AppSettingsView(chosenTheme: $kdeConnectSettings.chosenTheme, appIcon: $kdeConnectSettings.appIcon)
|
||||
.tabItem {
|
||||
Label("Application", systemImage: "app")
|
||||
}
|
||||
AdvancedSettingsView()
|
||||
.tabItem {
|
||||
Label("Advanced", systemImage: "wrench.and.screwdriver")
|
||||
}
|
||||
}
|
||||
.frame(width: 450, height: 250)
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// PeerSettingsView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
//struct EditButtonStyle: ButtonStyle {
|
||||
// func makeBody(configuration: Configuration) -> some View {
|
||||
// HStack {
|
||||
// configuration.label
|
||||
// .padding(.horizontal, 4)
|
||||
// }
|
||||
// .background(.gray)
|
||||
// .foregroundColor(.white)
|
||||
// .border(.black)
|
||||
// .clipShape(Rectangle())
|
||||
// }
|
||||
//}
|
||||
|
||||
struct PeerSettingsView: View {
|
||||
@Binding var directIPs: [String]
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State var selectedIndex = -1
|
||||
@State var editingIndex = -1
|
||||
|
||||
var peerList: some View {
|
||||
ForEach(self.$directIPs.indices, id:\.self) { ind in
|
||||
HStack {
|
||||
if self.selectedIndex == ind && self.editingIndex == ind {
|
||||
TextField("", text: self.$directIPs[ind])
|
||||
.padding(.horizontal, 4)
|
||||
.background(.background)
|
||||
.foregroundColor(.primary)
|
||||
.onSubmit {
|
||||
self.editingIndex = -1
|
||||
}
|
||||
} else {
|
||||
Text(self.directIPs[ind])
|
||||
.padding(.horizontal, 4)
|
||||
.foregroundColor(self.selectedIndex == ind ? .white : .black)
|
||||
.onTapGesture {
|
||||
if self.selectedIndex == ind {
|
||||
self.editingIndex = ind
|
||||
} else {
|
||||
self.selectedIndex = ind
|
||||
self.editingIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.background(self.selectedIndex == ind ? (self.colorScheme == .light ? .blue : .orange) : .white)
|
||||
.frame(alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
var mainFrame: some View {
|
||||
HStack {
|
||||
if self.directIPs.count > 0 {
|
||||
ScrollView(showsIndicators: true) {
|
||||
peerList
|
||||
}.background(.white)
|
||||
.onTapGesture {
|
||||
self.selectedIndex = -1
|
||||
self.editingIndex = -1
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
Rectangle()
|
||||
.fill(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("Configure Devices by IP:")
|
||||
Spacer()
|
||||
}
|
||||
if colorScheme == .light {
|
||||
mainFrame
|
||||
}
|
||||
else {
|
||||
mainFrame.colorInvert()
|
||||
}
|
||||
HStack {
|
||||
Button("+") {
|
||||
self.directIPs.append("IP")
|
||||
}
|
||||
Button("-") {
|
||||
if self.selectedIndex != -1 {
|
||||
self.directIPs.remove(at: self.selectedIndex)
|
||||
self.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.all)
|
||||
}
|
||||
}
|
||||
|
||||
struct PeerSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PeerSettingsView(directIPs: .constant([ "127.0.0.1", "192.168.1.1" ]))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Ruixuan Tu on 2022-01-16.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@@ -217,3 +219,5 @@ struct SettingsAboutView_Previews: PreviewProvider {
|
||||
.environmentObject(KdeConnectSettings.shared)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Lucas Wang on 2021-09-12.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsAdvancedView: View {
|
||||
@@ -134,3 +136,5 @@ struct SettingsAdvancedView: View {
|
||||
// SettingsAdvancedView()
|
||||
// }
|
||||
// }
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Lucas Wang on 2021-06-19.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsChosenThemeView: View {
|
||||
@@ -28,3 +30,5 @@ struct SettingsChosenThemeView_Previews: PreviewProvider {
|
||||
SettingsChosenThemeView(chosenTheme: .constant(nil))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Lucas Wang on 2021-06-17.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@@ -92,3 +94,5 @@ struct SettingsView: View {
|
||||
// SettingsView()
|
||||
// }
|
||||
// }
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,16 +12,39 @@
|
||||
// Created by Lucas Wang on 2021-06-17.
|
||||
//
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
import UserNotifications
|
||||
#endif
|
||||
import SwiftUI
|
||||
|
||||
// Intentional naming
|
||||
// swiftlint:disable:next type_name
|
||||
@main struct KDE_Connect_App: App {
|
||||
@ObservedObject var kdeConnectSettingsForTopLevel: KdeConnectSettings = .shared
|
||||
#if !os(macOS)
|
||||
@StateObject var alertManager: AlertManager = AlertManager()
|
||||
#else
|
||||
@StateObject var notificationManager: NotificationManager = NotificationManager()
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@State var disabledByNotGrantedNotificationPermission: Bool = false
|
||||
@State var showingHelpWindow: Bool = false
|
||||
|
||||
func requestNotification() {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||
if error != nil {
|
||||
self.disabledByNotGrantedNotificationPermission = true
|
||||
} else {
|
||||
self.disabledByNotGrantedNotificationPermission = false
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var body: some Scene {
|
||||
#if !os(macOS)
|
||||
WindowGroup {
|
||||
MainTabView()
|
||||
.preferredColorScheme(kdeConnectSettingsForTopLevel.chosenTheme)
|
||||
@@ -63,5 +86,69 @@ import SwiftUI
|
||||
message: alertManager.currentAlert.content
|
||||
)
|
||||
}
|
||||
#else
|
||||
WindowGroup("Connect", id: "connect") {
|
||||
if !self.disabledByNotGrantedNotificationPermission {
|
||||
MainView(showingHelpWindow: self.$showingHelpWindow)
|
||||
.preferredColorScheme(kdeConnectSettingsForTopLevel.chosenTheme)
|
||||
.onAppear {
|
||||
NSApplication.shared.applicationIconImage = NSImage(named: (kdeConnectSettingsForTopLevel.appIcon.rawValue ?? "AppIcon"))
|
||||
requestNotification()
|
||||
backgroundService.startDiscovery()
|
||||
requestBatteryStatusAllDevices()
|
||||
}
|
||||
.environmentObject(notificationManager)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.willUpdateNotification)) { _ in
|
||||
requestNotification()
|
||||
}
|
||||
} else {
|
||||
AskNotificationView()
|
||||
.preferredColorScheme(kdeConnectSettingsForTopLevel.chosenTheme)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.willUpdateNotification)) { _ in
|
||||
requestNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
.commands {
|
||||
CommandMenu("Devices") {
|
||||
if !self.disabledByNotGrantedNotificationPermission {
|
||||
Button("Refresh Discovery") {
|
||||
MainView.mainViewSingleton?.refreshDiscoveryAndList()
|
||||
}
|
||||
} else {
|
||||
Label("Refresh Discovery", systemImage: "")
|
||||
}
|
||||
Button("Show Received Files in Finder") {
|
||||
let fileManager = FileManager.default
|
||||
do {
|
||||
// see Share plugin
|
||||
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
NSWorkspace.shared.open(documentDirectory)
|
||||
} catch {
|
||||
print("Error showing received files in Finder \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
|
||||
WindowGroup("Help", id: "help") {
|
||||
HelpView(showingHelpWindow: self.$showingHelpWindow)
|
||||
.preferredColorScheme(kdeConnectSettingsForTopLevel.chosenTheme)
|
||||
}
|
||||
.windowStyle(.hiddenTitleBar)
|
||||
|
||||
Settings {
|
||||
if !self.disabledByNotGrantedNotificationPermission {
|
||||
SettingsView()
|
||||
.preferredColorScheme(kdeConnectSettingsForTopLevel.chosenTheme)
|
||||
.environmentObject(kdeConnectSettingsForTopLevel)
|
||||
} else {
|
||||
AskNotificationView()
|
||||
.preferredColorScheme(kdeConnectSettingsForTopLevel.chosenTheme)
|
||||
.frame(width: 450, height: 250)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// AskNotificationView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/12.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AskNotificationView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Please grant notification permission in order to run KDE Connect.")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.all)
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AskNotificationView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AskNotificationView()
|
||||
}
|
||||
}
|
||||
34
KDE Connect/KDE Connect/Views/Top Level/Mac/ErrorView.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// ErrorView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2023/08/31.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ErrorView: View {
|
||||
let reason: String
|
||||
|
||||
init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text("Error Occured").font(.largeTitle)
|
||||
Spacer()
|
||||
Text("Reason: " + self.reason)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.pink)
|
||||
}
|
||||
}
|
||||
|
||||
struct ErrorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ErrorView("Preview")
|
||||
}
|
||||
}
|
||||
15
KDE Connect/KDE Connect/Views/Top Level/Mac/HelpManual.md
Normal file
@@ -0,0 +1,15 @@
|
||||
There are three states shown as device icon:
|
||||
|
||||
- **Connected** (green): paired and reachable,
|
||||
- **Saved** (gray): paired but not reachable, and
|
||||
- **Visible** (cyan): unpaired but reachable.
|
||||
|
||||
There are some actions you can do with the devices on the main window:
|
||||
|
||||
- **Select and right-click** to pair/unpair and interact with connected devices;
|
||||
- **Drag-and-drop** file to a connected device to send; otherwise the icon will turn red if the device is not reachable.
|
||||
- **Battery information** will be displayed on icons of eligible devices as an arc:
|
||||
- **Blue** is for battery consuming and having abundant remaining charge;
|
||||
- **Yellow** is for battery consuming and having < 40% charge;
|
||||
- **Red** is for battery consuming and having < 10% charge or reached a threshold; and
|
||||
- **Green** is for battery charging.
|
||||
76
KDE Connect/KDE Connect/Views/Top Level/Mac/HelpView.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// HelpView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2023/08/31.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LeadingText: View {
|
||||
let text: String
|
||||
|
||||
init(_ text: String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(self.text).frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct HelpView: View {
|
||||
@Binding var showingHelpWindow: Bool
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(showingHelpWindow: Binding<Bool>) {
|
||||
self._showingHelpWindow = showingHelpWindow
|
||||
}
|
||||
|
||||
func helpText() -> AttributedString {
|
||||
let file = Bundle.main.url(forResource: "HelpManual", withExtension: "md")
|
||||
guard let contents = try? String(contentsOf: file!, encoding: String.Encoding.utf8) else {
|
||||
return "Help manual not found"
|
||||
}
|
||||
guard let markdown = try? AttributedString(
|
||||
markdown: contents,
|
||||
options: AttributedString.MarkdownParsingOptions(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||
) else {
|
||||
return "Help manual parsing failed"
|
||||
}
|
||||
return markdown
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
VStack {
|
||||
Text("Playground").font(.title)
|
||||
DevicesView(genMode: .demo).background(.brown)
|
||||
}
|
||||
|
||||
HStack {
|
||||
VStack(spacing: 5) {
|
||||
Text("Help").font(.title)
|
||||
Text(helpText()).fixedSize()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.all)
|
||||
}.onDisappear {
|
||||
self.showingHelpWindow = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HelpView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HelpView(showingHelpWindow: .constant(true))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
179
KDE Connect/KDE Connect/Views/Top Level/Mac/MainView.swift
Normal file
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// KDE Connect
|
||||
//
|
||||
// Created by Ruixuan Tu on 2022/05/11.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import AVFoundation
|
||||
|
||||
struct MainView: View {
|
||||
static var mainViewSingleton: MainView?
|
||||
var deviceView: DevicesView?
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@State static var findMyPhoneTimer = Empty<Date, Never>().eraseToAnyPublisher()
|
||||
@ObservedObject var selfData = KdeConnectSettings.shared
|
||||
@State var disabledSingletonConflict: Bool
|
||||
@Binding var showingHelpWindow: Bool
|
||||
|
||||
init(showingHelpWindow: Binding<Bool>) {
|
||||
self.deviceView = DevicesView()
|
||||
self._disabledSingletonConflict = State(initialValue: false)
|
||||
self._showingHelpWindow = showingHelpWindow
|
||||
}
|
||||
|
||||
func helpButton(_ action: @escaping () -> Void) -> some View {
|
||||
// ref: https://blog.urtti.com/creating-a-macos-help-button-in-swiftui
|
||||
Button(action: action, label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.strokeBorder(Color(NSColor.controlShadowColor), lineWidth: 0.5)
|
||||
.background(Circle().foregroundColor(Color(NSColor.controlColor)))
|
||||
.shadow(color: Color(NSColor.controlShadowColor).opacity(0.3), radius: 1)
|
||||
.frame(width: 20, height: 20)
|
||||
Text("?").font(.system(size: 15, weight: .medium ))
|
||||
}
|
||||
})
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !self.disabledSingletonConflict {
|
||||
VStack {
|
||||
deviceView
|
||||
Divider()
|
||||
HStack {
|
||||
Spacer().frame(maxWidth: .infinity)
|
||||
HStack {
|
||||
DeviceItemView(deviceId: "0", parent: nil, deviceName: $selfData.deviceName, emoji: DevicesView.getEmojiFromDeviceType(DeviceType.current), connState: .local)
|
||||
.padding(.all)
|
||||
}.frame(maxWidth: .infinity)
|
||||
if !self.showingHelpWindow {
|
||||
helpButton {
|
||||
self.showingHelpWindow = true
|
||||
openWindow(id: "help")
|
||||
}
|
||||
.padding(.all)
|
||||
.frame(maxWidth: .infinity, maxHeight: 128, alignment: .bottomTrailing)
|
||||
} else {
|
||||
Spacer().frame(maxWidth: .infinity, maxHeight: 128, alignment: .bottomTrailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.refreshable(action: {
|
||||
refreshDiscoveryAndList()
|
||||
})
|
||||
.onAppear {
|
||||
self.disabledSingletonConflict = MainView.mainViewSingleton != nil
|
||||
if !self.disabledSingletonConflict {
|
||||
MainView.mainViewSingleton = self
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .didReceivePairRequestNotification, object: nil)
|
||||
.receive(on: RunLoop.main)) { notification in
|
||||
onPairRequest(fromDeviceWithID: notification.userInfo?["deviceID"] as? String)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pairRequestTimedOutNotification, object: nil)
|
||||
.receive(on: RunLoop.main)) { notification in
|
||||
onPairTimeout(toDeviceWithID: notification.userInfo?["deviceID"] as? String)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pairRequestSucceedNotification, object: nil)
|
||||
.receive(on: RunLoop.main)) { notification in
|
||||
onPairSuccess(withDeviceWithID: notification.userInfo?["deviceID"] as? String)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pairRequestRejectedNotification, object: nil)
|
||||
.receive(on: RunLoop.main)) { notification in
|
||||
onPairRejected(byDeviceWithID: notification.userInfo?["deviceID"] as? String)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .didReceivePingNotification, object: nil)
|
||||
.receive(on: RunLoop.main)) { _ in
|
||||
showPingAlert()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .didReceiveFindMyPhoneRequestNotification, object: nil)
|
||||
.receive(on: RunLoop.main)) { _ in
|
||||
showFindMyPhoneAlert()
|
||||
MainView.updateFindMyPhoneTimer(isRunning: true) // TODO: alert sound does not work
|
||||
}
|
||||
.onReceive(MainView.findMyPhoneTimer) { _ in
|
||||
SystemSound.calendarAlert.play()
|
||||
}
|
||||
} else {
|
||||
ErrorView("There cannot be multiple main windows. Please close this window to proceed.")
|
||||
}
|
||||
}
|
||||
|
||||
@EnvironmentObject var notificationManager: NotificationManager
|
||||
|
||||
func currentPairingDeviceName(id: String) -> String? {
|
||||
backgroundService._devices[id]?._deviceInfo.name
|
||||
}
|
||||
|
||||
func deleteDevice(at offsets: IndexSet) {
|
||||
offsets
|
||||
.map { (offset: $0, id: deviceView!.savedDevicesIds[$0]) }
|
||||
.forEach { device in
|
||||
// TODO: Update Device.m to indicate nullability
|
||||
let name = backgroundService._devices[device.id]!._deviceInfo.name
|
||||
print("Remembered device \(name) removed at index \(device.offset)")
|
||||
backgroundService.unpairDevice(device.id)
|
||||
}
|
||||
}
|
||||
|
||||
func onPairRequest(fromDeviceWithID deviceId: String!) {
|
||||
notificationManager.pairRequestPost(title: "Incoming Pairing Request", body: "\(currentPairingDeviceName(id: deviceId) ?? "Unknown device") wants to pair with this device", deviceId: deviceId)
|
||||
}
|
||||
|
||||
func onPairTimeout(toDeviceWithID deviceId: String!) {
|
||||
notificationManager.post(title: "Pairing Timed Out", body: "Pairing with \(currentPairingDeviceName(id: deviceId) ?? "Unknown device") failed")
|
||||
}
|
||||
|
||||
func onPairSuccess(withDeviceWithID deviceId: String!) {
|
||||
notificationManager.post(title: "Pairing Complete", body: "Pairing with \(currentPairingDeviceName(id: deviceId) ?? "Unknown device") succeeded")
|
||||
}
|
||||
|
||||
func onPairRejected(byDeviceWithID deviceId: String!) {
|
||||
notificationManager.post(title: "Pairing Rejected", body: "Pairing with \(currentPairingDeviceName(id: deviceId) ?? "Unknown device") failed")
|
||||
}
|
||||
|
||||
func showPingAlert() {
|
||||
SystemSound.smsReceived.play()
|
||||
notificationManager.post(title: "Ping!", body: "Ping received from a connected device.")
|
||||
}
|
||||
|
||||
func showFindMyPhoneAlert() {
|
||||
notificationManager.post(title: "Find My Mac Mode", body: "Find My Mac initiated from a remote device", categoryIdentifier: "FIND_MY_DEVICE", interruptionLevel: .critical)
|
||||
// TODO: notification does not stay
|
||||
}
|
||||
|
||||
static func updateFindMyPhoneTimer(isRunning: Bool) {
|
||||
if isRunning {
|
||||
MainView.findMyPhoneTimer = Deferred {
|
||||
Just(Date())
|
||||
}
|
||||
.append(Timer.publish(every: 4, on: .main, in: .common).autoconnect())
|
||||
.eraseToAnyPublisher()
|
||||
} else {
|
||||
MainView.findMyPhoneTimer = Empty<Date, Never>().eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
||||
func refreshDiscoveryAndList() {
|
||||
withAnimation {
|
||||
backgroundService.refreshDiscovery()
|
||||
broadcastBatteryStatusAllDevices()
|
||||
requestBatteryStatusAllDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainView(showingHelpWindow: .constant(false))
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,9 @@
|
||||
// Created by Lucas Wang on 2021-06-17.
|
||||
//
|
||||
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@@ -86,3 +89,5 @@ struct TabView_Previews: PreviewProvider {
|
||||
.environmentObject(connectedDevicesViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The Official Repository of KDE Connect iOS
|
||||
|
||||
**TL;DR: [Download from the App Store](https://apps.apple.com/app/kde-connect/id1580245991) on an iOS >= 14 device!**
|
||||
**TL;DR: [Download from the App Store](https://apps.apple.com/app/kde-connect/id1580245991) on an iOS >= 14 or macOS >= 13 device!**
|
||||
|
||||
---
|
||||
|
||||
@@ -13,9 +13,7 @@ If you would like to talk to the KDE Connect developers & contributors (for ques
|
||||
|
||||
## Beta Testing
|
||||
|
||||
In addition to the App Store, you can also get the public testing version of KDE Connect iOS
|
||||
by opening [this TestFlight link](https://testflight.apple.com/join/vxCluwBF).
|
||||
This version also supports running on Macs with Apple silicon.
|
||||
In addition to the App Store, you can also get the public testing version of KDE Connect iOS/macOS by opening [this TestFlight link](https://testflight.apple.com/join/vxCluwBF).
|
||||
|
||||
## Known Behavior and Problems
|
||||
|
||||
@@ -58,7 +56,6 @@ Many tasks only include a high level description and could be easily misinterpre
|
||||
### Extending to Additional Platforms
|
||||
|
||||
- [ ] [Expand to a watchOS](https://community.kde.org/SoK/Ideas/2022#Investigate_Feasibility_of_KDE_Connect_for_watchOS) companion/standalone app?
|
||||
- [ ] [Expand to macOS](https://community.kde.org/GSoC/2022/Ideas#Project:_Porting_the_KDE_Connect_iOS_app_to_macOS) with catalyst/native SwiftUI?
|
||||
|
||||
## History of KDE Connect iOS
|
||||
|
||||
|
||||