diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index bf34b9c..0287407 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -61,14 +61,29 @@ jobs: push: false load: true 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" run: | set -x docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm backend-ci inv ci - name: "Run `packages` checks" + if: always() run: | set -x 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" if: always() run: | diff --git a/docker-bake.json b/docker-bake.json index 7e99f73..51a16d5 100644 --- a/docker-bake.json +++ b/docker-bake.json @@ -4,6 +4,7 @@ "targets": [ "backend-management", "caddy", + "extension-management", "keycloak", "packages-management", "postgres", @@ -67,6 +68,28 @@ "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": { "context": "services/", "dockerfile": "caddy/Dockerfile", diff --git a/docker-compose-ci.yaml b/docker-compose-ci.yaml index a89a36b..ea1dc20 100644 --- a/docker-compose-ci.yaml +++ b/docker-compose-ci.yaml @@ -13,3 +13,4 @@ services: include: - path: "./services/backend/docker-compose-ci.yaml" - path: "./services/packages/docker-compose-ci.yaml" + - path: "./services/extension/docker-compose-ci.yaml" diff --git a/docker-compose.yaml b/docker-compose.yaml index ddb80d1..f4e24c7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,6 +5,7 @@ include: - path: "./docker-compose-cloud.yaml" - path: "./services/backend/docker-compose.yaml" - path: "./services/packages/docker-compose.yaml" + - path: "./services/extension/docker-compose.yaml" volumes: {} diff --git a/services/.dockerignore b/services/.dockerignore index 0ce45c8..68b7604 100644 --- a/services/.dockerignore +++ b/services/.dockerignore @@ -1,4 +1,5 @@ _tmp/ +apple/ backend/node_modules/ backend/ops/metal/ backend/hotpocket_backend/playground.py @@ -7,4 +8,6 @@ backend/hotpocket_backend/settings/docker/ backend/hotpocket_backend/secrets/metal/ backend/hotpocket_backend/settings/metal/ backend/hotpocket_backend/static/ +extension/node_modules/ +extension/dist/ .envrc* diff --git a/services/apple/.gitignore b/services/apple/.gitignore new file mode 100644 index 0000000..911edee --- /dev/null +++ b/services/apple/.gitignore @@ -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 diff --git a/services/apple/HotPocket.xcodeproj/project.pbxproj b/services/apple/HotPocket.xcodeproj/project.pbxproj new file mode 100644 index 0000000..18f8697 --- /dev/null +++ b/services/apple/HotPocket.xcodeproj/project.pbxproj @@ -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 = ""; + }; + 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 = ""; + }; + 4CABCAB22E56F0C900D8A354 /* iOS (App) */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4CABCB0D2E56F0C900D8A354 /* Exceptions for "iOS (App)" folder in "HotPocket (iOS)" target */, + ); + path = "iOS (App)"; + sourceTree = ""; + }; + 4CABCAC72E56F0C900D8A354 /* macOS (App) */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "macOS (App)"; + sourceTree = ""; + }; + 4CABCAD92E56F0C900D8A354 /* iOS (Extension) */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4CABCB082E56F0C900D8A354 /* Exceptions for "iOS (Extension)" folder in "HotPocket Extension (iOS)" target */, + ); + path = "iOS (Extension)"; + sourceTree = ""; + }; + 4CABCAE32E56F0C900D8A354 /* macOS (Extension) */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4CABCB122E56F0C900D8A354 /* Exceptions for "macOS (Extension)" folder in "HotPocket Extension (macOS)" target */, + ); + path = "macOS (Extension)"; + sourceTree = ""; + }; +/* 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 = ""; + }; + 4CABCAB12E56F0C900D8A354 /* Products */ = { + isa = PBXGroup; + children = ( + 4CABCAB02E56F0C900D8A354 /* HotPocket.app */, + 4CABCAC62E56F0C900D8A354 /* HotPocket.app */, + 4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */, + 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */, + ); + name = Products; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/services/apple/HotPocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/services/apple/HotPocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/services/apple/HotPocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/services/apple/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json b/services/apple/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..43943b2 --- /dev/null +++ b/services/apple/Shared (App)/Assets.xcassets/AccentColor.colorset/Contents.json @@ -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 + } +} diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..7ba1282 --- /dev/null +++ b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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 + } +} diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024 1.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024 1.png new file mode 100644 index 0000000..0ef60f0 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024 1.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024 2.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024 2.png new file mode 100644 index 0000000..0ef60f0 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024 2.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100644 index 0000000..0ef60f0 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-16.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-16.png new file mode 100644 index 0000000..c74bd86 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-16.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-32 1.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-32 1.png new file mode 100644 index 0000000..dab6b99 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-32 1.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-32.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-32.png new file mode 100644 index 0000000..dab6b99 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-32.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-64.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-64.png new file mode 100644 index 0000000..7c391df Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-64.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-mac-1024.png b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-mac-1024.png new file mode 100644 index 0000000..046d9d0 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/AppIcon.appiconset/icon-mac-1024.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/BackgroundColor.colorset/Contents.json b/services/apple/Shared (App)/Assets.xcassets/BackgroundColor.colorset/Contents.json new file mode 100644 index 0000000..65807d7 --- /dev/null +++ b/services/apple/Shared (App)/Assets.xcassets/BackgroundColor.colorset/Contents.json @@ -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 + } +} diff --git a/services/apple/Shared (App)/Assets.xcassets/Contents.json b/services/apple/Shared (App)/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/services/apple/Shared (App)/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json new file mode 100644 index 0000000..e941ea3 --- /dev/null +++ b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/Contents.json @@ -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 + } +} diff --git a/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128.png b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128.png new file mode 100644 index 0000000..f99be1c Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128@2x.png b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128@2x.png new file mode 100644 index 0000000..7765914 Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128@2x.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128@3x.png b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128@3x.png new file mode 100644 index 0000000..73df64b Binary files /dev/null and b/services/apple/Shared (App)/Assets.xcassets/LargeIcon.imageset/icon-large-128@3x.png differ diff --git a/services/apple/Shared (App)/Assets.xcassets/SecondaryColor.colorset/Contents.json b/services/apple/Shared (App)/Assets.xcassets/SecondaryColor.colorset/Contents.json new file mode 100644 index 0000000..69fcd08 --- /dev/null +++ b/services/apple/Shared (App)/Assets.xcassets/SecondaryColor.colorset/Contents.json @@ -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 + } +} diff --git a/services/apple/Shared (App)/Resources/Base.lproj/Main.html b/services/apple/Shared (App)/Resources/Base.lproj/Main.html new file mode 100644 index 0000000..28b6f6f --- /dev/null +++ b/services/apple/Shared (App)/Resources/Base.lproj/Main.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + HotPocket Icon +

