BTHLABS-50: Safari Web extension

Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
Tomek Wójcik 2025-09-08 18:11:36 +00:00 committed by Tomek Wójcik
parent ffecf780ee
commit b6d02dbe78
184 changed files with 7536 additions and 163 deletions

View File

@ -61,14 +61,29 @@ jobs:
push: false push: false
load: true load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local" tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local"
- name: "Build `extension-ci` image"
uses: docker/build-push-action@v6
with:
file: "services/extension/Dockerfile"
context: "services/"
target: "ci"
push: false
load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-local"
- name: "Run `backend` checks" - name: "Run `backend` checks"
run: | run: |
set -x set -x
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm backend-ci inv ci docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm backend-ci inv ci
- name: "Run `packages` checks" - name: "Run `packages` checks"
if: always()
run: | run: |
set -x set -x
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm packages-ci inv ci docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm packages-ci inv ci
- name: "Run `extension` checks"
if: always()
run: |
set -x
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm extension-ci inv ci
- name: "Clean up" - name: "Clean up"
if: always() if: always()
run: | run: |

View File

@ -4,6 +4,7 @@
"targets": [ "targets": [
"backend-management", "backend-management",
"caddy", "caddy",
"extension-management",
"keycloak", "keycloak",
"packages-management", "packages-management",
"postgres", "postgres",
@ -67,6 +68,28 @@
"type=docker,load=true,push=false" "type=docker,load=true,push=false"
] ]
}, },
"extension-management": {
"context": "services/",
"dockerfile": "extension/Dockerfile",
"tags": [
"docker-hosted.nexus.bthlabs.pl/hotpocket/extension:local"
],
"target": "development",
"output": [
"type=docker,load=true,push=false"
]
},
"extension-ci": {
"context": "services/",
"dockerfile": "extension/Dockerfile",
"tags": [
"docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-local"
],
"target": "ci",
"output": [
"type=docker,load=true,push=false"
]
},
"caddy": { "caddy": {
"context": "services/", "context": "services/",
"dockerfile": "caddy/Dockerfile", "dockerfile": "caddy/Dockerfile",

View File

@ -13,3 +13,4 @@ services:
include: include:
- path: "./services/backend/docker-compose-ci.yaml" - path: "./services/backend/docker-compose-ci.yaml"
- path: "./services/packages/docker-compose-ci.yaml" - path: "./services/packages/docker-compose-ci.yaml"
- path: "./services/extension/docker-compose-ci.yaml"

View File

@ -5,6 +5,7 @@ include:
- path: "./docker-compose-cloud.yaml" - path: "./docker-compose-cloud.yaml"
- path: "./services/backend/docker-compose.yaml" - path: "./services/backend/docker-compose.yaml"
- path: "./services/packages/docker-compose.yaml" - path: "./services/packages/docker-compose.yaml"
- path: "./services/extension/docker-compose.yaml"
volumes: {} volumes: {}

View File

@ -1,4 +1,5 @@
_tmp/ _tmp/
apple/
backend/node_modules/ backend/node_modules/
backend/ops/metal/ backend/ops/metal/
backend/hotpocket_backend/playground.py backend/hotpocket_backend/playground.py
@ -7,4 +8,6 @@ backend/hotpocket_backend/settings/docker/
backend/hotpocket_backend/secrets/metal/ backend/hotpocket_backend/secrets/metal/
backend/hotpocket_backend/settings/metal/ backend/hotpocket_backend/settings/metal/
backend/hotpocket_backend/static/ backend/hotpocket_backend/static/
extension/node_modules/
extension/dist/
.envrc* .envrc*

92
services/apple/.gitignore vendored Normal file
View File

@ -0,0 +1,92 @@
# Xcode
## Build generated
build/
DerivedData/
## Various settings
ExportOptions.plist
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
## We don't want any memgrap's to leak ;)
**.memgraph
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/*.env
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# Extension stuff
Shared (Extension)/Resources/_locales/
Shared (Extension)/Resources/images/
Shared (Extension)/Resources/background-bundle.js
Shared (Extension)/Resources/content-bundle.js
Shared (Extension)/Resources/manifest.json

View File

@ -0,0 +1,897 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
4CABCAD62E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4CABCAE02E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
4CABCAD72E56F0C900D8A354 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4CABCA922E56F0C800D8A354 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4CABCAD42E56F0C900D8A354;
remoteInfo = "HotPocket Extension (iOS)";
};
4CABCAE12E56F0C900D8A354 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4CABCA922E56F0C800D8A354 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4CABCADE2E56F0C900D8A354;
remoteInfo = "HotPocket Extension (macOS)";
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
4CABCB092E56F0C900D8A354 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
4CABCAD62E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
4CABCB132E56F0C900D8A354 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
4CABCAE02E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4CABCAB02E56F0C900D8A354 /* HotPocket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HotPocket.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAC62E56F0C900D8A354 /* HotPocket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HotPocket.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
4CABCB042E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (iOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
"/Localized: Resources/Main.html",
Assets.xcassets,
"Resources/icon-mac-384.png",
Resources/Script.js,
Resources/Style.css,
ViewController.m,
);
target = 4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */;
};
4CABCB082E56F0C900D8A354 /* Exceptions for "iOS (Extension)" folder in "HotPocket Extension (iOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */;
};
4CABCB0D2E56F0C900D8A354 /* Exceptions for "iOS (App)" folder in "HotPocket (iOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */;
};
4CABCB0E2E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (macOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
"/Localized: Resources/Main.html",
Assets.xcassets,
"Resources/icon-mac-384.png",
Resources/Script.js,
Resources/Style.css,
ViewController.m,
);
target = 4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */;
};
4CABCB122E56F0C900D8A354 /* Exceptions for "macOS (Extension)" folder in "HotPocket Extension (macOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */;
};
4CABCB172E56F0C900D8A354 /* Exceptions for "Shared (Extension)" folder in "HotPocket Extension (iOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Resources/_locales,
"Resources/background-bundle.js",
"Resources/content-bundle.js",
Resources/images,
Resources/manifest.json,
SafariWebExtensionHandler.m,
);
target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */;
};
4CABCB182E56F0C900D8A354 /* Exceptions for "Shared (Extension)" folder in "HotPocket Extension (macOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Resources/_locales,
"Resources/background-bundle.js",
"Resources/content-bundle.js",
Resources/images,
Resources/manifest.json,
SafariWebExtensionHandler.m,
);
target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
4CABCA962E56F0C800D8A354 /* Shared (App) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB042E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (iOS)" target */,
4CABCB0E2E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (macOS)" target */,
);
path = "Shared (App)";
sourceTree = "<group>";
};
4CABCAA02E56F0C900D8A354 /* Shared (Extension) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB172E56F0C900D8A354 /* Exceptions for "Shared (Extension)" folder in "HotPocket Extension (iOS)" target */,
4CABCB182E56F0C900D8A354 /* Exceptions for "Shared (Extension)" folder in "HotPocket Extension (macOS)" target */,
);
explicitFolders = (
Resources/_locales,
Resources/images,
);
path = "Shared (Extension)";
sourceTree = "<group>";
};
4CABCAB22E56F0C900D8A354 /* iOS (App) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB0D2E56F0C900D8A354 /* Exceptions for "iOS (App)" folder in "HotPocket (iOS)" target */,
);
path = "iOS (App)";
sourceTree = "<group>";
};
4CABCAC72E56F0C900D8A354 /* macOS (App) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = "macOS (App)";
sourceTree = "<group>";
};
4CABCAD92E56F0C900D8A354 /* iOS (Extension) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB082E56F0C900D8A354 /* Exceptions for "iOS (Extension)" folder in "HotPocket Extension (iOS)" target */,
);
path = "iOS (Extension)";
sourceTree = "<group>";
};
4CABCAE32E56F0C900D8A354 /* macOS (Extension) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB122E56F0C900D8A354 /* Exceptions for "macOS (Extension)" folder in "HotPocket Extension (macOS)" target */,
);
path = "macOS (Extension)";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
4CABCAAD2E56F0C900D8A354 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAC32E56F0C900D8A354 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAD22E56F0C900D8A354 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCADC2E56F0C900D8A354 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
4CABCA912E56F0C800D8A354 = {
isa = PBXGroup;
children = (
4CABCA962E56F0C800D8A354 /* Shared (App) */,
4CABCAA02E56F0C900D8A354 /* Shared (Extension) */,
4CABCAB22E56F0C900D8A354 /* iOS (App) */,
4CABCAC72E56F0C900D8A354 /* macOS (App) */,
4CABCAD92E56F0C900D8A354 /* iOS (Extension) */,
4CABCAE32E56F0C900D8A354 /* macOS (Extension) */,
4CABCAB12E56F0C900D8A354 /* Products */,
);
sourceTree = "<group>";
};
4CABCAB12E56F0C900D8A354 /* Products */ = {
isa = PBXGroup;
children = (
4CABCAB02E56F0C900D8A354 /* HotPocket.app */,
4CABCAC62E56F0C900D8A354 /* HotPocket.app */,
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */,
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4CABCB0A2E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket (iOS)" */;
buildPhases = (
4CABCAAC2E56F0C900D8A354 /* Sources */,
4CABCAAD2E56F0C900D8A354 /* Frameworks */,
4CABCAAE2E56F0C900D8A354 /* Resources */,
4CABCB092E56F0C900D8A354 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
4CABCAD82E56F0C900D8A354 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4CABCAB22E56F0C900D8A354 /* iOS (App) */,
);
name = "HotPocket (iOS)";
packageProductDependencies = (
);
productName = "HotPocket (iOS)";
productReference = 4CABCAB02E56F0C900D8A354 /* HotPocket.app */;
productType = "com.apple.product-type.application";
};
4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4CABCB142E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket (macOS)" */;
buildPhases = (
4CABCAC22E56F0C900D8A354 /* Sources */,
4CABCAC32E56F0C900D8A354 /* Frameworks */,
4CABCAC42E56F0C900D8A354 /* Resources */,
4CABCB132E56F0C900D8A354 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
4CABCAE22E56F0C900D8A354 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4CABCAC72E56F0C900D8A354 /* macOS (App) */,
);
name = "HotPocket (macOS)";
packageProductDependencies = (
);
productName = "HotPocket (macOS)";
productReference = 4CABCAC62E56F0C900D8A354 /* HotPocket.app */;
productType = "com.apple.product-type.application";
};
4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4CABCB052E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket Extension (iOS)" */;
buildPhases = (
4CABCAD12E56F0C900D8A354 /* Sources */,
4CABCAD22E56F0C900D8A354 /* Frameworks */,
4CABCAD32E56F0C900D8A354 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
4CABCAD92E56F0C900D8A354 /* iOS (Extension) */,
);
name = "HotPocket Extension (iOS)";
packageProductDependencies = (
);
productName = "HotPocket Extension (iOS)";
productReference = 4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4CABCB0F2E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket Extension (macOS)" */;
buildPhases = (
4CABCADB2E56F0C900D8A354 /* Sources */,
4CABCADC2E56F0C900D8A354 /* Frameworks */,
4CABCADD2E56F0C900D8A354 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
4CABCAE32E56F0C900D8A354 /* macOS (Extension) */,
);
name = "HotPocket Extension (macOS)";
packageProductDependencies = (
);
productName = "HotPocket Extension (macOS)";
productReference = 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
4CABCA922E56F0C800D8A354 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastUpgradeCheck = 1640;
TargetAttributes = {
4CABCAAF2E56F0C900D8A354 = {
CreatedOnToolsVersion = 16.4;
};
4CABCAC52E56F0C900D8A354 = {
CreatedOnToolsVersion = 16.4;
};
4CABCAD42E56F0C900D8A354 = {
CreatedOnToolsVersion = 16.4;
};
4CABCADE2E56F0C900D8A354 = {
CreatedOnToolsVersion = 16.4;
};
};
};
buildConfigurationList = 4CABCA952E56F0C800D8A354 /* Build configuration list for PBXProject "HotPocket" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 4CABCA912E56F0C800D8A354;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 4CABCAB12E56F0C900D8A354 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */,
4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */,
4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */,
4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
4CABCAAE2E56F0C900D8A354 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAC42E56F0C900D8A354 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAD32E56F0C900D8A354 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCADD2E56F0C900D8A354 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
4CABCAAC2E56F0C900D8A354 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAC22E56F0C900D8A354 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAD12E56F0C900D8A354 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCADB2E56F0C900D8A354 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
4CABCAD82E56F0C900D8A354 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */;
targetProxy = 4CABCAD72E56F0C900D8A354 /* PBXContainerItemProxy */;
};
4CABCAE22E56F0C900D8A354 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */;
targetProxy = 4CABCAE12E56F0C900D8A354 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
4CABCB062E56F0C900D8A354 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.HotPocket.iOS.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
4CABCB072E56F0C900D8A354 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.HotPocket.iOS.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
4CABCB0B2E56F0C900D8A354 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS;
PRODUCT_NAME = HotPocket;
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
4CABCB0C2E56F0C900D8A354 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.iOS;
PRODUCT_NAME = HotPocket;
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
4CABCB102E56F0C900D8A354 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "macOS (Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Debug;
};
4CABCB112E56F0C900D8A354 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "macOS (Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Release;
};
4CABCB152E56F0C900D8A354 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS;
PRODUCT_NAME = HotPocket;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Debug;
};
4CABCB162E56F0C900D8A354 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSMainStoryboardFile = Main;
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
"-framework",
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.macOS;
PRODUCT_NAME = HotPocket;
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Release;
};
4CABCB192E56F0C900D8A354 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
};
name = Debug;
};
4CABCB1A2E56F0C900D8A354 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
4CABCA952E56F0C800D8A354 /* Build configuration list for PBXProject "HotPocket" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4CABCB192E56F0C900D8A354 /* Debug */,
4CABCB1A2E56F0C900D8A354 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4CABCB052E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket Extension (iOS)" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4CABCB062E56F0C900D8A354 /* Debug */,
4CABCB072E56F0C900D8A354 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4CABCB0A2E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket (iOS)" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4CABCB0B2E56F0C900D8A354 /* Debug */,
4CABCB0C2E56F0C900D8A354 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4CABCB0F2E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket Extension (macOS)" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4CABCB102E56F0C900D8A354 /* Debug */,
4CABCB112E56F0C900D8A354 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4CABCB142E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket (macOS)" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4CABCB152E56F0C900D8A354 /* Debug */,
4CABCB162E56F0C900D8A354 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 4CABCA922E56F0C800D8A354 /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xED",
"green" : "0xBA",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,93 @@
{
"images" : [
{
"filename" : "icon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "icon-1024 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "icon-1024 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "icon-16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon-32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon-32 1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon-64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon-mac-1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x29",
"green" : "0x25",
"red" : "0x21"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "icon-large-128.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "icon-large-128@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "icon-large-128@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xDD",
"green" : "0x82",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="stylesheet" href="../Style.css">
<script src="../Script.js" defer></script>
</head>
<body>
<img src="../icon-mac-384.png" width="128" height="128" alt="HotPocket Icon">
<p class="platform-ios">You can turn on Save to Hotpocket Safari extension in Settings.</p>
<p class="platform-mac state-unknown">You can turn on Save to Hotpocket extension in Safari Extensions preferences.</p>
<p class="platform-mac state-on">Save to Hotpocket extension is currently on. You can turn it off in Safari Extensions preferences.</p>
<p class="platform-mac state-off">Save to Hotpocket extension is currently off. You can turn it on in Safari Extensions preferences.</p>
<button class="platform-mac open-preferences">Quit and Open Safari Extensions Preferences…</button>
</body>
</html>

View File

@ -0,0 +1,24 @@
function show(platform, enabled, useSettingsInsteadOfPreferences) {
document.body.classList.add(`platform-${platform}`);
if (useSettingsInsteadOfPreferences) {
document.getElementsByClassName('platform-mac state-on')[0].innerText = "Save to Hotpocket extension is currently on. You can turn it off in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac state-off')[0].innerText = "Save to Hotpocket extension is currently off. You can turn it on in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on Save to Hotpocket extension in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…";
}
if (typeof enabled === "boolean") {
document.body.classList.toggle(`state-on`, enabled);
document.body.classList.toggle(`state-off`, !enabled);
} else {
document.body.classList.remove(`state-on`);
document.body.classList.remove(`state-off`);
}
}
function openPreferences() {
webkit.messageHandlers.controller.postMessage("open-preferences");
}
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);

View File

@ -0,0 +1,63 @@
* {
-webkit-user-select: none;
-webkit-user-drag: none;
cursor: default;
}
:root {
color-scheme: light dark;
--spacing: 20px;
}
html {
height: 100%;
}
body {
background: #212529;
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing);
margin: 0 calc(var(--spacing) * 2);
height: 100%;
font: -apple-system-short-body;
text-align: center;
}
body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) {
display: none;
}
body.platform-ios .platform-mac {
display: none;
}
body.platform-mac .platform-ios {
display: none;
}
body.platform-ios .platform-mac {
display: none;
}
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
display: none;
}
body.state-on :is(.state-off, .state-unknown) {
display: none;
}
body.state-off :is(.state-on, .state-unknown) {
display: none;
}
button {
font-size: 1em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,26 @@
//
// ViewController.h
// Shared (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <TargetConditionals.h>
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
typedef UIViewController PlatformViewController;
#elif TARGET_OS_OSX
#import <Cocoa/Cocoa.h>
typedef NSViewController PlatformViewController;
#endif
@interface ViewController : PlatformViewController
@end

View File

@ -0,0 +1,76 @@
//
// ViewController.m
// Shared (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "ViewController.h"
#import <WebKit/WebKit.h>
#if TARGET_OS_OSX
#import <SafariServices/SafariServices.h>
#endif
static NSString * const extensionBundleIdentifier = @"pl.bthlabs.HotPocket.HotPocket.Extension";
@interface ViewController () <WKNavigationDelegate, WKScriptMessageHandler>
@property (nonatomic) IBOutlet WKWebView *webView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_webView.navigationDelegate = self;
#if TARGET_OS_IOS
_webView.scrollView.scrollEnabled = NO;
#endif
[_webView.configuration.userContentController addScriptMessageHandler:self name:@"controller"];
[_webView loadFileURL:[NSBundle.mainBundle URLForResource:@"Main" withExtension:@"html"] allowingReadAccessToURL:NSBundle.mainBundle.resourceURL];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
#if TARGET_OS_IOS
[webView evaluateJavaScript:@"show('ios')" completionHandler:nil];
#elif TARGET_OS_OSX
[webView evaluateJavaScript:@"show('mac')" completionHandler:nil];
[SFSafariExtensionManager getStateOfSafariExtensionWithIdentifier:extensionBundleIdentifier completionHandler:^(SFSafariExtensionState *state, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!state) {
// Insert code to inform the user something went wrong.
return;
}
NSString *isExtensionEnabledAsString = state.isEnabled ? @"true" : @"false";
if (@available(macOS 13, *))
[webView evaluateJavaScript:[NSString stringWithFormat:@"show('mac', %@, true)", isExtensionEnabledAsString] completionHandler:nil];
else
[webView evaluateJavaScript:[NSString stringWithFormat:@"show('mac', %@, false)", isExtensionEnabledAsString] completionHandler:nil];
});
}];
#endif
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
#if TARGET_OS_OSX
if (![message.body isEqualToString:@"open-preferences"])
return;
[SFSafariApplication showPreferencesForExtensionWithIdentifier:extensionBundleIdentifier completionHandler:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp terminate:self];
});
}];
#endif
}
@end

