You've already forked hotpocket
BTHLABS-66: Prepping for public release: Take five
AKA "Using Apple reviewers as QA for your project". Thanks, y'all! :)
This commit is contained in:
20
README.md
20
README.md
@@ -1,6 +1,23 @@
|
|||||||
# HotPocket by BTHLabs
|
# HotPocket by BTHLabs
|
||||||
|
|
||||||
This repository contains the _HotPocket_ project.
|
Minimal self-hosted bookmarking app :).
|
||||||
|
|
||||||
|
## The what, the why and the ugly
|
||||||
|
|
||||||
|
HotPocket is a minimal self-hosted bookmarking app. It combines a Web
|
||||||
|
application, companion apps, browser extensions to give you a way to quickly
|
||||||
|
save links for later.
|
||||||
|
|
||||||
|
HotPocket came to be to fill in the blank left by Pocket, after Mozilla shut it
|
||||||
|
down. I looked at the existing alternatives and found them either too
|
||||||
|
feature-rich, too involved to self-host or otherwise not to my liking. So I
|
||||||
|
decided to sit down and build something for myself.
|
||||||
|
|
||||||
|
With the what and why out of the way, let's talk about the ugly... At its core
|
||||||
|
HotPocket is a personal project. I built it by myself and for myself. It may
|
||||||
|
or may not fit your needs. If it does, happy saving!
|
||||||
|
|
||||||
|
If you're feeling up for an adventure, continue reading below :).
|
||||||
|
|
||||||
## Development setup
|
## Development setup
|
||||||
|
|
||||||
@@ -128,6 +145,7 @@ that can be used to configure the services.
|
|||||||
| `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. |
|
| `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. |
|
||||||
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. |
|
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. |
|
||||||
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. |
|
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. |
|
||||||
|
| `HOTPOCKET_BACKEND_OPERATOR_EMAIL` | N/A | Instance operator's e-mail. Used to display extra language on login page. |
|
||||||
|
|
||||||
**Env and App settings**
|
**Env and App settings**
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: "hotpocket/backend:aio-v25.11.19-01"
|
image: "bthlabs/hotpocket:aio-v25.11.19-01"
|
||||||
environment:
|
environment:
|
||||||
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
|
||||||
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"
|
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
webapp:
|
webapp:
|
||||||
image: "hotpocket/backend:deployment-v25.11.19-01"
|
image: "bthlabs/hotpocket:deployment-v25.11.19-01"
|
||||||
environment:
|
environment:
|
||||||
<<: *x-backend-environment
|
<<: *x-backend-environment
|
||||||
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
|
||||||
@@ -21,7 +21,7 @@ services:
|
|||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
image: "hotpocket/backend:deployment-v25.11.19-01"
|
image: "bthlabs/hotpocket:deployment-v25.11.19-01"
|
||||||
environment:
|
environment:
|
||||||
<<: *x-backend-environment
|
<<: *x-backend-environment
|
||||||
HOTPOCKET_BACKEND_APP: "admin"
|
HOTPOCKET_BACKEND_APP: "admin"
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
|
|
||||||
celery-worker:
|
celery-worker:
|
||||||
image: "hotpocket/backend:deployment-v25.11.19-01"
|
image: "bthlabs/hotpocket:deployment-v25.11.19-01"
|
||||||
command:
|
command:
|
||||||
- "/srv/venv/bin/celery"
|
- "/srv/venv/bin/celery"
|
||||||
- "-A"
|
- "-A"
|
||||||
@@ -57,7 +57,7 @@ services:
|
|||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
|
|
||||||
celery-beat:
|
celery-beat:
|
||||||
image: "hotpocket/backend:deployment-v25.11.19-01"
|
image: "bthlabs/hotpocket:deployment-v25.11.19-01"
|
||||||
command:
|
command:
|
||||||
- "/srv/venv/bin/celery"
|
- "/srv/venv/bin/celery"
|
||||||
- "-A"
|
- "-A"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#import "HPAPI.h"
|
#import "HPAPI.h"
|
||||||
#import "HPCredentialsHelper.h"
|
#import "HPCredentialsHelper.h"
|
||||||
#import "HPRPCClient.h"
|
#import "HPRPCClient.h"
|
||||||
|
#import "NSBundle+HotPocketExtensions.h"
|
||||||
#import "NSURL+HotPocketExtensions.h"
|
#import "NSURL+HotPocketExtensions.h"
|
||||||
|
|
||||||
@implementation HPAuthParams
|
@implementation HPAuthParams
|
||||||
@@ -77,18 +78,19 @@
|
|||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSDictionary *postAuthenticateURLParams = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"];
|
NSString *expectedScheme = [NSBundle postAuthenticateURLScheme];
|
||||||
if (postAuthenticateURLParams == nil) {
|
NSString *expectedHost = [NSBundle postAuthenticateURLHost];
|
||||||
|
if (expectedScheme == nil || expectedHost == nil) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
|
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
|
||||||
|
|
||||||
if ([urlComponents.scheme isEqualToString:[postAuthenticateURLParams valueForKey:@"scheme"]] == NO) {
|
if ([urlComponents.scheme isEqualToString:expectedScheme] == NO) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([urlComponents.host isEqualToString:[postAuthenticateURLParams valueForKey:@"host"]] == NO) {
|
if ([urlComponents.host isEqualToString:expectedHost] == NO) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
@interface NSBundle (HotPocketExtensions)
|
@interface NSBundle (HotPocketExtensions)
|
||||||
|
|
||||||
+(NSString *)uname;
|
+(NSString *)uname;
|
||||||
|
+(NSDictionary *)postAuthenticateURLParams;
|
||||||
|
+(NSString *)postAuthenticateURLScheme;
|
||||||
|
+(NSString *)postAuthenticateURLHost;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,23 @@
|
|||||||
return [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
|
return [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+(NSDictionary *)postAuthenticateURLParams {
|
||||||
|
NSDictionary *result = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"];
|
||||||
|
if (result == nil) {
|
||||||
|
return [NSDictionary dictionary];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
+(NSString *)postAuthenticateURLScheme {
|
||||||
|
NSDictionary *params = [self postAuthenticateURLParams];
|
||||||
|
return [params valueForKey:@"scheme"];
|
||||||
|
}
|
||||||
|
|
||||||
|
+(NSString *)postAuthenticateURLHost {
|
||||||
|
NSDictionary *params = [self postAuthenticateURLParams];
|
||||||
|
return [params valueForKey:@"host"];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -6,15 +6,19 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <AuthenticationServices/AuthenticationServices.h>
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@class MultilineLabel;
|
@class MultilineLabel;
|
||||||
|
|
||||||
@interface AuthorizationProgressViewController : UIViewController
|
@interface AuthorizationProgressViewController : UIViewController<ASWebAuthenticationPresentationContextProviding>
|
||||||
|
|
||||||
@property IBOutlet UIActivityIndicatorView *progressIndicator;
|
@property IBOutlet UIActivityIndicatorView *progressIndicator;
|
||||||
@property IBOutlet MultilineLabel *progressLabel;
|
@property IBOutlet MultilineLabel *progressLabel;
|
||||||
|
@property (strong, nullable) NSURL *authorizationURL;
|
||||||
|
@property (strong, nullable) ASWebAuthenticationSession *webAuthenticationSession;
|
||||||
|
@property BOOL userCancelledSession;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -8,22 +8,36 @@
|
|||||||
#import "AuthorizationProgressViewController.h"
|
#import "AuthorizationProgressViewController.h"
|
||||||
|
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
|
#import "HPAuthFlow.h"
|
||||||
#import "HPCredentialsHelper.h"
|
#import "HPCredentialsHelper.h"
|
||||||
#import "MultilineLabel.h"
|
#import "MultilineLabel.h"
|
||||||
|
#import "NSBundle+HotPocketExtensions.h"
|
||||||
|
|
||||||
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
|
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
|
||||||
|
|
||||||
#pragma mark - Private interface
|
#pragma mark - Private interface
|
||||||
|
|
||||||
|
-(void)presentAuthorizationError;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation AuthorizationProgressViewController
|
@implementation AuthorizationProgressViewController
|
||||||
|
|
||||||
#pragma mark - View lifecycle
|
#pragma mark - View lifecycle
|
||||||
|
|
||||||
|
-(instancetype)initWithCoder:(NSCoder *)coder {
|
||||||
|
if (self = [super initWithCoder:coder]) {
|
||||||
|
self.authorizationURL = nil;
|
||||||
|
self.webAuthenticationSession = nil;
|
||||||
|
self.userCancelledSession = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
-(void)viewDidLoad {
|
-(void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
self.progressLabel.text = NSLocalizedString(@"Continue to sign out in your browser...", @"Continue to sign out in your browser...");
|
self.progressLabel.text = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser...");
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)viewWillAppear:(BOOL)animated {
|
-(void)viewWillAppear:(BOOL)animated {
|
||||||
@@ -42,14 +56,79 @@
|
|||||||
object:appDelegate.authFlow];
|
object:appDelegate.authFlow];
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)viewWillDisappear:(BOOL)animated {
|
-(void)viewDidAppear:(BOOL)animated {
|
||||||
[super viewWillDisappear:animated];
|
[super viewDidAppear:animated];
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
|
||||||
|
|
||||||
|
ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil) {
|
||||||
|
#ifdef DEBUG
|
||||||
|
NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error);
|
||||||
|
#endif
|
||||||
|
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
|
||||||
|
self.userCancelledSession = YES;
|
||||||
|
}
|
||||||
|
[self presentAuthorizationError];
|
||||||
|
} else {
|
||||||
|
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
|
||||||
|
if (receivedAuthParams != nil) {
|
||||||
|
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
|
||||||
|
} else {
|
||||||
|
[self presentAuthorizationError];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.webAuthenticationSession = nil;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]];
|
||||||
|
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL
|
||||||
|
callback:callback
|
||||||
|
completionHandler:completionHandler];
|
||||||
|
self.webAuthenticationSession.presentationContextProvider = self;
|
||||||
|
#ifdef DEBUG
|
||||||
|
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (self.webAuthenticationSession.canStart == NO) {
|
||||||
|
[self presentAuthorizationError];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.webAuthenticationSession start];
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)viewDidDisappear:(BOOL)animated {
|
-(void)viewDidDisappear:(BOOL)animated {
|
||||||
[super viewDidDisappear:animated];
|
[super viewDidDisappear:animated];
|
||||||
|
self.webAuthenticationSession = nil;
|
||||||
|
|
||||||
[self.progressIndicator stopAnimating];
|
[self.progressIndicator stopAnimating];
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
# pragma mark - Private interface
|
||||||
|
|
||||||
|
-(void)presentAuthorizationError {
|
||||||
|
if (self.userCancelledSession == NO) {
|
||||||
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!")
|
||||||
|
message:NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.")
|
||||||
|
preferredStyle:UIAlertControllerStyleAlert];
|
||||||
|
|
||||||
|
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Oh well", @"Oh well")
|
||||||
|
style:UIAlertActionStyleDefault
|
||||||
|
handler:^(UIAlertAction *action) {
|
||||||
|
[alert dismissViewControllerAnimated:YES completion:^{
|
||||||
|
[self.navigationController popViewControllerAnimated:YES];
|
||||||
|
}];
|
||||||
|
}]];
|
||||||
|
|
||||||
|
[self presentViewController:alert animated:YES completion:nil];
|
||||||
|
} else {
|
||||||
|
[self.navigationController popViewControllerAnimated:YES];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Notification handlers
|
#pragma mark - Notification handlers
|
||||||
@@ -62,19 +141,7 @@
|
|||||||
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
|
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
|
||||||
|
|
||||||
if (credentials.usable == NO) {
|
if (credentials.usable == NO) {
|
||||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!")
|
[self presentAuthorizationError];
|
||||||
message:NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.")
|
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Oh well", @"Oh well")
|
|
||||||
style:UIAlertActionStyleDefault
|
|
||||||
handler:^(UIAlertAction *action) {
|
|
||||||
[alert dismissViewControllerAnimated:YES completion:^{
|
|
||||||
[self.navigationController popViewControllerAnimated:YES];
|
|
||||||
}];
|
|
||||||
}]];
|
|
||||||
|
|
||||||
[self presentViewController:alert animated:YES completion:nil];
|
|
||||||
} else {
|
} else {
|
||||||
[self.navigationController popToRootViewControllerAnimated:YES];
|
[self.navigationController popToRootViewControllerAnimated:YES];
|
||||||
}
|
}
|
||||||
@@ -85,4 +152,10 @@
|
|||||||
self.progressLabel.text = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
|
self.progressLabel.text = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
|
||||||
|
|
||||||
|
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
|
||||||
|
return self.view.window;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -73,14 +73,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([application canOpenURL:authURL] == YES) {
|
AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"];
|
||||||
[application openURL:authURL options:@{} completionHandler:^(BOOL result) {
|
authorizationProgressViewController.authorizationURL = authURL;
|
||||||
if (result == YES) {
|
[self.navigationController pushViewController:authorizationProgressViewController animated:YES];
|
||||||
AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"];
|
|
||||||
[self.navigationController pushViewController:authorizationProgressViewController animated:YES];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Event handlers
|
#pragma mark - Event handlers
|
||||||
|
|||||||
@@ -21,19 +21,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
-(void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
|
-(void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
|
||||||
HPAuthParams *receivedAuthParams = nil;
|
|
||||||
for (NSURL *url in urls) {
|
|
||||||
receivedAuthParams = [self.authFlow handlePostAuthenticateURL:url];
|
|
||||||
|
|
||||||
if (receivedAuthParams != nil) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receivedAuthParams != nil) {
|
|
||||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
|
||||||
[self.authFlow handleAuthParams:receivedAuthParams];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <AuthenticationServices/AuthenticationServices.h>
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface AuthorizationProgressViewController : NSViewController
|
@interface AuthorizationProgressViewController : NSViewController<ASWebAuthenticationPresentationContextProviding>
|
||||||
|
|
||||||
@property IBOutlet NSProgressIndicator *progressIndicator;
|
@property IBOutlet NSProgressIndicator *progressIndicator;
|
||||||
@property NSString *progressLabelTitle;
|
@property NSString *progressLabelTitle;
|
||||||
|
@property (nullable, strong) NSURL *authorizationURL;
|
||||||
|
@property (nullable, strong) ASWebAuthenticationSession *webAuthenticationSession;
|
||||||
|
@property BOOL userCancelledSession;
|
||||||
|
|
||||||
|
-(IBAction)doCancel:(id)sender;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -9,27 +9,42 @@
|
|||||||
|
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import "AuthorizationViewController.h"
|
#import "AuthorizationViewController.h"
|
||||||
|
#import "HPAuthFlow.h"
|
||||||
#import "HPCredentialsHelper.h"
|
#import "HPCredentialsHelper.h"
|
||||||
#import "MainViewController.h"
|
#import "MainViewController.h"
|
||||||
|
#import "NSBundle+HotPocketExtensions.h"
|
||||||
#import "ReplaceAnimator.h"
|
#import "ReplaceAnimator.h"
|
||||||
|
|
||||||
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
|
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
|
||||||
|
|
||||||
#pragma mark - Private interface
|
#pragma mark - Private interface
|
||||||
|
|
||||||
|
-(void)presentAuthorizationError;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation AuthorizationProgressViewController
|
@implementation AuthorizationProgressViewController
|
||||||
|
|
||||||
#pragma mark - View lifecycle
|
#pragma mark - View lifecycle
|
||||||
|
|
||||||
|
-(instancetype)initWithCoder:(NSCoder *)coder {
|
||||||
|
if (self = [super initWithCoder:coder]) {
|
||||||
|
self.authorizationURL = nil;
|
||||||
|
self.webAuthenticationSession = nil;
|
||||||
|
self.userCancelledSession = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
-(void)viewDidLoad {
|
-(void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
self.progressLabelTitle = NSLocalizedString(@"Continue to sign out in your browser...", @"Continue to sign out in your browser...");
|
self.progressLabelTitle = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser...");
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void)viewWillAppear {
|
-(void)viewWillAppear {
|
||||||
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
|
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
selector:@selector(onAuthFlowDidFinish:)
|
selector:@selector(onAuthFlowDidFinish:)
|
||||||
name:@"AuthFlowDidFinish"
|
name:@"AuthFlowDidFinish"
|
||||||
@@ -43,12 +58,84 @@
|
|||||||
[self.progressIndicator startAnimation:self];
|
[self.progressIndicator startAnimation:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-(void)viewDidAppear {
|
||||||
|
[super viewDidAppear];
|
||||||
|
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
|
||||||
|
|
||||||
|
ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil) {
|
||||||
|
#ifdef DEBUG
|
||||||
|
NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error);
|
||||||
|
#endif
|
||||||
|
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
|
||||||
|
self.userCancelledSession = YES;
|
||||||
|
}
|
||||||
|
[self presentAuthorizationError];
|
||||||
|
} else {
|
||||||
|
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
|
||||||
|
if (receivedAuthParams != nil) {
|
||||||
|
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||||
|
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
|
||||||
|
} else {
|
||||||
|
[self presentAuthorizationError];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.webAuthenticationSession = nil;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]];
|
||||||
|
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL
|
||||||
|
callback:callback
|
||||||
|
completionHandler:completionHandler];
|
||||||
|
self.webAuthenticationSession.presentationContextProvider = self;
|
||||||
|
#ifdef DEBUG
|
||||||
|
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (self.webAuthenticationSession.canStart == NO) {
|
||||||
|
[self presentAuthorizationError];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.webAuthenticationSession start];
|
||||||
|
}
|
||||||
|
|
||||||
-(void)viewDidDisappear {
|
-(void)viewDidDisappear {
|
||||||
|
[super viewDidDisappear];
|
||||||
|
self.webAuthenticationSession = nil;
|
||||||
|
|
||||||
[self.progressIndicator stopAnimation:self];
|
[self.progressIndicator stopAnimation:self];
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - Actions
|
||||||
|
|
||||||
|
-(IBAction)doCancel:(id)sender {
|
||||||
|
[self.webAuthenticationSession cancel];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Private interface
|
||||||
|
|
||||||
|
-(void)presentAuthorizationError {
|
||||||
|
if (self.userCancelledSession == NO) {
|
||||||
|
NSAlert *alert = [[NSAlert alloc] init];
|
||||||
|
alert.alertStyle = NSAlertStyleCritical;
|
||||||
|
alert.messageText = NSLocalizedString(@"Oops!", @"Oops!");
|
||||||
|
alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.");
|
||||||
|
[alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) {
|
||||||
|
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
|
||||||
|
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
|
||||||
|
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Notification handlers
|
#pragma mark - Notification handlers
|
||||||
|
|
||||||
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
|
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
|
||||||
@@ -59,14 +146,7 @@
|
|||||||
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
|
||||||
|
|
||||||
if (credentials.usable == NO) {
|
if (credentials.usable == NO) {
|
||||||
NSAlert *alert = [[NSAlert alloc] init];
|
[self presentAuthorizationError];
|
||||||
alert.alertStyle = NSAlertStyleCritical;
|
|
||||||
alert.messageText = NSLocalizedString(@"Oops!", @"Oops!");
|
|
||||||
alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.");
|
|
||||||
[alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) {
|
|
||||||
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
|
|
||||||
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
|
|
||||||
}];
|
|
||||||
} else {
|
} else {
|
||||||
MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"];
|
MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"];
|
||||||
[self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]];
|
[self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]];
|
||||||
@@ -78,4 +158,10 @@
|
|||||||
self.progressLabelTitle = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
|
self.progressLabelTitle = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
|
||||||
|
|
||||||
|
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
|
||||||
|
return self.view.window;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -31,31 +31,24 @@
|
|||||||
#pragma mark - Actions
|
#pragma mark - Actions
|
||||||
|
|
||||||
-(IBAction)doStartAuthorizationFlow:(id)sender {
|
-(IBAction)doStartAuthorizationFlow:(id)sender {
|
||||||
NSAlert *alert = [[NSAlert alloc] init];
|
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
|
||||||
alert.alertStyle = NSAlertStyleInformational;
|
appDelegate.authFlow.baseURL = [NSURL URLWithString:self.baseURL];
|
||||||
alert.messageText = NSLocalizedString(@"Continue in the browser", @"Continue in the browser");
|
|
||||||
alert.informativeText = NSLocalizedString(@"HotPocket will now open the instance in your browser and you can continue to sign in.", @"HotPocket will now open the instance in your browser and you can continue to sign in.");
|
|
||||||
|
|
||||||
[alert addButtonWithTitle:NSLocalizedString(@"Let's go!", @"Let's go!")];
|
NSURL *authURL = [appDelegate.authFlow start];
|
||||||
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel")];
|
if (authURL == nil) {
|
||||||
|
NSBeep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) {
|
AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"];
|
||||||
if (response == NSAlertFirstButtonReturn) {
|
authProgressViewController.authorizationURL = authURL;
|
||||||
AppDelegate *appDeleate = [[NSApplication sharedApplication] delegate];
|
[self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]];
|
||||||
appDeleate.authFlow.baseURL = [NSURL URLWithString:self.baseURL];
|
}
|
||||||
|
|
||||||
NSURL *authURL = [appDeleate.authFlow start];
|
|
||||||
if (authURL == nil) {
|
|
||||||
NSBeep();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"];
|
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
|
||||||
[self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]];
|
|
||||||
|
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
|
||||||
[[NSWorkspace sharedWorkspace] openURL:authURL];
|
return self.view.window;
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -207,6 +207,20 @@
|
|||||||
<binding destination="OX4-Oj-1cw" name="value" keyPath="self.progressLabelTitle" id="ydU-jy-p3F"/>
|
<binding destination="OX4-Oj-1cw" name="value" keyPath="self.progressLabelTitle" id="ydU-jy-p3F"/>
|
||||||
</connections>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
|
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Sip-bU-V5i">
|
||||||
|
<rect key="frame" x="175" y="14" width="76" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nYJ-LO-V7O">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
<string key="keyEquivalent" base64-UTF8="YES">
|
||||||
|
Gw
|
||||||
|
</string>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="doCancel:" target="OX4-Oj-1cw" id="nAC-If-aib"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ class PSettings(typing.Protocol):
|
|||||||
|
|
||||||
UI_PAGE_HEAD_INCLUDES: list
|
UI_PAGE_HEAD_INCLUDES: list
|
||||||
UI_PAGE_SCRIPT_INCLUDES: list
|
UI_PAGE_SCRIPT_INCLUDES: list
|
||||||
|
|
||||||
|
OPERATOR_EMAIL: str | None
|
||||||
|
|||||||
@@ -100,3 +100,9 @@ def page_includes(request: HttpRequest) -> dict:
|
|||||||
'script': settings.UI_PAGE_SCRIPT_INCLUDES,
|
'script': settings.UI_PAGE_SCRIPT_INCLUDES,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def operator_email(request: HttpRequest) -> dict:
|
||||||
|
return {
|
||||||
|
'OPERATOR_EMAIL': settings.OPERATOR_EMAIL,
|
||||||
|
}
|
||||||
|
|||||||
19
services/backend/hotpocket_backend/apps/ui/dto/rpc.py
Normal file
19
services/backend/hotpocket_backend/apps/ui/dto/rpc.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
|
||||||
|
class SavesCreateOut(pydantic.BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
target_uuid: uuid.UUID
|
||||||
|
url: pydantic.AnyHttpUrl
|
||||||
|
|
||||||
|
def to_rpc(self) -> dict:
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'target_uuid': self.target_uuid,
|
||||||
|
'url': str(self.url),
|
||||||
|
}
|
||||||
@@ -3,19 +3,28 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from bthlabs_jsonrpc_core import register_method
|
from bthlabs_jsonrpc_core import register_method
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
|
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
|
||||||
|
from hotpocket_backend.apps.ui.dto.rpc import SavesCreateOut
|
||||||
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
||||||
from hotpocket_soa.dto.associations import AssociationOut
|
|
||||||
|
|
||||||
|
|
||||||
@register_method(method='saves.create')
|
@register_method(method='saves.create')
|
||||||
@wrap_soa_errors
|
@wrap_soa_errors
|
||||||
def create(request: HttpRequest, url: str) -> AssociationOut:
|
def create(request: HttpRequest, url: str) -> SavesCreateOut:
|
||||||
association = CreateSaveWorkflow().run_rpc(
|
association = CreateSaveWorkflow().run_rpc(
|
||||||
request=request,
|
request=request,
|
||||||
account=request.user,
|
account=request.user,
|
||||||
url=url,
|
url=url,
|
||||||
)
|
)
|
||||||
|
|
||||||
return association
|
result = SavesCreateOut.model_validate({
|
||||||
|
'id': association.pk,
|
||||||
|
'target_uuid': association.target_uuid,
|
||||||
|
'url': request.build_absolute_uri(reverse(
|
||||||
|
'ui.associations.view', args=(association.pk,),
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@@ -36,6 +36,12 @@
|
|||||||
{% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %}
|
{% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if OPERATOR_EMAIL %}
|
||||||
|
<p class="my-0 mt-2 text-center">
|
||||||
|
<small>{% blocktranslate %}For support with your account, contact the <a href="mailto:{{ OPERATOR_EMAIL }}">instance operator</a>.{% endblocktranslate %}</small>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "ui/ui/partials/uname.html" %}
|
{% include "ui/ui/partials/uname.html" %}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ TEMPLATES = [
|
|||||||
'hotpocket_backend.apps.ui.context_processors.debug',
|
'hotpocket_backend.apps.ui.context_processors.debug',
|
||||||
'hotpocket_backend.apps.ui.context_processors.version',
|
'hotpocket_backend.apps.ui.context_processors.version',
|
||||||
'hotpocket_backend.apps.ui.context_processors.appearance_settings',
|
'hotpocket_backend.apps.ui.context_processors.appearance_settings',
|
||||||
|
'hotpocket_backend.apps.ui.context_processors.operator_email',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -295,3 +296,5 @@ SAVES_ASSOCIATION_ADAPTER = os.environ.get(
|
|||||||
UPLOADS_PATH = None
|
UPLOADS_PATH = None
|
||||||
|
|
||||||
SITE_SHORT_TITLE = 'HotPocket'
|
SITE_SHORT_TITLE = 'HotPocket'
|
||||||
|
|
||||||
|
OPERATOR_EMAIL = os.environ.get('HOTPOCKET_BACKEND_OPERATOR_EMAIL', None)
|
||||||
|
|||||||
@@ -54,8 +54,12 @@ def test_ok(authenticated_client: Client,
|
|||||||
call_result = result.json()
|
call_result = result.json()
|
||||||
assert 'error' not in call_result
|
assert 'error' not in call_result
|
||||||
|
|
||||||
save_pk = uuid.UUID(call_result['result']['target_uuid'])
|
|
||||||
association_pk = uuid.UUID(call_result['result']['id'])
|
association_pk = uuid.UUID(call_result['result']['id'])
|
||||||
|
save_pk = uuid.UUID(call_result['result']['target_uuid'])
|
||||||
|
|
||||||
|
assert call_result['result']['url'].endswith(reverse(
|
||||||
|
'ui.associations.view', args=(association_pk,),
|
||||||
|
))
|
||||||
|
|
||||||
AssociationsTestingService().assert_created(
|
AssociationsTestingService().assert_created(
|
||||||
pk=association_pk,
|
pk=association_pk,
|
||||||
|
|||||||
@@ -30,5 +30,9 @@
|
|||||||
"content_popup_content_error_message": {
|
"content_popup_content_error_message": {
|
||||||
"message": "HotPocket couldn't complete this operation.",
|
"message": "HotPocket couldn't complete this operation.",
|
||||||
"description": "Title of the error content popup."
|
"description": "Title of the error content popup."
|
||||||
|
},
|
||||||
|
"content_popup_content_view_button_message": {
|
||||||
|
"message": "View in HotPocket",
|
||||||
|
"description": "Title of the view link button"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,10 +117,10 @@ const doSave = async (accessToken, tab) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const doCreateAndStoreAccessToken = async (authKey) => {
|
const doCreateAndStoreAccessToken = async (authKey) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class Popup {
|
|||||||
|
|
||||||
element.insertAdjacentHTML('beforeend', content);
|
element.insertAdjacentHTML('beforeend', content);
|
||||||
};
|
};
|
||||||
setContent = (content) => {
|
setContent = (content, saveUrl) => {
|
||||||
const shadow = this.container.shadowRoot;
|
const shadow = this.container.shadowRoot;
|
||||||
|
|
||||||
const body = shadow.querySelector('.hotpocket-extension-popup-body');
|
const body = shadow.querySelector('.hotpocket-extension-popup-body');
|
||||||
@@ -38,6 +38,13 @@ class Popup {
|
|||||||
i18nElement.dataset.message,
|
i18nElement.dataset.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (saveUrl) {
|
||||||
|
const viewLink = shadow.querySelector('.hotpocket-extension-popup-view');
|
||||||
|
if (viewLink) {
|
||||||
|
viewLink.href = saveUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
close = () => {
|
close = () => {
|
||||||
this.clearCloseTimeout();
|
this.clearCloseTimeout();
|
||||||
@@ -47,7 +54,7 @@ class Popup {
|
|||||||
this.container = null;
|
this.container = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
show = (content) => {
|
show = (content, saveUrl) => {
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement('div');
|
||||||
@@ -57,7 +64,7 @@ class Popup {
|
|||||||
const shadow = this.container.attachShadow({mode: 'open'});
|
const shadow = this.container.attachShadow({mode: 'open'});
|
||||||
shadow.setHTMLUnsafe(POPUP);
|
shadow.setHTMLUnsafe(POPUP);
|
||||||
|
|
||||||
this.setContent(content);
|
this.setContent(content, saveUrl);
|
||||||
|
|
||||||
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
|
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
|
||||||
for (const closeElement of closeElements) {
|
for (const closeElement of closeElements) {
|
||||||
@@ -66,8 +73,8 @@ class Popup {
|
|||||||
|
|
||||||
document.body.appendChild(this.container);
|
document.body.appendChild(this.container);
|
||||||
};
|
};
|
||||||
update = (content) => {
|
update = (content, saveUrl) => {
|
||||||
this.setContent(content);
|
this.setContent(content, saveUrl);
|
||||||
};
|
};
|
||||||
onCloseClick = (event) => {
|
onCloseClick = (event) => {
|
||||||
this.close();
|
this.close();
|
||||||
@@ -86,16 +93,16 @@ const doHandleBrowserActionClickedMessage = (message) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const doHandleSaveMessage = (message) => {
|
const doHandleSaveMessage = (message) => {
|
||||||
let content = POPUP_CONTENT_ERROR;
|
let content = POPUP_CONTENT_SUCCESS;
|
||||||
if (message.result === true) {
|
if (message.result === null) {
|
||||||
content = POPUP_CONTENT_SUCCESS;
|
content = POPUP_CONTENT_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopup === null) {
|
if (currentPopup === null) {
|
||||||
currentPopup = new Popup();
|
currentPopup = new Popup();
|
||||||
currentPopup.show(content);
|
currentPopup.show(content);
|
||||||
} else {
|
} else {
|
||||||
currentPopup.update(content);
|
currentPopup.update(content, (message.result || {}).url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,26 @@
|
|||||||
opacity: 0.83;
|
opacity: 0.83;
|
||||||
transform: rotate(60deg);
|
transform: rotate(60deg);
|
||||||
}
|
}
|
||||||
|
.hotpocket-extension-popup-view {
|
||||||
|
background-color: #1CBAED;
|
||||||
|
border: 1px solid #1CBAED;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: white;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-top: 0.25rem !important;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.hotpocket-extension-popup-view:hover {
|
||||||
|
background-color: #189ec9;
|
||||||
|
border-color: #1695be;
|
||||||
|
}
|
||||||
|
.hotpocket-extension-popup-view:active {
|
||||||
|
background-color: #1695be;
|
||||||
|
border-color: #158cb2;
|
||||||
|
}
|
||||||
@keyframes hotpocket-extension-popup-loader-animation {
|
@keyframes hotpocket-extension-popup-loader-animation {
|
||||||
100% {transform: rotate(1turn)}
|
100% {transform: rotate(1turn)}
|
||||||
}
|
}
|
||||||
@@ -104,6 +124,6 @@
|
|||||||
<strong>HotPocket by BTHLabs</strong>
|
<strong>HotPocket by BTHLabs</strong>
|
||||||
<a class="hotpocket-extension-popup-close">×</a>
|
<a class="hotpocket-extension-popup-close">×</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="hotpocket-extension-popup-body">
|
<div class="hotpocket-extension-popup-body hotpocket-extension-popup-message-success">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,4 +2,13 @@
|
|||||||
<strong data-message="content_popup_content_success_title"></strong>
|
<strong data-message="content_popup_content_success_title"></strong>
|
||||||
<br>
|
<br>
|
||||||
<span data-message="content_popup_content_success_message"></span>
|
<span data-message="content_popup_content_success_message"></span>
|
||||||
|
<br>
|
||||||
|
<a
|
||||||
|
class="hotpocket-extension-popup-view"
|
||||||
|
data-message="content_popup_content_view_button_message"
|
||||||
|
href="#"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user