Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(QRCode): Enhance 'Scan QR Code From Screen' notifications # #1250 #1501

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
- name: Build
run: |
brew install automake
brew install autoconf
brew install libtool
lkebin marked this conversation as resolved.
Show resolved Hide resolved
make VERSION="${GITHUB_SHA::7}" debug
make debug-dmg
shasum -a 256 build/Debug/ShadowsocksX-NG.dmg > build/Debug/ShadowsocksX-NG.dmg.checksum
Expand Down
47 changes: 24 additions & 23 deletions ShadowsocksX-NG/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -627,42 +627,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}

func handleFoundSSURL(_ note: Notification) {
let sendNotify = {
(title: String, subtitle: String, infoText: String) in

let sendNotify = { (title: String, subtitle: String, infoText: String) in
let userNote = NSUserNotification()
userNote.title = title
userNote.subtitle = subtitle
userNote.informativeText = infoText
userNote.soundName = NSUserNotificationDefaultSoundName

NSUserNotificationCenter.default
.deliver(userNote);
NSUserNotificationCenter.default.deliver(userNote)
}

if let userInfo = (note as NSNotification).userInfo {
let urls: [URL] = userInfo["urls"] as! [URL]
// 检查错误
if let error = userInfo["error"] as? String {
sendNotify("Scan Failed", "", error.localized)
return
}

// 使用新的通知信息
let title = (userInfo["title"] as? String) ?? ""
let subtitle = (userInfo["subtitle"] as? String) ?? ""
let body = (userInfo["body"] as? String) ?? ""

let mgr = ServerProfileManager.instance
let addCount = mgr.addServerProfileByURL(urls: urls)
let urls: [URL] = userInfo["urls"] as! [URL]
let addCount = ServerProfileManager.instance.addServerProfileByURL(urls: urls)

if addCount > 0 {
var subtitle: String = ""
if userInfo["source"] as! String == "qrcode" {
subtitle = "By scan QR Code".localized
} else if userInfo["source"] as! String == "url" {
subtitle = "By handle SS URL".localized
} else if userInfo["source"] as! String == "pasteboard" {
subtitle = "By import from pasteboard".localized
}

sendNotify("Add \(addCount) Shadowsocks Server Profile".localized, subtitle, "")
sendNotify(
title.localized,
subtitle.localized,
"Successfully added \(addCount) server configuration(s)".localized
)
} else {
if userInfo["source"] as! String == "qrcode" {
sendNotify("", "", "Not found valid QRCode of shadowsocks profile".localized)
} else if userInfo["source"] as! String == "url" {
sendNotify("", "", "Not found valid URL of shadowsocks profile".localized)
}
sendNotify(
title.localized,
subtitle.localized,
body.localized
)
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions ShadowsocksX-NG/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>SWBApplication</string>
<key>NSScreenCaptureUsageDescription</key>
<string>ShadowsocksX-NG needs Screen Recording permission to scan QR codes on your screen</string>
</dict>
</plist>
146 changes: 114 additions & 32 deletions ShadowsocksX-NG/Utils.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,71 +10,153 @@
#import <CoreImage/CoreImage.h>
#import <AppKit/AppKit.h>

void ScanQRCodeOnScreen(void) {
void ScanQRCodeOnScreen(void) {
/* check system version and permission status */
if (@available(macOS 10.12, *)) {
BOOL hasPermission = CGPreflightScreenCaptureAccess();
NSLog(@"Screen Recording Permission Status: %@", hasPermission ? @"Granted" : @"Not Granted");

if (!hasPermission) {
NSLog(@"Requesting Screen Recording Permission...");
CGRequestScreenCaptureAccess();

/* check permission status after request */
hasPermission = CGPreflightScreenCaptureAccess();
NSLog(@"Screen Recording Permission Status After Request: %@", hasPermission ? @"Granted" : @"Not Granted");

if (!hasPermission) {
NSLog(@"Screen Recording Permission Denied");

/* send notification about permission missing */
[[NSNotificationCenter defaultCenter]
postNotificationName:@"NOTIFY_FOUND_SS_URL"
object:nil
userInfo:@{
@"urls": @[],
@"source": @"qrcode",
@"error": @"Screen Recording permission required. Please grant permission in System Preferences and restart ShadowsocksX-NG"
}];

/* open system privacy settings */
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"]];
return;
}
}

NSLog(@"Proceeding with screen capture...");
}

/* displays[] Quartz display ID's */
CGDirectDisplayID *displays = nil;

CGError err = CGDisplayNoErr;
CGDisplayCount dspCount = 0;

/* How many active displays do we have? */
err = CGGetActiveDisplayList(0, NULL, &dspCount);
/* variables for collecting scan information */
NSMutableDictionary *scanInfo = [NSMutableDictionary dictionary];
NSMutableArray *foundSSUrls = [NSMutableArray array];
NSMutableArray *foundQRCodes = [NSMutableArray array];

/* If we are getting an error here then their won't be much to display. */
if(err != CGDisplayNoErr)
{
NSLog(@"Could not get active display count (%d)\n", err);
/* How many active displays do we have? */
CGError err = CGGetActiveDisplayList(0, NULL, &dspCount);

if(err != CGDisplayNoErr) {
[[NSNotificationCenter defaultCenter]
postNotificationName:@"NOTIFY_FOUND_SS_URL"
object:nil
userInfo:@{
@"urls": @[],
@"source": @"qrcode",
@"error": @"Failed to get display list"
}];
return;
}

scanInfo[@"displayCount"] = @(dspCount);
NSLog(@"Found %d displays", dspCount);

/* Allocate enough memory to hold all the display IDs we have. */
displays = calloc((size_t)dspCount, sizeof(CGDirectDisplayID));

// Get the list of active displays
err = CGGetActiveDisplayList(dspCount,
displays,
&dspCount);

/* More error-checking here. */
if(err != CGDisplayNoErr)
{
NSLog(@"Could not get active display list (%d)\n", err);
err = CGGetActiveDisplayList(dspCount, displays, &dspCount);

if(err != CGDisplayNoErr) {
free(displays);
[[NSNotificationCenter defaultCenter]
postNotificationName:@"NOTIFY_FOUND_SS_URL"
object:nil
userInfo:@{
@"urls": @[],
@"source": @"qrcode",
@"error": @"Failed to get display information"
}];
return;
}

NSMutableArray* foundSSUrls = [NSMutableArray array];

CIDetector *detector = [CIDetector detectorOfType:@"CIDetectorTypeQRCode"
context:nil
options:@{ CIDetectorAccuracy:CIDetectorAccuracyHigh }];
context:nil
options:@{ CIDetectorAccuracy:CIDetectorAccuracyHigh }];

for (unsigned int displaysIndex = 0; displaysIndex < dspCount; displaysIndex++)
{
/* Make a snapshot image of the current display. */
int totalQRCodesFound = 0;
int validSSUrlsFound = 0;

for (unsigned int displaysIndex = 0; displaysIndex < dspCount; displaysIndex++) {
CGImageRef image = CGDisplayCreateImage(displays[displaysIndex]);
NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image]];

/* count total QR codes found */
totalQRCodesFound += (int)features.count;

for (CIQRCodeFeature *feature in features) {
NSLog(@"%@", feature.messageString);
if ( [feature.messageString hasPrefix:@"ss://"] )
{
NSLog(@"Found QR Code: %@", feature.messageString);
[foundQRCodes addObject:feature.messageString];

if ([feature.messageString hasPrefix:@"ss://"]) {
NSURL *url = [NSURL URLWithString:feature.messageString];
if (url) {
[foundSSUrls addObject:url];
validSSUrlsFound++;
}
}
}
CGImageRelease(image);
CGImageRelease(image);
}

free(displays);

/* prepare notification information */
NSString *notificationTitle;
NSString *notificationSubtitle;
NSString *notificationBody;

if (totalQRCodesFound == 0) {
notificationTitle = [NSString stringWithFormat:@"Scanned %d displays", dspCount];
notificationSubtitle = @"No QR codes found";
notificationBody = @"Try adjusting the QR code position on your screen";
} else if (validSSUrlsFound == 0) {
notificationTitle = [NSString stringWithFormat:@"Found %d QR code(s)", totalQRCodesFound];
notificationSubtitle = @"No valid Shadowsocks URLs";
notificationBody = @"QR codes found are not Shadowsocks configuration";
} else {
notificationTitle = [NSString stringWithFormat:@"Found %d Shadowsocks URL(s)", validSSUrlsFound];
notificationSubtitle = [NSString stringWithFormat:@"Scanned %d displays, found %d QR codes", dspCount, totalQRCodesFound];
notificationBody = @"Processing Shadowsocks configuration...";
}

[[NSNotificationCenter defaultCenter]
postNotificationName:@"NOTIFY_FOUND_SS_URL"
object:nil
userInfo: @{ @"urls": foundSSUrls,
@"source": @"qrcode"
}
];
userInfo:@{
@"urls": foundSSUrls,
@"source": @"qrcode",
@"title": notificationTitle,
@"subtitle": notificationSubtitle,
@"body": notificationBody,
@"scanInfo": @{
@"displayCount": @(dspCount),
@"totalQRCodes": @(totalQRCodesFound),
@"validURLs": @(validSSUrlsFound)
}
}];
}

NSImage* createQRImage(NSString *string, NSSize size) {
Expand Down
Loading