View File

@ -0,0 +1,12 @@
//
// SafariWebExtensionHandler.h
// Shared (Extension)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <Foundation/Foundation.h>
@interface SafariWebExtensionHandler : NSObject <NSExtensionRequestHandling>
@end

View File

@ -0,0 +1,43 @@
//
// SafariWebExtensionHandler.m
// Shared (Extension)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "SafariWebExtensionHandler.h"
#import <SafariServices/SafariServices.h>
@implementation SafariWebExtensionHandler
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context {
NSExtensionItem *request = context.inputItems.firstObject;
NSUUID *profile;
if (@available(iOS 17.0, macOS 14.0, *)) {
profile = request.userInfo[SFExtensionProfileKey];
} else {
profile = request.userInfo[@"profile"];
}
id message;
if (@available(iOS 15.0, macOS 11.0, *)) {
message = request.userInfo[SFExtensionMessageKey];
} else {
message = request.userInfo[@"message"];
}
NSLog(@"Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", message, profile.UUIDString ?: @"none");
NSExtensionItem *response = [[NSExtensionItem alloc] init];
if (@available(iOS 15.0, macOS 11.0, *)) {
response.userInfo = @{ SFExtensionMessageKey: @{ @"echo": message } };
} else {
response.userInfo = @{ @"message": @{ @"echo": message } };
}
[context completeRequestReturningItems:@[ response ] completionHandler:nil];
}
@end