You can turn on Save to Hotpocket Safari extension in Settings.

+

You can turn on Save to Hotpocket extension in Safari Extensions preferences.

+

Save to Hotpocket extension is currently on. You can turn it off in Safari Extensions preferences.

+

Save to Hotpocket extension is currently off. You can turn it on in Safari Extensions preferences.

+ + + diff --git a/services/apple/Shared (App)/Resources/Script.js b/services/apple/Shared (App)/Resources/Script.js new file mode 100644 index 0000000..5ed12c9 --- /dev/null +++ b/services/apple/Shared (App)/Resources/Script.js @@ -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); diff --git a/services/apple/Shared (App)/Resources/Style.css b/services/apple/Shared (App)/Resources/Style.css new file mode 100644 index 0000000..fb90ccd --- /dev/null +++ b/services/apple/Shared (App)/Resources/Style.css @@ -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; +} diff --git a/services/apple/Shared (App)/Resources/icon-mac-384.png b/services/apple/Shared (App)/Resources/icon-mac-384.png new file mode 100644 index 0000000..d1901cc Binary files /dev/null and b/services/apple/Shared (App)/Resources/icon-mac-384.png differ diff --git a/services/apple/Shared (App)/ViewController.h b/services/apple/Shared (App)/ViewController.h new file mode 100644 index 0000000..25eea78 --- /dev/null +++ b/services/apple/Shared (App)/ViewController.h @@ -0,0 +1,26 @@ +// +// ViewController.h +// Shared (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import + +#if TARGET_OS_IOS + +#import + +typedef UIViewController PlatformViewController; + +#elif TARGET_OS_OSX + +#import + +typedef NSViewController PlatformViewController; + +#endif + +@interface ViewController : PlatformViewController + +@end diff --git a/services/apple/Shared (App)/ViewController.m b/services/apple/Shared (App)/ViewController.m new file mode 100644 index 0000000..3c76975 --- /dev/null +++ b/services/apple/Shared (App)/ViewController.m @@ -0,0 +1,76 @@ +// +// ViewController.m +// Shared (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import "ViewController.h" + +#import + +#if TARGET_OS_OSX +#import +#endif + +static NSString * const extensionBundleIdentifier = @"pl.bthlabs.HotPocket.HotPocket.Extension"; + +@interface ViewController () + +@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 diff --git a/services/apple/Shared (Extension)/Resources/.placeholder b/services/apple/Shared (Extension)/Resources/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/services/apple/Shared (Extension)/SafariWebExtensionHandler.h b/services/apple/Shared (Extension)/SafariWebExtensionHandler.h new file mode 100644 index 0000000..c26d4aa --- /dev/null +++ b/services/apple/Shared (Extension)/SafariWebExtensionHandler.h @@ -0,0 +1,12 @@ +// +// SafariWebExtensionHandler.h +// Shared (Extension) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import + +@interface SafariWebExtensionHandler : NSObject + +@end diff --git a/services/apple/Shared (Extension)/SafariWebExtensionHandler.m b/services/apple/Shared (Extension)/SafariWebExtensionHandler.m new file mode 100644 index 0000000..f21f33c --- /dev/null +++ b/services/apple/Shared (Extension)/SafariWebExtensionHandler.m @@ -0,0 +1,43 @@ +// +// SafariWebExtensionHandler.m +// Shared (Extension) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import "SafariWebExtensionHandler.h" + +#import + +@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 diff --git a/services/apple/iOS (App)/AppDelegate.h b/services/apple/iOS (App)/AppDelegate.h new file mode 100644 index 0000000..aa9c146 --- /dev/null +++ b/services/apple/iOS (App)/AppDelegate.h @@ -0,0 +1,12 @@ +// +// AppDelegate.h +// iOS (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import + +@interface AppDelegate : UIResponder + +@end diff --git a/services/apple/iOS (App)/AppDelegate.m b/services/apple/iOS (App)/AppDelegate.m new file mode 100644 index 0000000..ceb53ae --- /dev/null +++ b/services/apple/iOS (App)/AppDelegate.m @@ -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 diff --git a/services/apple/iOS (App)/Base.lproj/LaunchScreen.storyboard b/services/apple/iOS (App)/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..04af28d --- /dev/null +++ b/services/apple/iOS (App)/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/apple/iOS (App)/Base.lproj/Main.storyboard b/services/apple/iOS (App)/Base.lproj/Main.storyboard new file mode 100644 index 0000000..a01c47b --- /dev/null +++ b/services/apple/iOS (App)/Base.lproj/Main.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/apple/iOS (App)/Info.plist b/services/apple/iOS (App)/Info.plist new file mode 100644 index 0000000..81ed29b --- /dev/null +++ b/services/apple/iOS (App)/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/services/apple/iOS (App)/SceneDelegate.h b/services/apple/iOS (App)/SceneDelegate.h new file mode 100644 index 0000000..9b50a02 --- /dev/null +++ b/services/apple/iOS (App)/SceneDelegate.h @@ -0,0 +1,14 @@ +// +// SceneDelegate.h +// iOS (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow * window; + +@end diff --git a/services/apple/iOS (App)/SceneDelegate.m b/services/apple/iOS (App)/SceneDelegate.m new file mode 100644 index 0000000..4e08865 --- /dev/null +++ b/services/apple/iOS (App)/SceneDelegate.m @@ -0,0 +1,12 @@ +// +// SceneDelegate.m +// iOS (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import "SceneDelegate.h" + +@implementation SceneDelegate + +@end diff --git a/services/apple/iOS (App)/main.m b/services/apple/iOS (App)/main.m new file mode 100644 index 0000000..c413b33 --- /dev/null +++ b/services/apple/iOS (App)/main.m @@ -0,0 +1,18 @@ +// +// main.m +// iOS (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import +#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); +} diff --git a/services/apple/iOS (Extension)/Info.plist b/services/apple/iOS (Extension)/Info.plist new file mode 100644 index 0000000..3302434 --- /dev/null +++ b/services/apple/iOS (Extension)/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.Safari.web-extension + NSExtensionPrincipalClass + SafariWebExtensionHandler + + + diff --git a/services/apple/macOS (App)/AppDelegate.h b/services/apple/macOS (App)/AppDelegate.h new file mode 100644 index 0000000..9a4217f --- /dev/null +++ b/services/apple/macOS (App)/AppDelegate.h @@ -0,0 +1,12 @@ +// +// AppDelegate.h +// macOS (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import + +@interface AppDelegate : NSObject + +@end diff --git a/services/apple/macOS (App)/AppDelegate.m b/services/apple/macOS (App)/AppDelegate.m new file mode 100644 index 0000000..6f43d11 --- /dev/null +++ b/services/apple/macOS (App)/AppDelegate.m @@ -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 diff --git a/services/apple/macOS (App)/Base.lproj/Main.storyboard b/services/apple/macOS (App)/Base.lproj/Main.storyboard new file mode 100644 index 0000000..802e36f --- /dev/null +++ b/services/apple/macOS (App)/Base.lproj/Main.storyboard @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/apple/macOS (App)/HotPocket.entitlements b/services/apple/macOS (App)/HotPocket.entitlements new file mode 100644 index 0000000..625af03 --- /dev/null +++ b/services/apple/macOS (App)/HotPocket.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/services/apple/macOS (App)/main.m b/services/apple/macOS (App)/main.m new file mode 100644 index 0000000..d839785 --- /dev/null +++ b/services/apple/macOS (App)/main.m @@ -0,0 +1,15 @@ +// +// main.m +// macOS (App) +// +// Created by Tomek Wójcik on 21/08/2025. +// + +#import + +int main(int argc, const char * argv[]) { + @autoreleasepool { + // Setup code that might create autoreleased objects goes here. + } + return NSApplicationMain(argc, argv); +} diff --git a/services/apple/macOS (Extension)/HotPocket.entitlements b/services/apple/macOS (Extension)/HotPocket.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/services/apple/macOS (Extension)/HotPocket.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/services/apple/macOS (Extension)/Info.plist b/services/apple/macOS (Extension)/Info.plist new file mode 100644 index 0000000..3302434 --- /dev/null +++ b/services/apple/macOS (Extension)/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.Safari.web-extension + NSExtensionPrincipalClass + SafariWebExtensionHandler + + + diff --git a/services/backend/eslint.config.js b/services/backend/eslint.config.js index 649f90d..65559f2 100644 --- a/services/backend/eslint.config.js +++ b/services/backend/eslint.config.js @@ -64,4 +64,10 @@ export default defineConfig([ 'no-invalid-this': 'off', }, }, + { + ignores: [ + 'hotpocket_backend/apps/**/static/**/*.js', + 'hotpocket_backend/static/**/*.js', + ], + }, ]); diff --git a/services/backend/hotpocket_backend/apps/accounts/admin/__init__.py b/services/backend/hotpocket_backend/apps/accounts/admin/__init__.py index 6463d76..3dd6ff3 100644 --- a/services/backend/hotpocket_backend/apps/accounts/admin/__init__.py +++ b/services/backend/hotpocket_backend/apps/accounts/admin/__init__.py @@ -1 +1,2 @@ +from .access_token import AccessToken # noqa: F401 from .account import AccountAdmin # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/accounts/admin/access_token.py b/services/backend/hotpocket_backend/apps/accounts/admin/access_token.py new file mode 100644 index 0000000..a06ee38 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/admin/access_token.py @@ -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) diff --git a/services/backend/hotpocket_backend/apps/accounts/backends.py b/services/backend/hotpocket_backend/apps/accounts/backends.py new file mode 100644 index 0000000..6d0bf1f --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/backends.py @@ -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) diff --git a/services/backend/hotpocket_backend/apps/accounts/middleware.py b/services/backend/hotpocket_backend/apps/accounts/middleware.py new file mode 100644 index 0000000..6c9bce4 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/middleware.py @@ -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 diff --git a/services/backend/hotpocket_backend/apps/accounts/migrations/0005_accesstoken.py b/services/backend/hotpocket_backend/apps/accounts/migrations/0005_accesstoken.py new file mode 100644 index 0000000..0889117 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/migrations/0005_accesstoken.py @@ -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', + }, + ), + ] diff --git a/services/backend/hotpocket_backend/apps/accounts/models/__init__.py b/services/backend/hotpocket_backend/apps/accounts/models/__init__.py index f55b238..236027c 100644 --- a/services/backend/hotpocket_backend/apps/accounts/models/__init__.py +++ b/services/backend/hotpocket_backend/apps/accounts/models/__init__.py @@ -1 +1,2 @@ +from .access_token import AccessToken # noqa: F401 from .account import Account # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/accounts/models/access_token.py b/services/backend/hotpocket_backend/apps/accounts/models/access_token.py new file mode 100644 index 0000000..bd79c86 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/models/access_token.py @@ -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'' diff --git a/services/backend/hotpocket_backend/apps/accounts/services/__init__.py b/services/backend/hotpocket_backend/apps/accounts/services/__init__.py new file mode 100644 index 0000000..951ad1d --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/services/__init__.py @@ -0,0 +1 @@ +from .access_tokens import AccessTokensService # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/accounts/services/access_tokens.py b/services/backend/hotpocket_backend/apps/accounts/services/access_tokens.py new file mode 100644 index 0000000..14c5b02 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/accounts/services/access_tokens.py @@ -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 diff --git a/services/backend/hotpocket_backend/apps/core/rpc.py b/services/backend/hotpocket_backend/apps/core/rpc.py new file mode 100644 index 0000000..f4a0ffb --- /dev/null +++ b/services/backend/hotpocket_backend/apps/core/rpc.py @@ -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 diff --git a/services/backend/hotpocket_backend/apps/ui/constants.py b/services/backend/hotpocket_backend/apps/ui/constants.py index dca0fa6..df440d2 100644 --- a/services/backend/hotpocket_backend/apps/ui/constants.py +++ b/services/backend/hotpocket_backend/apps/ui/constants.py @@ -3,6 +3,8 @@ from __future__ import annotations import enum +from django.utils.translation import gettext_lazy as _ + class MessageLevelAlertClass(enum.Enum): debug = 'alert-secondary' @@ -15,3 +17,7 @@ class MessageLevelAlertClass(enum.Enum): class StarUnstarAssociationViewMode(enum.Enum): STAR = 'STAR' UNSTAR = 'UNSTAR' + + +class UIAccessTokenOriginApp(enum.Enum): + SAFARI_WEB_EXTENSION = _('Safari Web Extension') diff --git a/services/backend/hotpocket_backend/apps/ui/dto/accounts.py b/services/backend/hotpocket_backend/apps/ui/dto/accounts.py new file mode 100644 index 0000000..b0a96a9 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/dto/accounts.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from .base import BrowseParams as BaseBrowseParams + + +class AppsBrowseParams(BaseBrowseParams): + pass diff --git a/services/backend/hotpocket_backend/apps/ui/forms/accounts/__init__.py b/services/backend/hotpocket_backend/apps/ui/forms/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/hotpocket_backend/apps/ui/forms/accounts/apps.py b/services/backend/hotpocket_backend/apps/ui/forms/accounts/apps.py new file mode 100644 index 0000000..5afd5b7 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/forms/accounts/apps.py @@ -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') diff --git a/services/backend/hotpocket_backend/apps/ui/forms/accounts/auth.py b/services/backend/hotpocket_backend/apps/ui/forms/accounts/auth.py new file mode 100644 index 0000000..b984206 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/forms/accounts/auth.py @@ -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', + ), + ) diff --git a/services/backend/hotpocket_backend/apps/ui/forms/accounts.py b/services/backend/hotpocket_backend/apps/ui/forms/accounts/settings.py similarity index 85% rename from services/backend/hotpocket_backend/apps/ui/forms/accounts.py rename to services/backend/hotpocket_backend/apps/ui/forms/accounts/settings.py index 2f5de28..f822db1 100644 --- a/services/backend/hotpocket_backend/apps/ui/forms/accounts.py +++ b/services/backend/hotpocket_backend/apps/ui/forms/accounts/settings.py @@ -6,32 +6,11 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit from django import forms from django.contrib.auth.forms import ( - AuthenticationForm as BaseAuthenticationForm, PasswordChangeForm as BasePasswordChangeForm, ) from django.utils.translation import gettext_lazy as _ -from .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', - ), - ) +from hotpocket_backend.apps.ui.forms.base import Form class ProfileForm(Form): @@ -131,17 +110,17 @@ class PasswordForm(BasePasswordChangeForm): class FederatedPasswordForm(PasswordForm): - current_password = forms.CharField( + old_password = forms.CharField( label=_('Old password'), disabled=True, required=False, ) - new_password = forms.CharField( + new_password1 = forms.CharField( label=_('New password'), disabled=True, required=False, ) - new_password_again = forms.CharField( + new_password2 = forms.CharField( label=_('New password confirmation'), disabled=True, required=False, diff --git a/services/backend/hotpocket_backend/apps/ui/forms/associations.py b/services/backend/hotpocket_backend/apps/ui/forms/associations.py index 270c1cf..f870a03 100644 --- a/services/backend/hotpocket_backend/apps/ui/forms/associations.py +++ b/services/backend/hotpocket_backend/apps/ui/forms/associations.py @@ -5,19 +5,14 @@ from crispy_forms.layout import Submit from django import forms from django.utils.translation import gettext_lazy as _ -from .base import Form +from .base import ConfirmationMixin, Form class AssociationForm(Form): pass -class ConfirmationForm(AssociationForm): - canhazconfirm = forms.CharField( - label='', - required=True, - widget=forms.HiddenInput, - ) +class ConfirmationForm(ConfirmationMixin, AssociationForm): title = forms.CharField( label=_('Title'), required=False, diff --git a/services/backend/hotpocket_backend/apps/ui/forms/base.py b/services/backend/hotpocket_backend/apps/ui/forms/base.py index aa04608..7055a36 100644 --- a/services/backend/hotpocket_backend/apps/ui/forms/base.py +++ b/services/backend/hotpocket_backend/apps/ui/forms/base.py @@ -61,3 +61,11 @@ class Form(forms.Form): template=self.get_form_actions_template(), ), ) + + +class ConfirmationMixin(forms.Form): + canhazconfirm = forms.CharField( + label='', + required=True, + widget=forms.HiddenInput, + ) diff --git a/services/backend/hotpocket_backend/apps/ui/rpc_methods/__init__.py b/services/backend/hotpocket_backend/apps/ui/rpc_methods/__init__.py new file mode 100644 index 0000000..c725c31 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/rpc_methods/__init__.py @@ -0,0 +1,2 @@ +from . import accounts # noqa: F401 +from . import saves # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/__init__.py b/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/__init__.py new file mode 100644 index 0000000..cf4a4e5 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/__init__.py @@ -0,0 +1,2 @@ +from . import access_tokens # noqa: F401 +from . import auth # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/access_tokens.py b/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/access_tokens.py new file mode 100644 index 0000000..a54756c --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/access_tokens.py @@ -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 diff --git a/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/auth.py b/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/auth.py new file mode 100644 index 0000000..55a0af6 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/rpc_methods/accounts/auth.py @@ -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 diff --git a/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py b/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py new file mode 100644 index 0000000..1d40d09 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/rpc_methods/saves.py @@ -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 diff --git a/services/backend/hotpocket_backend/apps/ui/services/__init__.py b/services/backend/hotpocket_backend/apps/ui/services/__init__.py index 3778152..aa6372e 100644 --- a/services/backend/hotpocket_backend/apps/ui/services/__init__.py +++ b/services/backend/hotpocket_backend/apps/ui/services/__init__.py @@ -1,3 +1,4 @@ +from .access_tokens import UIAccessTokensService # noqa: F401 from .associations import UIAssociationsService # noqa: F401 from .imports import UIImportsService # noqa: F401 from .saves import UISavesService # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/ui/services/access_tokens.py b/services/backend/hotpocket_backend/apps/ui/services/access_tokens.py new file mode 100644 index 0000000..2e0a1bb --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/services/access_tokens.py @@ -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() diff --git a/services/backend/hotpocket_backend/apps/ui/services/workflows/saves/create.py b/services/backend/hotpocket_backend/apps/ui/services/workflows/saves/create.py index 321679a..4b1a1df 100644 --- a/services/backend/hotpocket_backend/apps/ui/services/workflows/saves/create.py +++ b/services/backend/hotpocket_backend/apps/ui/services/workflows/saves/create.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from __future__ import annotations +from bthlabs_jsonrpc_core import JSONRPCInternalError from django.contrib import messages +from django.core.exceptions import ValidationError import django.db from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect @@ -9,19 +11,19 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ 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 class CreateSaveWorkflow(SaveWorkflow): - def run(self, - *, - request: HttpRequest, - account: PAccount, - url: str, - force_post_save: bool = False, - ) -> HttpResponse: + def create_associate_and_process(self, + account: PAccount, + url: str, + ) -> tuple[SaveOut, AssociationOut, AsyncResultOut | None]: with django.db.transaction.atomic(): save = self.create( account.pk, @@ -30,6 +32,23 @@ class CreateSaveWorkflow(SaveWorkflow): 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')) if force_post_save is True or save.is_netloc_banned is True: response = redirect(reverse( @@ -46,7 +65,22 @@ class CreateSaveWorkflow(SaveWorkflow): response.headers['X-HotPocket-Testing-Save-PK'] = save.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 + + 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 diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend.css b/services/backend/hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend.css index 324e3df..5388a61 100644 --- a/services/backend/hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend.css +++ b/services/backend/hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend.css @@ -36,7 +36,7 @@ body:not(.ui-js-enabled) .ui-noscript-hide { } #navbar .ui-navbar-brand > img { - border-radius: 0.25rem; + border-radius: 20%; height: 1.5rem; vertical-align: top; } @@ -45,11 +45,11 @@ body:not(.ui-js-enabled) .ui-noscript-hide { height: 100%; } -.ui-save-card .card-footer .spinner-border { +.spinner-border.ui-htmx-indicator { display: none; } -.ui-save-card .card-footer .spinner-border.htmx-request { +.spinner-border.ui-htmx-indicator.htmx-request { display: block; } diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-16.png b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-16.png index 5d8e562..c74bd86 100644 Binary files a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-16.png and b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-16.png differ diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-180.png b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-180.png index c9db117..5a45378 100644 Binary files a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-180.png and b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-180.png differ diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-192.png b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-192.png index 6325565..3ad92af 100644 Binary files a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-192.png and b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-192.png differ diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-32.png b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-32.png index b1d4e7f..dab6b99 100644 Binary files a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-32.png and b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-32.png differ diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-48.png b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-48.png new file mode 100644 index 0000000..56a8e39 Binary files /dev/null and b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-48.png differ diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-512.png b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-512.png index a0101a6..ddfe7ea 100644 Binary files a/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-512.png and b/services/backend/hotpocket_backend/apps/ui/static/ui/img/icon-512.png differ diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseAccountAppsView.js b/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseAccountAppsView.js new file mode 100644 index 0000000..13550ee --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseAccountAppsView.js @@ -0,0 +1,63 @@ +/*! + * HotPocket by BTHLabs (https://hotpocket.app/) + * Copyright 2025-present BTHLabs (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); diff --git a/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseSavesView.js b/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseSavesView.js index 6ea5f94..0d60702 100644 --- a/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseSavesView.js +++ b/services/backend/hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend.ui.BrowseSavesView.js @@ -20,21 +20,25 @@ this.app = app; } onLoad = (event) => { - document.addEventListener('HotPocket:BrowseSavesView:updateLoadMoreButton', (event) => { - const button = document.querySelector('#BrowseSavesView .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('#BrowseSavesView .ui-load-more-button'); + document.addEventListener( + 'HotPocket:BrowseSavesView:updateLoadMoreButton', + this.onUpdateLoadMoreButton, + ); + }; + onUpdateLoadMoreButton = (event) => { + const button = document.querySelector('#BrowseSavesView .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('#BrowseSavesView .ui-load-more-button'); + } }; } diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/apps/browse.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/apps/browse.html new file mode 100644 index 0000000..482b930 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/apps/browse.html @@ -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 %} +
+