View File

@ -0,0 +1,12 @@
//
// AppDelegate.h
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end

View File

@ -0,0 +1,21 @@
//
// AppDelegate.m
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "AppDelegate.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return YES;
}
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
@end

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6HG-Um-bch">
<rect key="frame" x="131" y="363" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="LargeIcon"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" name="BackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LargeIcon" width="128" height="128"/>
<namedColor name="BackgroundColor">
<color red="0.12941176470588237" green="0.14509803921568629" blue="0.16078431372549021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<wkWebView contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RDB-ib-igF">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="BackgroundColor"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" name="BackgroundColor"/>
</view>
<connections>
<outlet property="webView" destination="RDB-ib-igF" id="avx-RC-qRB"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<namedColor name="BackgroundColor">
<color red="0.12941176470588237" green="0.14509803921568629" blue="0.16078431372549021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,14 @@
//
// SceneDelegate.h
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow * window;
@end

View File

@ -0,0 +1,12 @@
//
// SceneDelegate.m
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "SceneDelegate.h"
@implementation SceneDelegate
@end

View File

@ -0,0 +1,18 @@
//
// main.m
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString *appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.Safari.web-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>SafariWebExtensionHandler</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,12 @@
//
// AppDelegate.h
// macOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate>
@end

View File

@ -0,0 +1,20 @@
//
// AppDelegate.m
// macOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "AppDelegate.h"
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// Override point for customization after application launch.
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
return YES;
}
@end

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
<plugIn identifier="com.apple.WebKit2IBPlugin" version="23727"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="HotPocket" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="HotPocket" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About HotPocket" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Hide HotPocket" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit HotPocket" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="HotPocket Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="76" y="-134"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="HotPocket" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" sceneMemberID="viewController">
<view key="view" appearanceType="darkAqua" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
</subviews>
</view>
<connections>
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="655"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,15 @@
//
// main.m
// macOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
return NSApplicationMain(argc, argv);
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.Safari.web-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>SafariWebExtensionHandler</string>
</dict>
</dict>
</plist>

View File

@ -64,4 +64,10 @@ export default defineConfig([
'no-invalid-this': 'off', 'no-invalid-this': 'off',
}, },
}, },
{
ignores: [
'hotpocket_backend/apps/**/static/**/*.js',
'hotpocket_backend/static/**/*.js',
],
},
]); ]);

View File

@ -1 +1,2 @@
from .access_token import AccessToken # noqa: F401
from .account import AccountAdmin # noqa: F401 from .account import AccountAdmin # noqa: F401

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.contrib import admin
from hotpocket_backend.apps.accounts.models import AccessToken
class AccessTokenAdmin(admin.ModelAdmin):
list_display = ('pk', 'account_uuid', 'origin', 'created_at', 'is_active')
search_fields = ('pk', 'account_uuid', 'key', 'origin')
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
admin.site.register(AccessToken, AccessTokenAdmin)

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
import typing
from django.contrib.auth.backends import ModelBackend, UserModel
from django.http import HttpRequest
from hotpocket_backend.apps.accounts.models import AccessToken, Account
LOGGER = logging.getLogger(__name__)
class AccessTokenBackend(ModelBackend):
def authenticate(self,
request: HttpRequest,
access_token: AccessToken | None,
) -> Account | None:
if not access_token:
return None
try:
user = UserModel.objects.get(pk=access_token.account_uuid)
except UserModel.DoesNotExist as exception:
LOGGER.error(
'Unhandled exception in AccessToken auth: %s',
exception,
exc_info=exception,
)
if self.user_can_authenticate(user) is False:
return None
request.access_token = access_token
return typing.cast(Account, user)

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
from django.contrib import auth
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.utils.deprecation import MiddlewareMixin
from hotpocket_backend.apps.accounts.models import AccessToken, Account
LOGGER = logging.getLogger(__name__)
class AccessTokenMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest):
if not hasattr(request, 'user'):
raise ImproperlyConfigured('No `AuthenticationMiddleware`?')
authorization_header = request.headers.get('Authorization', None)
if authorization_header is None:
return
try:
scheme, authorization = authorization_header.split(' ', maxsplit=1)
assert scheme == 'Bearer', (
f'Unsupported authorization scheme: `{scheme}`'
)
access_token = AccessToken.active_objects.get(key=authorization)
except (ValueError, AssertionError, AccessToken.DoesNotExist, Account.DoesNotExist) as exception:
LOGGER.error(
'Unhandled exception in AccessToken middleware: %s',
exception,
exc_info=exception,
)
return
account = auth.authenticate(request, access_token=access_token)
if account:
request.user = account

View File

@ -0,0 +1,31 @@
# Generated by Django 5.2.3 on 2025-09-04 18:50
import uuid6
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_alter_account_settings_and_more'),
]
operations = [
migrations.CreateModel(
name='AccessToken',
fields=[
('id', models.UUIDField(default=uuid6.uuid7, editable=False, primary_key=True, serialize=False)),
('account_uuid', models.UUIDField(db_index=True, default=None)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('deleted_at', models.DateTimeField(blank=True, db_index=True, default=None, editable=False, null=True)),
('key', models.CharField(db_index=True, default=None, editable=False, max_length=128, unique=True)),
('origin', models.CharField(db_index=True, default=None)),
('meta', models.JSONField(blank=True, default=dict, null=True)),
],
options={
'verbose_name': 'Access Token',
'verbose_name_plural': 'Access Tokens',
},
),
]

View File

@ -1 +1,2 @@
from .access_token import AccessToken # noqa: F401
from .account import Account # noqa: F401 from .account import Account # noqa: F401

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.db import models
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.core.models import Model
class ActiveAccessTokensManager(models.Manager):
def get_queryset(self) -> models.QuerySet[AccessToken]:
return super().get_queryset().filter(
deleted_at__isnull=True,
)
class AccessToken(Model):
key = models.CharField(
blank=False,
default=None,
null=False,
max_length=128,
db_index=True,
unique=True,
editable=False,
)
origin = models.CharField(
blank=False, default=None, null=False, db_index=True,
)
meta = models.JSONField(blank=True, default=dict, null=True)
objects = models.Manager()
active_objects = ActiveAccessTokensManager()
class Meta:
verbose_name = _('Access Token')
verbose_name_plural = _('Access Tokens')
def __str__(self) -> str:
return f'<AccessToken pk={self.pk} account_uuid={self.account_uuid}>'

View File

@ -0,0 +1 @@
from .access_tokens import AccessTokensService # noqa: F401

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import hashlib
import hmac
import logging
import uuid
from django.db import models
import uuid6
from hotpocket_backend.apps.accounts.models import AccessToken
from hotpocket_backend.apps.core.conf import settings
from hotpocket_soa.dto.accounts import AccessTokensQuery
LOGGER = logging.getLogger(__name__)
class AccessTokensService:
class AccessTokensServiceError(Exception):
pass
class AccessTokenNotFound(AccessTokensServiceError):
pass
def create(self,
*,
account_uuid: uuid.UUID,
origin: str,
meta: dict,
) -> AccessToken:
pk = uuid6.uuid7()
key = hmac.new(
settings.SECRET_KEY.encode('ascii'),
msg=pk.bytes,
digestmod=hashlib.sha256,
)
return AccessToken.objects.create(
pk=pk,
account_uuid=account_uuid,
key=key.hexdigest(),
origin=origin,
meta=meta,
)
def get(self, *, pk: uuid.UUID) -> AccessToken:
try:
query_set = AccessToken.active_objects
return query_set.get(pk=pk)
except AccessToken.DoesNotExist as exception:
raise self.AccessTokenNotFound(
f'Access Token not found: pk=`{pk}`',
) from exception
def search(self,
*,
query: AccessTokensQuery,
offset: int = 0,
limit: int = 10,
order_by: str = '-pk',
) -> models.QuerySet[AccessToken]:
filters = [
models.Q(account_uuid=query.account_uuid),
]
if query.before is not None:
filters.append(models.Q(pk__lt=query.before))
result = AccessToken.active_objects.\
filter(*filters).\
order_by(order_by)
return result[offset:offset + limit]
def delete(self, *, pk: uuid.UUID) -> bool:
access_token = self.get(pk=pk)
access_token.soft_delete()
return True

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import typing
from bthlabs_jsonrpc_django import (
DjangoExecutor,
DjangoJSONRPCSerializer,
JSONRPCView as BaseJSONRPCView,
)
from django.core.exceptions import ValidationError
import uuid6
class JSONRPCSerializer(DjangoJSONRPCSerializer):
STRING_COERCIBLE_TYPES: typing.Any = (
*DjangoJSONRPCSerializer.STRING_COERCIBLE_TYPES,
uuid6.UUID,
)
def serialize_value(self, value: typing.Any) -> typing.Any:
if isinstance(value, ValidationError):
result: typing.Any = None
if hasattr(value, 'error_dict') is True:
result = {}
for field, errors in value.error_dict.items():
result[field] = [
error.code
for error
in errors
]
elif hasattr(value, 'error_list') is True:
result = [
error.code
for error in value.error_list
]
else:
result = value.code
return self.serialize_value(result)
return super().serialize_value(value)
class Executor(DjangoExecutor):
serializer = JSONRPCSerializer
class JSONRPCView(BaseJSONRPCView):
executor = Executor

View File

@ -3,6 +3,8 @@ from __future__ import annotations
import enum import enum
from django.utils.translation import gettext_lazy as _
class MessageLevelAlertClass(enum.Enum): class MessageLevelAlertClass(enum.Enum):
debug = 'alert-secondary' debug = 'alert-secondary'
@ -15,3 +17,7 @@ class MessageLevelAlertClass(enum.Enum):
class StarUnstarAssociationViewMode(enum.Enum): class StarUnstarAssociationViewMode(enum.Enum):
STAR = 'STAR' STAR = 'STAR'
UNSTAR = 'UNSTAR' UNSTAR = 'UNSTAR'
class UIAccessTokenOriginApp(enum.Enum):
SAFARI_WEB_EXTENSION = _('Safari Web Extension')

View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from .base import BrowseParams as BaseBrowseParams
class AppsBrowseParams(BaseBrowseParams):
pass

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from crispy_forms.layout import Submit
from django import forms
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.ui.forms.base import ConfirmationMixin, Form
class AppForm(Form):
pass
class ConfirmationForm(ConfirmationMixin, AppForm):
origin_app = forms.CharField(
label=_('App'),
required=False,
disabled=True,
show_hidden_initial=True,
)
platform = forms.CharField(
label=_('Platform'),
required=False,
disabled=True,
show_hidden_initial=True,
)
version = forms.CharField(
label=_('Version'),
required=False,
disabled=True,
show_hidden_initial=True,
)
def get_layout_fields(self) -> list[str]:
return [
'canhazconfirm',
'origin_app',
'platform',
'version',
]
class DeleteForm(ConfirmationForm):
def get_submit_button(self) -> Submit:
return Submit('submit', _('Delete'), css_class='btn btn-danger')

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
from django.contrib.auth.forms import (
AuthenticationForm as BaseAuthenticationForm,
)
from django.utils.translation import gettext_lazy as _
class LoginForm(BaseAuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.attrs = {
'id': self.__class__.__name__,
'novalidate': '',
}
self.helper.layout = Layout(
'username',
'password',
FormActions(
Submit('submit', _('Log in'), css_class='btn btn-primary'),
template='ui/ui/forms/formactions.html',
css_class='mb-0',
),
)

View File

@ -6,32 +6,11 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit from crispy_forms.layout import Layout, Submit
from django import forms from django import forms
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AuthenticationForm as BaseAuthenticationForm,
PasswordChangeForm as BasePasswordChangeForm, PasswordChangeForm as BasePasswordChangeForm,
) )
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .base import Form from hotpocket_backend.apps.ui.forms.base import Form
class LoginForm(BaseAuthenticationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.attrs = {
'id': self.__class__.__name__,
'novalidate': '',
}
self.helper.layout = Layout(
'username',
'password',
FormActions(
Submit('submit', _('Log in'), css_class='btn btn-primary'),
template='ui/ui/forms/formactions.html',
css_class='mb-0',
),
)
class ProfileForm(Form): class ProfileForm(Form):
@ -131,17 +110,17 @@ class PasswordForm(BasePasswordChangeForm):
class FederatedPasswordForm(PasswordForm): class FederatedPasswordForm(PasswordForm):
current_password = forms.CharField( old_password = forms.CharField(
label=_('Old password'), label=_('Old password'),
disabled=True, disabled=True,
required=False, required=False,
) )
new_password = forms.CharField( new_password1 = forms.CharField(
label=_('New password'), label=_('New password'),
disabled=True, disabled=True,
required=False, required=False,
) )
new_password_again = forms.CharField( new_password2 = forms.CharField(
label=_('New password confirmation'), label=_('New password confirmation'),
disabled=True, disabled=True,
required=False, required=False,

View File

@ -5,19 +5,14 @@ from crispy_forms.layout import Submit
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .base import Form from .base import ConfirmationMixin, Form
class AssociationForm(Form): class AssociationForm(Form):
pass pass
class ConfirmationForm(AssociationForm): class ConfirmationForm(ConfirmationMixin, AssociationForm):
canhazconfirm = forms.CharField(
label='',
required=True,
widget=forms.HiddenInput,
)
title = forms.CharField( title = forms.CharField(
label=_('Title'), label=_('Title'),
required=False, required=False,

View File

@ -61,3 +61,11 @@ class Form(forms.Form):
template=self.get_form_actions_template(), template=self.get_form_actions_template(),
), ),
) )
class ConfirmationMixin(forms.Form):
canhazconfirm = forms.CharField(
label='',
required=True,
widget=forms.HiddenInput,
)

View File

@ -0,0 +1,2 @@
from . import accounts # noqa: F401
from . import saves # noqa: F401

View File

@ -0,0 +1,2 @@
from . import access_tokens # noqa: F401
from . import auth # noqa: F401

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_soa.services import AccessTokensService
LOGGER = logging.getLogger(__name__)
@register_method('accounts.access_tokens.create')
def create(request: HttpRequest,
auth_key: str,
meta: dict,
) -> str:
with db.transaction.atomic():
try:
assert 'extension_auth_key' in request.session, 'Auth key missing'
assert request.session['extension_auth_key'] == auth_key, (
'Auth key mismatch'
)
except AssertionError as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
exc_info=exception,
)
raise
access_token = AccessTokensService().create(
account_uuid=request.user.pk,
origin=request.META['HTTP_ORIGIN'],
meta=meta,
)
request.session.pop('extension_auth_key')
request.session.save()
return access_token.key

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from bthlabs_jsonrpc_core import register_method
from django.http import HttpRequest
@register_method('accounts.auth.check')
def check(request: HttpRequest) -> bool:
return request.user.is_anonymous is False

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
from hotpocket_soa.dto.associations import AssociationOut
@register_method(method='saves.create')
def create(request: HttpRequest, url: str) -> AssociationOut:
with db.transaction.atomic():
association = CreateSaveWorkflow().run_rpc(
request=request,
account=request.user,
url=url,
)
return association