{% translate 'Apps' %}

+ + {% include 'ui/accounts/partials/nav.html' with active_tab='apps' %} + + + + + + + + + + + + {% include 'ui/accounts/partials/apps/apps.html' with access_tokens=access_tokens params=params %} + +
{% translate 'App' %}{% translate 'Platform' %}{% translate 'Version' %}{% translate 'Authorized at' %}
+ +

+ + + + {% translate 'Load more' %} + +

+ + +
+{% endblock %} diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/apps/delete.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/apps/delete.html new file mode 100644 index 0000000..c2397f0 --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/apps/delete.html @@ -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 %} +
+

{% translate 'Delete an app authorization?' %}

+ + {% include 'ui/accounts/partials/nav.html' with active_tab='apps' %} + + {% include 'ui/accounts/partials/apps/delete_confirmation.html' %} + + {% crispy form %} +
+{% endblock %} diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/apps/apps.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/apps/apps.html new file mode 100644 index 0000000..650409d --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/apps/apps.html @@ -0,0 +1,44 @@ +{% load i18n static ui %} + +{% for access_token in access_tokens %} + + {{ access_token|render_access_token_app }} + {{ access_token|render_access_token_platform }} + {{ access_token.meta.version }} + {{ access_token.created_at }} + + + +
+
+ {% translate 'Processing' %} +
+ + {% translate 'Delete' %} + +
+ + +{% empty %} + {% if not HTMX %} + + + {% if params.before is None %} + {% translate "You haven't authorized any apps yet." %} + {% else %} + {% translate "You've reached the end of the line." %} + {% endif %} + + + {% endif %} +{% endfor %} diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/apps/delete_confirmation.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/apps/delete_confirmation.html new file mode 100644 index 0000000..ce1429a --- /dev/null +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/apps/delete_confirmation.html @@ -0,0 +1,7 @@ +{% load i18n static ui %} + +
+