View File

@ -1,3 +1,4 @@
from .access_tokens import UIAccessTokensService # noqa: F401
from .associations import UIAssociationsService # noqa: F401 from .associations import UIAssociationsService # noqa: F401
from .imports import UIImportsService # noqa: F401 from .imports import UIImportsService # noqa: F401
from .saves import UISavesService # noqa: F401 from .saves import UISavesService # noqa: F401

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
import uuid
from django.core.exceptions import PermissionDenied
from django.http import Http404
from hotpocket_soa.dto.accounts import AccessTokenOut
from hotpocket_soa.services import AccessTokensService
LOGGER = logging.getLogger(__name__)
class UIAccessTokensService:
def __init__(self):
self.access_tokens_service = AccessTokensService()
def get_or_404(self,
*,
account_uuid: uuid.UUID,
pk: uuid.UUID,
) -> AccessTokenOut:
try:
return AccessTokensService().get(
account_uuid=account_uuid,
pk=pk,
)
except AccessTokensService.AccessTokenNotFound as exception:
LOGGER.error(
'Access Token not found: account_uuid=`%s` pk=`%s`',
account_uuid,
pk,
exc_info=exception,
)
raise Http404()
except AccessTokensService.AccessTokenAccessDenied as exception:
LOGGER.error(
'Access Token access denied: account_uuid=`%s` pk=`%s`',
account_uuid,
pk,
exc_info=exception,
)
raise PermissionDenied()

View File

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from bthlabs_jsonrpc_core import JSONRPCInternalError
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError
import django.db import django.db
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
@ -9,19 +11,19 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.types import PAccount from hotpocket_backend.apps.accounts.types import PAccount
from hotpocket_soa.dto.saves import SaveIn from hotpocket_soa.dto.associations import AssociationOut
from hotpocket_soa.dto.celery import AsyncResultOut
from hotpocket_soa.dto.saves import SaveIn, SaveOut
from hotpocket_soa.services import SavesService
from .base import SaveWorkflow from .base import SaveWorkflow
class CreateSaveWorkflow(SaveWorkflow): class CreateSaveWorkflow(SaveWorkflow):
def run(self, def create_associate_and_process(self,
*, account: PAccount,
request: HttpRequest, url: str,
account: PAccount, ) -> tuple[SaveOut, AssociationOut, AsyncResultOut | None]:
url: str,
force_post_save: bool = False,
) -> HttpResponse:
with django.db.transaction.atomic(): with django.db.transaction.atomic():
save = self.create( save = self.create(
account.pk, account.pk,
@ -30,6 +32,23 @@ class CreateSaveWorkflow(SaveWorkflow):
association = self.associate(account.pk, save) association = self.associate(account.pk, save)
processing_result: AsyncResultOut | None = None
if save.last_processed_at is None:
processing_result = self.schedule_processing(save)
return (save, association, processing_result)
def run(self,
*,
request: HttpRequest,
account: PAccount,
url: str,
force_post_save: bool = False,
) -> HttpResponse:
save, association, processing_result = self.create_associate_and_process(
account, url,
)
response = redirect(reverse('ui.associations.browse')) response = redirect(reverse('ui.associations.browse'))
if force_post_save is True or save.is_netloc_banned is True: if force_post_save is True or save.is_netloc_banned is True:
response = redirect(reverse( response = redirect(reverse(
@ -46,7 +65,22 @@ class CreateSaveWorkflow(SaveWorkflow):
response.headers['X-HotPocket-Testing-Save-PK'] = save.pk response.headers['X-HotPocket-Testing-Save-PK'] = save.pk
response.headers['X-HotPocket-Testing-Association-PK'] = association.pk response.headers['X-HotPocket-Testing-Association-PK'] = association.pk
if save.last_processed_at is None:
processing_result = self.schedule_processing(save) # noqa: F841
return response return response
def run_rpc(self,
*,
request: HttpRequest,
account: PAccount,
url: str,
) -> AssociationOut:
try:
save, association, processing_result = self.create_associate_and_process(
account, url,
)
return association
except SavesService.SavesServiceError as exception:
if isinstance(exception.__cause__, ValidationError) is True:
raise JSONRPCInternalError(data=exception.__cause__)
raise

View File

@ -36,7 +36,7 @@ body:not(.ui-js-enabled) .ui-noscript-hide {
} }
#navbar .ui-navbar-brand > img { #navbar .ui-navbar-brand > img {
border-radius: 0.25rem; border-radius: 20%;
height: 1.5rem; height: 1.5rem;
vertical-align: top; vertical-align: top;
} }
@ -45,11 +45,11 @@ body:not(.ui-js-enabled) .ui-noscript-hide {
height: 100%; height: 100%;
} }
.ui-save-card .card-footer .spinner-border { .spinner-border.ui-htmx-indicator {
display: none; display: none;
} }
.ui-save-card .card-footer .spinner-border.htmx-request { .spinner-border.ui-htmx-indicator.htmx-request {
display: block; display: block;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,63 @@
/*!
* HotPocket by BTHLabs (https://hotpocket.app/)
* Copyright 2025-present BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
((HotPocket, htmx) => {
class BrowseAccountAppsView {
constructor (app) {
this.app = app;
}
onLoad = (event) => {
document.addEventListener(
'HotPocket:BrowseAccountAppsView:updateLoadMoreButton',
this.onUpdateLoadMoreButton,
);
document.addEventListener(
'HotPocket:BrowseAccountAppsView:delete',
this.onDelete,
);
};
onUpdateLoadMoreButton = (event) => {
const button = document.querySelector('#BrowseAccountAppsView .ui-load-more-button');
if (button) {
if (event.detail.next_url) {
button.setAttribute('hx-get', event.detail.next_url);
button.classList.remove('disable');
button.removeAttribute('disabled', true);
} else {
button.classList.add('disable');
button.setAttribute('disabled', true);
}
htmx.process('#BrowseAccountAppsView .ui-load-more-button');
}
};
onDelete = (event) => {
if (event.detail && event.detail.pk) {
const elementsToRemove = document.querySelectorAll(
`[data-access-token="${event.detail.pk}"]`,
);
for (let elementToRemove of elementsToRemove) {
elementToRemove.remove();
}
}
};
}
HotPocket.addPlugin('UI.BrowseAccountAppsView', (app) => {
return new BrowseAccountAppsView(app);
});
})(window.HotPocket, window.htmx);

View File

@ -20,21 +20,25 @@
this.app = app; this.app = app;
} }
onLoad = (event) => { onLoad = (event) => {
document.addEventListener('HotPocket:BrowseSavesView:updateLoadMoreButton', (event) => { document.addEventListener(
const button = document.querySelector('#BrowseSavesView .ui-load-more-button'); 'HotPocket:BrowseSavesView:updateLoadMoreButton',
if (button) { this.onUpdateLoadMoreButton,
if (event.detail.next_url) { );
button.setAttribute('hx-get', event.detail.next_url); };
button.classList.remove('disable'); onUpdateLoadMoreButton = (event) => {
button.removeAttribute('disabled', true); const button = document.querySelector('#BrowseSavesView .ui-load-more-button');
} else { if (button) {
button.classList.add('disable'); if (event.detail.next_url) {
button.setAttribute('disabled', true); button.setAttribute('hx-get', event.detail.next_url);
} button.classList.remove('disable');
button.removeAttribute('disabled', true);
htmx.process('#BrowseSavesView .ui-load-more-button'); } else {
button.classList.add('disable');
button.setAttribute('disabled', true);
} }
});
htmx.process('#BrowseSavesView .ui-load-more-button');
}
}; };
} }

View File

@ -0,0 +1,68 @@
{% extends "ui/page.html" %}
{% load crispy_forms_tags i18n static ui %}
{% block title %}{% translate 'Apps' %} | {% translate 'Account' %}{% endblock %}
{% block page_body %}
<div id="BrowseAccountAppsView" class="container">
<p class="display-6 my-3">{% translate 'Apps' %}</p>
{% include 'ui/accounts/partials/nav.html' with active_tab='apps' %}
<table class="table table-striped">
<thead>
<tr>
<td>{% translate 'App' %}</td>
<td>{% translate 'Platform' %}</td>
<td>{% translate 'Version' %}</td>
<td>{% translate 'Authorized at' %}</td>
</tr>
</thead>
<tbody class="ui-account-apps">
{% include 'ui/accounts/partials/apps/apps.html' with access_tokens=access_tokens params=params %}
</tbody>
</table>
<p class="my-3 text-center {% if not access_tokens and params.before is None %}d-none{% endif %}">
<button
class="btn btn-primary {% if not before %}disabled{% endif %} ui-noscript-hide ui-load-more-button"
hx-get="{{ next_url }}"
hx-push-url="true"
hx-swap="beforeend"
hx-target="#BrowseAccountAppsView .ui-account-apps"
>
{% translate 'Load more' %}
</button>
<a
class="btn btn-primary {% if not before %}disabled{% endif %} ui-noscript-show"
href="{{ next_url }}"
>
{% translate 'Load more' %}
</a>
</p>
<template id="BrowseAccountAppsView-DeleteModal">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% translate 'Delete an app authorization?' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Close' %}"></button>
</div>
<div class="modal-body">
{% include 'ui/accounts/partials/apps/delete_confirmation.html' with alert_class="mb-0" %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate 'Cancel' %}</button>
<button type="button" class="btn btn-danger" data-ui-modal-action="confirm">
{% translate 'Delete' %}
</button>
</div>
</div>
</div>
</div>
</template>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "ui/page.html" %}
{% load crispy_forms_tags i18n static ui %}
{% block title %}{% translate 'Delete an app authorization?' %} | {% translate 'Account' %}{% endblock %}
{% block page_body %}
<div id="BrowseAccountAppsView" class="container">
<p class="display-6 my-3">{% translate 'Delete an app authorization?' %}</p>
{% include 'ui/accounts/partials/nav.html' with active_tab='apps' %}
{% include 'ui/accounts/partials/apps/delete_confirmation.html' %}
{% crispy form %}
</div>
{% endblock %}

View File

@ -0,0 +1,44 @@
{% load i18n static ui %}
{% for access_token in access_tokens %}
<tr data-access-token="{{ access_token.pk }}">
<td>{{ access_token|render_access_token_app }}</td>
<td>{{ access_token|render_access_token_platform }}</td>
<td><code>{{ access_token.meta.version }}</code></td>
<td>{{ access_token.created_at }}</td>
</tr>
<tr data-access-token="{{ access_token.pk }}">
<td colspan="4">
<div class="d-flex justify-content-end align-items-center">
<div class="spinner-border spinner-border-sm ui-htmx-indicator" role="status">
<span class="visually-hidden">{% translate 'Processing' %}</span>
</div>
<a
class="btn btn-sm btn-danger ms-2"
data-ui-modal="#BrowseAccountAppsView-DeleteModal"
hx-confirm='CANHAZCONFIRM'
hx-indicator='[data-access-token="{{ access_token.pk }}"] .ui-htmx-indicator'
hx-post="{% url 'ui.accounts.apps.delete' pk=access_token.pk %}"
hx-swap="delete"
hx-target='[data-access-token="{{ access_token.pk }}"]'
hx-vars='{"canhazconfirm":true}'
href="{% url 'ui.accounts.apps.delete' pk=access_token.pk %}"
>
<i class="bi bi-trash3-fill"></i> {% translate 'Delete' %}
</a>
</div>
</td>
</tr>
{% empty %}
{% if not HTMX %}
<tr>
<td colspan="4">
{% if params.before is None %}
<strong>{% translate "You haven't authorized any apps yet." %}</strong>
{% else %}
<span>{% translate "You've reached the end of the line." %}</span>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}

View File

@ -0,0 +1,7 @@
{% load i18n static ui %}
<div class="alert alert-danger {{ alert_class|default_if_none:'' }}">
<h4 class="alert-heading">{% translate 'Point of no return' %}</h4>
<p class="lead mb-0">{% translate 'Are you sure you want to delete this app authorization?' %}</p>
<p class="mb-0"><strong>You'll need to authorize again if you ever change your mind.</strong></p>
</div>

View File

@ -5,7 +5,7 @@
{% if active_tab == 'profile' %} {% if active_tab == 'profile' %}
<a class="nav-link active" aria-current="page" href="#"> <a class="nav-link active" aria-current="page" href="#">
{% else %} {% else %}
<a class="nav-link" aria-current="page" href="{% url 'ui.accounts.settings.profile' %}"> <a class="nav-link" href="{% url 'ui.accounts.settings.profile' %}">
{% endif %} {% endif %}
{% translate 'Profile' %} {% translate 'Profile' %}
</a> </a>
@ -14,7 +14,7 @@
{% if active_tab == 'password' %} {% if active_tab == 'password' %}
<a class="nav-link active" aria-current="page" href="#"> <a class="nav-link active" aria-current="page" href="#">
{% else %} {% else %}
<a class="nav-link" aria-current="page" href="{% url 'ui.accounts.settings.password' %}"> <a class="nav-link" href="{% url 'ui.accounts.settings.password' %}">
{% endif %} {% endif %}
{% translate 'Password' %} {% translate 'Password' %}
</a> </a>
@ -23,9 +23,18 @@
{% if active_tab == 'settings' %} {% if active_tab == 'settings' %}
<a class="nav-link active" aria-current="page" href="#"> <a class="nav-link active" aria-current="page" href="#">
{% else %} {% else %}
<a class="nav-link" aria-current="page" href="{% url 'ui.accounts.settings.settings' %}"> <a class="nav-link" href="{% url 'ui.accounts.settings.settings' %}">
{% endif %} {% endif %}
{% translate 'Settings' %} {% translate 'Settings' %}
</a> </a>
</li> </li>
<li class="nav-item">
{% if active_tab == 'apps' %}
<a class="nav-link active" aria-current="page" href="#">
{% else %}
<a class="nav-link" href="{% url 'ui.accounts.apps.index' %}">
{% endif %}
{% translate 'Apps' %}
</a>
</li>
</ul> </ul>

View File

@ -19,7 +19,7 @@
<a href="{{ association.target.url }}" target="_blank" rel="noopener noreferer"><small>{{ association.target.url|render_url_domain }}</small></a> <a href="{{ association.target.url }}" target="_blank" rel="noopener noreferer"><small>{{ association.target.url|render_url_domain }}</small></a>
<div class="ms-auto flex-shrink-0 d-flex align-items-center"> <div class="ms-auto flex-shrink-0 d-flex align-items-center">
{% if not association.archived_at %} {% if not association.archived_at %}
<div class="spinner-border spinner-border-sm" role="status"> <div class="spinner-border spinner-border-sm ui-htmx-indicator" role="status">
<span class="visually-hidden">{% translate 'Processing' %}</span> <span class="visually-hidden">{% translate 'Processing' %}</span>
</div> </div>
{% if association.is_starred %} {% if association.is_starred %}
@ -35,7 +35,7 @@
<a <a
class="dropdown-item" class="dropdown-item"
hx-get="{% url 'ui.associations.unstar' pk=association.pk %}" hx-get="{% url 'ui.associations.unstar' pk=association.pk %}"
hx-indicator='[data-association="{{ association.pk }}"] .spinner-border' hx-indicator='[data-association="{{ association.pk }}"] .ui-htmx-indicator'
hx-swap="innerHTML" hx-swap="innerHTML"
hx-target='[data-association="{{ association.pk }}"]' hx-target='[data-association="{{ association.pk }}"]'
href="{% url 'ui.associations.unstar' pk=association.pk %}" href="{% url 'ui.associations.unstar' pk=association.pk %}"
@ -48,7 +48,7 @@
<a <a
class="dropdown-item" class="dropdown-item"
hx-get="{% url 'ui.associations.star' pk=association.pk %}" hx-get="{% url 'ui.associations.star' pk=association.pk %}"
hx-indicator='[data-association="{{ association.pk }}"] .spinner-border' hx-indicator='[data-association="{{ association.pk }}"] .ui-htmx-indicator'
hx-swap="innerHTML" hx-swap="innerHTML"
hx-target='[data-association="{{ association.pk }}"]' hx-target='[data-association="{{ association.pk }}"]'
href="{% url 'ui.associations.star' pk=association.pk %}" href="{% url 'ui.associations.star' pk=association.pk %}"
@ -70,7 +70,7 @@
class="dropdown-item text-warning" class="dropdown-item text-warning"
data-ui-modal="#BrowseSavesView-RefreshModal" data-ui-modal="#BrowseSavesView-RefreshModal"
hx-confirm='CANHAZCONFIRM' hx-confirm='CANHAZCONFIRM'
hx-indicator='[data-association="{{ association.pk }}"] .spinner-border' hx-indicator='[data-association="{{ association.pk }}"] .ui-htmx-indicator'
hx-post="{% url 'ui.associations.refresh' pk=association.pk %}" hx-post="{% url 'ui.associations.refresh' pk=association.pk %}"
hx-swap="none" hx-swap="none"
hx-target='[data-association="{{ association.pk }}"]' hx-target='[data-association="{{ association.pk }}"]'

View File

@ -0,0 +1,18 @@
{% extends "ui/page.html" %}
{% load i18n static ui %}
{% block title %}{% translate 'Redirecting back to the extension...' %}{% endblock %}
{% block button_bar_class %}d-none{% endblock %}
{% block page_body %}
<div class="container">
<div class="alert alert-success mt-3" role="alert">
<h4 class="alert-heading">{% translate 'Done!' %}</h4>
<p class="lead mb-0">
{% translate "You've successfully logged in to the extension." %}
</p>
</div>
</div>
{% endblock %}

View File

@ -10,7 +10,7 @@
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle ui-navbar-brand" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle ui-navbar-brand" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{% spaceless %} {% spaceless %}
<img src="{% static 'ui/img/icon-180.png' %}"> <img src="{% static 'ui/img/icon-48.png' %}">
<span class="ms-2">{% block top_nav_title %}HotPocket{% endblock %}</span> <span class="ms-2">{% block top_nav_title %}HotPocket{% endblock %}</span>
{% endspaceless %} {% endspaceless %}
</a> </a>
@ -35,7 +35,7 @@
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link pe-none ui-navbar-brand"> <a class="nav-link pe-none ui-navbar-brand">
<img src="{% static 'ui/img/icon-180.png' %}"> <img src="{% static 'ui/img/icon-48.png' %}">
<span class="ms-2">{{ SITE_TITLE }}</span> <span class="ms-2">{{ SITE_TITLE }}</span>
</a> </a>
</li> </li>
@ -146,6 +146,7 @@
<script src="{% static 'ui/js/hotpocket-backend.ui.Modal.js' %}" type="text/javascript"></script> <script src="{% static 'ui/js/hotpocket-backend.ui.Modal.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.BrowseSavesView.js' %}" type="text/javascript"></script> <script src="{% static 'ui/js/hotpocket-backend.ui.BrowseSavesView.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.ViewAssociationView.js' %}" type="text/javascript"></script> <script src="{% static 'ui/js/hotpocket-backend.ui.ViewAssociationView.js' %}" type="text/javascript"></script>
<script src="{% static 'ui/js/hotpocket-backend.ui.BrowseAccountAppsView.js' %}" type="text/javascript"></script>
{% block page_scripts %}{% endblock %} {% block page_scripts %}{% endblock %}
<script type="text/javascript"> <script type="text/javascript">
(() => { (() => {

View File

@ -8,10 +8,19 @@ import urllib.parse
from django import template from django import template
from django.contrib.messages import Message from django.contrib.messages import Message
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.ui.constants import MessageLevelAlertClass from hotpocket_backend.apps.ui.constants import (
MessageLevelAlertClass,
UIAccessTokenOriginApp,
)
from hotpocket_backend.apps.ui.dto.base import BrowseParams from hotpocket_backend.apps.ui.dto.base import BrowseParams
from hotpocket_common.constants import AssociationsSearchMode from hotpocket_common.constants import (
AccessTokenOriginApp,
AssociationsSearchMode,
)
from hotpocket_soa.dto.accounts import AccessTokenOut
from hotpocket_soa.dto.saves import SaveOut from hotpocket_soa.dto.saves import SaveOut
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -115,3 +124,49 @@ def alert_class(message: Message | None) -> str:
) )
return 'alert-secondary' return 'alert-secondary'
@register.filter(name='render_access_token_app')
def render_access_token_app(access_token: AccessTokenOut) -> str:
app: str = access_token.get_origin_app_id()
variant = 'secondary'
origin_app = access_token.get_origin_app()
match origin_app:
case AccessTokenOriginApp.SAFARI_WEB_EXTENSION:
app = UIAccessTokenOriginApp[origin_app.value].value
variant = 'info'
return format_html(
'<span class="badge text-bg-{}">{}</span>',
variant,
app,
)
@register.filter(name='render_access_token_platform')
def render_access_token_platform(access_token: AccessTokenOut) -> str:
match access_token.meta.get('platform', None):
case 'MacIntel':
return 'macOS'
case 'iPhone':
return 'iOS'
case 'iPad':
return 'iPadOS'
case 'Win32':
return 'Windows'
case 'Linux x86_64':
return 'Linux'
case 'Linux armv81':
return 'Linux'
case None:
return _('Unknown')
case _:
return access_token.meta['platform']

View File

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from bthlabs_jsonrpc_django import is_authenticated
from django.urls import path from django.urls import path
from hotpocket_backend.apps.core.rpc import JSONRPCView
from hotpocket_backend.apps.ui.constants import StarUnstarAssociationViewMode from hotpocket_backend.apps.ui.constants import StarUnstarAssociationViewMode
# isort: off # isort: off
@ -20,33 +22,44 @@ from .views import (
urlpatterns = [ urlpatterns = [
path( path(
'accounts/login/', 'accounts/login/',
accounts.LoginView.as_view(), accounts.auth.LoginView.as_view(),
name='ui.accounts.login', name='ui.accounts.login',
), ),
path( path(
'accounts/post-login/', 'accounts/post-login/',
accounts.PostLoginView.as_view(), accounts.auth.PostLoginView.as_view(),
name='ui.accounts.post_login', name='ui.accounts.post_login',
), ),
path('accounts/logout/', accounts.logout, name='ui.accounts.logout'), path('accounts/logout/', accounts.auth.logout, name='ui.accounts.logout'),
path('accounts/browse/', accounts.browse, name='ui.accounts.browse'), path('accounts/browse/', accounts.browse.browse, name='ui.accounts.browse'),
path('accounts/settings/', accounts.settings, name='ui.accounts.settings'), path('accounts/settings/', accounts.settings.settings, name='ui.accounts.settings'),
path( path(
'accounts/settings/profile/', 'accounts/settings/profile/',
accounts.ProfileView.as_view(), accounts.settings.ProfileView.as_view(),
name='ui.accounts.settings.profile', name='ui.accounts.settings.profile',
), ),
path( path(
'accounts/settings/password/', 'accounts/settings/password/',
accounts.PasswordView.as_view(), accounts.settings.PasswordView.as_view(),
name='ui.accounts.settings.password', name='ui.accounts.settings.password',
), ),
path( path(
'accounts/settings/settings/', 'accounts/settings/settings/',
accounts.SettingsView.as_view(), accounts.settings.SettingsView.as_view(),
name='ui.accounts.settings.settings', name='ui.accounts.settings.settings',
), ),
path('accounts/', accounts.index, name='ui.accounts.index'), path('accounts/apps/', accounts.apps.index, name='ui.accounts.apps.index'),
path(
'accounts/apps/browse/',
accounts.apps.browse,
name='ui.accounts.apps.browse',
),
path(
'accounts/apps/delete/<str:pk>',
accounts.apps.DeleteView.as_view(),
name='ui.accounts.apps.delete',
),
path('accounts/', accounts.index.index, name='ui.accounts.index'),
path( path(
'imports/pocket/', 'imports/pocket/',
imports.PocketImportView.as_view(), imports.PocketImportView.as_view(),
@ -62,6 +75,16 @@ urlpatterns = [
integrations.android.share_sheet, integrations.android.share_sheet,
name='ui.integrations.android.share_sheet', name='ui.integrations.android.share_sheet',
), ),
path(
'integrations/extension/authenticate/',
integrations.extension.authenticate,
name='ui.integrations.extension.authenticate',
),
path(
'integrations/extension/post-authenticate/',
integrations.extension.post_authenticate,
name='ui.integrations.extension.post_authenticate',
),
path( path(
'saves/create/', 'saves/create/',
saves.CreateView.as_view(), saves.CreateView.as_view(),
@ -107,5 +130,12 @@ urlpatterns = [
), ),
path('associations/', associations.index, name='ui.associations.index'), path('associations/', associations.index, name='ui.associations.index'),
path('manifest.json', meta.manifest_json, name='ui.meta.manifest_json'), path('manifest.json', meta.manifest_json, name='ui.meta.manifest_json'),
path(
'rpc/',
JSONRPCView.as_view(
auth_checks=[is_authenticated],
),
name='ui.rpc',
),
path('', index.index, name='ui.index.index'), path('', index.index, name='ui.index.index'),
] ]

View File

@ -0,0 +1,5 @@
from . import apps # noqaz: F401
from . import auth # noqa: F401
from . import browse # noqa: F401
from . import index # noqa: F401
from . import settings # noqa: F401

View File

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import uuid
from django.contrib import messages
import django.db
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from django_htmx.http import trigger_client_event
from hotpocket_backend.apps.accounts.decorators import account_required
from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin
from hotpocket_backend.apps.htmx import messages as htmx_messages
from hotpocket_backend.apps.ui.constants import UIAccessTokenOriginApp
from hotpocket_backend.apps.ui.dto.accounts import AppsBrowseParams
from hotpocket_backend.apps.ui.forms.accounts.apps import DeleteForm
from hotpocket_backend.apps.ui.services import UIAccessTokensService
from hotpocket_soa.dto.accounts import AccessTokenOut, AccessTokensQuery
from hotpocket_soa.services import AccessTokensService
class AccessTokenMixin:
def get_access_token(self) -> AccessTokenOut:
if hasattr(self, '_access_token') is False:
setattr(
self,
'_access_token',
UIAccessTokensService().get_or_404(
account_uuid=self.request.user.pk, # type: ignore[attr-defined]
pk=self.kwargs['pk'], # type: ignore[attr-defined]
),
)
return self._access_token # type: ignore[attr-defined]
class DetailView(AccessTokenMixin, AccountRequiredMixin, FormView):
def get_context_data(self, **kwargs) -> dict:
result = super().get_context_data(**kwargs)
result.update({
'access_token': self.get_access_token(),
})
return result
class ConfirmationView(DetailView):
def get_initial(self) -> dict:
result = super().get_initial()
access_token: AccessTokenOut = self.get_access_token()
origin_app = access_token.get_origin_app()
if origin_app is not None:
origin_app = UIAccessTokenOriginApp[origin_app.value].value
result.update({
'canhazconfirm': 'hai',
'origin_app': origin_app or access_token.get_origin_app_id(),
'platform': access_token.meta.get('platform', '-'),
'version': access_token.meta.get('version', '-'),
})
return result
def get_success_url(self) -> str:
return reverse('ui.accounts.apps.browse')
@account_required
def index(request: HttpRequest) -> HttpResponse:
return redirect(reverse('ui.accounts.apps.browse'))
@account_required
def browse(request: HttpRequest) -> HttpResponse:
params = AppsBrowseParams.from_request(request=request)
access_tokens = AccessTokensService().search(
query=AccessTokensQuery.model_validate(dict(
account_uuid=request.user.pk,
before=params.before,
)),
limit=params.limit,
)
before: uuid.UUID | None = None
if len(access_tokens) == params.limit:
before = access_tokens[-1].pk
next_url: str | None = None
if before is not None:
next_url = reverse('ui.accounts.apps.browse', query=[
('before', before),
('limit', params.limit),
])
context = {
'access_tokens': access_tokens,
'params': params,
'before': before,
'next_url': next_url,
}
if request.htmx:
response = render(
request,
'ui/accounts/partials/apps/apps.html',
context,
)
return trigger_client_event(
response,
'HotPocket:BrowseAccountAppsView:updateLoadMoreButton',
{'next_url': next_url},
after='swap',
)
return render(
request,
'ui/accounts/apps/browse.html',
context,
)
class DeleteView(ConfirmationView):
template_name = 'ui/accounts/apps/delete.html'
form_class = DeleteForm
def form_valid(self, form: DeleteForm) -> HttpResponse:
with django.db.transaction.atomic():
result = AccessTokensService().delete(
access_token=self.get_access_token(),
)
if self.request.htmx:
response = JsonResponse({
'status': 'ok',
'result': result,
})
htmx_messages.add_htmx_message(
request=self.request,
response=response,
level=htmx_messages.SUCCESS,
message=_('The app auhtorization has been deleted.'),
)
return trigger_client_event(
response,
'HotPocket:BrowseAccountAppsView:delete',
{'pk': self.kwargs['pk']},
after='swap',
)
messages.add_message(
self.request,
messages.SUCCESS,
_('The app auhtorization has been deleted.'),
)
return super().form_valid(form)

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.views import LoginView as BaseLoginView
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
from django.views.generic import RedirectView
from hotpocket_backend.apps.core.conf import settings as django_settings
from hotpocket_backend.apps.ui.forms.accounts.auth import LoginForm
class LoginView(BaseLoginView):
template_name = 'ui/accounts/login.html'
form_class = LoginForm
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
request.session['post_login_next_url'] = request.GET.get('next', None)
request.session.save()
return super().get(request, *args, **kwargs)
def get_success_url(self) -> str:
return reverse('ui.accounts.post_login')
class PostLoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs) -> str:
next_url = self.request.session.pop('post_login_next_url', None)
self.request.session.save()
allowed_hosts = None
if len(django_settings.ALLOWED_HOSTS) > 0:
allowed_hosts = set(filter(
lambda value: value != '*',
django_settings.ALLOWED_HOSTS,
))
if next_url is not None:
next_url_is_safe = url_has_allowed_host_and_scheme(
url=next_url,
allowed_hosts=allowed_hosts,
require_https=self.request.is_secure(),
)
if next_url_is_safe is False:
next_url = None
return next_url or reverse('ui.index.index')
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if request.user.is_anonymous is True:
raise PermissionDenied('NOPE')
return super().get(request, *args, **kwargs)
def logout(request: HttpRequest) -> HttpResponse:
if request.user.is_authenticated is True:
auth_logout(request)
return render(request, 'ui/accounts/logout.html')

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.http import Http404, HttpRequest, HttpResponse
from hotpocket_backend.apps.accounts.decorators import account_required
@account_required
def browse(request: HttpRequest) -> HttpResponse:
raise Http404()

Some files were not shown because too many files have changed in this diff Show More