{% translate 'Point of no return' %}

+

{% translate 'Are you sure you want to delete this app authorization?' %}

+

You'll need to authorize again if you ever change your mind.

+
diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/nav.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/nav.html index 455ada8..9f73161 100644 --- a/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/nav.html +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/accounts/partials/nav.html @@ -5,7 +5,7 @@ {% if active_tab == 'profile' %} {% else %} - + {% endif %} {% translate 'Profile' %} @@ -14,7 +14,7 @@ {% if active_tab == 'password' %} {% else %} - + {% endif %} {% translate 'Password' %} @@ -23,9 +23,18 @@ {% if active_tab == 'settings' %} {% else %} - + {% endif %} {% translate 'Settings' %} + diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/associations/partials/association_card.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/associations/partials/association_card.html index c359193..3665c85 100644 --- a/services/backend/hotpocket_backend/apps/ui/templates/ui/associations/partials/association_card.html +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/associations/partials/association_card.html @@ -19,7 +19,7 @@ {{ association.target.url|render_url_domain }}
{% if not association.archived_at %} -
+
{% translate 'Processing' %}
{% if association.is_starred %} @@ -35,7 +35,7 @@ + +
+{% endblock %} diff --git a/services/backend/hotpocket_backend/apps/ui/templates/ui/page.html b/services/backend/hotpocket_backend/apps/ui/templates/ui/page.html index 56b98e6..e20f7c8 100644 --- a/services/backend/hotpocket_backend/apps/ui/templates/ui/page.html +++ b/services/backend/hotpocket_backend/apps/ui/templates/ui/page.html @@ -10,7 +10,7 @@ @@ -146,6 +146,7 @@ + {% block page_scripts %}{% endblock %}