diff --git a/.actrc b/.actrc index 9a978b4..cd64307 100644 --- a/.actrc +++ b/.actrc @@ -2,7 +2,7 @@ --eventpath .github/event.json --container-architecture linux/amd64 --log-prefix-job-id ---platform macos-14=-self-hosted +--platform macos-15=-self-hosted --pull=false --reuse --secret-file .secrets diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 88be230..f3b73d5 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: true jobs: build-test: - runs-on: macos-14 + runs-on: macos-15 defaults: run: # Prefixes all `run` commands with the following command to force them to run outside Rosetta. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0c1b803..a205e50 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ on: types: [published] jobs: start: - runs-on: macos-14 + runs-on: macos-15 outputs: dry_run: ${{ steps.dry_run.outputs.dry_run }} mas_version: ${{ steps.mas_version.outputs.mas_version }} @@ -44,7 +44,7 @@ jobs: echo "RELEASE_BRANCH=releases/release-${{ github.event.release.tag_name }}" >>"$GITHUB_OUTPUT" prepare-release: - runs-on: macos-14 + runs-on: macos-15 needs: [start] steps: - uses: actions/checkout@v4 @@ -98,7 +98,7 @@ jobs: --body "This PR contains the changes from releasing version [${MAS_VERSION}](https://github.com/mas-cli/mas/releases/tag/${MAS_VERSION})." pkg-installer: - runs-on: macos-14 + runs-on: macos-15 needs: [start, prepare-release] steps: - uses: actions/checkout@v4 @@ -132,7 +132,7 @@ jobs: .build/mas.pkg homebrew-tap: - runs-on: macos-14 + runs-on: macos-15 needs: [start, prepare-release] steps: - name: 📺 Checkout mas repo @@ -223,7 +223,7 @@ jobs: .build/bottles/mas-*.bottle.tar.gz homebrew-core: - runs-on: macos-14 + runs-on: macos-15 needs: [start, prepare-release, homebrew-tap] steps: - uses: actions/checkout@v4 @@ -244,4 +244,3 @@ jobs: run: | DRY_RUN=${DRY_RUN} \ script/brew_core_update ${MAS_VERSION} - diff --git a/.gitignore b/.gitignore index 073f705..2b6a070 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ .build/ .envrc .fseventsd +.idea/ .rubygems/ .secrets .swiftpm/ @@ -32,6 +33,8 @@ Carthage/ DerivedData Pods/ +Sources/mas/Package.swift +Sources/MasKit/Package.swift Temporary Items bin/ build/ diff --git a/.swift-format b/.swift-format index 9c12fd6..693d879 100644 --- a/.swift-format +++ b/.swift-format @@ -1,41 +1,62 @@ { - "indentation" : { - "spaces" : 4 + "indentConditionalCompilationBlocks": false, + "indentation": { + "spaces": 4 }, - "lineLength" : 120, - "rules" : { - "AllPublicDeclarationsHaveDocumentation" : false, - "AlwaysUseLowerCamelCase" : true, - "AmbiguousTrailingClosureOverload" : true, - "BeginDocumentationCommentWithOneLineSummary" : false, - "DoNotUseSemicolons" : true, - "DontRepeatTypeInStaticProperties" : true, - "FileScopedDeclarationPrivacy" : true, - "FullyIndirectEnum" : true, - "GroupNumericLiterals" : true, - "IdentifiersMustBeASCII" : true, - "NeverForceUnwrap" : false, - "NeverUseForceTry" : false, - "NeverUseImplicitlyUnwrappedOptionals" : false, - "NoAccessLevelOnExtensionDeclaration" : false, - "NoBlockComments" : true, - "NoCasesWithOnlyFallthrough" : true, - "NoEmptyTrailingClosureParentheses" : true, - "NoLabelsInCasePatterns" : true, - "NoLeadingUnderscores" : false, - "NoParensAroundConditions" : true, - "NoVoidReturnOnFunctionSignature" : true, - "OneCasePerLine" : true, - "OneVariableDeclarationPerLine" : true, - "OnlyOneTrailingClosureArgument" : true, - "OrderedImports" : true, - "ReturnVoidInsteadOfEmptyTuple" : true, - "UseLetInEveryBoundCaseVariable" : true, - "UseShorthandTypeNames" : true, - "UseSingleLinePropertyGetter" : true, - "UseSynthesizedInitializer" : true, - "UseTripleSlashForDocumentationComments" : true, - "ValidateDocumentationComments" : false + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineBreakBetweenDeclarationAttributes": true, + "lineLength": 120, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "prioritizeKeepingFunctionOutputTogether": true, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": true, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": true, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true }, - "version" : 1 + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "TrailingComma": false, + "version": 1 } diff --git a/.swiftformat b/.swiftformat index ce62d1c..9089de8 100644 --- a/.swiftformat +++ b/.swiftformat @@ -5,20 +5,34 @@ # https://github.com/nicklockwood/SwiftFormat#config-file # ---exclude docs/ - # Disabled rules ---disable blankLinesAroundMark ---disable consecutiveSpaces +--disable hoistAwait --disable hoistPatternLet +--disable hoistTry + +# Enable later --disable indent --disable trailingCommas # Enabled rules (disabled by default) ---enable trailingClosures +#--enable acronyms +#--enable blankLinesBetweenImports +--enable blockComments +--enable docComments +--enable isEmpty +--enable noExplicitOwnership +#--enable organizeDeclarations +--enable redundantProperty +--enable sortSwitchCases +--enable wrapConditionalBodies +--enable wrapEnumCases +--enable wrapMultilineConditionalAssignment +--enable wrapSwitchCases # Rule options --commas always --extensionacl on-declarations +--hexliteralcase lowercase --importgrouping testable-last +--lineaftermarks false --ranges no-space diff --git a/.swiftlint.yml b/.swiftlint.yml index a43a903..094a2b9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,10 +5,40 @@ # https://github.com/realm/SwiftLint#configuration # --- +opt_in_rules: + - all disabled_rules: -- non_optional_string_data_conversion + - balanced_xctest_lifecycle + - closure_body_length + - contrasted_opening_brace + - explicit_acl + - explicit_enum_raw_value + - explicit_top_level_acl + - explicit_type_interface + - file_header + - file_name + - final_test_case + - force_unwrapping + - function_body_length + - inert_defer + - legacy_objc_type + - no_grouping_extension + - number_separator + - one_declaration_per_file + - prefer_nimble + - prefixed_toplevel_constant + - quick_discouraged_call + - quick_discouraged_pending_test + - required_deinit + - sorted_enum_cases - trailing_comma -excluded: - - docs -opening_brace: - allow_multiline_func: true + - unused_capture_list + - vertical_whitespace_between_cases +file_types_order: + order: [ + [main_type], + [supporting_type], + [extension], + [preview_provider], + [library_content_provider] + ] diff --git a/Brewfile b/Brewfile index 7aa6f5f..f39a5c1 100644 --- a/Brewfile +++ b/Brewfile @@ -3,10 +3,13 @@ brew "mise" brew "sd" brew "shellcheck" brew "shfmt" +brew "swift-format" brew "swiftformat" +brew "trash" +brew "yamllint" -# Already installed on GitHub Actions runner. -# brew "swiftlint" - -tap "peripheryapp/periphery" -cask "periphery" +if OS.mac? && MacOS.version >= :ventura + brew "swiftlint" + tap "peripheryapp/periphery" + cask "periphery" +end diff --git a/Brewfile.lock.json b/Brewfile.lock.json index 597781d..2f0d391 100644 --- a/Brewfile.lock.json +++ b/Brewfile.lock.json @@ -2,89 +2,79 @@ "entries": { "brew": { "markdownlint-cli": { - "version": "0.39.0", + "version": "0.42.0", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" + }, "arm64_sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f", - "sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f" + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" }, "arm64_ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f", - "sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f" - }, - "arm64_monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f", - "sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f" + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" }, "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f", - "sha256": "0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f" + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5", + "sha256": "758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5" }, "ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f", - "sha256": "0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f" - }, - "monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f", - "sha256": "0af1b50fb5bef4a0c47e1bd233e39262586885b48bc4c5a60592c5f42c2edf9f" + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5", + "sha256": "758ba29dba62e69b33801d519cbf6186a84320adf3e8e08bda862c9b057c22e5" }, "x86_64_linux": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f", - "sha256": "7bc1310bfbeff34386c2b309988569bc6494b333edf9bf26d027fbecc1342c2f" + "url": "https://ghcr.io/v2/homebrew/core/markdownlint-cli/blobs/sha256:a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e", + "sha256": "a3e5181a75b8fb131ac22c9c63635573d403a3d720ef0c72bd58cecfb3c2fe3e" } } } }, "mise": { - "version": "2024.3.11", + "version": "2024.10.11", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:3d1ce9664736b5039466ceeb8286f87150a220d76cf62e5c5538ed4c42c01ff0", + "sha256": "3d1ce9664736b5039466ceeb8286f87150a220d76cf62e5c5538ed4c42c01ff0" + }, "arm64_sonoma": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:2ccc2946ac8d9af9fdde8ce0ef8d9271010eee24f8824086134533480b95b69f", - "sha256": "2ccc2946ac8d9af9fdde8ce0ef8d9271010eee24f8824086134533480b95b69f" + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:d28afbc2017aa2d5904da9ca14e1fc9d01d341bfd0adc5168ace961e326b5b1c", + "sha256": "d28afbc2017aa2d5904da9ca14e1fc9d01d341bfd0adc5168ace961e326b5b1c" }, "arm64_ventura": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:8e87fe7f49fa24545c99bcefc1fd1a22edfa6426b8c59e349bd445ced8544dd2", - "sha256": "8e87fe7f49fa24545c99bcefc1fd1a22edfa6426b8c59e349bd445ced8544dd2" - }, - "arm64_monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:2c0deb90ea8214e22f4f10fa53ef8d90b66c769f4784807ce99614c6c3a97b43", - "sha256": "2c0deb90ea8214e22f4f10fa53ef8d90b66c769f4784807ce99614c6c3a97b43" + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:c4005d63d073861cef944841aa88925b1b929bc150d94bebe65322a4c7a6886f", + "sha256": "c4005d63d073861cef944841aa88925b1b929bc150d94bebe65322a4c7a6886f" }, "sonoma": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:458438873c3a621d77849261f7ef57173670bdf0a64e018592e1682685be28ce", - "sha256": "458438873c3a621d77849261f7ef57173670bdf0a64e018592e1682685be28ce" + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:d392eae34949295556f9c3598ce97bf44f5392b996b02ea31956169332a41f1c", + "sha256": "d392eae34949295556f9c3598ce97bf44f5392b996b02ea31956169332a41f1c" }, "ventura": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:55196d84f5fc7d1b56dd03943dd8744a23e064b78928ebc4de85d8659a71c9ca", - "sha256": "55196d84f5fc7d1b56dd03943dd8744a23e064b78928ebc4de85d8659a71c9ca" - }, - "monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:31c0c0ca68d7b2d3d12492c97a7e36547c88cddc8c6e195a3a4fb7b443a74f31", - "sha256": "31c0c0ca68d7b2d3d12492c97a7e36547c88cddc8c6e195a3a4fb7b443a74f31" + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:fd1afe999715d971c78f4bfa90b74fae7b7f8688477b07ea89c67ee36ea8463a", + "sha256": "fd1afe999715d971c78f4bfa90b74fae7b7f8688477b07ea89c67ee36ea8463a" }, "x86_64_linux": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:97e003f7841847029e38986685ec809b9a6eab9b66409e6baad4e568860a23aa", - "sha256": "97e003f7841847029e38986685ec809b9a6eab9b66409e6baad4e568860a23aa" + "url": "https://ghcr.io/v2/homebrew/core/mise/blobs/sha256:22601cf4faf8764cf29d6cea72ef0e0789b10da581d899a229aace4e0069b2c4", + "sha256": "22601cf4faf8764cf29d6cea72ef0e0789b10da581d899a229aace4e0069b2c4" } } } @@ -95,6 +85,11 @@ "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/sd/blobs/sha256:3cf7ab4495f622a4f245bb1c7c30225ef881dc390ee5edc59a1d3c4381cecca1", + "sha256": "3cf7ab4495f622a4f245bb1c7c30225ef881dc390ee5edc59a1d3c4381cecca1" + }, "arm64_sonoma": { "cellar": ":any_skip_relocation", "url": "https://ghcr.io/v2/homebrew/core/sd/blobs/sha256:6bc773a70934364157591cd888e617601a42ed1f615fda8f77364fa45631d08d", @@ -139,6 +134,11 @@ "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:5045be1e530288251353848343322f5a423617d061830b7ea7465fe550787364", + "sha256": "5045be1e530288251353848343322f5a423617d061830b7ea7465fe550787364" + }, "arm64_sonoma": { "cellar": ":any_skip_relocation", "url": "https://ghcr.io/v2/homebrew/core/shellcheck/blobs/sha256:ef742b6992cfcdcd7289718ac64b27174e421d29ce3ad9b81e1856349059b117", @@ -178,117 +178,241 @@ } }, "shfmt": { - "version": "3.8.0", + "version": "3.10.0", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863", + "sha256": "88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863" + }, "arm64_sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34", - "sha256": "78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863", + "sha256": "88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863" }, "arm64_ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34", - "sha256": "78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34" - }, - "arm64_monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34", - "sha256": "78a5017f23e2d4b9fd9312ce1e4e06c09cfb838d47e78cfb02ddb4190acb6b34" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863", + "sha256": "88d60bca61406671618ecf94f2d81104882f9dd8db838a70d0c2cd6c0fa46863" }, "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62", - "sha256": "0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989", + "sha256": "788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989" }, "ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62", - "sha256": "0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62" - }, - "monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62", - "sha256": "0d7952151f13e850fa40b03d6ba3f39daa8ec9401735aa91d6cd8e950f880d62" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989", + "sha256": "788b7ecff02bbff7fa1563a4937999972799361b4a0c49b1ed8545983d6ff989" }, "x86_64_linux": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:772a5dfe3e281fc51f6200313fb62b454314bf4978a8fe70ba2026a4fe5af5c4", - "sha256": "772a5dfe3e281fc51f6200313fb62b454314bf4978a8fe70ba2026a4fe5af5c4" + "url": "https://ghcr.io/v2/homebrew/core/shfmt/blobs/sha256:0b15af30edec238edf607c38a95bd45249cdd6f48f30ab33bdd0a9c2ae2da956", + "sha256": "0b15af30edec238edf607c38a95bd45249cdd6f48f30ab33bdd0a9c2ae2da956" + } + } + } + }, + "swift-format": { + "version": "510.1.0", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:4bee0106201ba2a3036576610e61832b97fb65292c194f52fc15d62e1bdb2243", + "sha256": "4bee0106201ba2a3036576610e61832b97fb65292c194f52fc15d62e1bdb2243" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:ac50e5269ecc0bffb70a6c5077f97954e2e51c9158a3bfa36b86d89f9d6c5e43", + "sha256": "ac50e5269ecc0bffb70a6c5077f97954e2e51c9158a3bfa36b86d89f9d6c5e43" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:7fb047e8f80a72e5d8d7ae50c496d0cf59dd3ab654ce6048e4b7fa7b85afe69a", + "sha256": "7fb047e8f80a72e5d8d7ae50c496d0cf59dd3ab654ce6048e4b7fa7b85afe69a" + }, + "sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:a99a19c9fc177a57b2577e3c1b30feb70f13388fc9c4e4ea7968f783058e09a0", + "sha256": "a99a19c9fc177a57b2577e3c1b30feb70f13388fc9c4e4ea7968f783058e09a0" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:a652f68cc4bed9c3186b66c8ee68e79b7387d37943aaff0a0c2d4197367b73fe", + "sha256": "a652f68cc4bed9c3186b66c8ee68e79b7387d37943aaff0a0c2d4197367b73fe" + }, + "x86_64_linux": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swift-format/blobs/sha256:b1949fcb7f1b943fa5b0216bc6f18e12dc369c0538b093786332f851f22b0b03", + "sha256": "b1949fcb7f1b943fa5b0216bc6f18e12dc369c0538b093786332f851f22b0b03" } } } }, "swiftformat": { - "version": "0.53.5", + "version": "0.54.6_1", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:03eb08eb7de0e697e574b5d5c94104a88c9548ee880b942f1916536fe7ff897a", + "sha256": "03eb08eb7de0e697e574b5d5c94104a88c9548ee880b942f1916536fe7ff897a" + }, "arm64_sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:04e089d4b1ae1217dd6c8133b3c661add56d7c4f4f24ee67becd3cf8f54e6e80", - "sha256": "04e089d4b1ae1217dd6c8133b3c661add56d7c4f4f24ee67becd3cf8f54e6e80" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:0ff9c3c154fea61303bd060da1aecebb025a3a33460b24910cf55e6ae366574e", + "sha256": "0ff9c3c154fea61303bd060da1aecebb025a3a33460b24910cf55e6ae366574e" }, "arm64_ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:19a6ce102e7df1cdee150dee619025aa3b2a4980070bee4f8cdd6976c0936d46", - "sha256": "19a6ce102e7df1cdee150dee619025aa3b2a4980070bee4f8cdd6976c0936d46" - }, - "arm64_monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:745ba037da0e1fe62f2f22faa45a17655b89d8870bacd9db32597ce1fd779509", - "sha256": "745ba037da0e1fe62f2f22faa45a17655b89d8870bacd9db32597ce1fd779509" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:52200577da57cebd27e7d4b6a9ed84f6d3475b7f91e28ec4f5947fc2992cd943", + "sha256": "52200577da57cebd27e7d4b6a9ed84f6d3475b7f91e28ec4f5947fc2992cd943" }, "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:6830f0bd5d06dca19d2bcd614e6d0c87e7a3d703d33bce90d0448a83310dddcc", - "sha256": "6830f0bd5d06dca19d2bcd614e6d0c87e7a3d703d33bce90d0448a83310dddcc" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:9f79e28a0a5c7172be8bfcf23fca47de08f8bc03a3ddcdfbf52704445b9d8b18", + "sha256": "9f79e28a0a5c7172be8bfcf23fca47de08f8bc03a3ddcdfbf52704445b9d8b18" }, "ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:dacbfeca6cbe99fc73448f08c0289f135e807bc220ac1dcb61952410f1b43535", - "sha256": "dacbfeca6cbe99fc73448f08c0289f135e807bc220ac1dcb61952410f1b43535" - }, - "monterey": { - "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:a5e30f5378aca201ca8bc7a350ebac28b3202366be1b37cf254f77c27761753a", - "sha256": "a5e30f5378aca201ca8bc7a350ebac28b3202366be1b37cf254f77c27761753a" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:416528899d45dc25edc2f14c857239a2c922b4be548345423857f140c6b90f0f", + "sha256": "416528899d45dc25edc2f14c857239a2c922b4be548345423857f140c6b90f0f" }, "x86_64_linux": { - "cellar": "/home/linuxbrew/.linuxbrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:909ae79dbe735c9377355e202d07a58aeff1af1707ba7a3c843cf7c3b10f68a9", - "sha256": "909ae79dbe735c9377355e202d07a58aeff1af1707ba7a3c843cf7c3b10f68a9" + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:86c47e1a74da98ff5646c8d510ea5e6de45e9dc97bc59f151bd2a8848b5bc9f8", + "sha256": "86c47e1a74da98ff5646c8d510ea5e6de45e9dc97bc59f151bd2a8848b5bc9f8" } } } - } - }, - "tap": { - "peripheryapp/periphery": { - "revision": "4f73aefe6e01ba2543b9ee50f7653d866784fd61" - } - }, - "cask": { - "periphery": { - "version": "2.18.0", - "options": { - "full_name": "periphery" + }, + "trash": { + "version": "0.9.2", + "bottle": { + "rebuild": 1, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:f3b7a766bcc683b339c145ab7d8b484f2bbd65aac6903fd952dec7f4521efe5f", + "sha256": "f3b7a766bcc683b339c145ab7d8b484f2bbd65aac6903fd952dec7f4521efe5f" + }, + "arm64_sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:90cffd3d151720b768c48a8874f7b8dfaaf6f7a9e9000ffe23cfa3f9e4aa6b76", + "sha256": "90cffd3d151720b768c48a8874f7b8dfaaf6f7a9e9000ffe23cfa3f9e4aa6b76" + }, + "arm64_ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:60186a8823abc9dd734475e3f787edd7c2d6a2254fff25b7289de2db15447099", + "sha256": "60186a8823abc9dd734475e3f787edd7c2d6a2254fff25b7289de2db15447099" + }, + "arm64_monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:541af91d1cb128aa743460a529a3dcab5bac63b61ccde0a60d73aee23ab7d5c0", + "sha256": "541af91d1cb128aa743460a529a3dcab5bac63b61ccde0a60d73aee23ab7d5c0" + }, + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:11c0c85ec692ea6d4a125070f0a6ca576aff991608a6c9632b984cbf983e2481", + "sha256": "11c0c85ec692ea6d4a125070f0a6ca576aff991608a6c9632b984cbf983e2481" + }, + "sonoma": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:c26e06202022f708790f22f4477b65e3337d611c42e9a814ada1526bda03d923", + "sha256": "c26e06202022f708790f22f4477b65e3337d611c42e9a814ada1526bda03d923" + }, + "ventura": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:539093ca74c72ed8be974fd9042b14f55cde0ef2c1fadbedc7343099a394593e", + "sha256": "539093ca74c72ed8be974fd9042b14f55cde0ef2c1fadbedc7343099a394593e" + }, + "monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:09b8ac7ade28ca59bd578b90680ece838a507b90b35e44d06a16f4d8ab9ae6e6", + "sha256": "09b8ac7ade28ca59bd578b90680ece838a507b90b35e44d06a16f4d8ab9ae6e6" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:403ba52ce97d38535e1d127ca227afd4ea2d0e0c8b414118dbc5376c9ed8f094", + "sha256": "403ba52ce97d38535e1d127ca227afd4ea2d0e0c8b414118dbc5376c9ed8f094" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:b452d67cdeeb52db0aaadd258bc3e214a5ea5ed37da698b45017b01457115ea6", + "sha256": "b452d67cdeeb52db0aaadd258bc3e214a5ea5ed37da698b45017b01457115ea6" + }, + "mojave": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:d8ad5460b24a51a4a12b31ebf1a2887e9e86e029d061f6994c3c1caea7bf0551", + "sha256": "d8ad5460b24a51a4a12b31ebf1a2887e9e86e029d061f6994c3c1caea7bf0551" + }, + "high_sierra": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/trash/blobs/sha256:0ef5ea924ba8d01398686657a839ad270796f3f10eee86d6522980d32038df9a", + "sha256": "0ef5ea924ba8d01398686657a839ad270796f3f10eee86d6522980d32038df9a" + } + } + } + }, + "yamllint": { + "version": "1.35.1", + "bottle": { + "rebuild": 2, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_sequoia": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:cb74d6cc51501733531acd25b26fd474557d19374b98eb7de16271ff2c257860", + "sha256": "cb74d6cc51501733531acd25b26fd474557d19374b98eb7de16271ff2c257860" + }, + "arm64_sonoma": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:e36b43d6b87028fe2005878cb15c78edec6ddb898e9a86ff7b901fe093cf9c0e", + "sha256": "e36b43d6b87028fe2005878cb15c78edec6ddb898e9a86ff7b901fe093cf9c0e" + }, + "arm64_ventura": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:3591f98aaaebba5e9360926f5ca756dbd85c6a46de0554042376ac83548c7fb3", + "sha256": "3591f98aaaebba5e9360926f5ca756dbd85c6a46de0554042376ac83548c7fb3" + }, + "sonoma": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:3889369233f5f342b73cc70625748a52d72117603b92f352af00a9ebd27cb1c4", + "sha256": "3889369233f5f342b73cc70625748a52d72117603b92f352af00a9ebd27cb1c4" + }, + "ventura": { + "cellar": ":any", + "url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:209883378df0edf4a0691fd2dbf6f2e8da7776bd9c0de0fc70fa04dd0fc51c8d", + "sha256": "209883378df0edf4a0691fd2dbf6f2e8da7776bd9c0de0fc70fa04dd0fc51c8d" + }, + "x86_64_linux": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/yamllint/blobs/sha256:993514320174f1147d538719552131a73d34cf66dc9f82c38f6ed28b16cea287", + "sha256": "993514320174f1147d538719552131a73d34cf66dc9f82c38f6ed28b16cea287" + } + } } } } }, "system": { "macos": { - "sonoma": { - "HOMEBREW_VERSION": "4.2.15-75-g221fde4", - "HOMEBREW_PREFIX": "/opt/homebrew", + "monterey": { + "HOMEBREW_VERSION": "4.4.2-62-g59d56f8", + "HOMEBREW_PREFIX": "/usr/local", "Homebrew/homebrew-core": "api", - "CLT": "15.3.0.0.1.1708646388", - "Xcode": "15.3", - "macOS": "14.4.1" + "CLT": "14.2.0.0.1.1668646533", + "Xcode": "14.2", + "macOS": "12.7.6" } } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 249795c..16760d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ We love pull requests from everyone. By participating in this project, you agree - [Open an issue](https://github.com/mas-cli/mas/issues/new) to simply ask a question or request a new feature. - Search for similar issues with the [ERROR MESSAGE](https://github.com/mas-cli/mas/issues?utf8=%E2%9C%93&q=is%3Aopen+ERROR+MESSAGE) -you are exeriencing. +you are experiencing. - If one doesn't exist, [open a new issue](https://github.com/mas-cli/mas/issues/new) - Clearly describe the issue including steps to reproduce when it is a bug. - Include the earliest version of `mas` that you know has the issue. diff --git a/Package.resolved b/Package.resolved index 14f75d7..80d3efe 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,106 +1,77 @@ { - "object": { - "pins": [ - { - "package": "Commandant", - "repositoryURL": "https://github.com/Carthage/Commandant.git", - "state": { - "branch": null, - "revision": "a1671cf728db837cf5ec1980a80d276bbba748f6", - "version": "0.18.0" - } - }, - { - "package": "CwlCatchException", - "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", - "state": { - "branch": null, - "revision": "3b123999de19bf04905bc1dfdb76f817b0f2cc00", - "version": "2.1.2" - } - }, - { - "package": "CwlPreconditionTesting", - "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", - "state": { - "branch": null, - "revision": "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", - "version": "2.2.0" - } - }, - { - "package": "Nimble", - "repositoryURL": "https://github.com/Quick/Nimble.git", - "state": { - "branch": null, - "revision": "1f3bde57bde12f5e7b07909848c071e9b73d6edc", - "version": "10.0.0" - } - }, - { - "package": "PromiseKit", - "repositoryURL": "https://github.com/mxcl/PromiseKit.git", - "state": { - "branch": null, - "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", - "version": "6.22.1" - } - }, - { - "package": "Quick", - "repositoryURL": "https://github.com/Quick/Quick.git", - "state": { - "branch": null, - "revision": "f9d519828bb03dfc8125467d8f7b93131951124c", - "version": "5.0.1" - } - }, - { - "package": "Regex", - "repositoryURL": "https://github.com/sharplet/Regex.git", - "state": { - "branch": null, - "revision": "76c2b73d4281d77fc3118391877efd1bf972f515", - "version": "2.1.1" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version": "1.3.0" - } - }, - { - "package": "swift-format", - "repositoryURL": "https://github.com/apple/swift-format", - "state": { - "branch": "release/5.9", - "revision": "1323e87eced56bdcfed1bb78af1f16f39274d032", - "version": null - } - }, - { - "package": "swift-syntax", - "repositoryURL": "https://github.com/apple/swift-syntax.git", - "state": { - "branch": "release/5.9", - "revision": "9a101b70eee2a9dec04f92d2d47b22ebe57a1aae", - "version": null - } - }, - { - "package": "Version", - "repositoryURL": "https://github.com/mxcl/Version.git", - "state": { - "branch": null, - "revision": "1fe824b80d89201652e7eca7c9252269a1d85e25", - "version": "2.0.1" - } + "pins" : [ + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "07b2ba21d361c223e25e3c1e924288742923f08c", + "version" : "2.2.1" } - ] - }, - "version": 1 + }, + { + "identity" : "cwlpreconditiontesting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state" : { + "revision" : "0139c665ebb45e6a9fbdb68aabfd7c39f3fe0071", + "version" : "2.2.2" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble.git", + "state" : { + "revision" : "1f3bde57bde12f5e7b07909848c071e9b73d6edc", + "version" : "10.0.0" + } + }, + { + "identity" : "promisekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/PromiseKit.git", + "state" : { + "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version" : "6.22.1" + } + }, + { + "identity" : "quick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Quick.git", + "state" : { + "revision" : "f9d519828bb03dfc8125467d8f7b93131951124c", + "version" : "5.0.1" + } + }, + { + "identity" : "regex", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sharplet/Regex.git", + "state" : { + "revision" : "76c2b73d4281d77fc3118391877efd1bf972f515", + "version" : "2.1.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "version", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/Version.git", + "state" : { + "revision" : "303a0f916772545e1e8667d3104f83be708a723c", + "version" : "2.1.0" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 15da902..4e48ff7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -13,38 +13,24 @@ let package = Package( .executable( name: "mas", targets: ["mas"] - ), - .library( - name: "MasKit", - targets: ["MasKit"] - ), + ) ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/Carthage/Commandant.git", from: "0.18.0"), .package(url: "https://github.com/Quick/Nimble.git", from: "10.0.0"), - .package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"), - .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.16.2"), - .package(url: "https://github.com/mxcl/Version.git", from: "2.0.1"), + .package(url: "https://github.com/Quick/Quick.git", from: "5.0.1"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.22.1"), + .package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"), .package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( + .executableTarget( name: "mas", - dependencies: ["MasKit"], - swiftSettings: [ - .unsafeFlags([ - "-I", "Sources/PrivateFrameworks/CommerceKit", - "-I", "Sources/PrivateFrameworks/StoreFoundation", - ]) - ] - ), - .target( - name: "MasKit", dependencies: [ - "Commandant", + .product(name: "ArgumentParser", package: "swift-argument-parser"), "PromiseKit", "Regex", "Version", @@ -62,8 +48,8 @@ let package = Package( ] ), .testTarget( - name: "MasKitTests", - dependencies: ["MasKit", "Nimble", "Quick"], + name: "masTests", + dependencies: ["mas", "Nimble", "Quick"], resources: [.copy("JSON")], swiftSettings: [ .unsafeFlags([ @@ -75,30 +61,3 @@ let package = Package( ], swiftLanguageVersions: [.v5] ) - -// https://github.com/apple/swift-format#matching-swift-format-to-your-swift-version-swift-57-and-earlier -#if compiler(>=5.8) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("release/5.9")) - ] -#elseif compiler(>=5.7) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("release/5.7")) - ] -#elseif compiler(>=5.6) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("release/5.6")) - ] -#elseif compiler(>=5.5) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("swift-5.5-branch")) - ] -#elseif compiler(>=5.4) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("swift-5.4-branch")) - ] -#elseif compiler(>=5.3) - package.dependencies += [ - .package(url: "https://github.com/apple/swift-format", .branch("swift-5.3-branch")) - ] -#endif diff --git a/README.md b/README.md index 71258b0..5f8c452 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ -

mas-cli

+

mas-cli

-# mas-cli +# mas -A simple command line interface for the Mac App Store. Designed for scripting and automation. +A command-line interface for the Mac App Store. Designed for scripting and automation. -[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/mas-cli/mas/blob/main/LICENSE) -[![Swift 5](https://img.shields.io/badge/Language-Swift_5-orange.svg)](https://swift.org) [![GitHub Release](https://img.shields.io/github/release/mas-cli/mas.svg)](https://github.com/mas-cli/mas/releases) +[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)]( + https://github.com/mas-cli/mas/blob/main/LICENSE +) +[![Swift 5](https://img.shields.io/badge/Language-Swift_5-orange.svg)](https://swift.org) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) -[![Build, Test, & Lint](https://github.com/mas-cli/mas/actions/workflows/build-test.yml/badge.svg?branch=main)](https://github.com/mas-cli/mas/actions/workflows/build-test.yml?query=branch%3Amain) +[![Build, Test, & Lint](https://github.com/mas-cli/mas/actions/workflows/build-test.yml/badge.svg?branch=main)]( + https://github.com/mas-cli/mas/actions/workflows/build-test.yml?query=branch%3Amain +) -## 📲 Install +## 📲 Installation ### 🍺 Homebrew @@ -20,20 +24,24 @@ A simple command line interface for the Mac App Store. Designed for scripting an brew install mas ``` -### MacPorts +⚠️ macOS 10.15 (Catalina) or newer is required to install mas from the Homebrew core formula. -[MacPorts](https://www.macports.org/install.php) works as well: +### 🔌 MacPorts + +[MacPorts](https://www.macports.org/install.php) is an alternative way to install: ```bash sudo port install mas ``` -⚠️ Note that macOS 10.15 (Catalina) is required to install mas from MacPorts or the core Homebrew formula. +⚠️ macOS 10.15 (Catalina) or newer is required to install mas from MacPorts. ### ☎️ Older macOS Versions +#### 🍻 Custom Homebrew tap + We provide a [custom Homebrew tap](https://github.com/mas-cli/homebrew-tap) with pre-built bottles -for all macOS versions since 10.11. +for all macOS versions since 10.11 (El Capitan). To install mas from our tap: @@ -41,38 +49,48 @@ To install mas from our tap: brew install mas-cli/tap/mas ``` -#### Swift 5 Runtime Support +#### 🐙 GitHub Releases -mas requires Swift 5 runtime support. macOS 10.14.4 and later include it, but earlier releases did not. -Without it, running `mas` may report an error similar to this: +Alternatively, binaries and sources are available from the [GitHub Releases](https://github.com/mas-cli/mas/releases). + +#### 🕊 Swift 5 Runtime Support + +mas requires Swift 5 runtime support. macOS 10.14.4 (Mojave) and newer include it, but earlier releases do not. +Without it, running mas might report errors similar to: > dyld: Symbol not found: _$s11SubSequenceSlTl -To get Swift 5 support, you have a few options: +To get Swift 5 support on macOS versions older than 10.14.4 (Mojave), you can: +- Upgrade to macOS 10.14.4 (Mojave) or newer. - Install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998). -- Update to macOS 10.14.4 or later. -- Install Xcode 10.2 or later to `/Applications/Xcode.app`. - -### 🐙 GitHub Releases - -Alternatively, binaries are available in the [GitHub Releases](https://github.com/mas-cli/mas/releases). +- Install Xcode 10.2 or newer to `/Applications/Xcode.app`. ## 🤳🏻 Usage -Each application in the Mac App Store has a product identifier which is also -used for mas-cli commands. Using `mas list` will show all installed -applications and their product identifiers. +### 🪪 App IDs -```bash -$ mas list -446107677 Screens -407963104 Pixelmator -497799835 Xcode -``` +Each application in the Mac App Store has an integer app identifier (app ID). +mas commands accept app IDs as arguments and output App IDs to uniquely identify apps. -It is possible to search for applications by name using `mas search` which -will search the Mac App Store and return matching identifiers. -Include the `--price` flag to include prices in the result. +`mas search` and `mas list` can be used to find the app IDs of relevant apps. + +Alternatively, to find an app's app ID: + +1. Find the app in the Mac App Store +2. Select `Share` > `Copy Link` +3. Extract the app ID from the URL. e.g., the Mac App Store URL for Xcode, + [https://apps.apple.com/us/app/xcode/id497799835?mt=12](https://apps.apple.com/us/app/xcode/id497799835?mt=12), + has app ID `497799835` + +### 🛍 Info from the Mac App Store + +None of the commands in this section require you to be logged into an Apple ID, +neither for your macOS user, nor in the Mac App Store. + +#### `mas search` + +`mas search ` searches by name for applications available from the Mac App Store. +Providing the `--price` flag includes each app's price in the output. ```bash $ mas search Xcode @@ -82,151 +100,200 @@ $ mas search Xcode [...] ``` -Another way to find the identifier for an app is to +#### `mas info` -1. Find the app in the Mac App Store -2. Select `Share` > `Copy Link` -3. Grab the identifier from the string, e.g. for Xcode, - [https://apps.apple.com/us/app/xcode/id497799835?mt=12](https://apps.apple.com/us/app/xcode/id497799835?mt=12) - has identifier `497799835` - -To install or update an application simply run `mas install` with an -application identifier: +`mas info ` displays more detailed information about an application available from the Mac App Store. ```bash -$ mas install 808809998 -==> Downloading PaintCode 2 -==> Installed PaintCode 2 +$ mas info 497799835 +Xcode 16.0 [0.0] +By: Apple Inc. +Released: 2024-09-16 +Minimum OS: 14.5 +Size: 2.98 GB +From: https://apps.apple.com/us/app/xcode/id497799835?mt=12&uo=4 ``` -If you want to install the first result that the `search` command returns, use the `lucky` command. +### 📚 Info from Your Local App Library + +All the commands in this section require you to be logged into an Apple ID for your macOS user. + +#### `mas list` + +`mas list` displays all the applications on your Mac that were installed from the Mac App Store. ```bash -$ mas lucky twitter -==> Downloading Twitter -==> Installed Twitter +$ mas list +497799835 Xcode (15.4) +640199958 Developer (10.6.5) +899247664 TestFlight (3.5.2) ``` -> Please note that this command will not allow you to install (or even purchase) an app for the first time: -use the `purchase` command in that case. -> ⛔ The `purchase` command is not supported as of macOS 10.15 Catalina. Please see [Known Issues](#%EF%B8%8F-known-issues). +#### `mas outdated` -```bash -$ mas purchase 768053424 -==> Downloading Gapplin -==> Installed Gapplin -``` - -> Please note that you may have to re-authenticate yourself in the App Store to complete the purchase. -This is the case if the application is not free or if you configured your account not to remember the -credentials for free purchases. - -Use `mas outdated` to list all applications with pending updates. +`mas outdated` displays all applications installed from the Mac App Store on your computer that have pending upgrades. ```bash $ mas outdated -497799835 Xcode (7.0) -446107677 Screens VNC - Access Your Computer From Anywhere (3.6.7) +497799835 Xcode (15.4 -> 16.0) +640199958 Developer (10.6.5 -> 10.6.6) ``` -> `mas` is only able to install/update applications that are listed in the Mac App Store itself. -Use [`softwareupdate(8)`] utility for downloading system updates (e.g. Xcode Command Line Tools) +Run [`mas upgrade`](#mas-upgrade) to install pending upgrades. -To install all pending updates run `mas upgrade`. +### ⬇️ Installing Apps + +All the commands in this section require you to be logged into an Apple ID in the Mac App Store. + +> Depending on your Apple ID settings, you might need to re-authenticate yourself in the Mac App Store to perform a +> purchase, install, lucky, or upgrade, even if you are already signed in to an Apple ID in the Mac App Store. + +#### `mas purchase` + +`mas purchase …` installs free applications that you haven't yet gotten/"purchased" from the Mac App Store. + +> `purchase` is currently a misnomer, because it currently can only "purchase" free +> apps. To purchase apps that cost money, please purchase them directly in the Mac App Store. + +```bash +$ mas purchase 497799835 +==> Downloading Xcode +==> Installed Xcode +``` + +#### `mas install` + +`mas install …` installs apps that you have already gotten/"purchased" from the Mac App Store. +Providing the `--force` flag re-installs the app even if it is already installed on your computer. + +```bash +$ mas install 497799835 +==> Downloading Xcode +==> Installed Xcode +``` + +#### `mas lucky` + +`mas lucky ` installs the first result that would be returned by `mas search `. +Like `mas install`, `mas lucky` can only install apps that have previously been gotten/"purchased". + +```bash +$ mas lucky Xcode +==> Downloading Xcode +==> Installed Xcode +``` + +### 🆕 Upgrading Apps + +All the commands in this section require you to be logged into an Apple ID in the Mac App Store. + +> mas only installs/upgrades applications from the Mac App Store. +Use [`softwareupdate(8)`] to install system updates (e.g., Xcode Command Line Tools, Safari, etc.) + +#### `mas upgrade` + +`mas upgrade` upgrades outdated apps installed from the Mac App Store. Without any arguments, it upgrades all such apps. ```bash $ mas upgrade Upgrading 2 outdated applications: -Xcode (7.0), Screens VNC - Access Your Computer From Anywhere (3.6.7) +Xcode (15.4) -> (16.0) +Developer (10.6.5) -> (10.6.6) ==> Downloading Xcode ==> Installed Xcode -==> Downloading iFlicks -==> Installed iFlicks +==> Downloading Developer +==> Installed Developer ``` -Updates can be performed selectively by providing the app identifier(s) to -`mas upgrade` +Upgrades can be performed selectively by providing app IDs to `mas upgrade`. ```bash $ mas upgrade 715768417 Upgrading 1 outdated application: -Xcode (8.0) +Xcode (15.4) -> (16.0) ==> Downloading Xcode ==> Installed Xcode ``` -### 🚏📥 Sign-in +### Mac App Store Account Management -> ⛔ The `signin` command is not supported as of macOS 10.13 High Sierra. Please see [Known Issues](#%EF%B8%8F-known-issues). +All the commands in this section interact with the Apple ID for which you are signed in in the Mac App Store. +These commands do not interact with the Apple ID for which your macOS user is signed in. -To sign into the Mac App Store for the first time run `mas signin`. +#### `mas signin` + +> ⛔ The `signin` command is not supported on macOS 10.13 (High Sierra) or newer. On those macOS versions, please +> sign in via the Mac App Store instead. Please see [Known Issues](#known-issues). + +On macOS 10.12 (Sierra) or older, `mas signin ` signs in to the specified Apple ID in the Mac App Store. ```bash $ mas signin mas@example.com -==> Signing in to Apple ID: mas@example.com Password: ``` -If you experience issues signing in this way, you can ask to sign in using a graphical dialog -(provided by Mac App Store application): +Providing the `--dialog` flag signs in using a graphical dialog provided by Mac App Store. ```bash -$ mas signin --dialog mas@example.com -==> Signing in to Apple ID: mas@example.com +mas signin --dialog mas@example.com ``` You can also embed your password in the command. ```bash -$ mas signin mas@example.com 'ZdkM4f$gzF;gX3ABXNLf8KcCt.x.np' -==> Signing in to Apple ID: mas@example.com +mas signin mas@example.com MyPassword ``` -Use `mas signout` to sign out from the Mac App Store. +#### `mas signout` + +`mas signout` signs out from the current Apple ID in the Mac App Store. ## 🍺 Homebrew integration -`mas` is integrated with [homebrew-bundle]. If `mas` is installed, and you run `brew bundle dump`, -then your Mac App Store apps will be included in the Brewfile created. See the [homebrew-bundle] +mas integrates with [homebrew-bundle]. If mas is installed, when you run `brew bundle dump`, +your Mac App Store apps will be included in the created Brewfile. See the [homebrew-bundle] docs for more details. -## ⚠️ Known Issues + +## ⚠️ Known Issues -Over time, Apple has changed the APIs used by `mas` to manage App Store apps, limiting its capabilities. Please sign in -or purchase apps using the App Store app instead. Subsequent redownloads can be performed with `mas install`. +### 💥 Changed Apple Private Frameworks -- ⛔️ The `signin` command is not supported as of macOS 10.13 High Sierra. [#164](https://github.com/mas-cli/mas/issues/164) -- ⛔️ The `purchase` command is not supported as of macOS 10.15 Catalina. [#289](https://github.com/mas-cli/mas/issues/289) -- ⛔️ The `account` command is not supported as of macOS 12 Monterey. [#417](https://github.com/mas-cli/mas/issues/417) +mas uses multiple undocumented Apple private frameworks to implement much of its functionality. +Over time, Apple has silently changed these frameworks, breaking some functionality. Known issues include: -The versions `mas` sees from the app bundles on your Mac don't always match the versions reported by the App Store for -the same app bundles. This leads to some confusion when the `outdated` and `upgrade` commands differ in behavior from -what is shown as outdated in the App Store app. Further confusing matters, there is often some delay due to CDN -propagation and caching between the time a new app version is released to the App Store, and the time it appears -available in the App Store app or via the `mas` command. These issues cause symptoms like +- ⛔️ The `signin` command is not supported on macOS 10.13 (High Sierra) or newer. [#164]( + https://github.com/mas-cli/mas/issues/164 + ) +- ⛔️ The `account` command is not supported on macOS 12 (Monterey) or newer. [#417]( + https://github.com/mas-cli/mas/issues/417 + ) + +### 👀 Version Consistency + +mas might be using suboptimal app version sources to compare local app versions with Mac App Store app versions. +That current sources are frequently consistent with the Mac App Store, but are infrequently inconsistent. +This might cause symptoms like [#384](https://github.com/mas-cli/mas/issues/384) and +[#387](https://github.com/mas-cli/mas/issues/387). mas will be updated soon to fix any such problems, if possible. + +### ⏳ Eventual Consistency + +The Mac App Store operates on eventual consistency, so the versions seen by various parts of mas or the Mac App Store +might be inconsistent for some period of time. This might cause symptoms like [#384](https://github.com/mas-cli/mas/issues/384) and [#387](https://github.com/mas-cli/mas/issues/387). -Macs with Apple silicon can install and run iOS and iPadOS apps from the App Store. `mas` is not yet aware of these -apps, and is not yet able to install or update them. [#321](https://github.com/mas-cli/mas/issues/321) +### 📱 iOS and iPadOS Apps -## 💥 When something doesn't work +Macs with Apple Silicon can install and run iOS and iPadOS apps from the Mac App Store. mas is not yet aware of these +apps, and is not yet able to install or upgrade them. [#321](https://github.com/mas-cli/mas/issues/321) -If you see this error, it's probably because you haven't installed the app through the App Store yet. -See [#46](https://github.com/mas-cli/mas/issues/46#issuecomment-248581233). -> This redownload is not available for this Apple ID either because it was bought by a different user or the -> item was refunded or cancelled. +### 📺 Using `tmux` -If `mas` doesn't work for you as expected (e.g. you can't update/download apps), run `mas reset` and try again. -If the issue persists, please [file a bug](https://github.com/mas-cli/mas/issues/new). -All your feedback is much appreciated! ✨ - -## 📺 Using `tmux` - -`mas` operates via the same system services as the Mac App Store. These exist as -separate processes with communication through XPC. As a result of this, `mas` +mas operates via the same system services as the Mac App Store. These exist as +separate processes with communication through XPC. As a result of this, mas experiences similar problems as the pasteboard when running inside `tmux`. A [wrapper tool exists](https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard) to -fix pasteboard behaviour which also works for `mas`. +fix pasteboard behaviour which also works for mas. You should consider configuring `tmux` to use the wrapper but if you do not wish to do this it can be used on a one-off basis as follows: @@ -236,9 +303,20 @@ brew install reattach-to-user-namespace reattach-to-user-namespace mas install ``` +## 🚫 When something doesn't work + +If you see the following error, it's probably because you haven't yet "purchased" the app through the Mac App Store. +See [#46](https://github.com/mas-cli/mas/issues/46#issuecomment-248581233). +> This redownload is not available for this Apple ID either because it was bought by a different user or the +> item was refunded or cancelled. + +If mas doesn't work for you as expected (e.g. you can't install/upgrade apps), run `mas reset`, then try again. +If the issue persists, please [file a bug](https://github.com/mas-cli/mas/issues/new). +All feedback is much appreciated! ✨ + ## ℹ️ Build from source -You can build from Xcode by opening the root `mas` directory, or from the Terminal: +You can build from Xcode by opening the root mas directory, or from the Terminal: ```bash script/bootstrap @@ -251,7 +329,7 @@ Build output can be found in the `.build/` directory within the project. The tests in this project are a recent work-in-progress. Since Xcode does not officially support tests for command-line tool targets, -all logic is part of the MasKit target with tests in MasKitTests. +all logic is part of the mas target with tests in masTests. Tests are written using [Quick]. ```bash @@ -260,7 +338,7 @@ script/test ## 📄 License -mas-cli was created by [@argon](https://github.com/argon). +mas was created by [@argon](https://github.com/argon). Code is under the [MIT license](LICENSE). [homebrew-bundle]: https://github.com/Homebrew/homebrew-bundle diff --git a/Sources/MasKit/AppStore/Downloader.swift b/Sources/MasKit/AppStore/Downloader.swift deleted file mode 100644 index ca6530c..0000000 --- a/Sources/MasKit/AppStore/Downloader.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Downloader.swift -// mas-cli -// -// Created by Andrew Naylor on 21/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import CommerceKit -import PromiseKit -import StoreFoundation - -/// Downloads a list of apps, one after the other, printing progress to the console. -/// -/// - Parameter appIDs: The IDs of the apps to be downloaded -/// - Parameter purchase: Flag indicating whether the apps needs to be purchased. -/// Only works for free apps. Defaults to false. -/// - Returns: A promise that completes when the downloads are complete. If any fail, -/// the promise is rejected with the first error, after all remaining downloads are attempted. -func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise { - var firstError: Error? - return appIDs.reduce(Guarantee.value(())) { previous, appID in - previous.then { - downloadWithRetries(appID, purchase: purchase).recover { error in - if firstError == nil { - firstError = error - } - } - } - }.done { - if let error = firstError { - throw error - } - } -} - -private func downloadWithRetries( - _ appID: UInt64, purchase: Bool = false, attempts: Int = 3 -) -> Promise { - download(appID, purchase: purchase).recover { error -> Promise in - guard attempts > 1 else { - throw error - } - - // If the download failed due to network issues, try again. Otherwise, fail immediately. - guard case MASError.downloadFailed(let downloadError) = error, - case NSURLErrorDomain = downloadError?.domain - else { - throw error - } - - let attempts = attempts - 1 - printWarning((downloadError ?? error).localizedDescription) - printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").") - return downloadWithRetries(appID, purchase: purchase, attempts: attempts) - } -} - -/// Downloads an app, printing progress to the console. -/// -/// - Parameter appID: The ID of the app to be downloaded -/// - Parameter purchase: Flag indicating whether the app needs to be purchased. -/// Only works for free apps. Defaults to false. -/// - Returns: A promise the completes when the download is complete. -private func download(_ appID: UInt64, purchase: Bool = false) -> Promise { - var storeAccount: ISStoreAccount? - if #unavailable(macOS 12) { - // Monterey obscured the user's account information, but still allows - // redownloads without passing it to SSPurchase. - // https://github.com/mas-cli/mas/issues/417 - guard let account = ISStoreAccount.primaryAccount else { - return Promise(error: MASError.notSignedIn) - } - - storeAccount = account as? ISStoreAccount - guard storeAccount != nil else { - fatalError("Unable to cast StoreAccount to ISStoreAccount") - } - } - - return Promise { seal in - let purchase = SSPurchase(adamId: appID, account: storeAccount, purchase: purchase) - purchase.perform { purchase, _, error, response in - if let error { - seal.reject(MASError.purchaseFailed(error: error as NSError?)) - return - } - - guard response?.downloads.isEmpty == false, let purchase else { - print("No downloads") - seal.reject(MASError.noDownloads) - return - } - - seal.fulfill(purchase) - } - }.then { purchase -> Promise in - let observer = PurchaseDownloadObserver(purchase: purchase) - let download = Promise { seal in - observer.errorHandler = seal.reject - observer.completionHandler = seal.fulfill_ - } - - let downloadQueue = CKDownloadQueue.shared() - let observerID = downloadQueue.add(observer) - return download.ensure { - downloadQueue.remove(observerID) - } - } -} diff --git a/Sources/MasKit/AppStore/ISStoreAccount.swift b/Sources/MasKit/AppStore/ISStoreAccount.swift deleted file mode 100644 index 5865332..0000000 --- a/Sources/MasKit/AppStore/ISStoreAccount.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// ISStoreAccount.swift -// mas-cli -// -// Created by Andrew Naylor on 22/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import CommerceKit -import StoreFoundation - -extension ISStoreAccount: StoreAccount { - static var primaryAccount: StoreAccount? { - var account: ISStoreAccount? - - if #available(macOS 10.13, *) { - let group = DispatchGroup() - group.enter() - - let accountService: ISAccountService = ISServiceProxy.genericShared().accountService - accountService.primaryAccount { (storeAccount: ISStoreAccount) in - account = storeAccount - group.leave() - } - - _ = group.wait(timeout: .now() + 30) - } else { - // macOS 10.9-10.12 - let accountStore = CKAccountStore.shared() - account = accountStore.primaryAccount - } - - return account - } - - static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> StoreAccount { - var storeAccount: ISStoreAccount? - var maserror: MASError? - - let accountService: ISAccountService = ISServiceProxy.genericShared().accountService - let client = ISStoreClient(storeClientType: 0) - accountService.setStoreClient(client) - - let context = ISAuthenticationContext(accountID: 0) - context.appleIDOverride = username - - if systemDialog { - context.appleIDOverride = username - } else { - context.demoMode = true - context.demoAccountName = username - context.demoAccountPassword = password - context.demoAutologinMode = true - } - - let group = DispatchGroup() - group.enter() - - // Only works on macOS Sierra and below - accountService.signIn(with: context) { success, account, error in - if success { - storeAccount = account - } else { - maserror = .signInFailed(error: error as NSError?) - } - group.leave() - } - - if systemDialog { - group.wait() - } else { - _ = group.wait(timeout: .now() + 30) - } - - if let account = storeAccount { - return account - } - - throw maserror ?? MASError.signInFailed(error: nil) - } -} diff --git a/Sources/MasKit/AppStore/SSPurchase.swift b/Sources/MasKit/AppStore/SSPurchase.swift deleted file mode 100644 index c3d3dcb..0000000 --- a/Sources/MasKit/AppStore/SSPurchase.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// SSPurchase.swift -// mas-cli -// -// Created by Andrew Naylor on 25/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import CommerceKit -import StoreFoundation - -typealias SSPurchaseCompletion = - (_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void - -extension SSPurchase { - convenience init(adamId: UInt64, account: ISStoreAccount?, purchase: Bool = false) { - self.init() - - var parameters: [String: Any] = [ - "productType": "C", - "price": 0, - "salableAdamId": adamId, - "pg": "default", - "appExtVrsId": 0, - ] - - if purchase { - parameters["macappinstalledconfirmed"] = 1 - parameters["pricingParameters"] = "STDQ" - - } else { - // is redownload, use existing functionality - parameters["pricingParameters"] = "STDRDL" - } - - buyParameters = - parameters.map { key, value in - "\(key)=\(value)" - } - .joined(separator: "&") - - itemIdentifier = adamId - - if let account { - accountIdentifier = account.dsID - appleID = account.identifier - } - - // Not sure if this is needed, but lets use it here. - if purchase { - isRedownload = false - } - - let downloadMetadata = SSDownloadMetadata() - downloadMetadata.kind = "software" - downloadMetadata.itemIdentifier = adamId - - self.downloadMetadata = downloadMetadata - } - - func perform(_ completion: @escaping SSPurchaseCompletion) { - CKPurchaseController.shared().perform(self, withOptions: 0, completionHandler: completion) - } -} diff --git a/Sources/MasKit/Commands/Account.swift b/Sources/MasKit/Commands/Account.swift deleted file mode 100644 index 45249f7..0000000 --- a/Sources/MasKit/Commands/Account.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Account.swift -// mas-cli -// -// Created by Andrew Naylor on 21/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import Commandant -import StoreFoundation - -public struct AccountCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "account" - public let function = "Prints the primary account Apple ID" - - public init() {} - - /// Runs the command. - public func run(_: Options) -> Result { - if #available(macOS 12, *) { - // Account information is no longer available as of Monterey. - // https://github.com/mas-cli/mas/issues/417 - return .failure(.notSupported) - } - - if let account = ISStoreAccount.primaryAccount { - print(String(describing: account.identifier)) - } else { - printError("Not signed in") - return .failure(.notSignedIn) - } - return .success(()) - } -} diff --git a/Sources/MasKit/Commands/Home.swift b/Sources/MasKit/Commands/Home.swift deleted file mode 100644 index f3f6bbb..0000000 --- a/Sources/MasKit/Commands/Home.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Home.swift -// mas-cli -// -// Created by Ben Chatelain on 2018-12-29. -// Copyright © 2016 mas-cli. All rights reserved. -// - -import Commandant - -/// Opens app page on MAS Preview. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct HomeCommand: CommandProtocol { - public typealias Options = HomeOptions - - public let verb = "home" - public let function = "Opens MAS Preview app page in a browser" - - private let storeSearch: StoreSearch - private var openCommand: ExternalCommand - - public init() { - self.init( - storeSearch: MasStoreSearch(), - openCommand: OpenSystemCommand() - ) - } - - /// Designated initializer. - init( - storeSearch: StoreSearch = MasStoreSearch(), - openCommand: ExternalCommand = OpenSystemCommand() - ) { - self.storeSearch = storeSearch - self.openCommand = openCommand - } - - /// Runs the command. - public func run(_ options: HomeOptions) -> Result { - do { - guard let result = try storeSearch.lookup(app: options.appId).wait() else { - return .failure(.noSearchResultsFound) - } - - do { - try openCommand.run(arguments: result.trackViewUrl) - } catch { - printError("Unable to launch open command") - return .failure(.searchFailed) - } - if openCommand.failed { - let reason = openCommand.process.terminationReason - printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) - } - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) - } - - return .success(()) - } -} - -public struct HomeOptions: OptionsProtocol { - let appId: Int - - static func create(_ appId: Int) -> HomeOptions { - HomeOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "ID of app to show on MAS Preview") - } -} diff --git a/Sources/MasKit/Commands/Info.swift b/Sources/MasKit/Commands/Info.swift deleted file mode 100644 index 82974dc..0000000 --- a/Sources/MasKit/Commands/Info.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// Info.swift -// mas-cli -// -// Created by Denis Lebedev on 21/10/2016. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Commandant -import Foundation - -/// Displays app details. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct InfoCommand: CommandProtocol { - public let verb = "info" - public let function = "Display app information from the Mac App Store" - - private let storeSearch: StoreSearch - - public init() { - self.init(storeSearch: MasStoreSearch()) - } - - /// Designated initializer. - init(storeSearch: StoreSearch = MasStoreSearch()) { - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: InfoOptions) -> Result { - do { - guard let result = try storeSearch.lookup(app: options.appId).wait() else { - return .failure(.noSearchResultsFound) - } - - print(AppInfoFormatter.format(app: result)) - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) - } - - return .success(()) - } -} - -public struct InfoOptions: OptionsProtocol { - let appId: Int - - static func create(_ appId: Int) -> InfoOptions { - InfoOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "ID of app to show info") - } -} diff --git a/Sources/MasKit/Commands/Install.swift b/Sources/MasKit/Commands/Install.swift deleted file mode 100644 index f941cbf..0000000 --- a/Sources/MasKit/Commands/Install.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Install.swift -// mas-cli -// -// Created by Andrew Naylor on 21/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import Commandant -import CommerceKit - -/// Installs previously purchased apps from the Mac App Store. -public struct InstallCommand: CommandProtocol { - public typealias Options = InstallOptions - public let verb = "install" - public let function = "Install from the Mac App Store" - - private let appLibrary: AppLibrary - - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - // Try to download applications with given identifiers and collect results - let appIds = options.appIds.filter { appId in - if let product = appLibrary.installedApp(forId: appId), !options.forceInstall { - printWarning("\(product.appName) is already installed") - return false - } - - return true - } - - do { - try downloadAll(appIds).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } - - return .success(()) - } -} - -public struct InstallOptions: OptionsProtocol { - let appIds: [UInt64] - let forceInstall: Bool - - public static func create(_ appIds: [Int]) -> (_ forceInstall: Bool) -> InstallOptions { - { forceInstall in - InstallOptions(appIds: appIds.map { UInt64($0) }, forceInstall: forceInstall) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "app ID(s) to install") - <*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall") - } -} diff --git a/Sources/MasKit/Commands/List.swift b/Sources/MasKit/Commands/List.swift deleted file mode 100644 index 23752e7..0000000 --- a/Sources/MasKit/Commands/List.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// List.swift -// mas-cli -// -// Created by Andrew Naylor on 21/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import Commandant - -/// Command which lists all installed apps. -public struct ListCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "list" - public let function = "Lists apps from the Mac App Store which are currently installed" - - private let appLibrary: AppLibrary - - /// Public initializer. - /// - Parameter appLibrary: AppLibrary manager. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } - - /// Runs the command. - public func run(_: Options) -> Result { - let products = appLibrary.installedApps - if products.isEmpty { - printError("No installed apps found") - return .success(()) - } - - let output = AppListFormatter.format(products: products) - print(output) - - return .success(()) - } -} diff --git a/Sources/MasKit/Commands/Lucky.swift b/Sources/MasKit/Commands/Lucky.swift deleted file mode 100644 index f8288ab..0000000 --- a/Sources/MasKit/Commands/Lucky.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// Lucky.swift -// mas-cli -// -// Created by Pablo Varela on 05/11/17. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Commandant -import CommerceKit - -/// Command which installs the first search result. This is handy as many MAS titles -/// can be long with embedded keywords. -public struct LuckyCommand: CommandProtocol { - public typealias Options = LuckyOptions - public let verb = "lucky" - public let function = "Install the first result from the Mac App Store" - - private let appLibrary: AppLibrary - private let storeSearch: StoreSearch - - public init() { - self.init(storeSearch: MasStoreSearch()) - } - - /// Designated initializer. - /// - Parameter storeSearch: Search manager. - init(storeSearch: StoreSearch = MasStoreSearch()) { - self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - /// - Parameter storeSearch: Search manager. - init( - appLibrary: AppLibrary = MasAppLibrary(), - storeSearch: StoreSearch = MasStoreSearch() - ) { - self.appLibrary = appLibrary - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - var appId: Int? - - do { - let results = try storeSearch.search(for: options.appName).wait() - guard let result = results.first else { - printError("No results found") - return .failure(.noSearchResultsFound) - } - - appId = result.trackId - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) - } - - guard let identifier = appId else { fatalError() } - - return install(UInt64(identifier), options: options) - } - - /// Installs an app. - /// - /// - Parameters: - /// - appId: App identifier - /// - options: command opetions. - /// - Returns: Result of the operation. - fileprivate func install(_ appId: UInt64, options: Options) -> Result { - // Try to download applications with given identifiers and collect results - if let product = appLibrary.installedApp(forId: appId), !options.forceInstall { - printWarning("\(product.appName) is already installed") - return .success(()) - } - - do { - try downloadAll([appId]).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } - - return .success(()) - } -} - -public struct LuckyOptions: OptionsProtocol { - let appName: String - let forceInstall: Bool - - public static func create(_ appName: String) -> (_ forceInstall: Bool) -> LuckyOptions { - { forceInstall in - LuckyOptions(appName: appName, forceInstall: forceInstall) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "the app name to install") - <*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall") - } -} diff --git a/Sources/MasKit/Commands/Open.swift b/Sources/MasKit/Commands/Open.swift deleted file mode 100644 index 85e04db..0000000 --- a/Sources/MasKit/Commands/Open.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// Open.swift -// mas-cli -// -// Created by Ben Chatelain on 2018-12-29. -// Copyright © 2016 mas-cli. All rights reserved. -// - -import Commandant -import Foundation - -private let markerValue = "appstore" -private let masScheme = "macappstore" - -/// Opens app page in MAS app. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct OpenCommand: CommandProtocol { - public typealias Options = OpenOptions - - public let verb = "open" - public let function = "Opens app page in AppStore.app" - - private let storeSearch: StoreSearch - private var systemOpen: ExternalCommand - - public init() { - self.init( - storeSearch: MasStoreSearch(), - openCommand: OpenSystemCommand() - ) - } - - /// Designated initializer. - init( - storeSearch: StoreSearch = MasStoreSearch(), - openCommand: ExternalCommand = OpenSystemCommand() - ) { - self.storeSearch = storeSearch - systemOpen = openCommand - } - - /// Runs the command. - public func run(_ options: OpenOptions) -> Result { - do { - if options.appId == markerValue { - // If no app ID is given, just open the MAS GUI app - try systemOpen.run(arguments: masScheme + "://") - return .success(()) - } - - guard let appId = Int(options.appId) - else { - printError("Invalid app ID") - return .failure(.noSearchResultsFound) - } - - guard let result = try storeSearch.lookup(app: appId).wait() - else { - return .failure(.noSearchResultsFound) - } - - guard var url = URLComponents(string: result.trackViewUrl) - else { - return .failure(.searchFailed) - } - url.scheme = masScheme - - do { - try systemOpen.run(arguments: url.string!) - } catch { - printError("Unable to launch open command") - return .failure(.searchFailed) - } - if systemOpen.failed { - let reason = systemOpen.process.terminationReason - printError("Open failed: (\(reason)) \(systemOpen.stderr)") - return .failure(.searchFailed) - } - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) - } - - return .success(()) - } -} - -public struct OpenOptions: OptionsProtocol { - var appId: String - - static func create(_ appId: String) -> OpenOptions { - OpenOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(defaultValue: markerValue, usage: "the app ID") - } -} diff --git a/Sources/MasKit/Commands/Outdated.swift b/Sources/MasKit/Commands/Outdated.swift deleted file mode 100644 index ed3545f..0000000 --- a/Sources/MasKit/Commands/Outdated.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Outdated.swift -// mas-cli -// -// Created by Andrew Naylor on 21/08/2015. -// Copyright (c) 2015 Andrew Naylor. All rights reserved. -// - -import Commandant -import Foundation -import PromiseKit - -import enum Swift.Result - -/// Command which displays a list of installed apps which have available updates -/// ready to be installed from the Mac App Store. -public struct OutdatedCommand: CommandProtocol { - public typealias Options = OutdatedOptions - public let verb = "outdated" - public let function = "Lists pending updates from the Mac App Store" - - private let appLibrary: AppLibrary - private let storeSearch: StoreSearch - - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - /// - Parameter storeSearch: StoreSearch manager. - init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) { - self.appLibrary = appLibrary - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - let promises = appLibrary.installedApps.map { installedApp in - firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) - }.done { storeApp in - guard let storeApp else { - if options.verbose { - printWarning( - """ - Identifier \(installedApp.itemIdentifier) not found in store. \ - Was expected to identify \(installedApp.appName). - """) - } - return - } - - if installedApp.isOutdatedWhenComparedTo(storeApp) { - print( - """ - \(installedApp.itemIdentifier) \(installedApp.appName) \ - (\(installedApp.bundleVersion) -> \(storeApp.version)) - """) - } - } - } - - return firstly { - when(fulfilled: promises) - }.map { - Result.success(()) - }.recover { error in - // Bubble up MASErrors - .value(Result.failure(error as? MASError ?? .searchFailed)) - }.wait() - } -} - -public struct OutdatedOptions: OptionsProtocol { - public typealias ClientError = MASError - - let verbose: Bool - - static func create(verbose: Bool) -> OutdatedOptions { - OutdatedOptions(verbose: verbose) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps") - } -} diff --git a/Sources/MasKit/Commands/Purchase.swift b/Sources/MasKit/Commands/Purchase.swift deleted file mode 100644 index c8dcd2a..0000000 --- a/Sources/MasKit/Commands/Purchase.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Purchase.swift -// mas-cli -// -// Created by Jakob Rieck on 24/10/2017. -// Copyright (c) 2017 Jakob Rieck. All rights reserved. -// - -import Commandant -import CommerceKit - -public struct PurchaseCommand: CommandProtocol { - public typealias Options = PurchaseOptions - public let verb = "purchase" - public let function = "Purchase and download free apps from the Mac App Store" - - private let appLibrary: AppLibrary - - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - if #available(macOS 10.15, *) { - // Purchases are no longer possible as of Catalina. - // https://github.com/mas-cli/mas/issues/289 - return .failure(.notSupported) - } - - // Try to download applications with given identifiers and collect results - let appIds = options.appIds.filter { appId in - if let product = appLibrary.installedApp(forId: appId) { - printWarning("\(product.appName) has already been purchased.") - return false - } - - return true - } - - do { - try downloadAll(appIds, purchase: true).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } - - return .success(()) - } -} - -public struct PurchaseOptions: OptionsProtocol { - let appIds: [UInt64] - - public static func create(_ appIds: [Int]) -> PurchaseOptions { - PurchaseOptions(appIds: appIds.map { UInt64($0) }) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "app ID(s) to install") - } -} diff --git a/Sources/MasKit/Commands/Reset.swift b/Sources/MasKit/Commands/Reset.swift deleted file mode 100644 index 37d6c2d..0000000 --- a/Sources/MasKit/Commands/Reset.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// Reset.swift -// mas-cli -// -// Created by Andrew Naylor on 14/09/2016. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Commandant -import CommerceKit - -/// Kills several macOS processes as a means to reset the app store. -public struct ResetCommand: CommandProtocol { - public typealias Options = ResetOptions - public let verb = "reset" - public let function = "Resets the Mac App Store" - - public init() {} - - /// Runs the command. - public func run(_ options: Options) -> Result { - // The "Reset Application" command in the Mac App Store debug menu performs - // the following steps - // - // - killall Dock - // - killall storeagent (storeagent no longer exists) - // - rm com.apple.appstore download directory - // - clear cookies (appears to be a no-op) - // - // As storeagent no longer exists we will implement a slight variant and kill all - // App Store-associated processes - // - storeaccountd - // - storeassetd - // - storedownloadd - // - storeinstalld - // - storelegacy - - // Kill processes - let killProcs = [ - "Dock", - "storeaccountd", - "storeassetd", - "storedownloadd", - "storeinstalld", - "storelegacy", - ] - - let kill = Process() - let stdout = Pipe() - let stderr = Pipe() - - kill.launchPath = "/usr/bin/killall" - kill.arguments = killProcs - kill.standardOutput = stdout - kill.standardError = stderr - - kill.launch() - kill.waitUntilExit() - - if kill.terminationStatus != 0, options.debug { - let output = stderr.fileHandleForReading.readDataToEndOfFile() - printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)") - } - - // Wipe Download Directory - if let directory = CKDownloadDirectory(nil) { - do { - try FileManager.default.removeItem(atPath: directory) - } catch { - if options.debug { - printError("removeItemAtPath:\"\(directory)\" failed, \(error)") - } - } - } - - return .success(()) - } -} - -public struct ResetOptions: OptionsProtocol { - let debug: Bool - - public static func create(debug: Bool) -> ResetOptions { - ResetOptions(debug: debug) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Switch(flag: nil, key: "debug", usage: "Enable debug mode") - } -} diff --git a/Sources/MasKit/Commands/Search.swift b/Sources/MasKit/Commands/Search.swift deleted file mode 100644 index cc0f2c7..0000000 --- a/Sources/MasKit/Commands/Search.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// Search.swift -// mas-cli -// -// Created by Michael Schneider on 4/14/16. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Commandant - -/// Search the Mac App Store using the iTunes Search API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ -public struct SearchCommand: CommandProtocol { - public typealias Options = SearchOptions - public let verb = "search" - public let function = "Search for apps from the Mac App Store" - - private let storeSearch: StoreSearch - - public init() { - self.init(storeSearch: MasStoreSearch()) - } - - /// Designated initializer. - /// - /// - Parameter storeSearch: Search manager. - init(storeSearch: StoreSearch = MasStoreSearch()) { - self.storeSearch = storeSearch - } - - public func run(_ options: Options) -> Result { - do { - let results = try storeSearch.search(for: options.appName).wait() - if results.isEmpty { - return .failure(.noSearchResultsFound) - } - - let output = SearchResultFormatter.format(results: results, includePrice: options.price) - print(output) - - return .success(()) - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) - } - } -} - -public struct SearchOptions: OptionsProtocol { - let appName: String - let price: Bool - - public static func create(_ appName: String) -> (_ price: Bool) -> SearchOptions { - { price in - SearchOptions(appName: appName, price: price) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "the app name to search") - <*> mode <| Option(key: "price", defaultValue: false, usage: "Show price of found apps") - } -} diff --git a/Sources/MasKit/Commands/SignIn.swift b/Sources/MasKit/Commands/SignIn.swift deleted file mode 100644 index f1b7cf0..0000000 --- a/Sources/MasKit/Commands/SignIn.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// SignIn.swift -// mas-cli -// -// Created by Andrew Naylor on 14/02/2016. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Commandant -import StoreFoundation - -public struct SignInCommand: CommandProtocol { - public typealias Options = SignInOptions - - public let verb = "signin" - public let function = "Sign in to the Mac App Store" - - public init() {} - - /// Runs the command. - public func run(_ options: Options) -> Result { - if #available(macOS 10.13, *) { - // Signing in is no longer possible as of High Sierra. - // https://github.com/mas-cli/mas/issues/164 - return .failure(.notSupported) - } - - guard ISStoreAccount.primaryAccount == nil else { - return .failure(.alreadySignedIn) - } - - do { - printInfo("Signing in to Apple ID: \(options.username)") - - let password: String = { - if options.password.isEmpty, !options.dialog { - return String(validatingUTF8: getpass("Password: "))! - } - return options.password - }() - - _ = try ISStoreAccount.signIn(username: options.username, password: password, systemDialog: options.dialog) - } catch let error as NSError { - return .failure(.signInFailed(error: error)) - } - - return .success(()) - } -} - -public struct SignInOptions: OptionsProtocol { - public typealias ClientError = MASError - - let username: String - let password: String - let dialog: Bool - - static func create(username: String) -> (_ password: String) -> (_ dialog: Bool) -> SignInOptions { - { password in { dialog in SignInOptions(username: username, password: password, dialog: dialog) } } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "Apple ID") - <*> mode <| Argument(defaultValue: "", usage: "Password") - <*> mode <| Option(key: "dialog", defaultValue: false, usage: "Complete login with graphical dialog") - } -} diff --git a/Sources/MasKit/Commands/SignOut.swift b/Sources/MasKit/Commands/SignOut.swift deleted file mode 100644 index efdf467..0000000 --- a/Sources/MasKit/Commands/SignOut.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SignOut.swift -// mas-cli -// -// Created by Andrew Naylor on 14/02/2016. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Commandant -import CommerceKit - -public struct SignOutCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "signout" - public let function = "Sign out of the Mac App Store" - - public init() {} - - /// Runs the command. - public func run(_: Options) -> Result { - if #available(macOS 10.13, *) { - let accountService: ISAccountService = ISServiceProxy.genericShared().accountService - accountService.signOut() - } else { - // Using CKAccountStore to sign out does nothing on High Sierra - // https://github.com/mas-cli/mas/issues/129 - CKAccountStore.shared().signOut() - } - - return .success(()) - } -} diff --git a/Sources/MasKit/Commands/Uninstall.swift b/Sources/MasKit/Commands/Uninstall.swift deleted file mode 100644 index 97d3767..0000000 --- a/Sources/MasKit/Commands/Uninstall.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// Uninstall.swift -// mas-cli -// -// Created by Ben Chatelain on 2018-12-27. -// Copyright © 2015 Andrew Naylor. All rights reserved. -// - -import Commandant -import CommerceKit -import StoreFoundation - -/// Command which uninstalls apps managed by the Mac App Store. -public struct UninstallCommand: CommandProtocol { - public typealias Options = UninstallOptions - public let verb = "uninstall" - public let function = "Uninstall app installed from the Mac App Store" - - private let appLibrary: AppLibrary - - /// Public initializer. - /// - Parameter appLibrary: AppLibrary manager. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - init(appLibrary: AppLibrary = MasAppLibrary()) { - self.appLibrary = appLibrary - } - - /// Runs the uninstall command. - /// - /// - Parameter options: UninstallOptions (arguments) for this command - /// - Returns: Success or an error. - public func run(_ options: Options) -> Result { - let appId = UInt64(options.appId) - - guard let product = appLibrary.installedApp(forId: appId) else { - return .failure(.notInstalled) - } - - if options.dryRun { - printInfo("\(product.appName) \(product.bundlePath)") - printInfo("(not removed, dry run)") - - return .success(()) - } - - do { - try appLibrary.uninstallApp(app: product) - } catch { - return .failure(.uninstallFailed) - } - - return .success(()) - } -} - -/// Options for the uninstall command. -public struct UninstallOptions: OptionsProtocol { - /// Numeric app ID - let appId: Int - - /// Flag indicating that removal shouldn't be performed - let dryRun: Bool - - static func create(_ appId: Int) -> (_ dryRun: Bool) -> UninstallOptions { - { dryRun in - UninstallOptions(appId: appId, dryRun: dryRun) - } - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "ID of app to uninstall") - <*> mode <| Switch(flag: nil, key: "dry-run", usage: "dry run") - } -} diff --git a/Sources/MasKit/Commands/Upgrade.swift b/Sources/MasKit/Commands/Upgrade.swift deleted file mode 100644 index 1acf0f0..0000000 --- a/Sources/MasKit/Commands/Upgrade.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// Upgrade.swift -// mas-cli -// -// Created by Andrew Naylor on 30/12/2015. -// Copyright © 2015 Andrew Naylor. All rights reserved. -// - -import Commandant -import Foundation -import PromiseKit - -import enum Swift.Result - -/// Command which upgrades apps with new versions available in the Mac App Store. -public struct UpgradeCommand: CommandProtocol { - public typealias Options = UpgradeOptions - public let verb = "upgrade" - public let function = "Upgrade outdated apps from the Mac App Store" - - private let appLibrary: AppLibrary - private let storeSearch: StoreSearch - - /// Public initializer. - public init() { - self.init(appLibrary: MasAppLibrary()) - } - - /// Internal initializer. - /// - Parameter appLibrary: AppLibrary manager. - /// - Parameter storeSearch: StoreSearch manager. - init(appLibrary: AppLibrary = MasAppLibrary(), storeSearch: StoreSearch = MasStoreSearch()) { - self.appLibrary = appLibrary - self.storeSearch = storeSearch - } - - /// Runs the command. - public func run(_ options: Options) -> Result { - let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)] - do { - apps = try findOutdatedApps(options) - } catch { - // Bubble up MASErrors - return .failure(error as? MASError ?? .searchFailed) - } - - guard apps.count > 0 else { - printWarning("Nothing found to upgrade") - return .success(()) - } - - print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") - print( - apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } - .joined(separator: "\n")) - - let appIds = apps.map(\.installedApp.itemIdentifier.uint64Value) - do { - try downloadAll(appIds).wait() - } catch { - return .failure(error as? MASError ?? .downloadFailed(error: error as NSError)) - } - - return .success(()) - } - - private func findOutdatedApps(_ options: Options) throws -> [(SoftwareProduct, SearchResult)] { - let apps: [SoftwareProduct] = - options.apps.isEmpty - ? appLibrary.installedApps - : options.apps.compactMap { - if let appId = UInt64($0) { - // if argument a UInt64, lookup app by id using argument - return appLibrary.installedApp(forId: appId) - } else { - // if argument not a UInt64, lookup app by name using argument - return appLibrary.installedApp(named: $0) - } - } - - let promises = apps.map { installedApp in - // only upgrade apps whose local version differs from the store version - firstly { - storeSearch.lookup(app: installedApp.itemIdentifier.intValue) - }.map { result -> (SoftwareProduct, SearchResult)? in - guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { - return nil - } - - return (installedApp, storeApp) - } - } - - return try when(fulfilled: promises).wait().compactMap { $0 } - } -} - -public struct UpgradeOptions: OptionsProtocol { - let apps: [String] - - static func create(_ apps: [String]) -> UpgradeOptions { - UpgradeOptions(apps: apps) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(defaultValue: [], usage: "app(s) to upgrade") - } -} diff --git a/Sources/MasKit/Commands/Vendor.swift b/Sources/MasKit/Commands/Vendor.swift deleted file mode 100644 index 0fe8401..0000000 --- a/Sources/MasKit/Commands/Vendor.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Vendor.swift -// mas-cli -// -// Created by Ben Chatelain on 2018-12-29. -// Copyright © 2016 mas-cli. All rights reserved. -// - -import Commandant - -/// Opens vendor's app page in a browser. Uses the iTunes Lookup API: -/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/#lookup -public struct VendorCommand: CommandProtocol { - public typealias Options = VendorOptions - - public let verb = "vendor" - public let function = "Opens vendor's app page in a browser" - - private let storeSearch: StoreSearch - private var openCommand: ExternalCommand - - public init() { - self.init( - storeSearch: MasStoreSearch(), - openCommand: OpenSystemCommand() - ) - } - - /// Designated initializer. - init( - storeSearch: StoreSearch = MasStoreSearch(), - openCommand: ExternalCommand = OpenSystemCommand() - ) { - self.storeSearch = storeSearch - self.openCommand = openCommand - } - - /// Runs the command. - public func run(_ options: VendorOptions) -> Result { - do { - guard let result = try storeSearch.lookup(app: options.appId).wait() - else { - return .failure(.noSearchResultsFound) - } - - guard let vendorWebsite = result.sellerUrl - else { throw MASError.noVendorWebsite } - - do { - try openCommand.run(arguments: vendorWebsite) - } catch { - printError("Unable to launch open command") - return .failure(.searchFailed) - } - if openCommand.failed { - let reason = openCommand.process.terminationReason - printError("Open failed: (\(reason)) \(openCommand.stderr)") - return .failure(.searchFailed) - } - } catch { - // Bubble up MASErrors - if let error = error as? MASError { - return .failure(error) - } - return .failure(.searchFailed) - } - - return .success(()) - } -} - -public struct VendorOptions: OptionsProtocol { - let appId: Int - - static func create(_ appId: Int) -> VendorOptions { - VendorOptions(appId: appId) - } - - public static func evaluate(_ mode: CommandMode) -> Result> { - create - <*> mode <| Argument(usage: "the app ID to show the vendor's website") - } -} diff --git a/Sources/MasKit/Commands/Version.swift b/Sources/MasKit/Commands/Version.swift deleted file mode 100644 index d166170..0000000 --- a/Sources/MasKit/Commands/Version.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Version.swift -// mas-cli -// -// Created by Andrew Naylor on 20/09/2015. -// Copyright © 2015 Andrew Naylor. All rights reserved. -// - -import Commandant - -/// Command which displays the version of the mas tool. -public struct VersionCommand: CommandProtocol { - public typealias Options = NoOptions - public let verb = "version" - public let function = "Print version number" - - public init() {} - - /// Runs the command. - public func run(_: Options) -> Result { - print(Package.version) - return .success(()) - } -} diff --git a/Sources/MasKit/Controllers/AppLibrary.swift b/Sources/MasKit/Controllers/AppLibrary.swift deleted file mode 100644 index ac5ade4..0000000 --- a/Sources/MasKit/Controllers/AppLibrary.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// AppLibrary.swift -// MasKit -// -// Created by Ben Chatelain on 12/27/18. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Foundation - -/// Utility for managing installed apps. -protocol AppLibrary { - /// Entire set of installed apps. - var installedApps: [SoftwareProduct] { get } - - /// Finds an app by ID. - /// - /// - Parameter forId: MAS ID for app. - /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forId: UInt64) -> SoftwareProduct? - - /// Uninstalls an app. - /// - /// - Parameter app: App to be removed. - /// - Throws: Error if there is a problem. - func uninstallApp(app: SoftwareProduct) throws -} - -/// Common logic -extension AppLibrary { - /// Finds an app by name. - /// - /// - Parameter id: MAS ID for app. - /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forId identifier: UInt64) -> SoftwareProduct? { - let appId = NSNumber(value: identifier) - return installedApps.first { $0.itemIdentifier == appId } - } - - /// Finds an app by name. - /// - /// - Parameter appName: Full title of an app. - /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(named appName: String) -> SoftwareProduct? { - installedApps.first { $0.appName == appName } - } -} diff --git a/Sources/MasKit/Controllers/MasAppLibrary.swift b/Sources/MasKit/Controllers/MasAppLibrary.swift deleted file mode 100644 index 7cc86c5..0000000 --- a/Sources/MasKit/Controllers/MasAppLibrary.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// MasAppLibrary.swift -// MasKit -// -// Created by Ben Chatelain on 12/27/18. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import CommerceKit - -/// Utility for managing installed apps. -class MasAppLibrary: AppLibrary { - /// CommerceKit's singleton manager of installed software. - private let softwareMap: SoftwareMap - - /// Array of installed software products. - lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts().filter { product in - product.bundlePath.starts(with: "/Applications/") - } - - /// Internal initializer for providing a mock software map. - /// - Parameter softwareMap: SoftwareMap to use - init(softwareMap: SoftwareMap = CKSoftwareMap.shared()) { - self.softwareMap = softwareMap - } - - /// Finds an app using a bundle identifier. - /// - /// - Parameter bundleId: Bundle identifier of app. - /// - Returns: Software Product of app if found; nil otherwise. - func installedApp(forBundleId bundleId: String) -> SoftwareProduct? { - softwareMap.product(for: bundleId) - } - - /// Uninstalls an app. - /// - /// - Parameter app: App to be removed. - /// - Throws: Error if there is a problem. - func uninstallApp(app: SoftwareProduct) throws { - if !userIsRoot() { - printWarning("Apps installed from the Mac App Store require root permission to remove.") - } - - let appUrl = URL(fileURLWithPath: app.bundlePath) - do { - // Move item to trash - var trashUrl: NSURL? - try FileManager().trashItem(at: appUrl, resultingItemURL: &trashUrl) - if let path = trashUrl?.path { - printInfo("App moved to trash: \(path)") - } - } catch { - printError("Unable to move app to trash.") - throw MASError.uninstallFailed - } - } - - /// Detects whether the current user is root. - /// - /// - Returns: true if the current user is root; false otherwise - private func userIsRoot() -> Bool { - NSUserName() == "root" - } -} diff --git a/Sources/MasKit/Controllers/StoreSearch.swift b/Sources/MasKit/Controllers/StoreSearch.swift deleted file mode 100644 index f4866bb..0000000 --- a/Sources/MasKit/Controllers/StoreSearch.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// StoreSearch.swift -// MasKit -// -// Created by Ben Chatelain on 12/29/18. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Foundation -import PromiseKit - -/// Protocol for searching the MAS catalog. -protocol StoreSearch { - func lookup(app appId: Int) -> Promise - func search(for appName: String) -> Promise<[SearchResult]> -} - -enum Entity: String { - case macSoftware - case iPadSoftware - case iPhoneSoftware = "software" -} - -// MARK: - Common methods -extension StoreSearch { - /// Builds the search URL for an app. - /// - /// - Parameter appName: MAS app identifier. - /// - Returns: URL for the search service or nil if appName can't be encoded. - func searchURL(for appName: String, inCountry country: String?, ofEntity entity: Entity = .macSoftware) -> URL? { - guard var components = URLComponents(string: "https://itunes.apple.com/search") else { - return nil - } - - components.queryItems = [ - URLQueryItem(name: "media", value: "software"), - URLQueryItem(name: "entity", value: entity.rawValue), - URLQueryItem(name: "term", value: appName), - ] - - if let country { - components.queryItems!.append(URLQueryItem(name: "country", value: country)) - } - - return components.url - } - - /// Builds the lookup URL for an app. - /// - /// - Parameter appId: MAS app identifier. - /// - Returns: URL for the lookup service or nil if appId can't be encoded. - func lookupURL(forApp appId: Int, inCountry country: String?) -> URL? { - guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else { - return nil - } - - components.queryItems = [ - URLQueryItem(name: "id", value: "\(appId)"), - URLQueryItem(name: "entity", value: "desktopSoftware"), - ] - - if let country { - components.queryItems!.append(URLQueryItem(name: "country", value: country)) - } - - return components.url - } -} diff --git a/Sources/MasKit/Formatters/Utilities.swift b/Sources/MasKit/Formatters/Utilities.swift deleted file mode 100644 index 33f3b67..0000000 --- a/Sources/MasKit/Formatters/Utilities.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// Utilities.swift -// mas-cli -// -// Created by Andrew Naylor on 14/09/2016. -// Copyright © 2016 Andrew Naylor. All rights reserved. -// - -import Foundation - -/// A collection of output formatting helpers - -/// Terminal Control Sequence Indicator -let csi = "\u{001B}[" - -#if DEBUG - - var printObserver: ((String) -> Void)? - - // Override global print for testability. - // See MasKitTests/OutputListener.swift. - func print( - _ items: Any..., - separator: String = " ", - terminator: String = "\n" - ) { - if let observer = printObserver { - let output = - items - .map { "\($0)" } - .joined(separator: separator) - .appending(terminator) - observer(output) - } - - var prefix = "" - for item in items { - Swift.print(prefix, terminator: "") - Swift.print(item, terminator: "") - prefix = separator - } - - Swift.print(terminator, terminator: "") - } - - func print( - _ items: Any..., - separator: String = " ", - terminator: String = "\n", - to output: inout some TextOutputStream - ) { - if let observer = printObserver { - let output = - items - .map { "\($0)" } - .joined(separator: separator) - .appending(terminator) - observer(output) - } - - var prefix = "" - for item in items { - Swift.print(prefix, terminator: "", to: &output) - Swift.print(item, terminator: "", to: &output) - prefix = separator - } - - Swift.print(terminator, terminator: "", to: &output) - } - -#endif - -private var standardError = FileHandle.standardError - -extension FileHandle: TextOutputStream { - public func write(_ string: String) { - guard let data = string.data(using: .utf8) else { return } - write(data) - } -} - -/// Prints a message to stdout prefixed with a blue arrow. -func printInfo(_ message: String) { - guard isatty(fileno(stdout)) != 0 else { - print("==> \(message)") - return - } - - // Blue bold arrow, Bold text - print("\(csi)1;34m==>\(csi)0m \(csi)1m\(message)\(csi)0m") -} - -/// Prints a message to stderr prefixed with "Warning:" underlined in yellow. -public func printWarning(_ message: String) { - guard isatty(fileno(stderr)) != 0 else { - print("Warning: \(message)", to: &standardError) - return - } - - // Yellow, underlined "Warning:" prefix - print("\(csi)4;33mWarning:\(csi)0m \(message)", to: &standardError) -} - -/// Prints a message to stderr prefixed with "Error:" underlined in red. -public func printError(_ message: String) { - guard isatty(fileno(stderr)) != 0 else { - print("Error: \(message)", to: &standardError) - return - } - - // Red, underlined "Error:" prefix - print("\(csi)4;31mError:\(csi)0m \(message)", to: &standardError) -} - -/// Flushes stdout. -func clearLine() { - guard isatty(fileno(stdout)) != 0 else { - return - } - - print("\(csi)2K\(csi)0G", terminator: "") - fflush(stdout) -} diff --git a/Sources/MasKit/MasKit.swift b/Sources/MasKit/MasKit.swift deleted file mode 100644 index 20708db..0000000 --- a/Sources/MasKit/MasKit.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// MasKit.swift -// MasKit -// -// Created by Chris Araman on 4/22/21. -// Copyright © 2021 mas-cli. All rights reserved. -// - -import PromiseKit - -public enum MasKit { - public static func initialize() { - PromiseKit.conf.Q.map = .global() - PromiseKit.conf.Q.return = .global() - PromiseKit.conf.logHandler = { event in - switch event { - case .waitOnMainThread: - // Ignored. This is a console app that waits on the main thread for - // promises to be processed on the global DispatchQueue. - break - default: - // Other events indicate a programming error. - fatalError("PromiseKit event: \(event)") - } - } - } -} diff --git a/Sources/MasKit/Package.swift b/Sources/MasKit/Package.swift deleted file mode 100644 index eb9441f..0000000 --- a/Sources/MasKit/Package.swift +++ /dev/null @@ -1,4 +0,0 @@ -// Generated by: script/version -enum Package { - static let version = "1.8.6" -} diff --git a/Sources/MasKit/AppStore/CKSoftwareMap+SoftwareMap.swift b/Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift similarity index 97% rename from Sources/MasKit/AppStore/CKSoftwareMap+SoftwareMap.swift rename to Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift index 5993e4d..dde4535 100644 --- a/Sources/MasKit/AppStore/CKSoftwareMap+SoftwareMap.swift +++ b/Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift @@ -1,6 +1,6 @@ // // CKSoftwareMap+SoftwareMap.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift b/Sources/mas/AppStore/CKSoftwareProduct+SoftwareProduct.swift similarity index 95% rename from Sources/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift rename to Sources/mas/AppStore/CKSoftwareProduct+SoftwareProduct.swift index 8bcafcf..9516bac 100644 --- a/Sources/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift +++ b/Sources/mas/AppStore/CKSoftwareProduct+SoftwareProduct.swift @@ -1,6 +1,6 @@ // // CKSoftwareProduct+SoftwareProduct.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift new file mode 100644 index 0000000..6435ff6 --- /dev/null +++ b/Sources/mas/AppStore/Downloader.swift @@ -0,0 +1,62 @@ +// +// Downloader.swift +// mas +// +// Created by Andrew Naylor on 21/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import CommerceKit +import PromiseKit +import StoreFoundation + +/// Downloads a list of apps, one after the other, printing progress to the console. +/// +/// - Parameters: +/// - appIDs: The IDs of the apps to be downloaded +/// - purchase: Flag indicating whether the apps needs to be purchased. +/// Only works for free apps. Defaults to false. +/// - Returns: A promise that completes when the downloads are complete. If any fail, +/// the promise is rejected with the first error, after all remaining downloads are attempted. +func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { + var firstError: Error? + return + appIDs + .reduce(Guarantee.value(())) { previous, appID in + previous.then { + downloadWithRetries(appID, purchase: purchase) + .recover { error in + if firstError == nil { + firstError = error + } + } + } + } + .done { + if let error = firstError { + throw error + } + } +} + +private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { + SSPurchase().perform(appID: appID, purchase: purchase) + .recover { error in + guard attempts > 1 else { + throw error + } + + // If the download failed due to network issues, try again. Otherwise, fail immediately. + guard + case MASError.downloadFailed(let downloadError) = error, + case NSURLErrorDomain = downloadError?.domain + else { + throw error + } + + let attempts = attempts - 1 + printWarning((downloadError ?? error).localizedDescription) + printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").") + return downloadWithRetries(appID, purchase: purchase, attempts: attempts) + } +} diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift new file mode 100644 index 0000000..56b7bd6 --- /dev/null +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -0,0 +1,94 @@ +// +// ISStoreAccount.swift +// mas +// +// Created by Andrew Naylor on 22/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import CommerceKit +import PromiseKit +import StoreFoundation + +private let timeout = 30.0 + +extension ISStoreAccount: StoreAccount { + static var primaryAccount: Promise { + if #available(macOS 10.13, *) { + return race( + Promise { seal in + ISServiceProxy.genericShared().accountService + .primaryAccount { storeAccount in + seal.fulfill(storeAccount) + } + }, + after(seconds: timeout) + .then { + Promise(error: MASError.notSignedIn) + } + ) + } + + return .value(CKAccountStore.shared().primaryAccount) + } + + static func signIn(appleID: String, password: String, systemDialog: Bool) -> Promise { + // swift-format-ignore: UseEarlyExits + if #available(macOS 10.13, *) { + // Signing in is no longer possible as of High Sierra. + // https://github.com/mas-cli/mas/issues/164 + return Promise(error: MASError.notSupported) + // swiftlint:disable:next superfluous_else + } else { + return + primaryAccount + .then { account -> Promise in + if account.isSignedIn { + return Promise(error: MASError.alreadySignedIn(asAppleID: account.identifier)) + } + + let password = + password.isEmpty && !systemDialog + ? String(validatingUTF8: getpass("Password: ")) ?? "" + : password + + guard !password.isEmpty || systemDialog else { + return Promise(error: MASError.noPasswordProvided) + } + + let context = ISAuthenticationContext(accountID: 0) + context.appleIDOverride = appleID + + let signInPromise = + Promise { seal in + let accountService = ISServiceProxy.genericShared().accountService + accountService.setStoreClient(ISStoreClient(storeClientType: 0)) + accountService.signIn(with: context) { success, storeAccount, error in + if success, let storeAccount { + seal.fulfill(storeAccount) + } else { + seal.reject(MASError.signInFailed(error: error as NSError?)) + } + } + } + + if systemDialog { + return signInPromise + } + + context.demoMode = true + context.demoAccountName = appleID + context.demoAccountPassword = password + context.demoAutologinMode = true + + return race( + signInPromise, + after(seconds: timeout) + .then { + Promise(error: MASError.signInFailed(error: nil)) + } + ) + } + } + } +} diff --git a/Sources/MasKit/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift similarity index 89% rename from Sources/MasKit/AppStore/PurchaseDownloadObserver.swift rename to Sources/mas/AppStore/PurchaseDownloadObserver.swift index 355a91a..30b968c 100644 --- a/Sources/MasKit/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -1,6 +1,6 @@ // // PurchaseDownloadObserver.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. @@ -9,7 +9,8 @@ import CommerceKit import StoreFoundation -@objc class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { +@objc +class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { let purchase: SSPurchase var completionHandler: (() -> Void)? var errorHandler: ((MASError) -> Void)? @@ -19,7 +20,8 @@ import StoreFoundation } func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) { - guard download.metadata.itemIdentifier == purchase.itemIdentifier, + guard + download.metadata.itemIdentifier == purchase.itemIdentifier, let status = download.status else { return @@ -41,7 +43,8 @@ import StoreFoundation } func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) { - guard download.metadata.itemIdentifier == purchase.itemIdentifier, + guard + download.metadata.itemIdentifier == purchase.itemIdentifier, let status = download.status else { return @@ -64,6 +67,7 @@ struct ProgressState { let phase: String var percentage: String { + // swiftlint:disable:next no_magic_numbers String(format: "%.1f%%", floor(percentComplete * 1000) / 10) } } diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift new file mode 100644 index 0000000..cf712cd --- /dev/null +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -0,0 +1,94 @@ +// +// SSPurchase.swift +// mas +// +// Created by Andrew Naylor on 25/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import CommerceKit +import PromiseKit +import StoreFoundation + +extension SSPurchase { + func perform(appID: AppID, purchase: Bool) -> Promise { + var parameters: [String: Any] = [ + "productType": "C", + "price": 0, + "salableAdamId": appID, + "pg": "default", + "appExtVrsId": 0, + ] + + if purchase { + parameters["macappinstalledconfirmed"] = 1 + parameters["pricingParameters"] = "STDQ" + } else { + parameters["pricingParameters"] = "STDRDL" + } + + buyParameters = + parameters.map { key, value in + "\(key)=\(value)" + } + .joined(separator: "&") + + itemIdentifier = appID + + // Not sure if this is needed… + if purchase { + isRedownload = false + } + + downloadMetadata = SSDownloadMetadata() + downloadMetadata.kind = "software" + downloadMetadata.itemIdentifier = appID + + // Monterey obscures the user's App Store account, but allows + // redownloads without passing any account IDs to SSPurchase. + // https://github.com/mas-cli/mas/issues/417 + if #available(macOS 12, *) { + return perform() + } + + return + ISStoreAccount.primaryAccount + .then { storeAccount in + self.accountIdentifier = storeAccount.dsID + self.appleID = storeAccount.identifier + return self.perform() + } + } + + private func perform() -> Promise { + Promise { seal in + CKPurchaseController.shared() + .perform(self, withOptions: 0) { purchase, _, error, response in + if let error { + seal.reject(MASError.purchaseFailed(error: error as NSError?)) + return + } + + guard response?.downloads.isEmpty == false, let purchase else { + seal.reject(MASError.noDownloads) + return + } + + seal.fulfill(purchase) + } + } + .then { purchase in + let observer = PurchaseDownloadObserver(purchase: purchase) + let downloadQueue = CKDownloadQueue.shared() + let observerID = downloadQueue.add(observer) + + return Promise { seal in + observer.errorHandler = seal.reject + observer.completionHandler = seal.fulfill_ + } + .ensure { + downloadQueue.remove(observerID) + } + } + } +} diff --git a/Sources/MasKit/AppStore/StoreAccount.swift b/Sources/mas/AppStore/StoreAccount.swift similarity index 63% rename from Sources/MasKit/AppStore/StoreAccount.swift rename to Sources/mas/AppStore/StoreAccount.swift index 6de28e4..f434166 100644 --- a/Sources/MasKit/AppStore/StoreAccount.swift +++ b/Sources/mas/AppStore/StoreAccount.swift @@ -1,11 +1,15 @@ // // StoreAccount.swift -// mas-cli +// mas // // Created by Ben Chatelain on 4/3/18. // Copyright © 2018 Andrew Naylor. All rights reserved. // +import Foundation + +// periphery:ignore - save for future use in testing protocol StoreAccount { var identifier: String { get set } + var dsID: NSNumber { get set } } diff --git a/Sources/mas/Commands/Account.swift b/Sources/mas/Commands/Account.swift new file mode 100644 index 0000000..a9f90a6 --- /dev/null +++ b/Sources/mas/Commands/Account.swift @@ -0,0 +1,33 @@ +// +// Account.swift +// mas +// +// Created by Andrew Naylor on 21/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import StoreFoundation + +extension MAS { + struct Account: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Prints the primary account Apple ID" + ) + + /// Runs the command. + func run() throws { + if #available(macOS 12, *) { + // Account information is no longer available as of Monterey. + // https://github.com/mas-cli/mas/issues/417 + throw MASError.notSupported + } + + do { + print(try ISStoreAccount.primaryAccount.wait().identifier) + } catch { + throw error as? MASError ?? MASError.failed(error: error as NSError) + } + } + } +} diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift new file mode 100644 index 0000000..c9e186e --- /dev/null +++ b/Sources/mas/Commands/Home.swift @@ -0,0 +1,49 @@ +// +// Home.swift +// mas +// +// Created by Ben Chatelain on 2018-12-29. +// Copyright © 2016 mas-cli. All rights reserved. +// + +import ArgumentParser + +extension MAS { + /// Opens app page on MAS Preview. Uses the iTunes Lookup API: + /// https://performance-partners.apple.com/search-api + struct Home: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Opens MAS Preview app page in a browser" + ) + + @Argument(help: "ID of app to show on MAS Preview") + var appID: AppID + + /// Runs the command. + func run() throws { + try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand()) + } + + func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws { + do { + guard let result = try searcher.lookup(appID: appID).wait() else { + throw MASError.noSearchResultsFound + } + + do { + try openCommand.run(arguments: result.trackViewUrl) + } catch { + printError("Unable to launch open command") + throw MASError.searchFailed + } + if openCommand.failed { + let reason = openCommand.process.terminationReason + printError("Open failed: (\(reason)) \(openCommand.stderr)") + throw MASError.searchFailed + } + } catch { + throw error as? MASError ?? .searchFailed + } + } + } +} diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift new file mode 100644 index 0000000..5352097 --- /dev/null +++ b/Sources/mas/Commands/Info.swift @@ -0,0 +1,40 @@ +// +// Info.swift +// mas +// +// Created by Denis Lebedev on 21/10/2016. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import Foundation + +extension MAS { + /// Displays app details. Uses the iTunes Lookup API: + /// https://performance-partners.apple.com/search-api + struct Info: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Display app information from the Mac App Store" + ) + + @Argument(help: "ID of app to show info") + var appID: AppID + + /// Runs the command. + func run() throws { + try run(searcher: ITunesSearchAppStoreSearcher()) + } + + func run(searcher: AppStoreSearcher) throws { + do { + guard let result = try searcher.lookup(appID: appID).wait() else { + throw MASError.noSearchResultsFound + } + + print(AppInfoFormatter.format(app: result)) + } catch { + throw error as? MASError ?? .searchFailed + } + } + } +} diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift new file mode 100644 index 0000000..9b051e7 --- /dev/null +++ b/Sources/mas/Commands/Install.swift @@ -0,0 +1,47 @@ +// +// Install.swift +// mas +// +// Created by Andrew Naylor on 21/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import CommerceKit + +extension MAS { + /// Installs previously purchased apps from the Mac App Store. + struct Install: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Install from the Mac App Store" + ) + + @Flag(help: "force reinstall") + var force = false + @Argument(help: "app ID(s) to install") + var appIDs: [AppID] + + /// Runs the command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary()) + } + + func run(appLibrary: AppLibrary) throws { + // Try to download applications with given identifiers and collect results + let appIDs = appIDs.filter { appID in + if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force { + printWarning("\(appName) is already installed") + return false + } + + return true + } + + do { + try downloadAll(appIDs).wait() + } catch { + throw error as? MASError ?? .downloadFailed(error: error as NSError) + } + } + } +} diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift new file mode 100644 index 0000000..94544c9 --- /dev/null +++ b/Sources/mas/Commands/List.swift @@ -0,0 +1,32 @@ +// +// List.swift +// mas +// +// Created by Andrew Naylor on 21/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser + +extension MAS { + /// Command which lists all installed apps. + struct List: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Lists apps from the Mac App Store which are currently installed" + ) + + /// Runs the command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary()) + } + + func run(appLibrary: AppLibrary) throws { + let products = appLibrary.installedApps + if products.isEmpty { + printError("No installed apps found") + } else { + print(AppListFormatter.format(products: products)) + } + } + } +} diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift new file mode 100644 index 0000000..781f55f --- /dev/null +++ b/Sources/mas/Commands/Lucky.swift @@ -0,0 +1,72 @@ +// +// Lucky.swift +// mas +// +// Created by Pablo Varela on 05/11/17. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import CommerceKit + +extension MAS { + /// Command which installs the first search result. + /// + /// This is handy as many MAS titles can be long with embedded keywords. + struct Lucky: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Install the first result from the Mac App Store" + ) + + @Flag(help: "force reinstall") + var force = false + @Argument(help: "the app name to install") + var searchTerm: String + + /// Runs the command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) + } + + func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { + var appID: AppID? + + do { + let results = try searcher.search(for: searchTerm).wait() + guard let result = results.first else { + printError("No results found") + throw MASError.noSearchResultsFound + } + + appID = result.trackId + } catch { + throw error as? MASError ?? .searchFailed + } + + guard let appID else { + fatalError("app ID returned from Apple is null") + } + + try install(appID: appID, appLibrary: appLibrary) + } + + /// Installs an app. + /// + /// - Parameters: + /// - appID: App identifier + /// - appLibrary: Library of installed apps + /// - Throws: Any error that occurs while attempting to install the app. + private func install(appID: AppID, appLibrary: AppLibrary) throws { + // Try to download applications with given identifiers and collect results + if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force { + printWarning("\(appName) is already installed") + } else { + do { + try downloadAll([appID]).wait() + } catch { + throw error as? MASError ?? .downloadFailed(error: error as NSError) + } + } + } + } +} diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift new file mode 100644 index 0000000..a924895 --- /dev/null +++ b/Sources/mas/Commands/Open.swift @@ -0,0 +1,66 @@ +// +// Open.swift +// mas +// +// Created by Ben Chatelain on 2018-12-29. +// Copyright © 2016 mas-cli. All rights reserved. +// + +import ArgumentParser +import Foundation + +private let masScheme = "macappstore" + +extension MAS { + /// Opens app page in MAS app. Uses the iTunes Lookup API: + /// https://performance-partners.apple.com/search-api + struct Open: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Opens app page in 'App Store.app'" + ) + + @Argument(help: "the app ID") + var appID: AppID? + + /// Runs the command. + func run() throws { + try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand()) + } + + func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws { + do { + guard let appID else { + // If no app ID is given, just open the MAS GUI app + try openCommand.run(arguments: masScheme + "://") + return + } + + guard let result = try searcher.lookup(appID: appID).wait() else { + throw MASError.noSearchResultsFound + } + + guard var url = URLComponents(string: result.trackViewUrl) else { + throw MASError.searchFailed + } + url.scheme = masScheme + + guard let urlString = url.string else { + printError("Unable to construct URL") + throw MASError.searchFailed + } + do { + try openCommand.run(arguments: urlString) + } catch { + printError("Unable to launch open command") + throw MASError.searchFailed + } + if openCommand.failed { + printError("Open failed: (\(openCommand.process.terminationReason)) \(openCommand.stderr)") + throw MASError.searchFailed + } + } catch { + throw error as? MASError ?? .searchFailed + } + } + } +} diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift new file mode 100644 index 0000000..a495c1b --- /dev/null +++ b/Sources/mas/Commands/Outdated.swift @@ -0,0 +1,63 @@ +// +// Outdated.swift +// mas +// +// Created by Andrew Naylor on 21/08/2015. +// Copyright (c) 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import Foundation +import PromiseKit + +extension MAS { + /// Command which displays a list of installed apps which have available updates + /// ready to be installed from the Mac App Store. + struct Outdated: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Lists pending updates from the Mac App Store" + ) + + @Flag(help: "Show warnings about apps") + var verbose = false + + /// Runs the command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) + } + + func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { + _ = try when( + fulfilled: + appLibrary.installedApps.map { installedApp in + firstly { + searcher.lookup(appID: installedApp.itemIdentifier.appIDValue) + } + .done { storeApp in + guard let storeApp else { + if verbose { + printWarning( + """ + Identifier \(installedApp.itemIdentifier) not found in store. \ + Was expected to identify \(installedApp.appName). + """ + ) + } + return + } + + if installedApp.isOutdatedWhenComparedTo(storeApp) { + print( + """ + \(installedApp.itemIdentifier) \(installedApp.appName) \ + (\(installedApp.bundleVersion) -> \(storeApp.version)) + """ + ) + } + } + } + ) + .wait() + } + } +} diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift new file mode 100644 index 0000000..d775c64 --- /dev/null +++ b/Sources/mas/Commands/Purchase.swift @@ -0,0 +1,44 @@ +// +// Purchase.swift +// mas +// +// Created by Jakob Rieck on 24/10/2017. +// Copyright (c) 2017 Jakob Rieck. All rights reserved. +// + +import ArgumentParser +import CommerceKit + +extension MAS { + struct Purchase: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Purchase and download free apps from the Mac App Store" + ) + + @Argument(help: "app ID(s) to install") + var appIDs: [AppID] + + /// Runs the command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary()) + } + + func run(appLibrary: AppLibrary) throws { + // Try to download applications with given identifiers and collect results + let appIDs = appIDs.filter { appID in + if let appName = appLibrary.installedApps(withAppID: appID).first?.appName { + printWarning("\(appName) has already been purchased.") + return false + } + + return true + } + + do { + try downloadAll(appIDs, purchase: true).wait() + } catch { + throw error as? MASError ?? .downloadFailed(error: error as NSError) + } + } + } +} diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift new file mode 100644 index 0000000..37ff3e4 --- /dev/null +++ b/Sources/mas/Commands/Reset.swift @@ -0,0 +1,79 @@ +// +// Reset.swift +// mas +// +// Created by Andrew Naylor on 14/09/2016. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import CommerceKit + +extension MAS { + /// Kills several macOS processes as a means to reset the app store. + struct Reset: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Resets the Mac App Store" + ) + + @Flag(help: "Enable debug mode") + var debug = false + + /// Runs the command. + func run() throws { + // The "Reset Application" command in the Mac App Store debug menu performs + // the following steps + // + // - killall Dock + // - killall storeagent (storeagent no longer exists) + // - rm com.apple.appstore download directory + // - clear cookies (appears to be a no-op) + // + // As storeagent no longer exists we will implement a slight variant and kill all + // App Store-associated processes + // - storeaccountd + // - storeassetd + // - storedownloadd + // - storeinstalld + // - storelegacy + + // Kill processes + let killProcs = [ + "Dock", + "storeaccountd", + "storeassetd", + "storedownloadd", + "storeinstalld", + "storelegacy", + ] + + let kill = Process() + let stdout = Pipe() + let stderr = Pipe() + + kill.launchPath = "/usr/bin/killall" + kill.arguments = killProcs + kill.standardOutput = stdout + kill.standardError = stderr + + kill.launch() + kill.waitUntilExit() + + if kill.terminationStatus != 0, debug { + let output = stderr.fileHandleForReading.readDataToEndOfFile() + printError("killall failed:\n\(String(data: output, encoding: .utf8) ?? "Error info not available")") + } + + // Wipe Download Directory + if let directory = CKDownloadDirectory(nil) { + do { + try FileManager.default.removeItem(atPath: directory) + } catch { + if debug { + printError("removeItemAtPath:\"\(directory)\" failed, \(error)") + } + } + } + } + } +} diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift new file mode 100644 index 0000000..ae36163 --- /dev/null +++ b/Sources/mas/Commands/Search.swift @@ -0,0 +1,43 @@ +// +// Search.swift +// mas +// +// Created by Michael Schneider on 4/14/16. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import ArgumentParser + +extension MAS { + /// Search the Mac App Store using the iTunes Search API. + /// + /// See - https://performance-partners.apple.com/search-api + struct Search: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Search for apps from the Mac App Store" + ) + + @Flag(help: "Show price of found apps") + var price = false + @Argument(help: "the app name to search") + var searchTerm: String + + func run() throws { + try run(searcher: ITunesSearchAppStoreSearcher()) + } + + func run(searcher: AppStoreSearcher) throws { + do { + let results = try searcher.search(for: searchTerm).wait() + if results.isEmpty { + throw MASError.noSearchResultsFound + } + + let output = SearchResultFormatter.format(results: results, includePrice: price) + print(output) + } catch { + throw error as? MASError ?? .searchFailed + } + } + } +} diff --git a/Sources/mas/Commands/SignIn.swift b/Sources/mas/Commands/SignIn.swift new file mode 100644 index 0000000..c94da79 --- /dev/null +++ b/Sources/mas/Commands/SignIn.swift @@ -0,0 +1,35 @@ +// +// SignIn.swift +// mas +// +// Created by Andrew Naylor on 14/02/2016. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import StoreFoundation + +extension MAS { + struct SignIn: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "signin", + abstract: "Sign in to the Mac App Store" + ) + + @Flag(help: "Complete login with graphical dialog") + var dialog = false + @Argument(help: "Apple ID") + var appleID: String + @Argument(help: "Password") + var password: String = "" + + /// Runs the command. + func run() throws { + do { + _ = try ISStoreAccount.signIn(appleID: appleID, password: password, systemDialog: dialog).wait() + } catch { + throw error as? MASError ?? MASError.signInFailed(error: error as NSError) + } + } + } +} diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/SignOut.swift new file mode 100644 index 0000000..1c0d49d --- /dev/null +++ b/Sources/mas/Commands/SignOut.swift @@ -0,0 +1,30 @@ +// +// SignOut.swift +// mas +// +// Created by Andrew Naylor on 14/02/2016. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import CommerceKit + +extension MAS { + struct SignOut: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "signout", + abstract: "Sign out of the Mac App Store" + ) + + /// Runs the command. + func run() throws { + if #available(macOS 10.13, *) { + ISServiceProxy.genericShared().accountService.signOut() + } else { + // Using CKAccountStore to sign out does nothing on High Sierra + // https://github.com/mas-cli/mas/issues/129 + CKAccountStore.shared().signOut() + } + } + } +} diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift new file mode 100644 index 0000000..d9997a7 --- /dev/null +++ b/Sources/mas/Commands/Uninstall.swift @@ -0,0 +1,65 @@ +// +// Uninstall.swift +// mas +// +// Created by Ben Chatelain on 2018-12-27. +// Copyright © 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import Foundation + +extension MAS { + /// Command which uninstalls apps managed by the Mac App Store. + struct Uninstall: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Uninstall app installed from the Mac App Store" + ) + + /// Flag indicating that removal shouldn't be performed. + @Flag(help: "dry run") + var dryRun = false + @Argument(help: "ID of app to uninstall") + var appID: AppID + + /// Runs the uninstall command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary()) + } + + func run(appLibrary: AppLibrary) throws { + guard NSUserName() == "root" else { + throw MASError.macOSUserMustBeRoot + } + + guard let username = getSudoUsername() else { + throw MASError.runtimeError("Could not determine the original username") + } + + guard + let uid = getSudoUID(), + seteuid(uid) == 0 + else { + throw MASError.runtimeError("Failed to switch effective user from 'root' to '\(username)'") + } + + let installedApps = appLibrary.installedApps(withAppID: appID) + guard !installedApps.isEmpty else { + throw MASError.notInstalled(appID: appID) + } + + if dryRun { + for installedApp in installedApps { + printInfo("'\(installedApp.appName)' '\(installedApp.bundlePath)'") + } + printInfo("(not removed, dry run)") + } else { + guard seteuid(0) == 0 else { + throw MASError.runtimeError("Failed to revert effective user from '\(username)' back to 'root'") + } + + try appLibrary.uninstallApps(atPaths: installedApps.map(\.bundlePath)) + } + } + } +} diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift new file mode 100644 index 0000000..1c1630d --- /dev/null +++ b/Sources/mas/Commands/Upgrade.swift @@ -0,0 +1,88 @@ +// +// Upgrade.swift +// mas +// +// Created by Andrew Naylor on 30/12/2015. +// Copyright © 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser +import Foundation +import PromiseKit + +extension MAS { + /// Command which upgrades apps with new versions available in the Mac App Store. + struct Upgrade: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Upgrade outdated apps from the Mac App Store" + ) + + @Argument(help: "app(s) to upgrade") + var appIDs: [String] = [] + + /// Runs the command. + func run() throws { + try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher()) + } + + func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws { + let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)] + do { + apps = try findOutdatedApps(appLibrary: appLibrary, searcher: searcher) + } catch { + throw error as? MASError ?? .searchFailed + } + + guard !apps.isEmpty else { + printWarning("Nothing found to upgrade") + return + } + + print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") + print( + apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } + .joined(separator: "\n") + ) + + do { + try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait() + } catch { + throw error as? MASError ?? .downloadFailed(error: error as NSError) + } + } + + private func findOutdatedApps( + appLibrary: AppLibrary, + searcher: AppStoreSearcher + ) throws -> [(SoftwareProduct, SearchResult)] { + let apps = + appIDs.isEmpty + ? appLibrary.installedApps + : appIDs.flatMap { appID in + if let appID = AppID(appID) { + // argument is an AppID, lookup apps by id using argument + return appLibrary.installedApps(withAppID: appID) + } + + // argument is not an AppID, lookup apps by name using argument + return appLibrary.installedApps(named: appID) + } + + let promises = apps.map { installedApp in + // only upgrade apps whose local version differs from the store version + firstly { + searcher.lookup(appID: installedApp.itemIdentifier.appIDValue) + } + .map { result -> (SoftwareProduct, SearchResult)? in + guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { + return nil + } + + return (installedApp, storeApp) + } + } + + return try when(fulfilled: promises).wait().compactMap { $0 } + } + } +} diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift new file mode 100644 index 0000000..31189e3 --- /dev/null +++ b/Sources/mas/Commands/Vendor.swift @@ -0,0 +1,53 @@ +// +// Vendor.swift +// mas +// +// Created by Ben Chatelain on 2018-12-29. +// Copyright © 2016 mas-cli. All rights reserved. +// + +import ArgumentParser + +extension MAS { + /// Opens vendor's app page in a browser. Uses the iTunes Lookup API: + /// https://performance-partners.apple.com/search-api + struct Vendor: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Opens vendor's app page in a browser" + ) + + @Argument(help: "the app ID to show the vendor's website") + var appID: AppID + + /// Runs the command. + func run() throws { + try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand()) + } + + func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws { + do { + guard let result = try searcher.lookup(appID: appID).wait() else { + throw MASError.noSearchResultsFound + } + + guard let vendorWebsite = result.sellerUrl else { + throw MASError.noVendorWebsite + } + + do { + try openCommand.run(arguments: vendorWebsite) + } catch { + printError("Unable to launch open command") + throw MASError.searchFailed + } + if openCommand.failed { + let reason = openCommand.process.terminationReason + printError("Open failed: (\(reason)) \(openCommand.stderr)") + throw MASError.searchFailed + } + } catch { + throw error as? MASError ?? .searchFailed + } + } + } +} diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/Version.swift new file mode 100644 index 0000000..4493088 --- /dev/null +++ b/Sources/mas/Commands/Version.swift @@ -0,0 +1,23 @@ +// +// Version.swift +// mas +// +// Created by Andrew Naylor on 20/09/2015. +// Copyright © 2015 Andrew Naylor. All rights reserved. +// + +import ArgumentParser + +extension MAS { + /// Command which displays the version of the mas tool. + struct Version: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Print version number" + ) + + /// Runs the command. + func run() throws { + print(Package.version) + } + } +} diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift new file mode 100644 index 0000000..cd0432e --- /dev/null +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -0,0 +1,41 @@ +// +// AppLibrary.swift +// mas +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation + +/// Utility for managing installed apps. +protocol AppLibrary { + /// Entire set of installed apps. + var installedApps: [SoftwareProduct] { get } + + /// Uninstalls all apps located at any of the elements of `appPaths`. + /// + /// - Parameter appPaths: Paths to apps to be uninstalled. + /// - Throws: Error if any problem occurs. + func uninstallApps(atPaths appPaths: [String]) throws +} + +/// Common logic +extension AppLibrary { + /// Finds all installed instances of apps whose app ID is `appID`. + /// + /// - Parameter appID: app ID for app(s). + /// - Returns: [SoftwareProduct] of matching apps. + func installedApps(withAppID appID: AppID) -> [SoftwareProduct] { + let appID = NSNumber(value: appID) + return installedApps.filter { $0.itemIdentifier == appID } + } + + /// Finds all installed instances of apps whose name is `appName`. + /// + /// - Parameter appName: Full name of app(s). + /// - Returns: [SoftwareProduct] of matching apps. + func installedApps(named appName: String) -> [SoftwareProduct] { + installedApps.filter { $0.appName == appName } + } +} diff --git a/Sources/mas/Controllers/AppStoreSearcher.swift b/Sources/mas/Controllers/AppStoreSearcher.swift new file mode 100644 index 0000000..4e5b65e --- /dev/null +++ b/Sources/mas/Controllers/AppStoreSearcher.swift @@ -0,0 +1,96 @@ +// +// AppStoreSearcher.swift +// mas +// +// Created by Ben Chatelain on 12/29/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import PromiseKit + +/// Protocol for searching the MAS catalog. +protocol AppStoreSearcher { + func lookup(appID: AppID) -> Promise + func search(for searchTerm: String) -> Promise<[SearchResult]> +} + +enum Entity: String { + case desktopSoftware + case macSoftware + case iPadSoftware + case iPhoneSoftware = "software" +} + +private enum URLAction { + case lookup + case search + + var queryItemName: String { + switch self { + case .lookup: + return "id" + case .search: + return "term" + } + } +} + +// MARK: - Common methods +extension AppStoreSearcher { + /// Builds the search URL for an app. + /// + /// - Parameters: + /// - searchTerm: term for which to search in MAS. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. + /// - Returns: URL for the search service or nil if searchTerm can't be encoded. + func searchURL( + for searchTerm: String, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + url(.search, searchTerm, inCountry: country, ofEntity: entity) + } + + /// Builds the lookup URL for an app. + /// + /// - Parameters: + /// - appID: MAS app identifier. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. + /// - Returns: URL for the lookup service or nil if appID can't be encoded. + func lookupURL( + forAppID appID: AppID, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + url(.lookup, String(appID), inCountry: country, ofEntity: entity) + } + + private func url( + _ action: URLAction, + _ queryItemValue: String, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + guard var components = URLComponents(string: "https://itunes.apple.com/\(action)") else { + return nil + } + + var queryItems = [ + URLQueryItem(name: "media", value: "software"), + URLQueryItem(name: "entity", value: entity.rawValue), + ] + + if let country { + queryItems.append(URLQueryItem(name: "country", value: country)) + } + + queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) + + components.queryItems = queryItems + + return components.url + } +} diff --git a/Sources/mas/Controllers/Finder.swift b/Sources/mas/Controllers/Finder.swift new file mode 100644 index 0000000..a8133c2 --- /dev/null +++ b/Sources/mas/Controllers/Finder.swift @@ -0,0 +1,733 @@ +// swift-format-ignore-file +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable attributes discouraged_none_name file_length file_types_order identifier_name +// swiftlint:disable:next blanket_disable_command +// swiftlint:disable implicitly_unwrapped_optional line_length missing_docs +import AppKit +import ScriptingBridge + +// MARK: FinderEdfm +@objc +public enum FinderEdfm: AEKeyword { + case macOSFormat = 0x6466_6866 // 'dfhf' + case macOSExtendedFormat = 0x6466_682b // 'dfh+' + case ufsFormat = 0x6466_7566 // 'dfuf' + case nfsFormat = 0x6466_6e66 // 'dfnf' + case audioFormat = 0x6466_6175 // 'dfau' + case proDOSFormat = 0x6466_7072 // 'dfpr' + case msdosFormat = 0x6466_6d73 // 'dfms' + case ntfsFormat = 0x6466_6e74 // 'dfnt' + case iso9660Format = 0x6466_3936 // 'df96' + case highSierraFormat = 0x6466_6873 // 'dfhs' + case quickTakeFormat = 0x6466_7174 // 'dfqt' + case applePhotoFormat = 0x6466_7068 // 'dfph' + case appleShareFormat = 0x6466_6173 // 'dfas' + case udfFormat = 0x6466_7564 // 'dfud' + case webDAVFormat = 0x6466_7764 // 'dfwd' + case ftpFormat = 0x6466_6674 // 'dfft' + case packetWrittenUDFFormat = 0x6466_7075 // 'dfpu' + case xsanFormat = 0x6466_6163 // 'dfac' + case apfsFormat = 0x6466_6170 // 'dfap' + case exFATFormat = 0x6466_7866 // 'dfxf' + case smbFormat = 0x6466_736d // 'dfsm' + case unknownFormat = 0x6466_3f3f // 'df??' +} + +// MARK: FinderIpnl +@objc +public enum FinderIpnl: AEKeyword { + case generalInformationPanel = 0x6770_6e6c // 'gpnl' + case sharingPanel = 0x7370_6e6c // 'spnl' + case memoryPanel = 0x6d70_6e6c // 'mpnl' + case previewPanel = 0x7670_6e6c // 'vpnl' + case applicationPanel = 0x6170_6e6c // 'apnl' + case languagesPanel = 0x706b_6c67 // 'pklg' + case pluginsPanel = 0x706b_7067 // 'pkpg' + case nameExtensionPanel = 0x6e70_6e6c // 'npnl' + case commentsPanel = 0x6370_6e6c // 'cpnl' + case contentIndexPanel = 0x6369_6e6c // 'cinl' + case burningPanel = 0x6270_6e6c // 'bpnl' + case moreInfoPanel = 0x6d69_6e6c // 'minl' + case simpleHeaderPanel = 0x7368_6e6c // 'shnl' +} + +// MARK: FinderPple +@objc +public enum FinderPple: AEKeyword { + case generalPreferencesPanel = 0x7067_6e70 // 'pgnp' + case labelPreferencesPanel = 0x706c_6270 // 'plbp' + case sidebarPreferencesPanel = 0x7073_6964 // 'psid' + case advancedPreferencesPanel = 0x7061_6476 // 'padv' +} + +// MARK: FinderPriv +@objc +public enum FinderPriv: AEKeyword { + case readOnly = 0x7265_6164 // 'read' + case readWrite = 0x7264_7772 // 'rdwr' + case writeOnly = 0x7772_6974 // 'writ' + case none = 0x6e6f_6e65 // 'none' +} + +// MARK: FinderEcvw +@objc +public enum FinderEcvw: AEKeyword { + case iconView = 0x6963_6e76 // 'icnv' + case listView = 0x6c73_7677 // 'lsvw' + case columnView = 0x636c_7677 // 'clvw' + case groupView = 0x6772_7677 // 'grvw' + case flowView = 0x666c_7677 // 'flvw' +} + +// MARK: FinderEarr +@objc +public enum FinderEarr: AEKeyword { + case notArranged = 0x6e61_7272 // 'narr' + case snapToGrid = 0x6772_6461 // 'grda' + case arrangedByName = 0x6e61_6d61 // 'nama' + case arrangedByModificationDate = 0x6d64_7461 // 'mdta' + case arrangedByCreationDate = 0x6364_7461 // 'cdta' + case arrangedBySize = 0x7369_7a61 // 'siza' + case arrangedByKind = 0x6b69_6e61 // 'kina' + case arrangedByLabel = 0x6c61_6261 // 'laba' +} + +// MARK: FinderEpos +@objc +public enum FinderEpos: AEKeyword { + case right = 0x6c72_6774 // 'lrgt' + case bottom = 0x6c62_6f74 // 'lbot' +} + +// MARK: FinderSodr +@objc +public enum FinderSodr: AEKeyword { + case normal = 0x736e_726d // 'snrm' + case reversed = 0x7372_7673 // 'srvs' +} + +// MARK: FinderElsv +@objc +public enum FinderElsv: AEKeyword { + case nameColumn = 0x656c_736e // 'elsn' + case modificationDateColumn = 0x656c_736d // 'elsm' + case creationDateColumn = 0x656c_7363 // 'elsc' + case sizeColumn = 0x656c_7373 // 'elss' + case kindColumn = 0x656c_736b // 'elsk' + case labelColumn = 0x656c_736c // 'elsl' + case versionColumn = 0x656c_7376 // 'elsv' + case commentColumn = 0x656c_7343 // 'elsC' +} + +// MARK: FinderLvic +@objc +public enum FinderLvic: AEKeyword { + case smallIcon = 0x736d_6963 // 'smic' + case largeIcon = 0x6c67_6963 // 'lgic' +} + +@objc +public protocol SBObjectProtocol: NSObjectProtocol { + func get() -> Any! +} + +@objc +public protocol SBApplicationProtocol: SBObjectProtocol { + var delegate: SBApplicationDelegate! { get set } + var isRunning: Bool { get } + + func activate() +} + +// MARK: FinderGenericMethods +@objc +public protocol FinderGenericMethods { + @objc optional func openUsing(_ using_: SBObject!, withProperties: [AnyHashable: Any]!) // Open the specified object(s) + @objc optional func printWithProperties(_ withProperties: [AnyHashable: Any]!) // Print the specified object(s) + @objc optional func activate() // Activate the specified window (or the Finder) + @objc optional func close() // Close an object + @objc optional func dataSizeAs(_ as: NSNumber!) -> Int // Return the size in bytes of an object + @objc optional func delete() -> SBObject // Move an item from its container to the trash + @objc optional func duplicateTo(_ to: SBObject!, replacing: Bool, routingSuppressed: Bool, exactCopy: Bool) -> SBObject // Duplicate one or more object(s) + @objc optional func exists() -> Bool // Verify if an object exists + @objc optional func moveTo(_ to: SBObject!, replacing: Bool, positionedAt: [Any]!, routingSuppressed: Bool) -> SBObject // Move object(s) to a new location + @objc optional func select() // Select the specified object(s) + @objc optional func sortBy(_ by: Selector) -> SBObject // Return the specified object(s) in a sorted list + @objc optional func cleanUpBy(_ by: Selector) // Arrange items in window nicely (only applies to open windows in icon view that are not kept arranged) + @objc optional func eject() // Eject the specified disk(s) + @objc optional func emptySecurity(_ security: Bool) // Empty the trash + @objc optional func erase() // (NOT AVAILABLE) Erase the specified disk(s) + @objc optional func reveal() // Bring the specified object(s) into view + @objc optional func updateNecessity(_ necessity: Bool, registeringApplications: Bool) // Update the display of the specified object(s) to match their on-disk representation +} + +// MARK: FinderApplication +@objc +public protocol FinderApplication: SBApplicationProtocol { + @objc optional var clipboard: SBObject { get } // (NOT AVAILABLE YET) the Finder’s clipboard window (copy) + @objc optional var name: String { get } // the Finder’s name (copy) + @objc optional var visible: Bool { get } // Is the Finder’s layer visible? + @objc optional var frontmost: Bool { get } // Is the Finder the frontmost process? + @objc optional var selection: SBObject { get } // the selection in the frontmost Finder window (copy) + @objc optional var insertionLocation: SBObject { get } // the container in which a new folder would appear if “New Folder” was selected (copy) + @objc optional var productVersion: String { get } // the version of the System software running on this computer (copy) + @objc optional var version: String { get } // the version of the Finder (copy) + @objc optional var startupDisk: FinderDisk { get } // the startup disk (copy) + @objc optional var desktop: FinderDesktopObject { get } // the desktop (copy) + @objc optional var trash: FinderTrashObject { get } // the trash (copy) + @objc optional var home: FinderFolder { get } // the home directory (copy) + @objc optional var computerContainer: FinderComputerObject { get } // the computer location (as in Go > Computer) (copy) + @objc optional var FinderPreferences: FinderPreferences { get } // Various preferences that apply to the Finder as a whole (copy) + + @objc optional var desktopPicture: FinderFile { get } // the desktop picture of the main monitor + + @objc optional func setDesktopPicture(_ desktopPicture: FinderFile!) // the desktop picture of the main monitor + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func disks() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray + @objc optional func windows() -> SBElementArray + @objc optional func FinderWindows() -> SBElementArray + @objc optional func clippingWindows() -> SBElementArray + + @objc optional func quit() // Quit the Finder + @objc optional func activate() // Activate the specified window (or the Finder) + @objc optional func copy() // (NOT AVAILABLE YET) Copy the selected items to the clipboard (the Finder must be the front application) + @objc optional func eject() // Eject the specified disk(s) + @objc optional func emptySecurity(_ security: Bool) // Empty the trash + @objc optional func restart() // Restart the computer + @objc optional func shutDown() // Shut Down the computer + @objc optional func sleep() // Put the computer to sleep + @objc optional func setVisible(_ visible: Bool) // Is the Finder’s layer visible? + @objc optional func setFrontmost(_ frontmost: Bool) // Is the Finder the frontmost process? + @objc optional func setSelection(_ selection: SBObject!) // the selection in the frontmost Finder window +} + +extension SBApplication: FinderApplication {} + +// MARK: FinderItem +@objc +public protocol FinderItem: SBObjectProtocol, FinderGenericMethods { + @objc optional var name: String { get } // the name of the item (copy) + @objc optional var displayedName: String { get } // the user-visible name of the item (copy) + @objc optional var nameExtension: String { get } // the name extension of the item (such as “txt”) (copy) + @objc optional var extensionHidden: Bool { get } // Is the item's extension hidden from the user? + @objc optional var index: Int { get } // the index in the front-to-back ordering within its container + @objc optional var container: SBObject { get } // the container of the item (copy) + @objc optional var disk: SBObject { get } // the disk on which the item is stored (copy) + @objc optional var position: NSPoint { get } // the position of the item within its parent window (can only be set for an item in a window viewed as icons or buttons) + @objc optional var desktopPosition: NSPoint { get } // the position of the item on the desktop + @objc optional var bounds: NSRect { get } // the bounding rectangle of the item (can only be set for an item in a window viewed as icons or buttons) + @objc optional var labelIndex: Int { get } // the label of the item + @objc optional var locked: Bool { get } // Is the file locked? + @objc optional var kind: String { get } // the kind of the item (copy) + @objc optional var objectDescription: String { get } // a description of the item (copy) + @objc optional var comment: String { get } // the comment of the item, displayed in the “Get Info” window (copy) + @objc optional var size: Int64 { get } // the logical size of the item + @objc optional var physicalSize: Int64 { get } // the actual space used by the item on disk + @objc optional var creationDate: Date { get } // the date on which the item was created (copy) + @objc optional var modificationDate: Date { get } // the date on which the item was last modified (copy) + @objc optional var icon: FinderIconFamily { get } // the icon bitmap of the item (copy) + @objc optional var URL: String { get } // the URL of the item (copy) + @objc optional var owner: String { get } // the user that owns the container (copy) + @objc optional var group: String { get } // the user or group that has special access to the container (copy) + @objc optional var ownerPrivileges: FinderPriv { get } + @objc optional var groupPrivileges: FinderPriv { get } + @objc optional var everyonesPrivileges: FinderPriv { get } + @objc optional var informationWindow: SBObject { get } // the information window for the item (copy) + @objc optional var properties: [AnyHashable: Any] { get } // every property of an item (copy) + + @objc optional func setName(_ name: String!) // the name of the item + @objc optional func setNameExtension(_ nameExtension: String!) // the name extension of the item (such as “txt”) + @objc optional func setExtensionHidden(_ extensionHidden: Bool) // Is the item's extension hidden from the user? + @objc optional func setPosition(_ position: NSPoint) // the position of the item within its parent window (can only be set for an item in a window viewed as icons or buttons) + @objc optional func setDesktopPosition(_ desktopPosition: NSPoint) // the position of the item on the desktop + @objc optional func setBounds(_ bounds: NSRect) // the bounding rectangle of the item (can only be set for an item in a window viewed as icons or buttons) + @objc optional func setLabelIndex(_ labelIndex: Int) // the label of the item + @objc optional func setLocked(_ locked: Bool) // Is the file locked? + @objc optional func setComment(_ comment: String!) // the comment of the item, displayed in the “Get Info” window + @objc optional func setModificationDate(_ modificationDate: Date!) // the date on which the item was last modified + @objc optional func setIcon(_ icon: FinderIconFamily!) // the icon bitmap of the item + @objc optional func setOwner(_ owner: String!) // the user that owns the container + @objc optional func setGroup(_ group: String!) // the user or group that has special access to the container + @objc optional func setOwnerPrivileges(_ ownerPrivileges: FinderPriv) + @objc optional func setGroupPrivileges(_ groupPrivileges: FinderPriv) + @objc optional func setEveryonesPrivileges(_ everyonesPrivileges: FinderPriv) + @objc optional func setProperties(_ properties: [AnyHashable: Any]!) // every property of an item +} + +extension SBObject: FinderItem {} + +// MARK: FinderContainer +@objc +public protocol FinderContainer: FinderItem { + @objc optional var entireContents: SBObject { get } // the entire contents of the container, including the contents of its children (copy) + @objc optional var expandable: Bool { get } // (NOT AVAILABLE YET) Is the container capable of being expanded as an outline? + @objc optional var expanded: Bool { get } // (NOT AVAILABLE YET) Is the container opened as an outline? (can only be set for containers viewed as lists) + @objc optional var completelyExpanded: Bool { get } // (NOT AVAILABLE YET) Are the container and all of its children opened as outlines? (can only be set for containers viewed as lists) + @objc optional var containerWindow: SBObject { get } // the container window for this folder (copy) + + @objc optional func setExpanded(_ expanded: Bool) // (NOT AVAILABLE YET) Is the container opened as an outline? (can only be set for containers viewed as lists) + @objc optional func setCompletelyExpanded(_ completelyExpanded: Bool) // (NOT AVAILABLE YET) Are the container and all of its children opened as outlines? (can only be set for containers viewed as lists) + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderContainer {} + +// MARK: FinderComputerObject +@objc +public protocol FinderComputerObject: FinderItem {} + +extension SBObject: FinderComputerObject {} + +// MARK: FinderDisk +@objc +public protocol FinderDisk: FinderContainer { + @objc optional var capacity: Int64 { get } // the total number of bytes (free or used) on the disk + @objc optional var freeSpace: Int64 { get } // the number of free bytes left on the disk + @objc optional var ejectable: Bool { get } // Can the media be ejected (floppies, CDs, and so on)? + @objc optional var localVolume: Bool { get } // Is the media a local volume (as opposed to a file server)? + @objc optional var startup: Bool { get } // Is this disk the boot disk? + @objc optional var format: FinderEdfm { get } // the filesystem format of this disk + @objc optional var journalingEnabled: Bool { get } // Does this disk do file system journaling? + @objc optional var ignorePrivileges: Bool { get } // Ignore permissions on this disk? + + @objc optional func setIgnorePrivileges(_ ignorePrivileges: Bool) // Ignore permissions on this disk? + + @objc optional func id() -> Int // the unique id for this disk (unchanged while disk remains connected and Finder remains running) + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderDisk {} + +// MARK: FinderFolder +@objc +public protocol FinderFolder: FinderContainer { + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderFolder {} + +// MARK: FinderDesktopObject +@objc +public protocol FinderDesktopObject: FinderContainer { + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func disks() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderDesktopObject {} + +// MARK: FinderTrashObject +@objc +public protocol FinderTrashObject: FinderContainer { + @objc optional var warnsBeforeEmptying: Bool { get } // Display a dialog when emptying the trash? + + @objc optional func setWarnsBeforeEmptying(_ warnsBeforeEmptying: Bool) // Display a dialog when emptying the trash? + + @objc optional func items() -> SBElementArray + @objc optional func containers() -> SBElementArray + @objc optional func folders() -> SBElementArray + @objc optional func files() -> SBElementArray + @objc optional func aliasFiles() -> SBElementArray + @objc optional func applicationFiles() -> SBElementArray + @objc optional func documentFiles() -> SBElementArray + @objc optional func internetLocationFiles() -> SBElementArray + @objc optional func clippings() -> SBElementArray + @objc optional func packages() -> SBElementArray +} + +extension SBObject: FinderTrashObject {} + +// MARK: FinderFile +@objc +public protocol FinderFile: FinderItem { + @objc optional var fileType: NSNumber { get } // the OSType identifying the type of data contained in the item (copy) + @objc optional var creatorType: NSNumber { get } // the OSType identifying the application that created the item (copy) + @objc optional var stationery: Bool { get } // Is the file a stationery pad? + @objc optional var productVersion: String { get } // the version of the product (visible at the top of the “Get Info” window) (copy) + @objc optional var version: String { get } // the version of the file (visible at the bottom of the “Get Info” window) (copy) + + @objc optional func setFileType(_ fileType: NSNumber!) // the OSType identifying the type of data contained in the item + @objc optional func setCreatorType(_ creatorType: NSNumber!) // the OSType identifying the application that created the item + @objc optional func setStationery(_ stationery: Bool) // Is the file a stationery pad? +} + +extension SBObject: FinderFile {} + +// MARK: FinderAliasFile +@objc +public protocol FinderAliasFile: FinderFile { + @objc optional var originalItem: SBObject { get } // the original item pointed to by the alias (copy) + + @objc optional func setOriginalItem(_ originalItem: SBObject!) // the original item pointed to by the alias +} + +extension SBObject: FinderAliasFile {} + +// MARK: FinderApplicationFile +@objc +public protocol FinderApplicationFile: FinderFile { + @objc optional var suggestedSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the developer recommends the application be launched + @objc optional var minimumSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the smallest memory size with which the application can be launched + @objc optional var preferredSize: Int { get } // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the application will be launched + @objc optional var acceptsHighLevelEvents: Bool { get } // Is the application high-level event aware? (OBSOLETE: always returns true) + @objc optional var hasScriptingTerminology: Bool { get } // Does the process have a scripting terminology, i.e., can it be scripted? + @objc optional var opensInClassic: Bool { get } // (AVAILABLE IN 10.1 TO 10.4) Should the application launch in the Classic environment? + + @objc optional func setMinimumSize(_ minimumSize: Int) // (AVAILABLE IN 10.1 TO 10.4) the smallest memory size with which the application can be launched + @objc optional func setPreferredSize(_ preferredSize: Int) // (AVAILABLE IN 10.1 TO 10.4) the memory size with which the application will be launched + @objc optional func setOpensInClassic(_ opensInClassic: Bool) // (AVAILABLE IN 10.1 TO 10.4) Should the application launch in the Classic environment? + + @objc optional func id() -> String // the bundle identifier or creator type of the application +} + +extension SBObject: FinderApplicationFile {} + +// MARK: FinderDocumentFile +@objc +public protocol FinderDocumentFile: FinderFile {} + +extension SBObject: FinderDocumentFile {} + +// MARK: FinderInternetLocationFile +@objc +public protocol FinderInternetLocationFile: FinderFile { + @objc optional var location: String { get } // the internet location (copy) +} + +extension SBObject: FinderInternetLocationFile {} + +// MARK: FinderClipping +@objc +public protocol FinderClipping: FinderFile { + @objc optional var clippingWindow: SBObject { get } // (NOT AVAILABLE YET) the clipping window for this clipping (copy) +} + +extension SBObject: FinderClipping {} + +// MARK: FinderPackage +@objc +public protocol FinderPackage: FinderItem {} + +extension SBObject: FinderPackage {} + +// MARK: FinderWindow +@objc +public protocol FinderWindow: SBObjectProtocol, FinderGenericMethods { + @objc optional var position: NSPoint { get } // the upper left position of the window + @objc optional var bounds: NSRect { get } // the boundary rectangle for the window + @objc optional var titled: Bool { get } // Does the window have a title bar? + @objc optional var name: String { get } // the name of the window (copy) + @objc optional var index: Int { get } // the number of the window in the front-to-back layer ordering + @objc optional var closeable: Bool { get } // Does the window have a close box? + @objc optional var floating: Bool { get } // Does the window have a title bar? + @objc optional var modal: Bool { get } // Is the window modal? + @objc optional var resizable: Bool { get } // Is the window resizable? + @objc optional var zoomable: Bool { get } // Is the window zoomable? + @objc optional var zoomed: Bool { get } // Is the window zoomed? + @objc optional var visible: Bool { get } // Is the window visible (always true for open Finder windows)? + @objc optional var collapsed: Bool { get } // Is the window collapsed + @objc optional var properties: [AnyHashable: Any] { get } // every property of a window (copy) + + @objc optional func setPosition(_ position: NSPoint) // the upper left position of the window + @objc optional func setBounds(_ bounds: NSRect) // the boundary rectangle for the window + @objc optional func setIndex(_ index: Int) // the number of the window in the front-to-back layer ordering + @objc optional func setZoomed(_ zoomed: Bool) // Is the window zoomed? + @objc optional func setCollapsed(_ collapsed: Bool) // Is the window collapsed + @objc optional func setProperties(_ properties: [AnyHashable: Any]!) // every property of a window + + @objc optional func id() -> Int // the unique id for this window +} + +extension SBObject: FinderWindow {} + +// MARK: FinderFinderWindow +@objc +public protocol FinderFinderWindow: FinderWindow { + @objc optional var target: SBObject { get } // the container at which this file viewer is targeted (copy) + @objc optional var currentView: FinderEcvw { get } // the current view for the container window + @objc optional var iconViewOptions: FinderIconViewOptions { get } // the icon view options for the container window (copy) + @objc optional var listViewOptions: FinderListViewOptions { get } // the list view options for the container window (copy) + @objc optional var columnViewOptions: FinderColumnViewOptions { get } // the column view options for the container window (copy) + @objc optional var toolbarVisible: Bool { get } // Is the window's toolbar visible? + @objc optional var statusbarVisible: Bool { get } // Is the window's status bar visible? + @objc optional var sidebarWidth: Int { get } // the width of the sidebar for the container window + + @objc optional func setTarget(_ target: SBObject!) // the container at which this file viewer is targeted + @objc optional func setCurrentView(_ currentView: FinderEcvw) // the current view for the container window + @objc optional func setToolbarVisible(_ toolbarVisible: Bool) // Is the window's toolbar visible? + @objc optional func setStatusbarVisible(_ statusbarVisible: Bool) // Is the window's status bar visible? + @objc optional func setSidebarWidth(_ sidebarWidth: Int) // the width of the sidebar for the container window +} + +extension SBObject: FinderFinderWindow {} + +// MARK: FinderDesktopWindow +@objc +public protocol FinderDesktopWindow: FinderFinderWindow {} + +extension SBObject: FinderDesktopWindow {} + +// MARK: FinderInformationWindow +@objc +public protocol FinderInformationWindow: FinderWindow { + @objc optional var item: SBObject { get } // the item from which this window was opened (copy) + @objc optional var currentPanel: FinderIpnl { get } // the current panel in the information window + + @objc optional func setCurrentPanel(_ currentPanel: FinderIpnl) // the current panel in the information window +} + +extension SBObject: FinderInformationWindow {} + +// MARK: FinderPreferencesWindow +@objc +public protocol FinderPreferencesWindow: FinderWindow { + @objc optional var currentPanel: FinderPple { get } // The current panel in the Finder preferences window + + @objc optional func setCurrentPanel(_ currentPanel: FinderPple) // The current panel in the Finder preferences window +} + +extension SBObject: FinderPreferencesWindow {} + +// MARK: FinderClippingWindow +@objc +public protocol FinderClippingWindow: FinderWindow {} + +extension SBObject: FinderClippingWindow {} + +// MARK: FinderProcess +@objc +public protocol FinderProcess: SBObjectProtocol, FinderGenericMethods { + @objc optional var name: String { get } // the name of the process (copy) + @objc optional var visible: Bool { get } // Is the process' layer visible? + @objc optional var frontmost: Bool { get } // Is the process the frontmost process? + @objc optional var file: SBObject { get } // the file from which the process was launched (copy) + @objc optional var fileType: NSNumber { get } // the OSType of the file type of the process (copy) + @objc optional var creatorType: NSNumber { get } // the OSType of the creator of the process (the signature) (copy) + @objc optional var acceptsHighLevelEvents: Bool { get } // Is the process high-level event aware (accepts open application, open document, print document, and quit)? + @objc optional var acceptsRemoteEvents: Bool { get } // Does the process accept remote events? + @objc optional var hasScriptingTerminology: Bool { get } // Does the process have a scripting terminology, i.e., can it be scripted? + @objc optional var totalPartitionSize: Int { get } // the size of the partition with which the process was launched + @objc optional var partitionSpaceUsed: Int { get } // the number of bytes currently used in the process' partition + + @objc optional func setVisible(_ visible: Bool) // Is the process' layer visible? + @objc optional func setFrontmost(_ frontmost: Bool) // Is the process the frontmost process? +} + +extension SBObject: FinderProcess {} + +// MARK: FinderApplicationProcess +@objc +public protocol FinderApplicationProcess: FinderProcess { + @objc optional var applicationFile: FinderApplicationFile { get } // the application file from which this process was launched (copy) +} + +extension SBObject: FinderApplicationProcess {} + +// MARK: FinderDeskAccessoryProcess +@objc +public protocol FinderDeskAccessoryProcess: FinderProcess { + @objc optional var deskAccessoryFile: SBObject { get } // the desk accessory file from which this process was launched (copy) +} + +extension SBObject: FinderDeskAccessoryProcess {} + +// MARK: FinderPreferences +@objc +public protocol FinderPreferences: SBObjectProtocol, FinderGenericMethods { + @objc optional var window: FinderPreferencesWindow { get } // the window that would open if Finder preferences was opened (copy) + @objc optional var iconViewOptions: FinderIconViewOptions { get } // the default icon view options (copy) + @objc optional var listViewOptions: FinderListViewOptions { get } // the default list view options (copy) + @objc optional var columnViewOptions: FinderColumnViewOptions { get } // the column view options for all windows (copy) + @objc optional var foldersSpringOpen: Bool { get } // Spring open folders after the specified delay? + @objc optional var delayBeforeSpringing: Double { get } // the delay before springing open a container in seconds (from 0.167 to 1.169) + @objc optional var desktopShowsHardDisks: Bool { get } // Hard disks appear on the desktop? + @objc optional var desktopShowsExternalHardDisks: Bool { get } // External hard disks appear on the desktop? + @objc optional var desktopShowsRemovableMedia: Bool { get } // CDs, DVDs, and iPods appear on the desktop? + @objc optional var desktopShowsConnectedServers: Bool { get } // Connected servers appear on the desktop? + @objc optional var newWindowTarget: SBObject { get } // target location for a newly-opened Finder window (copy) + @objc optional var foldersOpenInNewWindows: Bool { get } // Folders open into new windows? + @objc optional var foldersOpenInNewTabs: Bool { get } // Folders open into new tabs? + @objc optional var newWindowsOpenInColumnView: Bool { get } // Open new windows in column view? + @objc optional var allNameExtensionsShowing: Bool { get } // Show name extensions, even for items whose “extension hidden” is true? + + @objc optional func setFoldersSpringOpen(_ foldersSpringOpen: Bool) // Spring open folders after the specified delay? + @objc optional func setDelayBeforeSpringing(_ delayBeforeSpringing: Double) // the delay before springing open a container in seconds (from 0.167 to 1.169) + @objc optional func setDesktopShowsHardDisks(_ desktopShowsHardDisks: Bool) // Hard disks appear on the desktop? + @objc optional func setDesktopShowsExternalHardDisks(_ desktopShowsExternalHardDisks: Bool) // External hard disks appear on the desktop? + @objc optional func setDesktopShowsRemovableMedia(_ desktopShowsRemovableMedia: Bool) // CDs, DVDs, and iPods appear on the desktop? + @objc optional func setDesktopShowsConnectedServers(_ desktopShowsConnectedServers: Bool) // Connected servers appear on the desktop? + @objc optional func setNewWindowTarget(_ newWindowTarget: SBObject!) // target location for a newly-opened Finder window + @objc optional func setFoldersOpenInNewWindows(_ foldersOpenInNewWindows: Bool) // Folders open into new windows? + @objc optional func setFoldersOpenInNewTabs(_ foldersOpenInNewTabs: Bool) // Folders open into new tabs? + @objc optional func setNewWindowsOpenInColumnView(_ newWindowsOpenInColumnView: Bool) // Open new windows in column view? + @objc optional func setAllNameExtensionsShowing(_ allNameExtensionsShowing: Bool) // Show name extensions, even for items whose “extension hidden” is true? +} + +extension SBObject: FinderPreferences {} + +// MARK: FinderLabel +@objc +public protocol FinderLabel: SBObjectProtocol, FinderGenericMethods { + @objc optional var name: String { get } // the name associated with the label (copy) + @objc optional var index: Int { get } // the index in the front-to-back ordering within its container + @objc optional var color: NSColor { get } // the color associated with the label (copy) + + @objc optional func setName(_ name: String!) // the name associated with the label + @objc optional func setIndex(_ index: Int) // the index in the front-to-back ordering within its container + @objc optional func setColor(_ color: NSColor!) // the color associated with the label +} + +extension SBObject: FinderLabel {} + +// MARK: FinderIconFamily +@objc +public protocol FinderIconFamily: SBObjectProtocol, FinderGenericMethods { + @objc optional var largeMonochromeIconAndMask: Any { get } // the large black-and-white icon and the mask for large icons (copy) + @objc optional var large8BitMask: Any { get } // the large 8-bit mask for large 32-bit icons (copy) + @objc optional var large32BitIcon: Any { get } // the large 32-bit color icon (copy) + @objc optional var large8BitIcon: Any { get } // the large 8-bit color icon (copy) + @objc optional var large4BitIcon: Any { get } // the large 4-bit color icon (copy) + @objc optional var smallMonochromeIconAndMask: Any { get } // the small black-and-white icon and the mask for small icons (copy) + @objc optional var small8BitMask: Any { get } // the small 8-bit mask for small 32-bit icons (copy) + @objc optional var small32BitIcon: Any { get } // the small 32-bit color icon (copy) + @objc optional var small8BitIcon: Any { get } // the small 8-bit color icon (copy) + @objc optional var small4BitIcon: Any { get } // the small 4-bit color icon (copy) +} + +extension SBObject: FinderIconFamily {} + +// MARK: FinderIconViewOptions +@objc +public protocol FinderIconViewOptions: SBObjectProtocol, FinderGenericMethods { + @objc optional var arrangement: FinderEarr { get } // the property by which to keep icons arranged + @objc optional var iconSize: Int { get } // the size of icons displayed in the icon view + @objc optional var showsItemInfo: Bool { get } // additional info about an item displayed in icon view + @objc optional var showsIconPreview: Bool { get } // displays a preview of the item in icon view + @objc optional var textSize: Int { get } // the size of the text displayed in the icon view + @objc optional var labelPosition: FinderEpos { get } // the location of the label in reference to the icon + @objc optional var backgroundPicture: FinderFile { get } // the background picture of the icon view (copy) + @objc optional var backgroundColor: NSColor { get } // the background color of the icon view (copy) + + @objc optional func setArrangement(_ arrangement: FinderEarr) // the property by which to keep icons arranged + @objc optional func setIconSize(_ iconSize: Int) // the size of icons displayed in the icon view + @objc optional func setShowsItemInfo(_ showsItemInfo: Bool) // additional info about an item displayed in icon view + @objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in icon view + @objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the icon view + @objc optional func setLabelPosition(_ labelPosition: FinderEpos) // the location of the label in reference to the icon + @objc optional func setBackgroundPicture(_ backgroundPicture: FinderFile!) // the background picture of the icon view + @objc optional func setBackgroundColor(_ backgroundColor: NSColor!) // the background color of the icon view +} + +extension SBObject: FinderIconViewOptions {} + +// MARK: FinderColumnViewOptions +@objc +public protocol FinderColumnViewOptions: SBObjectProtocol, FinderGenericMethods { + @objc optional var textSize: Int { get } // the size of the text displayed in the column view + @objc optional var showsIcon: Bool { get } // displays an icon next to the label in column view + @objc optional var showsIconPreview: Bool { get } // displays a preview of the item in column view + @objc optional var showsPreviewColumn: Bool { get } // displays the preview column in column view + @objc optional var disclosesPreviewPane: Bool { get } // discloses the preview pane of the preview column in column view + + @objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the column view + @objc optional func setShowsIcon(_ showsIcon: Bool) // displays an icon next to the label in column view + @objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in column view + @objc optional func setShowsPreviewColumn(_ showsPreviewColumn: Bool) // displays the preview column in column view + @objc optional func setDisclosesPreviewPane(_ disclosesPreviewPane: Bool) // discloses the preview pane of the preview column in column view +} + +extension SBObject: FinderColumnViewOptions {} + +// MARK: FinderListViewOptions +@objc +public protocol FinderListViewOptions: SBObjectProtocol, FinderGenericMethods { + @objc optional var calculatesFolderSizes: Bool { get } // Are folder sizes calculated and displayed in the window? + @objc optional var showsIconPreview: Bool { get } // displays a preview of the item in list view + @objc optional var iconSize: FinderLvic { get } // the size of icons displayed in the list view + @objc optional var textSize: Int { get } // the size of the text displayed in the list view + @objc optional var sortColumn: FinderColumn { get } // the column that the list view is sorted on (copy) + @objc optional var usesRelativeDates: Bool { get } // Are relative dates (e.g., today, yesterday) shown in the list view? + + @objc optional func setCalculatesFolderSizes(_ calculatesFolderSizes: Bool) // Are folder sizes calculated and displayed in the window? + @objc optional func setShowsIconPreview(_ showsIconPreview: Bool) // displays a preview of the item in list view + @objc optional func setIconSize(_ iconSize: FinderLvic) // the size of icons displayed in the list view + @objc optional func setTextSize(_ textSize: Int) // the size of the text displayed in the list view + @objc optional func setSortColumn(_ sortColumn: FinderColumn!) // the column that the list view is sorted on + @objc optional func setUsesRelativeDates(_ usesRelativeDates: Bool) // Are relative dates (e.g., today, yesterday) shown in the list view? + + @objc optional func columns() -> SBElementArray +} + +extension SBObject: FinderListViewOptions {} + +// MARK: FinderColumn +@objc +public protocol FinderColumn: SBObjectProtocol, FinderGenericMethods { + @objc optional var index: Int { get } // the index in the front-to-back ordering within its container + @objc optional var name: FinderElsv { get } // the column name + @objc optional var sortDirection: FinderSodr { get } // The direction in which the window is sorted + @objc optional var width: Int { get } // the width of this column + @objc optional var minimumWidth: Int { get } // the minimum allowed width of this column + @objc optional var maximumWidth: Int { get } // the maximum allowed width of this column + @objc optional var visible: Bool { get } // is this column visible + + @objc optional func setIndex(_ index: Int) // the index in the front-to-back ordering within its container + @objc optional func setSortDirection(_ sortDirection: FinderSodr) // The direction in which the window is sorted + @objc optional func setWidth(_ width: Int) // the width of this column + @objc optional func setVisible(_ visible: Bool) // is this column visible +} + +extension SBObject: FinderColumn {} + +// MARK: FinderAliasList +@objc +public protocol FinderAliasList: SBObjectProtocol, FinderGenericMethods {} + +extension SBObject: FinderAliasList {} diff --git a/Sources/MasKit/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift similarity index 58% rename from Sources/MasKit/Controllers/MasStoreSearch.swift rename to Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift index f5a32e4..c6de321 100644 --- a/Sources/MasKit/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift @@ -1,6 +1,6 @@ // -// MasStoreSearch.swift -// MasKit +// ITunesSearchAppStoreSearcher.swift +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -12,14 +12,14 @@ import Regex import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. -class MasStoreSearch: StoreSearch { +class ITunesSearchAppStoreSearcher: AppStoreSearcher { private static let appVersionExpression = Regex(#"\"versionDisplay\"\:\"([^\"]+)\""#) // CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed // into the App Store. Instead, we'll make an educated guess that it matches the currently // selected locale in macOS. This obviously isn't always going to match, but it's probably // better than passing no "country" at all to the iTunes Search API. - // https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ + // https://performance-partners.apple.com/search-api private let country: String? private let networkManager: NetworkManager @@ -34,56 +34,59 @@ class MasStoreSearch: StoreSearch { /// Searches for an app. /// - /// - Parameter appName: MAS ID of app - /// - Parameter completion: A closure that receives the search results or an Error if there is a - /// problem with the network request. Results array will be empty if there were no matches. - func search(for appName: String) -> Promise<[SearchResult]> { + /// - Parameter searchTerm: a search term matched against app names + /// - Returns: A Promise of an Array of SearchResults matching searchTerm + func search(for searchTerm: String) -> Promise<[SearchResult]> { // Search for apps for compatible platforms, in order of preference. // Macs with Apple Silicon can run iPad and iPhone apps. - var entities = [Entity.macSoftware] + var entities = [Entity.desktopSoftware] if SysCtlSystemCommand.isAppleSilicon { entities += [.iPadSoftware, .iPhoneSoftware] } - let results = entities.map { entity -> Promise<[SearchResult]> in - guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else { - fatalError("Failed to build URL for \(appName)") + let results = entities.map { entity in + guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else { + fatalError("Failed to build URL for \(searchTerm)") } return loadSearchResults(url) } // Combine the results, removing any duplicates. - var seenAppIDs = Set() - return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in - seenAppIDs.insert(result.trackId).inserted - } + var seenAppIDs = Set() + return when(fulfilled: results) + .flatMapValues { $0 } + .filterValues { result in + seenAppIDs.insert(result.trackId).inserted + } } /// Looks up app details. /// - /// - Parameter appId: MAS ID of app + /// - Parameter appID: MAS ID of app /// - Returns: A Promise for the search result record of app, or nil if no apps match the ID, /// or an Error if there is a problem with the network request. - func lookup(app appId: Int) -> Promise { - guard let url = lookupURL(forApp: appId, inCountry: country) else { - fatalError("Failed to build URL for \(appId)") + func lookup(appID: AppID) -> Promise { + guard let url = lookupURL(forAppID: appID, inCountry: country) else { + fatalError("Failed to build URL for \(appID)") } return firstly { loadSearchResults(url) - }.then { results -> Guarantee in + } + .then { results -> Guarantee in guard let result = results.first else { return .value(nil) } - guard let pageUrl = URL(string: result.trackViewUrl) - else { + guard let pageURL = URL(string: result.trackViewUrl) else { return .value(result) } return firstly { - self.scrapeAppStoreVersion(pageUrl) - }.map { pageVersion in - guard let pageVersion, + self.scrapeAppStoreVersion(pageURL) + } + .map { pageVersion in + guard + let pageVersion, let searchVersion = Version(tolerant: result.version), pageVersion > searchVersion else { @@ -94,7 +97,8 @@ class MasStoreSearch: StoreSearch { var result = result result.version = pageVersion.description return result - }.recover { _ in + } + .recover { _ in // If we were unable to scrape the App Store page, assume compatibility. .value(result) } @@ -104,26 +108,27 @@ class MasStoreSearch: StoreSearch { private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> { firstly { networkManager.loadData(from: url) - }.map { data -> [SearchResult] in + } + .map { data in do { return try JSONDecoder().decode(SearchResultList.self, from: data).results } catch { - throw MASError.jsonParsing(error: error as NSError) + throw MASError.jsonParsing(data: data) } } } - // App Store pages indicate: - // - compatibility with Macs with Apple Silicon - // - (often) a version that is newer than what is listed in search results - // - // We attempt to scrape this information here. - private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { + /// Scrape the app version from the App Store webpage at the given URL. + /// + /// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API. + private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise { firstly { - networkManager.loadData(from: pageUrl) - }.map { data in - let html = String(decoding: data, as: UTF8.self) - guard let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], + networkManager.loadData(from: pageURL) + } + .map { data in + guard + let html = String(data: data, encoding: .utf8), + let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) else { return nil diff --git a/Sources/MasKit/Controllers/SoftwareMap.swift b/Sources/mas/Controllers/SoftwareMap.swift similarity index 84% rename from Sources/MasKit/Controllers/SoftwareMap.swift rename to Sources/mas/Controllers/SoftwareMap.swift index f30301f..b131d42 100644 --- a/Sources/MasKit/Controllers/SoftwareMap.swift +++ b/Sources/mas/Controllers/SoftwareMap.swift @@ -1,12 +1,12 @@ // // SoftwareMap.swift -// MasKit +// mas // // Created by Ben Chatelain on 3/1/20. // Copyright © 2020 mas-cli. All rights reserved. // -/// Somewhat analygous to CKSoftwareMap +/// Somewhat analogous to CKSoftwareMap. protocol SoftwareMap { func allSoftwareProducts() -> [SoftwareProduct] func product(for bundleIdentifier: String) -> SoftwareProduct? diff --git a/Sources/mas/Controllers/SoftwareMapAppLibrary.swift b/Sources/mas/Controllers/SoftwareMapAppLibrary.swift new file mode 100644 index 0000000..7fc48e7 --- /dev/null +++ b/Sources/mas/Controllers/SoftwareMapAppLibrary.swift @@ -0,0 +1,159 @@ +// +// SoftwareMapAppLibrary.swift +// mas +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import CommerceKit +import ScriptingBridge + +/// Utility for managing installed apps. +class SoftwareMapAppLibrary: AppLibrary { + /// CommerceKit's singleton manager of installed software. + private let softwareMap: SoftwareMap + + /// Array of installed software products. + lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts() + .filter { product in + product.bundlePath.starts(with: "/Applications/") + } + + /// Internal initializer for providing a mock software map. + /// - Parameter softwareMap: SoftwareMap to use + init(softwareMap: SoftwareMap = CKSoftwareMap.shared()) { + self.softwareMap = softwareMap + } + + /// Finds an app using a bundle identifier. + /// + /// - Parameter bundleID: Bundle identifier of app. + /// - Returns: `SoftwareProduct` for app if found; `nil` otherwise. + func installedApp(forBundleID bundleID: String) -> SoftwareProduct? { + softwareMap.product(for: bundleID) + } + + /// Uninstalls all apps located at any of the elements of `appPaths`. + /// + /// - Parameter appPaths: Paths to apps to be uninstalled. + /// - Throws: Error if any problem occurs. + func uninstallApps(atPaths appPaths: [String]) throws { + try delete(pathsFromOwnerIDsByPath: try chown(paths: appPaths)) + } +} + +func getSudoUsername() -> String? { + ProcessInfo.processInfo.environment["SUDO_USER"] +} + +func getSudoUID() -> uid_t? { + guard let uid = ProcessInfo.processInfo.environment["SUDO_UID"] else { + return nil + } + return uid_t(uid) +} + +func getSudoGID() -> gid_t? { + guard let gid = ProcessInfo.processInfo.environment["SUDO_GID"] else { + return nil + } + return gid_t(gid) +} + +private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t) { + do { + let attributes = try FileManager.default.attributesOfItem(atPath: path) + guard + let uid = attributes[.ownerAccountID] as? uid_t, + let gid = attributes[.groupOwnerAccountID] as? gid_t + else { + throw MASError.runtimeError("Failed to determine running user's uid & gid") + } + return (uid, gid) + } +} + +private func chown(paths: [String]) throws -> [String: (uid_t, gid_t)] { + guard let sudoUID = getSudoUID() else { + throw MASError.runtimeError("Failed to get original uid") + } + + guard let sudoGID = getSudoGID() else { + throw MASError.runtimeError("Failed to get original gid") + } + + let ownerIDsByPath = try paths.reduce(into: [String: (uid_t, gid_t)]()) { dict, path in + dict[path] = try getOwnerAndGroupOfItem(atPath: path) + } + + var chownedIDsByPath: [String: (uid_t, gid_t)] = [:] + for (path, ownerIDs) in ownerIDsByPath { + guard chown(path, sudoUID, sudoGID) == 0 else { + for (chownedPath, chownedIDs) in chownedIDsByPath + where chown(chownedPath, chownedIDs.0, chownedIDs.1) != 0 { + printError("Failed to revert ownership of '\(path)' back to uid \(chownedIDs.0) & gid \(chownedIDs.1)") + } + throw MASError.runtimeError("Failed to change ownership of '\(path)' to uid \(sudoUID) & gid \(sudoGID)") + } + + chownedIDsByPath[path] = ownerIDs + } + + return ownerIDsByPath +} + +private func delete(pathsFromOwnerIDsByPath ownerIDsByPath: [String: (uid_t, gid_t)]) throws { + guard let finder: FinderApplication = SBApplication(bundleIdentifier: "com.apple.finder") else { + throw MASError.runtimeError("Failed to obtain Finder access: com.apple.finder does not exist") + } + + guard let items = finder.items else { + throw MASError.runtimeError("Failed to obtain Finder access: finder.items does not exist") + } + + for (path, ownerIDs) in ownerIDsByPath { + let object = items().object(atLocation: URL(fileURLWithPath: path)) + + guard let item = object as? FinderItem else { + throw MASError.runtimeError( + """ + Failed to obtain Finder access: finder.items().object(atLocation: URL(fileURLWithPath: \ + \"\(path)\") is a '\(type(of: object))' that does not conform to 'FinderItem' + """ + ) + } + + guard let delete = item.delete else { + throw MASError.runtimeError("Failed to obtain Finder access: FinderItem.delete does not exist") + } + + let uid = ownerIDs.0 + let gid = ownerIDs.1 + guard let deletedURLString = (delete() as FinderItem).URL else { + throw MASError.runtimeError( + """ + Failed to revert ownership of deleted '\(path)' back to uid \(uid) & gid \(gid): \ + delete result did not have a URL + """ + ) + } + + guard let deletedURL = URL(string: deletedURLString) else { + throw MASError.runtimeError( + """ + Failed to revert ownership of deleted '\(path)' back to uid \(uid) & gid \(gid): \ + delete result URL is invalid: \(deletedURLString) + """ + ) + } + + let deletedPath = deletedURL.path + print("Deleted '\(path)' to '\(deletedPath)'") + guard chown(deletedPath, uid, gid) == 0 else { + throw MASError.runtimeError( + "Failed to revert ownership of deleted '\(deletedPath)' back to uid \(uid) & gid \(gid)" + ) + } + } +} diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift similarity index 51% rename from Sources/MasKit/Errors/MASError.swift rename to Sources/mas/Errors/MASError.swift index 716f4ea..1d31b7f 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -1,6 +1,6 @@ // // MASError.swift -// mas-cli +// mas // // Created by Andrew Naylor on 21/08/2015. // Copyright (c) 2015 Andrew Naylor. All rights reserved. @@ -8,12 +8,17 @@ import Foundation -public enum MASError: Error, Equatable { +enum MASError: Error, Equatable { case notSupported + case failed(error: NSError?) + + case runtimeError(String) + case notSignedIn + case noPasswordProvided case signInFailed(error: NSError?) - case alreadySignedIn + case alreadySignedIn(asAppleID: String) case purchaseFailed(error: NSError?) case downloadFailed(error: NSError?) @@ -24,77 +29,81 @@ public enum MASError: Error, Equatable { case noSearchResultsFound case noVendorWebsite - case notInstalled - case uninstallFailed + case notInstalled(appID: AppID) + case uninstallFailed(error: NSError?) + case macOSUserMustBeRoot case noData - case jsonParsing(error: NSError?) + case jsonParsing(data: Data?) } // MARK: - CustomStringConvertible extension MASError: CustomStringConvertible { - public var description: String { + var description: String { switch self { case .notSignedIn: return "Not signed in" - + case .noPasswordProvided: + return "No password provided" case .notSupported: return """ This command is not supported on this macOS version due to changes in macOS. \ For more information see: \ - https://github.com/mas-cli/mas#%EF%B8%8F-known-issues + https://github.com/mas-cli/mas#known-issues """ - + case .failed(let error): + if let error { + return "Failed: \(error.localizedDescription)" + } + return "Failed" + case .runtimeError(let message): + return "Runtime Error: \(message)" case .signInFailed(let error): if let error { return "Sign in failed: \(error.localizedDescription)" - } else { - return "Sign in failed" } - - case .alreadySignedIn: - return "Already signed in" - + return "Sign in failed" + case .alreadySignedIn(let appleID): + return "Already signed in as \(appleID)" case .purchaseFailed(let error): if let error { return "Download request failed: \(error.localizedDescription)" - } else { - return "Download request failed" } - + return "Download request failed" case .downloadFailed(let error): if let error { return "Download failed: \(error.localizedDescription)" - } else { - return "Download failed" } - + return "Download failed" case .noDownloads: return "No downloads began" - case .cancelled: return "Download cancelled" - case .searchFailed: return "Search failed" - case .noSearchResultsFound: return "No results found" - case .noVendorWebsite: return "App does not have a vendor website" - - case .notInstalled: - return "Not installed" - - case .uninstallFailed: + case .notInstalled(let appID): + return "No apps installed with app ID \(appID)" + case .uninstallFailed(let error): + if let error { + return "Uninstall failed: \(error.localizedDescription)" + } return "Uninstall failed" - + case .macOSUserMustBeRoot: + return "Apps installed from the Mac App Store require root permission to remove." case .noData: return "Service did not return data" - - case .jsonParsing: - return "Unable to parse response JSON" + case .jsonParsing(let data): + if let data { + if let unparsable = String(data: data, encoding: .utf8) { + return "Unable to parse response as JSON: \n\(unparsable)" + } + return "Received defective response" + } + return "Received empty response" } } } diff --git a/Sources/MasKit/ExternalCommands/ExternalCommand.swift b/Sources/mas/ExternalCommands/ExternalCommand.swift similarity index 97% rename from Sources/MasKit/ExternalCommands/ExternalCommand.swift rename to Sources/mas/ExternalCommands/ExternalCommand.swift index 785bc84..2168a05 100644 --- a/Sources/MasKit/ExternalCommands/ExternalCommand.swift +++ b/Sources/mas/ExternalCommands/ExternalCommand.swift @@ -1,6 +1,6 @@ // // ExternalCommand.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/1/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,7 +8,7 @@ import Foundation -/// CLI command +/// Represents a CLI command. protocol ExternalCommand { var binaryPath: String { get set } diff --git a/Sources/MasKit/ExternalCommands/OpenSystemCommand.swift b/Sources/mas/ExternalCommands/OpenSystemCommand.swift similarity index 80% rename from Sources/MasKit/ExternalCommands/OpenSystemCommand.swift rename to Sources/mas/ExternalCommands/OpenSystemCommand.swift index eddb2d6..5d5e3cc 100644 --- a/Sources/MasKit/ExternalCommands/OpenSystemCommand.swift +++ b/Sources/mas/ExternalCommands/OpenSystemCommand.swift @@ -1,6 +1,6 @@ // // OpenSystemCommand.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/2/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,8 +8,7 @@ import Foundation -/// Wrapper for the external open system command. -/// https://ss64.com/osx/open.html +/// Wrapper for the external 'open' system command (https://ss64.com/osx/open.html). struct OpenSystemCommand: ExternalCommand { var binaryPath: String diff --git a/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift similarity index 85% rename from Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift rename to Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index 3c2a401..1d04a2e 100644 --- a/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -1,6 +1,6 @@ // // SysCtlSystemCommand.swift -// MasKit +// mas // // Created by Chris Araman on 6/3/21. // Copyright © 2021 mas-cli. All rights reserved. @@ -8,22 +8,12 @@ import Foundation -/// Wrapper for the external sysctl system command. -/// https://ss64.com/osx/sysctl.html +/// Wrapper for the external 'sysctl' system command. +/// +/// See - https://ss64.com/osx/sysctl.html struct SysCtlSystemCommand: ExternalCommand { - var binaryPath: String - - let process = Process() - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - - init(binaryPath: String = "/usr/sbin/sysctl") { - self.binaryPath = binaryPath - } - static var isAppleSilicon: Bool = { - let sysctl = SysCtlSystemCommand() + let sysctl = Self() do { // Returns 1 on Apple Silicon even when run in an Intel context in Rosetta. try sysctl.run(arguments: "-in", "hw.optional.arm64") @@ -37,4 +27,15 @@ struct SysCtlSystemCommand: ExternalCommand { return sysctl.stdout.trimmingCharacters(in: .newlines) == "1" }() + + let process = Process() + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + var binaryPath: String + + init(binaryPath: String = "/usr/sbin/sysctl") { + self.binaryPath = binaryPath + } } diff --git a/Sources/MasKit/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift similarity index 94% rename from Sources/MasKit/Formatters/AppInfoFormatter.swift rename to Sources/mas/Formatters/AppInfoFormatter.swift index 6810cfd..a162d99 100644 --- a/Sources/MasKit/Formatters/AppInfoFormatter.swift +++ b/Sources/mas/Formatters/AppInfoFormatter.swift @@ -1,6 +1,6 @@ // // AppInfoFormatter.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/7/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -18,7 +18,7 @@ enum AppInfoFormatter { let headline = [ "\(app.trackName)", "\(app.version)", - "[\(app.price ?? 0)]", + "[\(app.formattedPrice)]", ] .joined(separator: " ") @@ -27,7 +27,7 @@ enum AppInfoFormatter { "By: \(app.sellerName)", "Released: \(humanReadableDate(app.currentVersionReleaseDate))", "Minimum OS: \(app.minimumOsVersion)", - "Size: \(humanReadableSize(app.fileSizeBytes ?? "0"))", + "Size: \(humanReadableSize(app.fileSizeBytes))", "From: \(app.trackViewUrl)", ] .joined(separator: "\n") diff --git a/Sources/MasKit/Formatters/AppListFormatter.swift b/Sources/mas/Formatters/AppListFormatter.swift similarity index 80% rename from Sources/MasKit/Formatters/AppListFormatter.swift rename to Sources/mas/Formatters/AppListFormatter.swift index e147595..8bafbf8 100644 --- a/Sources/MasKit/Formatters/AppListFormatter.swift +++ b/Sources/mas/Formatters/AppListFormatter.swift @@ -1,6 +1,6 @@ // // AppListFormatter.swift -// MasKit +// mas // // Created by Ben Chatelain on 6/7/20. // Copyright © 2019 mas-cli. All rights reserved. @@ -15,8 +15,8 @@ enum AppListFormatter { /// Formats text output with list results. /// - /// - Parameter products: List of sortware products app data. - /// - Returns: Multiliune text outoutp. + /// - Parameter products: List of software products app data. + /// - Returns: Multiline text output. static func format(products: [SoftwareProduct]) -> String { // find longest appName for formatting, default 50 let maxLength = products.map(\.appNameOrBundleIdentifier.count).max() ?? nameColumnMinWidth @@ -24,12 +24,12 @@ enum AppListFormatter { var output = "" for product in products { - let appId = product.itemIdentifier.stringValue + let appID = product.itemIdentifier.stringValue .padding(toLength: idColumnMinWidth, withPad: " ", startingAt: 0) let appName = product.appNameOrBundleIdentifier.padding(toLength: maxLength, withPad: " ", startingAt: 0) let version = product.bundleVersion - output += "\(appId) \(appName) (\(version))\n" + output += "\(appID) \(appName) (\(version))\n" } return output.trimmingCharacters(in: .newlines) diff --git a/Sources/MasKit/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift similarity index 53% rename from Sources/MasKit/Formatters/SearchResultFormatter.swift rename to Sources/mas/Formatters/SearchResultFormatter.swift index 03160cf..c0bb513 100644 --- a/Sources/MasKit/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -1,6 +1,6 @@ // // SearchResultFormatter.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/11/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -10,25 +10,28 @@ import Foundation /// Formats text output for the search command. enum SearchResultFormatter { - /// Formats text output with search results. + /// Formats search results as text. /// - /// - Parameter results: Search results with app data - /// - Returns: Multiliune text outoutp. + /// - Parameters: + /// - results: Search results containing app data + /// - includePrice: Indicates whether to include prices in the output + /// - Returns: Multiline text output. static func format(results: [SearchResult], includePrice: Bool = false) -> String { - // find longest appName for formatting, default 50 - let maxLength = results.map(\.trackName.count).max() ?? 50 + guard let maxLength = results.map(\.trackName.count).max() else { + return "" + } + var output = "" for result in results { - let appId = result.trackId + let appID = result.trackId let appName = result.trackName.padding(toLength: maxLength, withPad: " ", startingAt: 0) let version = result.version - let price = result.price ?? 0.0 if includePrice { - output += String(format: "%12d %@ $%5.2f (%@)\n", appId, appName, price, version) + output += String(format: "%12lu %@ (%@) %@\n", appID, appName, version, result.formattedPrice) } else { - output += String(format: "%12d %@ (%@)\n", appId, appName, version) + output += String(format: "%12lu %@ (%@)\n", appID, appName, version) } } diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift new file mode 100644 index 0000000..63e436c --- /dev/null +++ b/Sources/mas/Formatters/Utilities.swift @@ -0,0 +1,96 @@ +// +// Utilities.swift +// mas +// +// Created by Andrew Naylor on 14/09/2016. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import Foundation + +// A collection of output formatting helpers + +/// Terminal Control Sequence Indicator. +let csi = "\u{001B}[" + +private var standardError = FileHandle.standardError + +extension FileHandle: TextOutputStream { + /// Appends the given string to the stream. + public func write(_ string: String) { + guard let data = string.data(using: .utf8) else { + return + } + write(data) + } +} + +/// Prints a message to stdout prefixed with a blue arrow. +func printInfo(_ message: String) { + guard isatty(fileno(stdout)) != 0 else { + print("==> \(message)") + return + } + + // Blue bold arrow, Bold text + print("\(csi)1;34m==>\(csi)0m \(csi)1m\(message)\(csi)0m") +} + +/// Prints a message to stderr prefixed with "Warning:" underlined in yellow. +func printWarning(_ message: String) { + guard isatty(fileno(stderr)) != 0 else { + print("Warning: \(message)", to: &standardError) + return + } + + // Yellow, underlined "Warning:" prefix + print("\(csi)4;33mWarning:\(csi)0m \(message)", to: &standardError) +} + +/// Prints a message to stderr prefixed with "Error:" underlined in red. +func printError(_ message: String) { + guard isatty(fileno(stderr)) != 0 else { + print("Error: \(message)", to: &standardError) + return + } + + // Red, underlined "Error:" prefix + print("\(csi)4;31mError:\(csi)0m \(message)", to: &standardError) +} + +/// Flushes stdout. +func clearLine() { + guard isatty(fileno(stdout)) != 0 else { + return + } + + print("\(csi)2K\(csi)0G", terminator: "") + fflush(stdout) +} + +func captureStream( + _ stream: UnsafeMutablePointer, + encoding: String.Encoding = .utf8, + _ block: @escaping () throws -> Void +) rethrows -> String { + let originalFd = fileno(stream) + let duplicateFd = dup(originalFd) + defer { + close(duplicateFd) + } + + let pipe = Pipe() + dup2(pipe.fileHandleForWriting.fileDescriptor, originalFd) + + do { + defer { + fflush(stream) + dup2(duplicateFd, originalFd) + pipe.fileHandleForWriting.closeFile() + } + + try block() + } + + return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: encoding) ?? "" +} diff --git a/Sources/mas/MAS.swift b/Sources/mas/MAS.swift new file mode 100644 index 0000000..aee1262 --- /dev/null +++ b/Sources/mas/MAS.swift @@ -0,0 +1,65 @@ +// +// MAS.swift +// mas +// +// Created by Chris Araman on 4/22/21. +// Copyright © 2021 mas-cli. All rights reserved. +// + +import ArgumentParser +import Foundation +import PromiseKit + +@main +struct MAS: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Mac App Store command-line interface", + subcommands: [ + Account.self, + Home.self, + Info.self, + Install.self, + List.self, + Lucky.self, + Open.self, + Outdated.self, + Purchase.self, + Reset.self, + Search.self, + SignIn.self, + SignOut.self, + Uninstall.self, + Upgrade.self, + Vendor.self, + Version.self, + ] + ) + + static func initialize() { + PromiseKit.conf.Q.map = .global() + PromiseKit.conf.Q.return = .global() + PromiseKit.conf.logHandler = { event in + switch event { + case .waitOnMainThread: + // Ignored. This is a console app that waits on the main thread for + // promises to be processed on the global DispatchQueue. + break + default: + // Other events indicate a programming error. + fatalError("PromiseKit event: \(event)") + } + } + } + + func validate() throws { + Self.initialize() + } +} + +typealias AppID = UInt64 + +extension NSNumber { + var appIDValue: AppID { + uint64Value + } +} diff --git a/Sources/MasKit/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift similarity index 84% rename from Sources/MasKit/Models/SearchResult.swift rename to Sources/mas/Models/SearchResult.swift index fe928b6..0f67287 100644 --- a/Sources/MasKit/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -1,6 +1,6 @@ // // SearchResult.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,13 +9,13 @@ struct SearchResult: Decodable { var bundleId: String var currentVersionReleaseDate: String - var fileSizeBytes: String? - var kind: String + var fileSizeBytes: String + var formattedPrice: String var minimumOsVersion: String - var price: Double? + var price: Double var sellerName: String var sellerUrl: String? - var trackId: Int + var trackId: AppID var trackName: String var trackViewUrl: String var version: String @@ -24,12 +24,12 @@ struct SearchResult: Decodable { bundleId: String = "", currentVersionReleaseDate: String = "", fileSizeBytes: String = "0", - kind: String = "", + formattedPrice: String = "0", minimumOsVersion: String = "", price: Double = 0.0, sellerName: String = "", sellerUrl: String = "", - trackId: Int = 0, + trackId: AppID = 0, trackName: String = "", trackViewUrl: String = "", version: String = "" @@ -37,7 +37,7 @@ struct SearchResult: Decodable { self.bundleId = bundleId self.currentVersionReleaseDate = currentVersionReleaseDate self.fileSizeBytes = fileSizeBytes - self.kind = kind + self.formattedPrice = formattedPrice self.minimumOsVersion = minimumOsVersion self.price = price self.sellerName = sellerName diff --git a/Sources/MasKit/Models/SearchResultList.swift b/Sources/mas/Models/SearchResultList.swift similarity index 95% rename from Sources/MasKit/Models/SearchResultList.swift rename to Sources/mas/Models/SearchResultList.swift index cd29756..68f40d1 100644 --- a/Sources/MasKit/Models/SearchResultList.swift +++ b/Sources/mas/Models/SearchResultList.swift @@ -1,6 +1,6 @@ // // SearchResultList.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/29/18. // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Models/SoftwareProduct.swift b/Sources/mas/Models/SoftwareProduct.swift similarity index 74% rename from Sources/MasKit/Models/SoftwareProduct.swift rename to Sources/mas/Models/SoftwareProduct.swift index 280266a..29c393f 100644 --- a/Sources/MasKit/Models/SoftwareProduct.swift +++ b/Sources/mas/Models/SoftwareProduct.swift @@ -1,6 +1,6 @@ // // SoftwareProduct.swift -// MasKit +// mas // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,7 +9,7 @@ import Foundation import Version -/// Protocol describing the members of CKSoftwareProduct used throughout MasKit. +/// Protocol describing the members of CKSoftwareProduct used throughout mas. protocol SoftwareProduct { var appName: String { get } var bundleIdentifier: String { get set } @@ -19,24 +19,25 @@ protocol SoftwareProduct { } extension SoftwareProduct { - /// Returns bundleIdentifier if appName is empty string. + /// - Returns: bundleIdentifier if appName is empty string. var appNameOrBundleIdentifier: String { appName.isEmpty ? bundleIdentifier : appName } - /// Determines whether the app is considered outdated. Updates that require a higher OS version are excluded. + /// Determines whether the app is considered outdated. + /// + /// Updates that require a higher OS version are excluded. + /// /// - Parameter storeApp: App from search result. /// - Returns: true if the app is outdated; false otherwise. func isOutdatedWhenComparedTo(_ storeApp: SearchResult) -> Bool { - // Only look at min OS version if we have one, also only consider macOS apps - // Replace string literal with MasStoreSearch.Entity once `search` branch is merged. - if let osVersion = Version(tolerant: storeApp.minimumOsVersion), storeApp.kind == "mac-software" { + // If storeApp requires a version of macOS newer than the running version, do not consider self outdated. + if let osVersion = Version(tolerant: storeApp.minimumOsVersion) { let requiredVersion = OperatingSystemVersion( majorVersion: osVersion.major, minorVersion: osVersion.minor, patchVersion: osVersion.patch ) - // Don't consider an app outdated if the version in the app store requires a higher OS version. guard ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion) else { return false } @@ -44,7 +45,8 @@ extension SoftwareProduct { // The App Store does not enforce semantic versioning, but we assume most apps follow versioning // schemes that increase numerically over time. - guard let semanticBundleVersion = Version(tolerant: bundleVersion), + guard + let semanticBundleVersion = Version(tolerant: bundleVersion), let semanticAppStoreVersion = Version(tolerant: storeApp.version) else { // If a version string can't be parsed as a Semantic Version, our best effort is to check for diff --git a/Sources/MasKit/Network/NetworkManager.swift b/Sources/mas/Network/NetworkManager.swift similarity index 83% rename from Sources/MasKit/Network/NetworkManager.swift rename to Sources/mas/Network/NetworkManager.swift index 7333612..5f0ab66 100644 --- a/Sources/MasKit/Network/NetworkManager.swift +++ b/Sources/mas/Network/NetworkManager.swift @@ -1,6 +1,6 @@ // // NetworkManager.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -9,11 +9,11 @@ import Foundation import PromiseKit -/// Network abstraction +/// Network abstraction. class NetworkManager { private let session: NetworkSession - /// Designated initializer + /// Designated initializer. /// /// - Parameter session: A networking session. init(session: NetworkSession = URLSession(configuration: .ephemeral)) { @@ -23,13 +23,14 @@ class NetworkManager { do { let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Caches/com.mphys.mas-cli") try FileManager.default.removeItem(at: url) - } catch {} + } catch { + // Ignore + } } /// Loads data asynchronously. /// - /// - Parameters: - /// - url: URL to load data from. + /// - Parameter url: URL from which to load data. /// - Returns: A Promise for the Data of the response. func loadData(from url: URL) -> Promise { session.loadData(from: url) diff --git a/Sources/MasKit/Network/NetworkSession.swift b/Sources/mas/Network/NetworkSession.swift similarity index 95% rename from Sources/MasKit/Network/NetworkSession.swift rename to Sources/mas/Network/NetworkSession.swift index 28cd649..af590ce 100644 --- a/Sources/MasKit/Network/NetworkSession.swift +++ b/Sources/mas/Network/NetworkSession.swift @@ -1,6 +1,6 @@ // // NetworkSession.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Sources/MasKit/Network/URLSession+NetworkSession.swift b/Sources/mas/Network/URLSession+NetworkSession.swift similarity index 89% rename from Sources/MasKit/Network/URLSession+NetworkSession.swift rename to Sources/mas/Network/URLSession+NetworkSession.swift index 7c15df0..5dca9dd 100644 --- a/Sources/MasKit/Network/URLSession+NetworkSession.swift +++ b/Sources/mas/Network/URLSession+NetworkSession.swift @@ -1,6 +1,6 @@ // // URLSession+NetworkSession.swift -// MasKit +// mas // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -10,7 +10,7 @@ import Foundation import PromiseKit extension URLSession: NetworkSession { - public func loadData(from url: URL) -> Promise { + func loadData(from url: URL) -> Promise { Promise { seal in dataTask(with: url) { data, _, error in if let data { diff --git a/Sources/mas/main.swift b/Sources/mas/main.swift deleted file mode 100644 index e872960..0000000 --- a/Sources/mas/main.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// main.swift -// mas-cli -// -// Created by Andrew Naylor on 11/07/2015. -// Copyright © 2015 Andrew Naylor. All rights reserved. -// - -import Commandant -import MasKit - -MasKit.initialize() - -let registry = CommandRegistry() -let helpCommand = HelpCommand(registry: registry) - -registry.register(AccountCommand()) -registry.register(HomeCommand()) -registry.register(InfoCommand()) -registry.register(InstallCommand()) -registry.register(PurchaseCommand()) -registry.register(ListCommand()) -registry.register(LuckyCommand()) -registry.register(OpenCommand()) -registry.register(OutdatedCommand()) -registry.register(ResetCommand()) -registry.register(SearchCommand()) -registry.register(SignInCommand()) -registry.register(SignOutCommand()) -registry.register(UninstallCommand()) -registry.register(UpgradeCommand()) -registry.register(VendorCommand()) -registry.register(VersionCommand()) -registry.register(helpCommand) - -registry.main(defaultVerb: helpCommand.verb) { error in - printError(String(describing: error)) -} diff --git a/Tests/MasKitTests/Commands/AccountCommandSpec.swift b/Tests/MasKitTests/Commands/AccountCommandSpec.swift deleted file mode 100644 index b78f16e..0000000 --- a/Tests/MasKitTests/Commands/AccountCommandSpec.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// AccountCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -// Deprecated test -public class AccountCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues - xdescribe("Account command") { - xit("displays active account") { - let cmd = AccountCommand() - let result = cmd.run(AccountCommand.Options()) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/HomeCommandSpec.swift b/Tests/MasKitTests/Commands/HomeCommandSpec.swift deleted file mode 100644 index ece1252..0000000 --- a/Tests/MasKitTests/Commands/HomeCommandSpec.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// HomeCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-29. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class HomeCommandSpec: QuickSpec { - override public func spec() { - let result = SearchResult( - trackId: 1111, - trackViewUrl: "mas preview url", - version: "0.0" - ) - let storeSearch = StoreSearchMock() - let openCommand = OpenSystemCommandMock() - let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand) - - beforeSuite { - MasKit.initialize() - } - describe("home command") { - beforeEach { - storeSearch.reset() - } - it("fails to open app with invalid ID") { - let result = cmd.run(HomeCommand.Options(appId: -999)) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) - } - it("can't find app with unknown ID") { - let result = cmd.run(HomeCommand.Options(appId: 999)) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) - } - it("opens app on MAS Preview") { - storeSearch.apps[result.trackId] = result - - let cmdResult = cmd.run(HomeCommand.Options(appId: result.trackId)) - expect(cmdResult).to(beSuccess()) - expect(openCommand.arguments).toNot(beNil()) - expect(openCommand.arguments!.first!) == result.trackViewUrl - } - } - } -} diff --git a/Tests/MasKitTests/Commands/InfoCommandSpec.swift b/Tests/MasKitTests/Commands/InfoCommandSpec.swift deleted file mode 100644 index b2bdc9c..0000000 --- a/Tests/MasKitTests/Commands/InfoCommandSpec.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// InfoCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class InfoCommandSpec: QuickSpec { - override public func spec() { - let result = SearchResult( - currentVersionReleaseDate: "2019-01-07T18:53:13Z", - fileSizeBytes: "1024", - minimumOsVersion: "10.14", - price: 2.0, - sellerName: "Awesome Dev", - trackId: 1111, - trackName: "Awesome App", - trackViewUrl: "https://awesome.app", - version: "1.0" - ) - let storeSearch = StoreSearchMock() - let cmd = InfoCommand(storeSearch: storeSearch) - let expectedOutput = """ - Awesome App 1.0 [2.0] - By: Awesome Dev - Released: 2019-01-07 - Minimum OS: 10.14 - Size: 1 KB - From: https://awesome.app - - """ - - beforeSuite { - MasKit.initialize() - } - describe("Info command") { - beforeEach { - storeSearch.reset() - } - it("fails to open app with invalid ID") { - let result = cmd.run(InfoCommand.Options(appId: -999)) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) - } - it("can't find app with unknown ID") { - let result = cmd.run(InfoCommand.Options(appId: 999)) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) - } - it("displays app details") { - storeSearch.apps[result.trackId] = result - let output = OutputListener() - - let result = cmd.run(InfoCommand.Options(appId: result.trackId)) - - expect(result).to(beSuccess()) - expect(output.contents) == expectedOutput - } - } - } -} diff --git a/Tests/MasKitTests/Commands/InstallCommandSpec.swift b/Tests/MasKitTests/Commands/InstallCommandSpec.swift deleted file mode 100644 index a870649..0000000 --- a/Tests/MasKitTests/Commands/InstallCommandSpec.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// InstallCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class InstallCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("install command") { - it("installs apps") { - let cmd = InstallCommand() - let result = cmd.run(InstallCommand.Options(appIds: [], forceInstall: false)) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/ListCommandSpec.swift b/Tests/MasKitTests/Commands/ListCommandSpec.swift deleted file mode 100644 index 2901753..0000000 --- a/Tests/MasKitTests/Commands/ListCommandSpec.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ListCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-27. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class ListCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("list command") { - it("lists apps") { - let list = ListCommand() - let result = list.run(ListCommand.Options()) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/LuckyCommandSpec.swift b/Tests/MasKitTests/Commands/LuckyCommandSpec.swift deleted file mode 100644 index 034d15c..0000000 --- a/Tests/MasKitTests/Commands/LuckyCommandSpec.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// LuckyCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class LuckyCommandSpec: QuickSpec { - override public func spec() { - let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") - let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) - - beforeSuite { - MasKit.initialize() - } - describe("lucky command") { - xit("installs the first app matching a search") { - let cmd = LuckyCommand(storeSearch: storeSearch) - let result = cmd.run(LuckyCommand.Options(appName: "Slack", forceInstall: false)) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/OpenCommandSpec.swift b/Tests/MasKitTests/Commands/OpenCommandSpec.swift deleted file mode 100644 index c01dcd4..0000000 --- a/Tests/MasKitTests/Commands/OpenCommandSpec.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// OpenCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2019-01-03. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Foundation -import Nimble -import Quick - -@testable import MasKit - -public class OpenCommandSpec: QuickSpec { - override public func spec() { - let result = SearchResult( - trackId: 1111, - trackViewUrl: "fakescheme://some/url", - version: "0.0" - ) - let storeSearch = StoreSearchMock() - let openCommand = OpenSystemCommandMock() - let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand) - - beforeSuite { - MasKit.initialize() - } - describe("open command") { - beforeEach { - storeSearch.reset() - } - it("fails to open app with invalid ID") { - let result = cmd.run(OpenCommand.Options(appId: "-999")) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) - } - it("can't find app with unknown ID") { - let result = cmd.run(OpenCommand.Options(appId: "999")) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) - } - it("opens app in MAS") { - storeSearch.apps[result.trackId] = result - - let cmdResult = cmd.run(OpenCommand.Options(appId: result.trackId.description)) - expect(cmdResult).to(beSuccess()) - expect(openCommand.arguments).toNot(beNil()) - let url = URL(string: openCommand.arguments!.first!) - expect(url).toNot(beNil()) - expect(url?.scheme) == "macappstore" - } - it("just opens MAS if no app specified") { - let cmdResult = cmd.run(OpenCommand.Options(appId: "appstore")) - expect(cmdResult).to(beSuccess()) - expect(openCommand.arguments).toNot(beNil()) - let url = URL(string: openCommand.arguments!.first!) - expect(url).toNot(beNil()) - expect(url) == URL(string: "macappstore://") - } - } - } -} diff --git a/Tests/MasKitTests/Commands/OutdatedCommandSpec.swift b/Tests/MasKitTests/Commands/OutdatedCommandSpec.swift deleted file mode 100644 index 6b7c72d..0000000 --- a/Tests/MasKitTests/Commands/OutdatedCommandSpec.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// OutdatedCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class OutdatedCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("outdated command") { - it("displays apps with pending updates") { - let cmd = OutdatedCommand() - let result = cmd.run(OutdatedCommand.Options(verbose: true)) - print(result) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift b/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift deleted file mode 100644 index ce85977..0000000 --- a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PurchaseCommandSpec.swift -// MasKitTests -// -// Created by Maximilian Blochberger on 2020-03-21. -// Copyright © 2020 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class PurchaseCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("purchase command") { - it("purchases apps") { - let cmd = PurchaseCommand() - let result = cmd.run(PurchaseCommand.Options(appIds: [])) - expect(result) - .to( - beFailure { error in - expect(error) == .notSupported - }) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/SearchCommandSpec.swift b/Tests/MasKitTests/Commands/SearchCommandSpec.swift deleted file mode 100644 index 7e2e98d..0000000 --- a/Tests/MasKitTests/Commands/SearchCommandSpec.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// SearchCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class SearchCommandSpec: QuickSpec { - override public func spec() { - let result = SearchResult( - trackId: 1111, - trackName: "slack", - trackViewUrl: "mas preview url", - version: "0.0" - ) - let storeSearch = StoreSearchMock() - - beforeSuite { - MasKit.initialize() - } - describe("search command") { - beforeEach { - storeSearch.reset() - } - it("can find slack") { - storeSearch.apps[result.trackId] = result - - let search = SearchCommand(storeSearch: storeSearch) - let searchOptions = SearchOptions(appName: "slack", price: false) - let result = search.run(searchOptions) - expect(result).to(beSuccess()) - } - it("fails when searching for nonexistent app") { - let search = SearchCommand(storeSearch: storeSearch) - let searchOptions = SearchOptions(appName: "nonexistent", price: false) - let result = search.run(searchOptions) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/SignInCommandSpec.swift b/Tests/MasKitTests/Commands/SignInCommandSpec.swift deleted file mode 100644 index 5f44888..0000000 --- a/Tests/MasKitTests/Commands/SignInCommandSpec.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SignInCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -// Deprecated test -public class SignInCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues - xdescribe("signin command") { - xit("signs in") { - let cmd = SignInCommand() - let result = cmd.run(SignInCommand.Options(username: "", password: "", dialog: false)) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/UninstallCommandSpec.swift b/Tests/MasKitTests/Commands/UninstallCommandSpec.swift deleted file mode 100644 index 5aafeac..0000000 --- a/Tests/MasKitTests/Commands/UninstallCommandSpec.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// UninstallCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-27. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Foundation -import Nimble -import Quick - -@testable import MasKit - -public class UninstallCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("uninstall command") { - let appId = 12345 - let app = SoftwareProductMock( - appName: "Some App", - bundleIdentifier: "com.some.app", - bundlePath: "/tmp/Some.app", - bundleVersion: "1.0", - itemIdentifier: NSNumber(value: appId) - ) - let mockLibrary = AppLibraryMock() - let uninstall = UninstallCommand(appLibrary: mockLibrary) - - context("dry run") { - let options = UninstallCommand.Options(appId: appId, dryRun: true) - - beforeEach { - mockLibrary.reset() - } - it("can't remove a missing app") { - let result = uninstall.run(options) - expect(result) - .to( - beFailure { error in - expect(error) == .notInstalled - }) - } - it("finds an app") { - mockLibrary.installedApps.append(app) - - let result = uninstall.run(options) - expect(result).to(beSuccess()) - } - } - context("wet run") { - let options = UninstallCommand.Options(appId: appId, dryRun: false) - - beforeEach { - mockLibrary.reset() - } - it("can't remove a missing app") { - let result = uninstall.run(options) - expect(result) - .to( - beFailure { error in - expect(error) == .notInstalled - }) - } - it("removes an app") { - mockLibrary.installedApps.append(app) - - let result = uninstall.run(options) - expect(result).to(beSuccess()) - } - it("fails if there is a problem with the trash command") { - var brokenUninstall = app // make mutable copy - brokenUninstall.bundlePath = "/dev/null" - mockLibrary.installedApps.append(brokenUninstall) - - let result = uninstall.run(options) - expect(result) - .to( - beFailure { error in - expect(error) == .uninstallFailed - }) - } - } - } - } -} diff --git a/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift b/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift deleted file mode 100644 index 8a544d3..0000000 --- a/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// UpgradeCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class UpgradeCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("upgrade command") { - it("upgrades stuff") { - let cmd = UpgradeCommand() - let result = cmd.run(UpgradeCommand.Options(apps: [""])) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Commands/VendorCommandSpec.swift b/Tests/MasKitTests/Commands/VendorCommandSpec.swift deleted file mode 100644 index 5e342cb..0000000 --- a/Tests/MasKitTests/Commands/VendorCommandSpec.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// VendorCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2019-01-03. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class VendorCommandSpec: QuickSpec { - override public func spec() { - let result = SearchResult( - trackId: 1111, - trackViewUrl: "https://awesome.app", - version: "0.0" - ) - let storeSearch = StoreSearchMock() - let openCommand = OpenSystemCommandMock() - let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand) - - beforeSuite { - MasKit.initialize() - } - describe("vendor command") { - beforeEach { - storeSearch.reset() - } - it("fails to open app with invalid ID") { - let result = cmd.run(VendorCommand.Options(appId: -999)) - expect(result) - .to( - beFailure { error in - expect(error) == .searchFailed - }) - } - it("can't find app with unknown ID") { - let result = cmd.run(VendorCommand.Options(appId: 999)) - expect(result) - .to( - beFailure { error in - expect(error) == .noSearchResultsFound - }) - } - it("opens vendor app page in browser") { - storeSearch.apps[result.trackId] = result - - let cmdResult = cmd.run(VendorCommand.Options(appId: result.trackId)) - expect(cmdResult).to(beSuccess()) - expect(openCommand.arguments).toNot(beNil()) - expect(openCommand.arguments!.first!) == result.sellerUrl - } - } - } -} diff --git a/Tests/MasKitTests/Commands/VersionCommandSpec.swift b/Tests/MasKitTests/Commands/VersionCommandSpec.swift deleted file mode 100644 index ba10acc..0000000 --- a/Tests/MasKitTests/Commands/VersionCommandSpec.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// VersionCommandSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 2018-12-28. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class VersionCommandSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("version command") { - it("displays the current version") { - let cmd = VersionCommand() - let result = cmd.run(VersionCommand.Options()) - expect(result).to(beSuccess()) - } - } - } -} diff --git a/Tests/MasKitTests/Controllers/AppLibraryMock.swift b/Tests/MasKitTests/Controllers/AppLibraryMock.swift deleted file mode 100644 index 4ca7256..0000000 --- a/Tests/MasKitTests/Controllers/AppLibraryMock.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppLibraryMock.swift -// MasKitTests -// -// Created by Ben Chatelain on 12/27/18. -// Copyright © 2018 mas-cli. All rights reserved. -// - -@testable import MasKit - -class AppLibraryMock: AppLibrary { - var installedApps = [SoftwareProduct]() - - func uninstallApp(app: SoftwareProduct) throws { - if !installedApps.contains(where: { product -> Bool in - app.itemIdentifier == product.itemIdentifier - }) { - throw MASError.notInstalled - } - - // Special case for testing where we pretend the trash command failed - if app.bundlePath == "/dev/null" { - throw MASError.uninstallFailed - } - - // Success is the default, watch out for false positives! - } -} - -/// Members not part of the AppLibrary protocol that are only for test state managment. -extension AppLibraryMock { - /// Clears out the list of installed apps. - func reset() { - installedApps = [] - } -} diff --git a/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift b/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift deleted file mode 100644 index 5fed9e5..0000000 --- a/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// MasStoreSearchSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 1/4/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class MasStoreSearchSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("url string") { - it("contains the app name") { - let appName = "myapp" - let urlString = MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString - expect(urlString) == """ - https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appName)&country=US - """ - } - it("contains the encoded app name") { - let appName = "My App" - let appNameEncoded = "My%20App" - let urlString = MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString - expect(urlString) == """ - https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appNameEncoded)&country=US - """ - } - } - describe("store") { - context("when searched") { - it("can find slack") { - let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") - let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) - - var results: [SearchResult] - do { - results = try storeSearch.search(for: "slack").wait() - expect(results.count) == 39 - } catch { - let maserror = error as! MASError - if case .jsonParsing(let nserror) = maserror { - fail("\(maserror) \(nserror!)") - } - } - } - } - - context("when lookup used") { - it("can find slack") { - let appId = 803_453_959 - let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") - let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) - - var lookup: SearchResult? - do { - lookup = try storeSearch.lookup(app: appId).wait() - } catch { - let maserror = error as! MASError - if case .jsonParsing(let nserror) = maserror { - fail("\(maserror) \(nserror!)") - } - } - - guard let result = lookup else { fatalError("lookup result was nil") } - - expect(result.trackId) == appId - expect(result.bundleId) == "com.tinyspeck.slackmacgap" - expect(result.price) == 0 - expect(result.sellerName) == "Slack Technologies, Inc." - expect(result.sellerUrl) == "https://slack.com" - expect(result.trackName) == "Slack" - expect(result.trackViewUrl) == "https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4" - expect(result.version) == "3.3.3" - } - } - } - } -} diff --git a/Tests/MasKitTests/Controllers/StoreSearchMock.swift b/Tests/MasKitTests/Controllers/StoreSearchMock.swift deleted file mode 100644 index 69f518d..0000000 --- a/Tests/MasKitTests/Controllers/StoreSearchMock.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// StoreSearchMock.swift -// MasKitTests -// -// Created by Ben Chatelain on 1/4/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import PromiseKit - -@testable import MasKit - -class StoreSearchMock: StoreSearch { - var apps: [Int: SearchResult] = [:] - - func search(for appName: String) -> Promise<[SearchResult]> { - let filtered = apps.filter { $1.trackName.contains(appName) } - let results = filtered.map { $1 } - return .value(results) - } - - func lookup(app appId: Int) -> Promise { - // Negative numbers are invalid - guard appId > 0 else { - return Promise(error: MASError.searchFailed) - } - - guard let result = apps[appId] - else { - return Promise(error: MASError.noSearchResultsFound) - } - - return .value(result) - } - - func reset() { - apps = [:] - } -} diff --git a/Tests/MasKitTests/JSON/search/things.json b/Tests/MasKitTests/JSON/search/things.json deleted file mode 100644 index 091e35d..0000000 --- a/Tests/MasKitTests/JSON/search/things.json +++ /dev/null @@ -1,3333 +0,0 @@ -{ - "resultCount": 50, - "results": [ - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/6f/4c/e5/6f4ce5d6-7caa-d1eb-bbc9-86558e97d2ba/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/92/b4/f8/92b4f8f5-f133-abd8-db17-135ac27bb1fa/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/72/63/63/726363b9-45ff-f93e-975c-fb69836eaf1a/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/29/fa/63/29fa63e3-3cb2-8b8a-8541-31fa9b7ef27f/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/da/17/5f/da175f95-c2cd-e5df-8cbc-d800d6770c64/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/f1/82/37/f182376c-4f25-6dbb-c6a8-5e6c1c617620/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/cultured-code-gmbh-co-kg/id284971784?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.13.0", - "trackName": "Things 3", - "trackId": 904280696, - "sellerName": "Cultured Code GmbH & Co. KG", - "releaseNotes": "• Moved the database file to a new location (now at /Library/Group Containers/).\n• Increased the clickable area of items in the sidebar.\n• Improved the formatting of years in Japanese.\n• Fixed some crashes that could occur when hitting Cmd+[ or ] in Quick Entry while the When popover was visible.\n• Updated the crash reporter.\n• Some sync improvements.\n\n\nNEW IN 3.12\n\nWe’re excited to release Things 3.12 – a big update for our Watch app!\n\nWe’ve entirely rebuilt its foundation to allow it to sync and operate without your phone being nearby. We’ve also taken this opportunity to add some often-requested features to the app. For more information about this release, please visit our blog: thingsapp.com\n\nThere are no huge changes in this release for Mac, but there’s one great new feature you should know about: you can now edit the Tags or Deadlines of collapsed to-dos – even for multiple to-dos at once – by hitting Cmd+Shift+T or D. It’s super convenient :)", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-05-18T16:42:04Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2020-08-04T07:57:44Z", - "trackCensoredName": "Things 3", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "RU", - "ZH", - "ES", - "ZH" - ], - "fileSizeBytes": "17474797", - "sellerUrl": "https://culturedcode.com/things/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/things-3/id904280696?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Get things done! The award-winning Things app helps you plan your day, manage your projects, and make real progress toward your goals.\n\nBest of all, it’s easy to use. Within the hour, you’ll have everything off your mind and neatly organized—from routine tasks to your biggest life goals—and you can start focusing on what matters today.\n\n“Things offers the best combination of design and functionality of any app we tested, with nearly all the features of other power user applications and a delightful interface that never gets in the way of your work.”\n—Wirecutter, The New York Times\n\n\nKEY FEATURES\n\n• Your To-Dos\nYour basic building block is the almighty To-Do—each a small step toward a great accomplishment. You can add notes, tag it, schedule it, and break it down into smaller steps.\n\n• Your Projects\nCreate a Project for any big goal, then add the to-dos to reach it. Use headings to structure your list as you outline your plan. There’s also a place to jot down your notes, and a deadline to keep you on schedule.\n\n• Your Areas\nCreate an Area for each sphere of your life, such as Work, Family, Finance, and so on. This keeps everything neatly organized, and helps you see the big picture as you set your plans in motion.\n\n• Your Plan\nEverything on your schedule is neatly laid out in the Today and Upcoming lists, which show your to-dos and calendar events. Each morning, see what you planned for Today and decide what you want to do. The rest is down to you :)\n\n\nMORE THINGS TO LOVE\n\nAs you dive deeper, you’ll find Things packed with helpful features. Here are just a few:\n\n• Reminders — set a time and Things will remind you.\n• Repeaters — automatically repeat to-dos on a schedule you set.\n• This Evening — a special place for your evening plans.\n• Calendar integration — see your events and to-dos together.\n• Tags — categorize your to-dos and quickly filter lists.\n• Quick Entry — create to-dos from anywhere, as soon as the thought hits you.\n• Quick Find — instantly locate to-dos, headings, or tags.\n• Type Travel — jump from list to list with your keyboard; just start typing!\n• Mail to Things — forward an email to Things; now it’s a to-do.\n• And much more!\n\n\nMADE FOR MAC\n\nThings is tailored to the Mac with deep system integrations as well. A great example is Quick Entry with Autofill: a shortcut that grabs content from other apps and adds it to Things for you, such as a link to a website or an email you want to get back to.\n\nYou can also enjoy a beautiful dark mode at sunset, connect your calendars, enable a Things widget, use your Mac’s Touch Bar, import from Reminders—Things can do it all! There’s even AppleScript support if you need powerful automation.\n\n\nSTAY PRODUCTIVE ON THE GO\n\nThings has full-featured apps for iPhone, iPad, and Apple Watch as well (sold separately). All your devices sync seamlessly via our free Things Cloud service. It’s great to have everything at your fingertips when you need it!\n\n\nAWARD-WINNING DESIGN\n\nMade in Stuttgart, with two Apple Design Awards to its name, Things is a fine example of German engineering: designed, not only to look fantastic, but to be perfectly functional as well. Every detail is thoughtfully considered, then polished to perfection.\n\n“It’s like the unicorn of productivity tools: deep enough for serious work, surprisingly easy to use, and gorgeous enough to enjoy staring at.”\n—Apple\n\n\nGET THINGS TODAY\n\nWhatever it is you want to accomplish in life, Things can help you get there. Install the app today and see what you can do!\n\nVisit our website now and get a free 15-day trial for your Mac: thingsapp.com\n\nIf you have any questions, please get in touch. We provide professional support and will be glad to help you!", - "currency": "USD", - "artistId": 284971784, - "artistName": "Cultured Code GmbH & Co. KG", - "genres": [ - "Productivity", - "Business" - ], - "price": 49.99, - "bundleId": "com.culturedcode.ThingsMac", - "version": "3.12.6", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c9/5d/af/c95daf17-c405-56f0-90f5-9411828e44d2/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/94/32/3b/94323b37-f81b-7ba8-a280-b951e7e840de/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/82/f1/35/82f1356d-1e68-8f9d-3967-566e256f9265/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/dc/ad/83/dcad839b-7705-e4c0-180a-2f97cb68054d/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/21/50/6a/21506a09-c48c-d25e-dfa5-c0d6aa4cdd9d/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/appest-limited/id434073155?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "TickTick: Things & Tasks To Do", - "trackId": 966085870, - "sellerName": "Appest Limited", - "releaseNotes": "- Bug fixes and improvements.\n\nRecent Updates:\n- Customizable Section in List View.\n- Tag names can be capitalized.\n- The number of Pomos can now be estimated beforehand.\n- Lists under different folders can share the same name.\n- New city themes! Los Angeles and Cairo.\n\nThanks for using TickTick! We'll bring regular updates to give you more pleasant experience with performance and stability.\nWe'll read all reviews in App Store and evaluate your feedbacks carefully. Any issues encountered during the use, you may write to us via Avatar -> Feedback & Suggestions -> Submit feedback, we will get back to you asap.\nTickTick team with love.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-03-04T06:37:31Z", - "genreIds": [ - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-27T01:27:34Z", - "trackCensoredName": "TickTick: Things & Tasks To Do", - "languageCodesISO2A": [ - "EN", - "ZH" - ], - "fileSizeBytes": "24698702", - "sellerUrl": "https://ticktick.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/ticktick-things-tasks-to-do/id966085870?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Design exclusively for macOS, TickTick is your daily must-have to-do & task list to get all things done.\nTickTick can be accessed on more than 10 different platforms including Mac, iPhone, iPad, Apple Watch which enables you to manage tasks on all your devices/Web.\n\nKey features: \n- Add task via shortcut (Command+Shift+A)\n- Instant reminder\n- Set priority levels to tasks\n- Set flexible recurring tasks \n- Create checklists within tasks \n- Sort tasks by order/date/name/priority \n- Sync all your tasks across all devices \n\nTickTick is free but you can also upgrade to Premium account for full access of premium features for $2.99 a month or $27.99 a year through an auto-renewing subscription.\n\nPremium Features: \n- Grid view and Timeline view of calendar\n- Duration\n- Custom Smart List\n- Description for checklist\n- Reminders for sub-tasks\n- More lists and tasks (299 lists, 999 tasks in each list, 199 subtasks in each task)\n- Add at most 5 reminders to each task\n- Share a task list up to 19 members for better task collaboration\n- Upload up to 99 attachments every day\n\nSubscriptions for Premium account will be charged to your credit card through your iTunes account. Your subscription will automatically renew unless cancelled at least 24-hours before the end of the current period. You will not be able to cancel a subscription during the active period. You can manage your subscriptions in the Account Settings after purchase. \n\nHow TickTick makes you productive: \n- Get all things done \n- Never miss a schedule\n- Make work more productive \n- Keep life on track \n\nConnect with us: \nFacebook: https://www.facebook.com/TickTickApp\nTwitter: https://twitter.com/TickTickTeam @TickTickTeam\nHelp Center: https://help.ticktick.com/\n\nPrivacy Policy: https://www.ticktick.com/about/privacy\nTerms of Use: https://www.ticktick.com/about/tos", - "currency": "USD", - "artistId": 434073155, - "artistName": "Appest Limited", - "genres": [ - "Productivity" - ], - "price": 0.00, - "bundleId": "com.TickTick.task.mac", - "version": "3.7.11", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/79/5c/63/795c63aa-698c-1c6c-b6da-e7ebba718d01/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/15/4d/40/154d4071-4a6f-dcd7-0d15-2e495f6f4710/mzm.mvtkjcyn.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple2/v4/e0/31/dc/e031dc74-ce06-afe3-fd8e-8693f6c7c50c/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/fc/8d/23/fc8d2367-725d-11dd-6da9-816a7780a1d9/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "Simple Antnotes", - "trackId": 846599902, - "sellerName": "Mykola Olshevskyi", - "releaseNotes": "- added option to disable gradient background\n- added option to create new notes in bottom left/right corners\n- changed delay for close/options buttons showing\n- some minor compatibility and UI fixes\n- fixed German localisation", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-03-28T12:49:14Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2016-09-24T17:06:52Z", - "trackCensoredName": "Simple Antnotes", - "languageCodesISO2A": [ - "EN", - "DE", - "RU", - "UK" - ], - "fileSizeBytes": "1002100", - "sellerUrl": "https://www.antlogic.com/apps/antnotes", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/simple-antnotes/id846599902?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Antnotes are like paper notes: they are glued to your monitor, but from the other side of the screen.\n\nThis nice and handy application lives in the menu bar for faster access and has the following features:\n\n- customizable background, font and text color\n- pin note to desktop to make it stay atop of other windows\n- translucent notes\n- make new notes by dragging text, images and files to the menu bar icon\n- drag images and sounds to note contents\n- automatically hide notes when inactive\n- quick access via menu bar icon\n- configurable global shortcuts to create new note or show/hide all notes\n- integration with services: create new note from any text in any application\n- snap to screen bounds and other notes\n- archive with all closed notes - do not lose your information by accidentally closing a note\n- smart position choosing for different display configurations\n\nWant more features? Let us know, or check out our Antnotes application!\n\nVisit our support forums: https://www.antlogic.com/forum/", - "currency": "USD", - "artistId": 364746702, - "artistName": "AntLogic", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "ua.com.AntLogic.SimpleAntnotes", - "version": "1.6.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "Watch4-Watch4", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/2b/ce/8f/2bce8ffa-545b-050c-1dd9-2aeef532facd/pr_source.png/406x228bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/fb/36/14/fb36142e-17ba-fdab-90c6-e8f9d3c080ef/pr_source.png/406x228bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/4e/e2/de/4ee2de74-d0ef-010b-19f6-63755aa0175c/pr_source.png/406x228bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/46/8e/bd/468ebdd3-73a9-ec6a-b4da-6931ce887cff/pr_source.png/406x228bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/a4/9c/fa/a49cfa14-69e0-f1cf-3924-6ff878027b2d/pr_source.png/406x228bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c1/94/cc/c194ccb6-c15a-c0a5-47a6-3ddd625fd98d/pr_source.png/406x228bb.png" - ], - "ipadScreenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/3f/23/5e/3f235e16-c049-8ee8-ebdc-3d52f25f2636/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/80/48/1d/80481dff-e404-721c-920e-4688f860cf27/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/58/a2/c9/58a2c970-1bd3-6f4d-1bdc-502f75faaa6a/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/2c/a6/06/2ca606eb-8b40-219a-34c5-626f79b7e593/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/d7/04/c7d70441-51bd-1417-c7bf-a5d2702380e4/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/87/e0/75/87e075fd-a979-6151-5744-56ab76ac8f18/pr_source.png/552x414bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/volodymyr-yahenskyi/id961335645?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.0", - "trackName": "Random: Lists & Decision Maker", - "trackId": 1128190780, - "sellerName": "Volodymyr Yahenskyi", - "releaseNotes": "• Fixed crash when adding items to a new list\n• Fixed lists sync on Apple Watch\n\nThanks for using the Random!\nThis release also contains bug fixes and performance improvements.", - "primaryGenreId": 6012, - "primaryGenreName": "Lifestyle", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-07-05T22:00:04Z", - "genreIds": [ - "6012", - "7009", - "6014", - "7004" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-29T19:21:51Z", - "trackCensoredName": "Random: Lists & Decision Maker", - "languageCodesISO2A": [ - "EN", - "RU", - "UK" - ], - "fileSizeBytes": "76392448", - "sellerUrl": "https://yahenskyi.dev/random/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.6104900000000004212097337585873901844024658203125, - "userRatingCountForCurrentVersion": 1525, - "averageUserRating": 4.6104900000000004212097337585873901844024658203125, - "trackViewUrl": "https://apps.apple.com/us/app/random-lists-decision-maker/id1128190780?uo=4", - "trackContentRating": "4+", - "description": "Need a random number? Or can’t you decide what to do? Random is a powerful app that will solve all such problems.\n\nFeatures:\n• Number generator (from a range 0 - 999999999)\n• Letter generator\n• Dice roller (roll up to 4 regular dices in one go)\n• A custom item from a list generator\n• Yes or No \n• Coin flipper\n• Card generator\n• Rock-Paper-Scissors\n• Map Point\n\nGenerate a new random number simply by tapping a ​randomize button or by touching the Apple Watch screen. For those who want a bit of additional exercise, shaking your iOS device will also result in a new random response.\n\nUse Force Touch for setting the minimum or maximum values in your Apple Watch app. Same for the number of dices​, cards, and selection of lists.\n\nRandom Premium subscription benefits:\n• Sync: Get access to your data from all your devices.\n• Themes: Customize the app with various themes and background images.\n• No advertising.\n\nIf you decide to get Random Premium subscription, your purchase will be charged to your iTunes account. 1 month costs $2.99 and 1 year costs $11.99. Active subscriptions will be auto-renewed 24 hours before the expiry date. You can manage subscriptions from Account in iTunes after subscribing, you’ll also be able to cancel the auto-renewing subscription from there at any time. Any unused portion of the free trial period will be forfeited if you purchase a subscription to Random Premium before your trial expires.\n\nTerms & Conditions: https://yahenskyi.dev/terms-conditions/\nPrivacy Policy: https://yahenskyi.dev/privacy-policy/", - "currency": "USD", - "artistId": 961335645, - "artistName": "Volodymyr Yahenskyi", - "genres": [ - "Lifestyle", - "Family", - "Games", - "Board" - ], - "price": 0.00, - "bundleId": "com.yahenskyi.random", - "version": "2.2.10", - "wrapperType": "software", - "userRatingCount": 1525 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/0e/74/bb/0e74bb9a-5ac2-5d5f-516a-5f1c12e95328/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/36/54/f0/3654f064-4013-95e6-2683-c89ab8e51102/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/1a/75/86/1a758637-9db5-007c-595b-b724e9083321/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/any-case-solutions/id1396419026?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "Task Planner - To Do List", - "trackId": 1063681909, - "sellerName": "Any Case Solutions, OOO", - "releaseNotes": "We’ve updated the app! In the new version:\n- less bugs;\n- minor changes in the interface;\n- some general improvements.\nYour opinion is important to us! Please, leave your feedback - we will gladly consider all your wishes and suggestions.", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-01-07T00:04:36Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-17T23:48:12Z", - "trackCensoredName": "Task Planner - To Do List", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "27930644", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/task-planner-to-do-list/id1063681909?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Plan Your Tasks is a productivity tool that allows you to capture your ideas and duties in one place. \nManage everything you have to do while working with many different tasks!\n\nEasy task management - create, organize, and prioritize tasks;\n- Set notifications;\n- Add comments;\n- Sort tasks by categories;\n- Track due dates.\n\nNew approach to agenda\n- Build-in calendar;\n- Coherent tutorial mode;\n- Magic Trackpad 2 support.\n\nCapture all your flash ideas and duties in the calendar and manage your to dos while working with many tasks more effectively.\n\n\nPrivacy Policy: https://anycasesolutions.com/privacy\nTerms Of Use: https://anycasesolutions.com/tos", - "currency": "USD", - "artistId": 1396419026, - "artistName": "Any Case Solutions", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.newtechnologies.iPlanTasksinapp", - "version": "2.1.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/69/a0/58/69a0583d-02fd-1d37-cb33-19b80578e9e5/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/09/be/29/09be2981-4d08-a021-423a-29cc212c1b59/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple7/v4/28/5a/f4/285af4d8-37e6-118a-ff28-a4211eeb1122/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/50/4d/52/504d520c-4f8c-b011-d1e0-18addb5700a8/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/f2/6e/07/f26e0760-efb1-0353-3ebd-9fd2803f4b3d/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/realmac-software/id310591643?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "Clear – Tasks, Reminders & To-Do Lists", - "trackId": 504544917, - "sellerName": "Realmac Software Limited", - "releaseNotes": "Thanks for using Clear! Just two small enhancements in today’s update:\n\n- We’ve tweaked (increased) the delay before “Click to Clear” appears.\n- We’ve ensured compatibility with OS X El Capitan.\n\nStay productive, and follow @realmacsoftware on Twitter for the latest news!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-11-08T08:00:00Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2015-08-19T14:05:32Z", - "trackCensoredName": "Clear – Tasks, Reminders & To-Do Lists", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "13109875", - "sellerUrl": "http://impending.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/clear-tasks-reminders-to-do-lists/id504544917?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Over 2.5 million people de-clutter their lives with Clear, so stop stalling and start organizing your daily routine.\n\nClear is the revolutionary to-do and reminders app that makes you more productive. Just start typing to add to-dos, and once you start organizing your life with Clear you’ll wonder how you ever managed without it.\n\n- Simple gestural design that allows you to focus on your to-dos. Designed for the Magic Trackpad, but works great with a mouse too!\n- Full keyboard navigation. Just start typing to create to-dos.\n- Use separate lists to organize every aspect of your life.\n- iCloud sync built-in so you can be productive everywhere.\n- Set reminders so you’ll never forget important tasks.\n- Personalize your Clear lists with themes and make them your own.\n- Syncs with Clear for iOS (available separately on the App Store).\n\nClear is built by a small team, dedicated to bringing you frequent free feature updates. We’d love to know how we can make you even more productive, so get in touch via the App Store “Support” link, or tweet us @UseClear.\n\nClear for Mac and Clear for iOS are not affiliated with or endorsed by CLEAR Wireless.", - "currency": "USD", - "artistId": 310591643, - "artistName": "Realmac Software", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 9.99, - "bundleId": "com.realmacsoftware.clear.mac", - "version": "1.1.7", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/cd/bd/44/cdbd44af-06eb-21d6-a793-43dae1077c47/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/e9/8f/17/e98f17c6-787b-b180-6f8d-fb8385ceedd3/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/43/12/a2/4312a25b-f773-9c1a-ddd4-2515d948cc27/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/97/0d/b4/970db444-bbd9-ed77-439e-001bce006e17/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/shenzhen-tomato-software-technology-co-ltd/id966057212?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "Focus To-Do: Pomodoro & Tasks", - "trackId": 1258530160, - "sellerName": "Shenzhen Tomato Software Technology Co., Ltd.", - "releaseNotes": "1.Support new languages\n2.Bug fix", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-08-02T03:45:26Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-05-03T04:38:29Z", - "trackCensoredName": "Focus To-Do: Pomodoro & Tasks", - "languageCodesISO2A": [ - "CS", - "EN", - "FR", - "DE", - "ID", - "IT", - "JA", - "KO", - "PL", - "PT", - "RO", - "RU", - "ZH", - "ES", - "ZH", - "TR", - "VI" - ], - "fileSizeBytes": "12135791", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/focus-to-do-pomodoro-tasks/id1258530160?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Focus To-Do combines Pomodoro Timer with Task Management, it is a science-based app that will motivate you to stay focused and get things done. \n\nIt brings Pomodoro Technique and To Do List into one place, you can capture and organize tasks into your todo lists, start focus timer and focus on work & study, set reminders for important tasks and errands, check the time spent at work. \n\nIt's the ultimate app for managing Tasks, Reminders, Lists, Calendar events, Grocery lists, checklist, helping you focus on work & study and tracking your working hours.\n\nFocus To-Do syncs between your phone and computer, so you can access your lists from anywhere.\n\nHow it works:\n 1. Pick a task you need to accomplish.\n 2. Set a timer for 25 minutes, keep focused and start working.\n 3. When the pomodoro timer rings, take a 5 minute break.\n \nKey Features:\n\n- Pomodoro Timer:Stay focused and get more things done.\n Pause and resume Pomodoro\n Customizable pomodoro/breaks lengths\n Notification before the end of a Pomodoro\n Support for short and long breaks\n Skip a break after the end of a Pomodoro\n Continuous Mode\n \n- Tasks Management: Task Organizer, Schedule Planner, Reminder, Habit Tracker, Time Tracker\n Tasks and projects: Organise your day with Focus To-Do and complete your to do, study, work, homework or housework you need to get done.\n Recurring tasks: Build lasting habits with powerful recurring due dates like \"Every Monday\".\n Reminders: Setting a Reminder ensures you never forget important things ever again, you can set up recurring due dates to remind you each and every time. \n Sub-tasks: Break down your task into smaller, actionable items or add a checklist .\n Task Priority: Highlight your day’s most important To-Do with color-coded priority levels.\n Estimated Pomodoro Number: Estimate the workload or set a goal.\n Note: Record more detailed about the task.\n\n- Report: Detailed statistics of your time distribution, tasks completed.\n Support the calculation of the total time of Focus Time.\n Gantt Chart of the Focus Time.\n Statistics on completed To Do. \n Statistics on time distribution of project.\n Trend chart of the completed To Do and the focus time.\n\n- All-Platform synchronization: View and manage your goals wherever you are for better goal achieving.\n Support seamless synchronization within iPhone、Mac、iPad、Apple Watch and other platforms.\n \n- Various Reminding:\n Focus Timer finished alarm, vibration reminding.\n Various white noise to help you focus on work & study.\n\nContact Us: focustodo@163.com, reply within 24 hours.\nWebsite: http://www.focustodo.cn\nPomodoro ™ and Pomodoro Technique ® are registered trademarks of Francesco Cirillo. This app is not affiliated with Francesco Cirillo.\n\nUsers have been focused on our app for 200 million hours, join us and we help you to be focused and increase your productivity, reduce procrastination and anxiety.", - "currency": "USD", - "artistId": 966057212, - "artistName": "Shenzhen Tomato Software Technology Co., Ltd.", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.macpomodoro", - "version": "6.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "Watch4-Watch4", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/95/33/5f/95335f94-26d3-3567-93ac-77d60ab821dd/pr_source.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/03/53/b9/0353b9b1-ef2a-7ff1-a1b8-4124867af41b/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/ef/63/ce/ef63ce41-371a-2508-b101-fb99e9c7758f/pr_source.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/19/bb/4319bb4b-5700-0f6b-2c19-7bd386bf186c/pr_source.jpg/552x414bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/5d/51/1a/5d511a30-7fab-fd18-6967-c0caf9674d55/pr_source.jpg/552x414bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/60x60bb.jpg", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/512x512bb.jpg", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/kevin-reutter/id1273424431?uo=4", - "isGameCenterEnabled": true, - "advisories": [], - "features": [ - "gameCenter", - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "13.0", - "trackName": "Planny 3 - Smart To Do List", - "trackId": 1289070327, - "sellerName": "Kevin Reutter", - "releaseNotes": "Stay tuned! Planny 4 ships in a few week and will be a free update with many great features!\n\n• SwiftUI \nNow Planny uses SwiftUI in some parts of the app. SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Over time more and more of the app will be created with SwiftUI to avoid crashes and improve performance. \n\n• Advanced Cursor Support\nWhen using a Trackpad on iPadOS or on the Mac, specific Elements become larger when you come closer to make clicking easier\n\n• Alternative App icons\nChoose the icon color you’d like in settings (iOS for iPhone only)\n\n• New Onboarding Experience\nA new tutorial shows the key features \n\n• New Purchase View\nThe purchase view is now much simpler. Feel free to subscribe :) \n\n• Fixed deadlines on macOS\n• Direct Deadlines now support days and time \n• Fixed issues with overdue tasks \n\nDo you have any wishes for Planny 4? Feel free to submit ideas on the website!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-10-13T19:16:40Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-30T17:37:31Z", - "trackCensoredName": "Planny 3 - Smart To Do List", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "RU", - "ZH", - "ES", - "TR" - ], - "fileSizeBytes": "47687680", - "sellerUrl": "https://www.kevinreutter.de/planny-3/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.3897300000000001318767317570745944976806640625, - "userRatingCountForCurrentVersion": 331, - "averageUserRating": 4.3897300000000001318767317570745944976806640625, - "trackViewUrl": "https://apps.apple.com/us/app/planny-3-smart-to-do-list/id1289070327?uo=4", - "trackContentRating": "4+", - "description": "++ Planny was part of Apples favorite Apps from October ++\n\nPlanny is all new and has been rethought from the ground up.\n\nPlanny is your new friend helping you to be more productive. Planny learned everything important from common to do list apps but combines them with intelligence and gamification. In the morning and during the day Planny intelligently recommends tasks and also reminds you if you tend to forget them. You earn productivity points for adding and completing tasks, and also lose them if you shift tasks or forget them. Users can compare their productivity with friends over the week. \n\nPlanny also features all the important features like deadlines, lists / projects, tagging, location based reminders, notes and attachments, routines and more. \n\nKey features\n• Daily list to focus on today's tasks\n• Assistant for creating a productive daily plan\n• Daily review of the last day\n• Routines to train your habits\n• Deadlines and reminders\n• Smart reminders if you tend to forget your tasks\n• Notes for your tasks\n• Weekly productivity ranking of your contacts\n• Rewards\n• Dark mode\n• Lists\n• Siri support\n• Advanced Apple Watch app\n\nPlanny Premium offers additional features like:\n• Calendar view\n• Teamwork with your friends\n• Add Photos from your library to tasks\n• Add Photos from your camera to tasks\n• Location based reminders\n• iCloud sync\n• iCloud backup \n• FaceID Unlock\n• More than 2 lists\n• Printing\n• Sketches\n• Review your recent days\n• Tagging\n\n+++ Planny Premium - Unlock all features and use Planny on iPhone, iPad and Apple Watch (Mac soon) - And get free feature updates over time! +++ \n\nA Planny Premium subscription unlocks all features. Note that iCloud features require an iCloud-Account. \n\nPlanny offers two auto-renewing subscriptions\n\nPremium 3 Months\n$6,99 / 3 Months (may differ in your country & currency)\n\nPremium Annual\n$19,99 / Year (may differ in your country & currency)\n\nPayment will be charged to iTunes Account at confirmation of purchase\nSubscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period\nAccount will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal\n\nSubscriptions may be managed by the user and auto-renewal may be turned off by going to the user's Account Settings after purchase\n\nWhen your subscription is cancelled and expires, all the features of Planny Pro won't be available any longer. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.\n\nPrivacy policy for Planny: http://kevinreutter.de/privacy\nTerms of use / Conditions: http://kevinreutter.de/privacy", - "currency": "USD", - "artistId": 1273424431, - "artistName": "Kevin Reutter", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.kevinreutter.Callisto", - "version": "3.4.2", - "wrapperType": "software", - "userRatingCount": 331 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/4f/ff/f9/4ffff968-2932-48af-431f-fd1b086026cf/mzl.srudbvwp.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/2a/4f/9f/2a4f9fad-c1a7-fa80-56d7-fea2d3beaa0a/mzl.dcyubghz.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple6/v4/56/3a/a7/563aa771-8288-e21a-cfe8-e28e77ffad83/mzl.lzjpfyct.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/1e/8d/4e/1e8d4eaa-17f7-5295-1f36-975b62164d19/mzl.yufjavxy.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/fb/9b/85/fb9b851d-6ef5-792f-0945-ff2f1a78ce7a/mzl.gycjiioz.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6.6", - "trackName": "To-do Lists", - "trackId": 416993121, - "sellerName": "Mykola Olshevskyi", - "releaseNotes": "fixed accidentally broken compatibility for Mac OS 10.6-10.7", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-03-01T03:09:22Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$4.99", - "currentVersionReleaseDate": "2015-04-16T19:07:37Z", - "trackCensoredName": "To-do Lists", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "RU", - "UK" - ], - "fileSizeBytes": "2095731", - "sellerUrl": "http://www.antlogic.com/apps/todo-lists/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/to-do-lists/id416993121?mt=12&uo=4", - "trackContentRating": "4+", - "description": "To-do Lists provides simple but powerful interface for tasks management.\n\nTo-do Lists features:\n- Quick, one-click tasks addition/removal.\n- Rich-text editing, in-text links support.\n- Seamless iCloud Reminders synchronization.\n- DropBox synchronization between computers and To-do Lists Mobile for iOS\n- Import/export of to-do lists via text files.\n- Printing of to-do lists or mailing them directly from the application.\n- Backup and restore of whole to-do database.\n- Full drag'n'drop support (make new to-do from web link, file, document, e-mail, or any other text by simply dropping them on to-do list).\n- System services support (make new to-do from any text in any application).\n- Rolled-up, translucent or floating to-do lists.\n- Customized background color, text color, font and checkbox appearance.\n- Reminders.\n- Quick-access icon in system menu.\n\nTo-do Lists usage video:\nhttp://youtu.be/5KB-4sYcelo (or http://youtube.com/AntlogicCompany )\n\nFor more information, visit our site at http://www.antlogic.com/\nor Facebook page:\nhttp://facebook.com/AntlogicCompany\n\nIf you have any problems or questions using To-do Lists - visit our support forums at http://www.antlogic.com/forum/", - "currency": "USD", - "artistId": 364746702, - "artistName": "AntLogic", - "genres": [ - "Productivity", - "Business" - ], - "price": 4.99, - "bundleId": "ua.com.AntLogic.ToDoLists", - "version": "1.7.7", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/a8/e9/42/a8e942ec-8eea-03b3-ea37-cc6e2837fb5e/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/5f/62/5c/5f625c38-c559-3b8d-5042-94e241735ef1/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/aa/a1/e7/aaa1e746-e660-2b6e-6833-d751e7879752/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c0/6f/7d/c06f7de4-17b9-7475-8778-22a97c13cdce/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/84/3d/8d/843d8de0-6257-7bf0-6a66-6f3ce41af803/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/ad/0d/28/ad0d28c6-ff1d-c394-266a-fdff0b8e9cc6/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/36/28/a3/3628a3e9-6073-0ce4-17d7-9d9a5f479c64/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a9/ac/b9/a9acb983-76e2-d76d-fbc8-c35388dcee48/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/8f/41/90/8f4190f5-ebb8-370b-c8a6-afb47c56140d/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/09/37/83/09378396-de11-2185-24f4-360be20dbcac/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/the-omni-group/id281731738?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "OmniFocus 3", - "trackId": 1346203938, - "sellerName": "The Omni Group", - "releaseNotes": "OmniFocus 3.9.2 is a minor update focused on bug fixes.\n\n• Omni Automation — OmniFocus now recognizes simple plug-ins that use the .omnifocusjs file extention.\n• First Run — Improved reliability of the first run flow.\n• Notice Bar — Fixed bugs related to the Trial Mode & Free Viewer notice bars.\n\nIf you have any feedback or questions, we’d love to hear from you! The Omni Group offers free tech support; you can email omnifocus@omnigroup.com, call 1–800–315–6664 or 1–206–523–4152, or tweet @OmniFocus.\n\nIf OmniFocus empowers you, we would appreciate an App Store review. Your review will help other people find OmniFocus and make them more productive too.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-09-24T12:28:36Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-27T17:54:49Z", - "trackCensoredName": "OmniFocus 3", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "64931473", - "sellerUrl": "https://www.omnigroup.com/omnifocus/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/omnifocus-3/id1346203938?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Two-week free trial! OmniFocus Standard and Pro are in-app purchases, with discounts for people who bought earlier versions of OmniFocus for Mac through the Mac App Store. Or you can get OmniFocus for iOS, Mac, and web for just one price with the OmniFocus Subscription. Download the app for details.\n\nUse OmniFocus to accomplish more every day. Create projects and tasks, organize them with tags, focus on what you can do right now — and get stuff done.\n\nOmniFocus — now celebrating 10 years as the trusted, gold-standard to-do list app — brings unrivaled power and flexibility to your Mac, making it easy to work the way you want to work.\n\nOmniFocus manages everything in your busy life. Use projects to organize tasks naturally, and then add tags to organize across projects. Easily enter tasks when you’re on the go, and process them when you have time. Tap the Forecast view — which shows both tasks and calendar events — to get a handle on your day. Use the Review perspective to keep your projects and tasks on track.\n\nThen let our free syncing system make sure your data is the same on every Mac. (And on OmniFocus for iOS and Web, available separately.) Because your data is encrypted, it’s safe in the cloud.\n\nSTANDARD FEATURES (VIA IN-APP PURCHASE)\n\n• NEW: Tags add a powerful additional organizing tool. Create tags for people, energy levels, priorities, locations, and more.\n• NEW: The Forecast view shows your tasks and calendar events in order, so you can better see what’s coming up in your day.\n• NEW: Enhanced repeating tasks are easier than ever to set up — and they work with real-world examples such as the first weekday of the month.\n• NEW: The Modern, fresh-but-familiar design helps you focus on your content.\n• Inbox is where you quickly add tasks — save them when you think of them, and organize them later.\n• Syncing supports end-to-end encryption so that your data is safe wherever it’s stored, on our server or yours.\n• Notes can be attached to your tasks, so you have all the information you need.\n• Attachments — graphics, video, audio, whatever you want — add richness to your tasks.\n• View Options let you customize each perspective by deciding what it should show and how it should filter your tasks.\n• The Review perspective takes you through your projects and tasks — so you stay on track.\n• OmniFocus Mail Drop adds tasks via email and works with services like IFTTT and Zapier (if you’re using our free syncing server).\n• The Today Widget shows you your most important items — you don’t even have to switch to the app to know what’s up.\n• Support for TaskPaper Text and omnifocus:///add and /paste lets you automate using URLs.\n\nPro features make OmniFocus even more powerful:\n\nPRO FEATURES (VIA IN-APP PURCHASE)\n\n• Custom perspectives help you create new ways to see your data by filtering and grouping projects and tags. NEW: The filtering rules are simpler to use while being more powerful than ever, letting you combine rules with “all,” “any,” and “none.” You can also choose any image to use as your custom perspective’s icon, and a custom tint color to go with it.\n• NEW: Today’s Forecast can include items with a specific tag, and you can reorder those tasks however you choose, so you can plan your day better.\n• The customizable sidebar lets you organize your perspectives the way you want to, for super-fast access.\n• The Today Widget shows a perspective of your choice in Notification Center.\n• AppleScript support opens up a world of automation, using Apple’s Mac scripting language.\n\nDownload OmniFocus right now and start your free trial! The app includes a manual, and there’s plenty more documentation on the website.\n\nSUPPORT\n\nIf you have feedback or questions, our Support Humans would love to hear from you! Send email to omnifocus@omnigroup.com, call us at at 1-800-315-6664 or +1-206-523-4152, or reach us on Twitter at @omnifocus.\n\n\nSubscription Terms of Service: https://www.omnigroup.com/legal", - "currency": "USD", - "artistId": 281731738, - "artistName": "The Omni Group", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.omnigroup.OmniFocus3.MacAppStore", - "version": "3.9.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/72/1d/1a721d98-fbc4-ed9e-2aae-ef9d5b538693/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a2/b1/10/a2b110fd-aa90-286a-658b-2abd85bd1c68/mzl.menowpkq.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/26/f3/32/26f3322f-8ef9-6171-2864-715f571300e6/mzl.qxibkqwt.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a4/1b/12/a41b12dc-6e1d-74ff-7ad3-1d6888c31462/mzl.fdlcjqnh.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/83/83/24/83832412-85d1-78ff-a9ab-86a37b31121d/mzl.ateekpxr.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/e6/7c/c7/e67cc703-d274-a1fc-88af-b5a8ce9cbfd8/mzl.grtjmgef.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/18/a8/f2/18a8f211-61b5-7b7e-b343-784b260de31d/mzl.ulsghntx.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/masterbuilders/id896347016?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "Focus - Time Management", - "trackId": 777233759, - "sellerName": "Masterbuilders", - "releaseNotes": "Subscription status is now properly unlocked on all devices.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2013-12-19T19:16:50Z", - "genreIds": [ - "6007", - "6017" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-02-12T20:37:50Z", - "trackCensoredName": "Focus - Time Management", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "JA", - "ZH", - "ES" - ], - "fileSizeBytes": "24637530", - "sellerUrl": "https://www.focusapp.io", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/focus-time-management/id777233759?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Meet Focus: the best time manager for iPhone, iPad, Apple Watch and Mac. Focus is the most elegant and professional way to get more wore done, working in highly efficient work sessions, one task at a time.\n\n“[…] a tool that can genuinely make people more productive\" – MacStories.net\n\n“[…] a must-have for anyone who finds themselves easily getting distracted or forgetting to take occasional breaks.\" – iDownloadBlog.com\n\n======================\nFEATURES\n======================\n\nFOCUS SESSIONS\nFocus Sessions are a highly efficient way to work. Focus for 25 minutes, then take a short break to relax your mind. After four sessions, take a 15 to 20 minute break. This method maximizes energy, stimulates creativity and promotes a sense of achievement.\n\nTASK MANAGER\nFocus includes a lightweight task manager that lets you organize the things you want to work on intuitively. By working on one task at a time, you won’t be distracted and can focus all your attention towards completing that goal. That way you’ll be perfectly organized on your path to success.\n\nIN-DEPTH STATISTICS\nCheck what you’ve already done! Focus keeps track of your work and offers in-depth and motivating statistics. See your daily, weekly and monthly activity so you don’t lose sight of the big picture. \n\nFOCUS EVERYWHERE\nSeamlessly use Focus on your Mac, iPad, iPhone, and Apple Watch. Sync across your devices using iCloud; use Handoff to pick up your current work on another device and get up-to-the-second data with iCloud Push. You can also use the Today widget to quickly glance at your progress, import tasks using the handy Action extension, and more.\n\nFOCUS & APPLE WATCH: A PERFECT FIT\nUsing Focus on your wrist is a natural fit. The independent Apple Watch app is made for for easy and lightweight interactions that lets you control sessions and track your progress throughout the day. With the Focus complication, you can customize your watch face to see your current progress at a glance.\n\nBEAUTIFUL INTERFACE\nThe name says it all: Focus draws your attention to the most important things. It’s designed to be unobtrusive, accessible and easy-to-use. You’ll intuitively master its collection of features just by using them.\n\n======================\nSUBSCRIPTION PRICING\n======================\n\nFocus offers two subscription options: \nFocus Monthly at $4.99/ month \nFocus Yearly at $39.99/ year\n\nThe subscription unlocks all features on all devices (Mac, iPhone, iPad and Apple Watch).\n\nTRY IT FREE \nFocus Monthly comes with a 3-day free trial period, Focus Yearly with a 7-day free trial period. If you cancel before the end of the trial, you will not be charged for the subscription.\n\nSUBSCRIPTION TERMS\nPayment will be charged to your Apple ID account at the confirmation of purchase or after the free trial period if offered. \n\nYou subscription will automatically renew unless it is canceled at least 24 hours before the end of the current period. Your account will be charged 24 hours prior to the end of the current period. \n\nYou can manage and cancel your subscriptions by going to your account settings in the App Store after purchase. Any unused portion of a free trial will be forfeited when you purchase a subscription\n\n======================\nCONTACT\n======================\n\nIf you have any questions or ideas, please write us at hello@masterbuilders.io\n\nTwitter: @focusappio\nhttps://www.masterbuilders.io\n\n\n\nPrivacy Policy: https://www.masterbuilders.io/privacy\nTerms of Service: https://www.masterbuilders.io/terms", - "currency": "USD", - "artistId": 896347016, - "artistName": "Masterbuilders", - "genres": [ - "Productivity", - "Education" - ], - "price": 0.00, - "bundleId": "com.malteundjan.focus-osx", - "version": "6.2.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/35/03/5a/35035a62-e2da-2f4b-6ece-63475bd7cd02/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/b5/ac/1f/b5ac1fe2-431d-e45d-63e0-57ddbfbd525f/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/6c/18/ee/6c18eeff-ca66-2b01-82f3-f81576336ab7/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/niklas-behrens/id969210609?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "1Focus: Website & App Blocker", - "trackId": 969210610, - "sellerName": "Niklas Behrens", - "releaseNotes": "- Allows updating 1Focus while blocking is active\n- Fixed toolbar overflow on macOS High Sierra\n- Improved status item width\n- Fixed quick start menu starting wrong task\n- Other bug fixes", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-03-15T05:54:46Z", - "genreIds": [ - "6007", - "6017" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-18T23:51:25Z", - "trackCensoredName": "1Focus: Website & App Blocker", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "JA", - "KO", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "8338821", - "sellerUrl": "https://onefocusapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/1focus-website-app-blocker/id969210610?mt=12&uo=4", - "trackContentRating": "4+", - "description": "1Focus creates an oasis for focused work by disabling access to specific websites and apps. Use it to schedule a bit of automated self-restraint when you find yourself clicking away from what really needs to get done. Ideal for students, freelancers and writers.\n\n\"If you find yourself on Facebook or checking your email every five minutes, you need 1Focus.\" – Pagoda Technologies\n\n\"1Focus is one of the best apps for tuning out the diversions that are most distracting for you.\" – Tyler Horvath, CEO of Tyton Media\n\n\nFREE FEATURES\n\n• Block websites in Google Chrome, Safari, Opera, Microsoft Edge and Brave\n• Block apps (e.g. email, games)\n• Block internet access by blocking web browsers\n• You cannot cancel active blocks once you close the 1Focus window\n• Create your own task presets (up to 2)\n• Dark Mode\n\n\n1FOCUS PRO\n\n• Schedule recurring block events (e.g. Mon - Fri)\n• Work break timer\n• Unlimited task presets\n• Block all websites/apps except specific ones\n• Suspend blocking for a limited time\n• Block URL keywords (e.g. *gaming*)\n• Block popular websites by category (e.g. Social Media)\n\nTry it free for 14 days. $1.99/month or $9.99/year after.\n\nPrices may vary by location. Subscriptions are charged to your iTunes Account. They automatically renew unless you cancel them in your Account Settings at least 24 hours before the end of the current period. Your Account is charged for renewal within 24 hours prior to the end of the current period. Terms of use: https://onefocusapp.com/terms\n\n\nCUSTOMER SUPPORT\n\nDo you have any questions or suggestions?\nonefocusapp.com/support", - "currency": "USD", - "artistId": 969210609, - "artistName": "Niklas Behrens", - "genres": [ - "Productivity", - "Education" - ], - "price": 0.00, - "bundleId": "com.onefocusapp.OneFocus", - "version": "3.4.4", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/5f/6c/af/5f6cafb3-8f9d-5165-285e-b3da4d640d58/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/9e/15/3a/9e153a86-f869-6972-5144-9c45c699678a/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/52/bd/4b/52bd4b45-da16-b678-19e1-76ff109252b4/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/ec/24/3f/ec243f38-e6a5-88ed-ca7e-e503042e2888/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a8/a8/a8/a8a8a8be-4d88-e2aa-8683-fcfc2c6c8e63/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/9d/07/bf/9d07bf3a-c2f0-f3ef-083e-1745e69c8274/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/9d/07/bf/9d07bf3a-c2f0-f3ef-083e-1745e69c8274/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/9d/07/bf/9d07bf3a-c2f0-f3ef-083e-1745e69c8274/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/nozbe-com/id303308791?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "Nozbe: Productive team", - "trackId": 508957583, - "sellerName": "apivision.com", - "releaseNotes": "• Fixed a problem with stats for completed tasks", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-05-23T07:00:00Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-02-03T09:32:43Z", - "trackCensoredName": "Nozbe: Productive team", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PL", - "PT", - "RU", - "ZH", - "ES", - "ZH", - "TR" - ], - "fileSizeBytes": "9348964", - "sellerUrl": "https://nozbe.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/nozbe-productive-team/id508957583?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Nozbe – Collaborate with your team efficiently and get everything done!\n\nKEY NOZBE FEATURES: FOR TEAMS AND INDIVIDUALS\n\nAn invaluable project management app for teams\n\n• Collaborate to get your work done – quickly create and share projects with your team \n• Communicate smoothly and ditch chaotic e-mails – use task comments to exchange information with your colleagues and customers, and have all the info required in one safe system\n• Stay up-to-date – set up reminders and productivity reports\n• Move your projects forward – delegate tasks, and use deadlines and handy integrations\n• Organize your team's work – using a flat structure, task categories and project labels\n• Access your team projects from anywhere – install Nozbe on your desktop and mobile devices\n• Make sure your data is safe – all Nozbe connections are secure and encrypted using SSL\n• Integrations – Dropbox, Box, One Drive, Google Drive, Evernote, Google Calendar\n\nPersonal to-do list and task manager\n\n• Easily add 'to-do's and tasks on the go – with Siri, through e-mail and more\n• Access your tasks from anywhere – install Nozbe on all your devices, including Apple Watch\n• Manage your priorities – add deadlines, time needed and categories\n• Run your projects efficiently – keep all tasks, comments and files in one place, manage projects, and use labels\n• Never miss a thing thanks to reminders and calendar integrations\n• Use templates and repeat tasks to automate things and save your time\n• Work offline and have your data synchronized once you're back online\n\nJoin over 500,000 professionals and their teams from all over the world in using Nozbe and boost your efficiency. Use Nozbe's flexibility to create your own productivity system. Get your projects done on time and stay organized – alone and with your team.\n\nNOZBE Subscriptions (monthly or yearly):\n\n• NOZBE FREE – The full version of the app with up to 5 active projects and max. 100MB of data. Available after 30 days of the Nozbe Trial. Exclusively for single-user accounts.\n• NOZBE SOLO/DUO - For busy professionals, unlimited projects, shared projects, unlimited storage.\n• NOZBE SMALL BUSINESS - For small growing teams, unlimited projects, shared projects, unlimited storage.\n• NOZBE BUSINESS - For growing teams and businesses, additional shared projects features, more comprehensive productivity reports, dedicated premium support.\n\nYour payment will be charged to your iTunes Account once you confirm your purchase. Your iTunes account will be charged again when your subscription automatically, renews at the end of your current subscription period unless auto-renew is turned off at least 24 hours prior to the end of the current period. You can manage or turn off auto-renew in your Apple ID Account Settings at any time after purchase.\n\nTerms and Privacy policy: nozbe.com/terms", - "currency": "USD", - "artistId": 303308791, - "artistName": "Nozbe.com", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.nozbe.mac", - "version": "3.13", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/cd/0a/55/cd0a552e-00e7-a1ef-b9be-8cf55a1f22bd/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/da/df/a2/dadfa2c7-67de-d41b-c862-c860be7ec80b/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple128/v4/89/89/3b/89893baf-cb17-bc37-da1b-4acaac52313e/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/ac/71/9f/ac719fb2-2e24-c746-3e3c-e1990ae45430/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/cf/c9/6f/cfc96f41-5169-ba4e-7c91-d306a7affdbf/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple128/v4/ec/cf/bf/eccfbf43-32ed-9df3-5f59-251a326d6522/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/71/73/d27173b8-0b26-2ea4-b072-a45a4403d331/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/71/73/d27173b8-0b26-2ea4-b072-a45a4403d331/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/71/73/d27173b8-0b26-2ea4-b072-a45a4403d331/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ind-ie/id1042780453?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.11", - "trackName": "Better Blocker", - "trackId": 1121192229, - "sellerName": "Article 12", - "releaseNotes": "# Good news!\n\nIn the last update, we informed you that that version would be the last update to Better under the Ind.ie account as we are moving to Small Technology Foundation. Since then, Apple has stepped in and answered our call for help so we can keep updating the app from our current account.\n\n## What this means for you:\n\n - Everything will continue as before, you don’t need to do anything.\n - This app will continue to get updates.\n - At some point, you will see our organisation name change from Ind.ie (Article 12) to Small Technology Foundation.\n\nLaura and I would like to take this opportunity to thank all of you who expressed your support during this period and donated to our not for profit to help us during this difficult time.", - "primaryGenreId": 6002, - "primaryGenreName": "Utilities", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-09-22T02:55:17Z", - "genreIds": [ - "6002", - "6007" - ], - "formattedPrice": "$1.99", - "currentVersionReleaseDate": "2020-01-20T21:31:19Z", - "trackCensoredName": "Better Blocker", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "10318394", - "sellerUrl": "https://better.fyi", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/better-blocker/id1121192229?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Better is a privacy tool for Safari that protects you from trackers and privacy-eroding ads on the web.\n\nMake your web experience safer, lighter, and faster in Safari on Mac. \n\nHow is Better different?\n• Blocks tracking scripts, tracking pixels, and behavioural advertising.\n• Uses our unique, hand-curated, and open tracker database based on our own web crawls. Browse the encyclopaedia-like tracker entries on our web site if you fancy learning more. \n• We identify and block the most prevalent trackers. This makes Better’s block list lightweight and effective.\n• We base our blocking decisions on the principles of the Ethical Design Manifesto (https://ind.ie/ethical-design). \n• Developed and curated by Ind.ie – that’s Laura and Aral – a tiny two-person-and-one-husky not-for-profit striving for social justice in the digital age.\n• Open, transparent, and free as in freedom.\n\nOther features:\n• Improves the readability of popular web sites. \n• Spotlights the worst offenders… Did you know that the worst site we found on the web had 172 trackers? Better blocks them all and makes that site 4× lighter and 15.8× faster!\n• Blocks blocker blockers (try saying that quickly a few times!) Whenever we can, we try to protect you from sites that try to put you at risk by forcing you to turn off your privacy tools.\n• Your personal Do Not Block list enables you to turn Better off for sites that don’t play well with Better. Easily contact us about those sites so we can fix the experience for everyone.\n\n5 star App Store reviews from around the world:\n• “Best blocker I've tried yet: Clear and straight forward business model plus actually works. Feel safe blocking trackers and knowing the developers won't sell you out. Can't beat that.” (USA)\n• “This app does everything it promises to do and sets a new gold standard in content blockers.” (Belgium)\n• “This piece of software is just THE most essential thing to stay sane on today’s world wide web.” (Germany)\n• “Thanks for such an incredibly simple app to make the web just that bit better!” (UK)\n• “I love better, I use it on my phone and I’m very happy to see it on the Mac!” (Germany)\n• “Does exactly what it advertises, constantly updated, and I really support the mission of this company.” (USA)\n• “Fantastic blocker with a rational, ethical approach to privacy and monetization in this digital age.” (USA)\n• “Honest privacy: Thanks for defending the public against surveillance capitalism!” (USA)\n• “Buying this not only benefits you, it actively supports the ethos of internet privacy.” (USA)\n• “Fantastic: The best adblocker I have tried.” (USA)\n• “Best Adblocker out there: Unobtrusive. Works fast & reliable” (Germany)\n• “It just works!” (Germany)\n• “A fantastic approach to ad blocking & preservation of user privacy” (Australia)\n• “Thanks to these two brave people who made this app. It gives me at least some peace of mind about the nasty surveillance practises of the big tech companies.” (Netherlands)\n• “Better web: Fixes so many sites without you even noticing it.” (Finland)\n• “Best ad & tracking blocker-type app I've used on the appstore!” (Norway)\n• “Great app: Does exactly as described, works it’s magic in the background whilst I’m browsing the web.” (Sweden)\n• “Speed and privacy: Easy to install and does exactly what it’s supposed to: frees your browsing of megabytes of privacy-invading tracking. Even if you don’t mind unknown third parties building up and selling profiles of your browsing behaviour (and everything that can be inferred form it), the speed increases of both download and rendering by removing those trackers really surprised me. It’s simply made browsing the web far quicker and is worth the money for that alone.” (UK)\n• “It's good to see such nice people stand up to tracking and malvertising. This app does everything it promises to do and sets a new gold standard in content blockers. Spend a few bucks to take back the web from disrespectful business models. You won't regret it.” (Belgium)", - "currency": "USD", - "artistId": 1042780453, - "artistName": "Ind.ie", - "genres": [ - "Utilities", - "Productivity" - ], - "price": 1.99, - "bundleId": "better.fyi.mac", - "version": "2020.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple5/v4/e4/4a/75/e44a75a7-624c-d60b-0e40-3f0e655da61a/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/34/9e/03/349e0304-90fc-3838-9cec-6c706efc92ea/mzl.zwihxdxb.tif/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/1f/23/2e/1f232eaf-95d1-e5af-1f9e-2d7c4061bb55/mzl.qvfjfjne.tif/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/8d/6c/0b/8d6c0b27-fc9e-c290-1329-7e2049f34412/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/ce/62/8f/ce628f72-a55d-cbca-5544-a327e12762be/mzl.pqwcptst.tif/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/68/d7/4e/68d74e3e-919e-bf82-6c4f-b57ec0ea30e9/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/68/d7/4e/68d74e3e-919e-bf82-6c4f-b57ec0ea30e9/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/68/d7/4e/68d74e3e-919e-bf82-6c4f-b57ec0ea30e9/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/delullo-software-llc/id409729333?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "All Things Money Lite", - "trackId": 892274130, - "sellerName": "DeLullo Software, LLC", - "releaseNotes": "The All Things Money Lite 1.4.0 update improves functionality and stability, and is recommended for all users.\n\nNew features include:\n\n- Enabled QFX files with transactions to be imported.\n- When importing a file, if transaction categories are not specified then they will be automatically set to the last category used by the payee.\n- Added stock market technical indicators for Aroon, Money Flow, Relative Strength, and Fast Stochastic.\n- Added the ability to simultaneously view weekly and daily stock technicals.\n\nStability improvements include:\n\n- Added support for importing files with international character encodings.\n- Fixed two bugs that affected printing functionality.\n- Fixed QIF exports, which incorrectly had a percent symbol in the date field.\n- Fixed international currency formatting of the payment and deposit columns.\n\nFor detailed release notes, please visit: http://www.delullosoftware.com/apps/AllThingsMoney/ReleaseNotes.pdf", - "primaryGenreId": 6015, - "primaryGenreName": "Finance", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-07-07T07:00:00Z", - "genreIds": [ - "6015", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2015-09-15T12:55:11Z", - "trackCensoredName": "All Things Money Lite", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "1358452", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/all-things-money-lite/id892274130?mt=12&uo=4", - "trackContentRating": "4+", - "description": "All Things Money (ATM) was created with the mindset that good finance software doesn’t need to be expensive, to contain advertisements, or to log into your bank account. It should be comprehensive and provide interconnected services. It should also be efficient by supporting multiple input formats, auto-completing certain fields, and automatically logging recurring transactions. ATM delivers on these goals and more.\n\nIf you are curious, please give it a try. Download All Things Money Lite for free to determine whether ATM is a good fit for you. When you want to upgrade, use the export and import menu options to transfer data between apps.\n\nATM capabilities include the following.\n\nAccount Dashboard\n- The innovative layout gives perspective to all of your liquid assets.\n- Track bank, credit card, stock market, and U.S. savings bond accounts.\n- Calculate net worth.\n- Sort transactions.\n- Split transactions.\n- Add memos to transactions.\n- Import bank, credit card, and stock market transactions from QIF and CSV files.\n- Import bank and credit card transactions from OFX files. (At this time, stock market transactions are not supported via OFX imports.)\n- Import U.S. savings bonds from treasurydirect.gov HTML files.\n- ATM auto-fills payee names and categories.\n- The bank-account paradigm enables you to earmark money.\n\nBill Planner\n- Track payments and deposits.\n- Track automatic stock market trades that are typical of 401k-type accounts.\n- Elect to automatically move up bill dates to avoid weekends and U.S. holidays. \n- ATM supports the following payment intervals: daily, weekly, bi-weekly, semi-monthly, monthly, bi-monthly, quarterly, once every four months, semi-annually, annually, bi-annually.\n\nStay on Budget\n- Create a budget based on transactions from the prior year.\n- Update budget goals based on your expected bills and deposits.\n- Update actuals from account transactions.\n- ATM supports annual, monthly, and custom-period budgets.\n- ATM provides color-coded alerts to track spending excess.\n\nPlan for Retirement\n- Seed the retirement calculator with your account information.\n- Calculate how much money you will have when you retire.\n- Calculate the annuity that your retirement will generate.\n- ATM also includes a general-purpose, compound-interest calculator that has six unique equations and two combination options for expediency. This calculator enables reverse computations; it calculates how much money you need to retire based on your desired annuity, interest rate, and life expectancy.\n\nMonitor the Stock Market\n- Create a watch list that saves stock symbols and time intervals.\n- Display a stock’s price and volume alongside technical indicators like Bollinger Bands and Moving Average Convergence Divergence (MACD).\n- ATM supports daily and weekly stock charts.\n\nMortgage Analysis\n- Create an amortization table and track your mortgage balance as you pay down principal. \n- Compare the costs of buying and renting.\n\nInventory Tracking\n- Keep track of your collections.\n\nCreate Reports\n- Generate plots of net worth and stock prices.\n\nLite Version Restrictions\n- One bank\n- Two accounts\n- Three bills\n- Two stock watch list entries\n- Two inventory categories with three items each\n- Two retirement calculations per session\n- One compound-interest calculation per session\n- One amortization calculation per session\n\nMinimum requirements: screen resolution of 1280 x 800 or higher.", - "currency": "USD", - "artistId": 409729333, - "artistName": "DeLullo Software, LLC", - "genres": [ - "Finance", - "Business" - ], - "price": 0.00, - "bundleId": "com.delullosoftware.AllThingsMoneyLite", - "version": "1.4.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/36/fe/ff/36feffbc-a07b-e61e-f0e5-88dcc4455871/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/c6/85/09/c68509b2-c2c8-3000-bf85-4ead056b26f3/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/18/42/aa/1842aab5-0500-b08b-b9a5-fc364f83fbdb/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/de/b9/99/deb99962-f1d0-a7ad-0fc8-ef4bf906515b/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/41/70/7d/41707d88-8ba1-5a28-1f2f-0f2e43a73706/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/be/a3/a2/bea3a233-d82f-34bf-b0cd-38f262b04939/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e5/41/b4/e541b49d-06ed-9ec6-1544-3df88c8dc340/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/8f/08/49/8f0849f4-7d20-567f-47e6-ef1bfb901619/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/7d/74/8a/7d748af9-50fa-e009-39a8-b5eb7774b2be/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/b9/74/d4b974d7-0c4c-1515-49ec-ecedec84c5a0/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/b9/74/d4b974d7-0c4c-1515-49ec-ecedec84c5a0/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d4/b9/74/d4b974d7-0c4c-1515-49ec-ecedec84c5a0/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/tinybop-inc/id682046582?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15.0", - "trackName": "Things That Go Bump", - "trackId": 1472954003, - "sellerName": "Tinybop Inc.", - "releaseNotes": "* BOOM *, this is a BIG update. The house spawns a game room, complete with video games you can ENTER INTO. It's fun and a little bit weird! Try it! \n»-(¯`·.·´¯)->", - "primaryGenreId": 6014, - "primaryGenreName": "Games", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-10-18T07:00:00Z", - "genreIds": [ - "6014", - "7001", - "7009" - ], - "currentVersionReleaseDate": "2020-03-18T17:39:23Z", - "trackCensoredName": "Things That Go Bump", - "languageCodesISO2A": [ - "EN" - ], - "sellerUrl": "http://tinybop.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/things-that-go-bump/id1472954003?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Have you ever heard something go bump in the night? \nPerhaps you’ve caught wind of a spirit or sprite. \nWhen the house is asleep,\nand there’s dark all around, \nspirits from objects awake and abound. \n\nThe spirits are crafty and like to cause trouble. \nThey're called yōkai and together, they double. \nMixing and mashing, they join to fight. \nCan you help them conquer this mysterious night? \n\nPlay with one person, two, three or four. \nFirst you’ll need to escape the dark junk drawer. \n\n. . . . . . . . . . . . . . . . . . . .\nIn Things That go Bump, familiar objects and rooms come to life every night, and nothing looks quite as does in the day. Create your creature, and battle your friends, but beware the house spirits! They can destroy and they can give life. Battle, create, and make your way through the rooms of the house, and slowly you will unravel the secret of Things that Go Bump. \n\nFeatures:\n * Spirits wake up objects and create yōkai (spirit creatures)\n * Combine everyday objects like umbrellas, staplers, cheese graters and more to create everchanging characters \n * Connect to other players via Game Center and face-off against other spirit creatures and house spirits\n * Add or swap objects to give your spirit creature new skills\n * Gain energy by making mischief, defeating other yōkai, and conquering the house spirits\n * Advance through the house (new rooms will be added throughout the year)\n * Test your curiosity and creativity with new challenges in every room\n * Play with 1-4 players across iPads, iPhones, iPods, AppleTVs and Macs\n * Fun and challenging for the whole family\n * Intuitive, safe, hilarious kid-friendly design\n * New levels introduced roughly every 2 months\n * Original artwork by Adrian Fernandez\n * Original sound design\n\nTinybop, Inc. is a Brooklyn-based studio of designers, engineers, and artists. We make toys for tomorrow. We’re all over the internet.\n\n Visit us: www.tinybop.com\n Follow us: twitter.com/tinybop\n Like us: facebook.com/tinybop\n Peek behind the scenes: instagram.com/tinybop\n\nWe love hearing your stories! If you have ideas, or something isn’t working as you expect it to, please contact us: support@tinybop.com.\n\nPsst! It's not Tiny Bop, or Tiny Bob, or Tiny Pop. It's Tinybop. :)", - "currency": "USD", - "artistId": 682046582, - "artistName": "Tinybop Inc.", - "genres": [ - "Games", - "Action", - "Family" - ], - "bundleId": "uikitformac.com.tinybop.thingamabops", - "version": "1.3.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/8d/13/b2/8d13b22b-e96a-a0dd-219c-892ec2ba6f60/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/bf/53/6a/bf536a44-dd1e-43c3-89d0-5fc1928001e8/mzl.lphzucrn.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/7e/09/9c/7e099c9b-f7c8-3079-f1a0-a0fac192807b/mzl.fpmmmnpy.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/16/a9/6c/16a96c08-ad0d-fd57-4b66-baeb37cd02a2/mzl.jaithmaa.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/0e/13/90/0e139010-77ef-9d9e-2171-1d146d4467ad/mzl.kquqgdzh.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/36/07/3f/36073f03-7e1e-208d-35c4-9b1e3f57029a/mzl.skrcrjvg.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/3d/06/42/3d0642af-9593-5788-5f31-4a8433e6eb97/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/3d/06/42/3d0642af-9593-5788-5f31-4a8433e6eb97/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/3d/06/42/3d0642af-9593-5788-5f31-4a8433e6eb97/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/adolfo-vera-blasco/id898601649?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "All Things Done", - "trackId": 1062002496, - "sellerName": "Adolfo Vera Blasco", - "releaseNotes": "New year, new name\n\nSay hello to All Thing Done", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-12-17T01:13:07Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "$2.99", - "currentVersionReleaseDate": "2019-03-25T05:37:45Z", - "trackCensoredName": "All Things Done", - "languageCodesISO2A": [ - "EN", - "ZH", - "ES" - ], - "fileSizeBytes": "8243793", - "sellerUrl": "http://tomates.desappstre.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/all-things-done/id1062002496?mt=12&uo=4", - "trackContentRating": "4+", - "description": "All Things Done helps you to improve your productivity using one of the most effective management method in personal or pair working environments.\n\nThe app is highly configurable in aspects like time for breaks or tasks, notifications, task series, all of it in a beautiful and detailed interface.\n\nWe say Hi! to Touch Bar\nProud to announce All Things Done adds support to Touch Bar on the new MacBook Pro.\n\nInternational.\nSince version 5 we speak english and Español también.\n\nInside All Things Done you will find...\n+ iCloud sync with all your devices\n+ Theme support\n+ Reports to compare your productivity\n+ Beautiful graphs to check your progress in a blink of an eye\n+ Customize task time\n+ Customize short and long break time\n+ Choose to play a sound or not when a break or a task has finished\n+ Set how many tasks are a Work Serie\n+ The task timer can be paused/resumed/reseted\n+ Session and Goal counters can be reseted", - "currency": "USD", - "artistId": 898601649, - "artistName": "Adolfo Vera Blasco", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 2.99, - "bundleId": "com.desappstre.Pomodoro", - "version": "10", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/b3/3f/0eb33fce-c85e-01ca-93ad-951ad273c1d9/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/6e/6e/84/6e6e8457-d30d-8055-cd0b-3064bbdba0cf/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/78/e7/e4/78e7e488-2a0a-8a13-75cd-6dac68e52e3e/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e2/29/99/e2299950-e368-2b21-3de9-6ef021488150/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d7/50/0f/d7500f21-56b5-9884-3f4b-018b17bddc85/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/44/72/40/44724058-efac-9d28-950c-e546dfc772be/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/2c/c0/05/2cc0056c-97cf-3cf8-f596-0ca192744dd4/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/96/9c/3b/969c3b88-3180-6daf-7766-9c3b626a8ca8/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/41/7b/68/417b68f8-e692-dd05-e8e4-032aa225d43a/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ee/09/11/ee091117-a13d-9517-b03a-a2ffc62b8f7b/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ee/09/11/ee091117-a13d-9517-b03a-a2ffc62b8f7b/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/ee/09/11/ee091117-a13d-9517-b03a-a2ffc62b8f7b/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/eduard-metzger/id1137020763?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "NotePlan 2", - "trackId": 1137020764, - "sellerName": "Eduard Metzger", - "releaseNotes": "- Fixed sidebar crash.", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-10-18T11:04:27Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-07-22T23:44:00Z", - "trackCensoredName": "NotePlan 2", - "languageCodesISO2A": [ - "EN", - "DE" - ], - "fileSizeBytes": "15096159", - "sellerUrl": "http://noteplan.co", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/noteplan-2/id1137020764?mt=12&uo=4", - "trackContentRating": "4+", - "description": "NotePlan is the All in One Digital Planner to help you organize your life, your work, and everything in between. You can manage your calendar, notes, and tasks all in one intelligent interface on both Mac and iOS (sold separately). Try NotePlan for free for 14 days, no payment info required. \n\nINCREDIBLE FEATURES\n\n- Integrates with your iCloud Calendars (supporting Google Calendar, Exchange, Yahoo, and more.)\n- Dynamic tasks allow you to schedule tasks, see upcoming, overdue, and unscheduled.\n- Syncs with your Apple Reminders, use Siri on the go to create reminders in NotePlan.\n- Powerful calendar views to see events, reminders, and tasks in one place.\n- Organize thoughts in Nested Tags\n- Easily Add (and remove) image attachments to your notes.\n- Syncs your notes via iCloud Drive to all your Apple devices (this is your data, not ours!).\n- Global search across calendar events, reminders, tasks, and notes.\n- Multiple dark and light themes to suit your style. \n- Works offline, no internet required (sync does require connection).\n- Built from day one to support Markdown editing within notes!\n\n\nQUOTES FROM OUR AMAZING CUSTOMERS\n\n- “To keep up with all the stuff I have to do, I have tried most to-do apps. The simplicity of NotePlan’s approach makes it the winner in this category for me.”\n- “The new design and layout is awesome. I’m a big fan of NotePlan and this makes information even more accessible and logically laid out.”\n- “This is a fantastic app that solves a lot of pain points for me. You can start small and gather complexity at your own pace.”\n- “Finally, an app that does exactly what I want.”\n\n\nDownload and try today for 14 days, no signup required, no payment info required. NotePlan is a one time purchase (no subscription) if you like the app!\n\nHERE IS THE BACKSTORY\n\nIf you’re like us, work life and personal life is starting to merge. Managing meetings, notes, next steps, projects, tasks, reminders in multiple apps, sites, or tools - can be pretty hard - but it doesn’t have to be! With NotePlan for the Mac and iOS, you can manage your calendar, notes, and tasks in one place. Instead of constantly context switching, you can stay focused and accomplish what you set your mind to.\n\nWe recommend starting by getting everything out of your head and captured in one place - such as a “Planning” note. After gathering your thoughts, NotePlan allows you to seamlessly translate items into tasks. Set dates to these tasks, such as “tomorrow” or a specific day of the week. \n\nBy integrating with Apple’s Calendar and Reminders, NotePlan can show you your meetings and events next to the tasks you set. Weekly or Monthly calendar view gives you a quick snapshot of what is scheduled, and where you may have gaps in your day so you can quickly schedule additional things to accomplish. \n \nReviewing and managing open tasks and deadlines is incredibly easy. On top of that, notes are perfect for brainstorming, saving links, taking meeting notes, or storing reference materials. NotePlan is highly-customizable too with handsome light and dark themes as well as tagging for organizing and wiki-style linking between notes.\n\n\nQUESTIONS?\n\nIf you have any questions, suggestions or problems, please contact us. We provide fast and professional support: hello@noteplan.co", - "currency": "USD", - "artistId": 1137020763, - "artistName": "Eduard Metzger", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "co.noteplan.NotePlan", - "version": "2.4.6", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/9f/ec/16/9fec16c9-a8e4-109f-68da-bcdbdbac0f02/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/18/81/9f/18819f96-b13a-5194-db9f-998ed8831875/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/61/27/25/6127254c-5515-2d34-20cb-ec4c0f778053/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/61/27/25/6127254c-5515-2d34-20cb-ec4c0f778053/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple7/v4/61/27/25/6127254c-5515-2d34-20cb-ec4c0f778053/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/adam-mathes/id453295387?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "DoOneThing - Single Task Manager", - "trackId": 464910442, - "sellerName": "Adam Mathes", - "releaseNotes": "Bug fixes, dark mode support, icon change", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-09-16T18:48:52Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2015-07-22T03:11:21Z", - "trackCensoredName": "DoOneThing - Single Task Manager", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "834152", - "sellerUrl": "http://mermodynamics.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/doonething-single-task-manager/id464910442?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Instead of being a source of constant distraction, your computer should help you accomplish what's most important to you every day.\n\nIt's easy to spend hours sitting in front of a computer immersed in doing little things, while avoiding the most important one.\n\nDoOneThing won't nag you, or beep at you, or harass you.\n\nIts presence at the top of your screen in a place of primacy, near the time, is intended to bring your focus back to what is most important for the day, to make your primary goal present in your computer screen and life.", - "currency": "USD", - "artistId": 453295387, - "artistName": "Adam Mathes", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.mermodynamics.DoOneThing", - "version": "2.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/9a/f3/34/9af334d9-953f-52b6-2943-5e7105edd267/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/7a/73/fc/7a73fc94-5100-48f6-947a-a9d487e73b50/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/d3/88/f0/d388f0a9-8b1a-761b-8c95-d56985c422bd/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/85/a6/cf/85a6cfd6-6c45-e5f1-5189-cff16a2796a8/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/ac/f1/45/acf1450b-9380-b2b9-db1c-770f2ed81f76/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/1f/bb/de/1fbbde79-a7d4-e630-a51b-b389fceffd51/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/f0/9d/d2f09d29-df16-296c-d0a3-02586ca581fa/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/f0/9d/d2f09d29-df16-296c-d0a3-02586ca581fa/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/d2/f0/9d/d2f09d29-df16-296c-d0a3-02586ca581fa/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/busy-apps-fze/id409966805?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.11", - "trackName": "BusyCal", - "trackId": 1173663647, - "sellerName": "Busy Apps FZE", - "releaseNotes": "* NEW: Accessibility option added to Appearance Preferences for turning on darker grid lines\n* NEW: Optionally switch between Apple Contacts or BusyContacts for Birthdays / Anniversaries from under General Preferences\n* NEW: URL links, inside of notes displayed in the menu bar helper app, are now clickable\n* NEW: Clickable and selectable links / urls / locations in events displayed in the Menu app\n* Improved email address validation checks when dragging and dropping from Apple Contacts\n* Sound alarm option disabled for Google Calendar as these are not supported by Google and would get silently ignored\n* Attendee email parsing improved for emails containing apostrophes\n* Fixed a crash experienced by some users when importing large .ics files\n* Fixed a bug where manually selecting \"email attendees\" would open multiple copies of the compose email sheet\n* Fixed duplicate set of snooze options displaying when using Notification Center alerts\n* Fixed a macOS 10.11 issue with NotificationCenter and alerts\n* Ongoing stability and performance improvements", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-11-21T02:20:39Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2020-06-26T02:53:21Z", - "trackCensoredName": "BusyCal", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "ZH", - "ES" - ], - "fileSizeBytes": "24084319", - "sellerUrl": "https://www.busymac.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/busycal/id1173663647?mt=12&uo=4", - "trackContentRating": "4+", - "description": "BusyCal 3 is the most powerful, flexible, reliable calendar app for macOS. It's packed with innovative, time-saving features including customizable views, calendar sets, integrated to dos, travel time, smart filters, natural language input, weather, moons, graphics, tags, do not disturb mode and much more.\n\nCUSTOMIZABLE VIEWS\n\nBusyCal displays your calendar in Day, Week, Month, Year and List views. What makes BusyCal unique is its ability to customize these views to more precisely meet your needs. You can choose the number of weeks shown per month, or days shown per week, and you can even customize the calendar's appearance by choosing a font face and size, calendar colors, time format and more.\n\nINTEGRATED TO DOS\n\nIn BusyCal, To Dos are integrated into your calendar, display on the date or time they are due, and carry forward until completed. You can also display a To Do List in the sidebar. NOTE: Reminders in iOS 13 / macOS 10.15 is not compatible with BusyCal.\n\nINFO PANEL\n\nBusyCal's info panel enables you to quickly view and edit event details with speed and precision. The info panel can be displayed in the sidebar, as a popup or a floating window. And it's completely customizable, you can choose from a wide range of attributes to display including time zones, tags, maps, private notes, last edit time and more.\n\nNATURAL LANGUAGE INPUT\n\nBusyCal enables you to create events and to dos using natural language. A preview of the event details are displayed while you type as it recognizes titles, dates and times, locations and more. You can even use it to add contacts or attendees to an event, set alarms, add URLs, and to indicate the calendar to create the event on.\n\nTRAVEL TIME\n\nBusyCal allows you to block out time for walking, driving or taking mass transit to an event or location. You can set a fixed amount of travel time or determine it automatically using the integrated support for Location Services and Apple Maps. You can even receive alerts when it's time to leave as traffic conditions change.\n\nMENU BAR APP\n\nThe BusyCal menu bar app is always running, even when the main BusyCal app is not running, so you always have access to your schedule. \n\nSMART FILTERS\n\nBusyCal's Smart Filters are a powerful tool for managing your calendar. Smart Filters can be accessed with a keyboard shortcut or a button on the toolbar to display calendar sets (showing/hiding multiple calendars), perform saved searches (events that contain 'Joe'), apply view settings (an 8-week month view) and much more.\n\nALARMS\n\nBusyCal displays alarms in a movable, resizable floating window that offers the ability to snooze an alarm for any number of minutes from now or before the start of an event, or snooze multiple alarms at once. And BusyCal Alarms trigger even when the main BusyCal app isn't running, so you'll never miss an important appointment.\n\nWEATHER & MOONS\n\nBusyCal displays a live 10-day weather forecast, phases of the moon, and sunrise and sunset times. \n\nGRAPHICS\n\nBusyCal lets you add graphics to your calendar to highlight holidays and special events. You can choose from the built-in Emoji and IconFinder images, or drag images from your desktop or web.\n\nBUSYCONTACTS INTEGRATION\n\nBusyCal integrates with its sister app, BusyContacts, forming a flexible easy-to-use CRM solution. By adding contacts to events in BusyCal, you have instant access to a contact's email address and phone number, as well as a record in BusyContacts of your interactions with them.\n\nSYNC AND SHARE CALENDARS\n\nBusyCal supports iCloud, Reminders, Google, Exchange, Office 365, CalDAV, LAN sharing as well as WebDAV subscriptions, enabling you to sync calendars with other Macs and iOS devices running BusyCal or the built-in Calendar app. This includes the ability to share calendars, schedule meetings, and view the availability of others.", - "currency": "USD", - "artistId": 409966805, - "artistName": "Busy Apps FZE", - "genres": [ - "Productivity", - "Business" - ], - "price": 49.99, - "bundleId": "com.busymac.busycal3", - "version": "3.10.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/b1/75/14/b17514b7-25fd-2aa6-b147-a1ede3bc2952/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/d0/0c/d6/d00cd62c-9865-6602-3f1d-e7a50e62067a/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/f3/f0/39/f3f03912-aa89-8b53-81b0-8faa70b5611f/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/1d/3b/78/1d3b780c-d23a-78de-d6fc-d03985d772dd/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/9f/7a/e3/9f7ae3e3-40d1-6880-c27b-6e293cd39656/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/5f/87/ad5f872c-f86c-26b1-db29-ce1c9d9286ab/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/5f/87/ad5f872c-f86c-26b1-db29-ce1c9d9286ab/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/ad/5f/87/ad5f872c-f86c-26b1-db29-ce1c9d9286ab/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/massimo-moiso/id566833668?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "InerziaThings Lite", - "trackId": 642103566, - "sellerName": "Massimo Moiso", - "releaseNotes": "New\n- added a \"Measure\" field. It can contain dimensions info as \"12x3 inch\" or similar. You can add it also to already present objects.\n\nBug fixes\n- Some locale string was missed.\n- Fixed a bug when importing from proprietary files on OS 10.10 through 10.13\n- Some optimisation.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2013-05-07T17:54:45Z", - "genreIds": [ - "6007", - "6006" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-04T18:49:46Z", - "trackCensoredName": "InerziaThings Lite", - "languageCodesISO2A": [ - "EN", - "FR", - "IT" - ], - "fileSizeBytes": "6157827", - "sellerUrl": "https://inerziasoft.eu/products/showcase/inerziathings/0", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/inerziathings-lite/id642103566?mt=12&uo=4", - "trackContentRating": "4+", - "description": "If you need assistance, please refer to our Technical Support Team by clicking the link on the right of this page.\nIf you have an idea for a new feature that could be useful for you, please contact us and we will be glad to consider it!\n\n===\n\nInerziaThings is your personal archive to keep all your Things under control.\n\nWhen did I purchase my new Mac? How much did I pay my last car? When the warranty of the washing machine will expire? I would like to read the technical data of the audio amplifier without having to remove the whole rack...\n\nInerziaThings can answer to all of these questions!\n\nKey features:\n\n• Management of all the important data (name, brand, model, serial number, warranty expiring date, shop, the date we sold it at what price, etc….)\n\n• Formatted free text notes.\n\n• Tags management (name change, one or more tag per Thing, automatic tag creation, auto-complete while typing).\n\n• Insert and manage images linked to a Thing: import, export, delete, open in default application.\n\n• Assign a Thing to a room within a place; a place can have more than one room. A Thing can be assigned to nowhere (not related to a room).\n\n• Search by name, brand, model, text within notes\n\n• Filter Things on place, room, tags.\n\n• Searching and filtering are one another compatible (that is: I want to find all objects in a room of a defined brand and with certain tags).\n\n• Automatic calculation of the total value of the visible objects\n\n• Unified management of more than one identical Things and their partial selling in one record\n\n• Complete Undo support.\n\n• Export Things in a text file (comma or tab delimited, ready to be imported in a Office suite).\n\n• Export all Things data, with Tag and Images in a proprietary format for backup or transport purposes\n\n• Objects multi-page print.\n\n• Dark theme support\n\n• New exciting features to come!\n\n\nNOTE: This Lite version is limited with regards to the number of Places, Room and Things.", - "currency": "USD", - "artistId": 566833668, - "artistName": "Massimo Moiso", - "genres": [ - "Productivity", - "Reference" - ], - "price": 0.00, - "bundleId": "eu.inerziasoft.InerziaThings-Lite", - "version": "3.7.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/b5/d0/e0/b5d0e04e-380a-19e7-e1ea-42d9090306a2/22518e83-8e14-495b-b780-843bb635a853_Desktop_01.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/99/03/09/990309a8-f057-8553-aaf6-9289dfa6cdec/ad0253ed-cdb3-4202-9afd-656f9b574d7d_Desktop_02.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/13/3a/5f/133a5f6c-d302-9af7-0859-d82f9cee2330/5b80f3aa-8722-414c-a621-58a97cf82103_Desktop_03.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/05/aa/6b/05aa6b80-9dbb-4968-9444-874cb9583fc7/de77889b-3f4d-438b-adb8-4c6bb715d6df_Desktop_04.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/86/c3/d7/86c3d72b-6e29-bc68-729c-7e0bc29290cb/668c81b6-a7db-402d-a036-e31bb109ffc7_Desktop_05.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/17/b8/c3/17b8c3bc-ab5b-6532-e59e-c804f70f40ad/f802848c-264f-4cbb-a322-a699dd0b69c2_Desktop_06.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/82/7b/5a/827b5acc-ea24-4ffc-ad2d-f008131dbedc/9d573032-de53-4221-b1d9-9195c845d6c9_Desktop_07.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e9/3d/96/e93d964a-d5c2-9020-c2cc-f15dd5a2d309/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e9/3d/96/e93d964a-d5c2-9020-c2cc-f15dd5a2d309/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/e9/3d/96/e93d964a-d5c2-9020-c2cc-f15dd5a2d309/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ciarlo-software-llc/id1066322955?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.14", - "trackName": "Doo 3: Get Things Done", - "trackId": 1515371334, - "sellerName": "Ciarlo Software, LLC", - "releaseNotes": "Fixes a bug that prevented scrolling of tasks", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2020-08-25T07:00:00Z", - "genreIds": [ - "6007" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2020-08-29T18:17:38Z", - "trackCensoredName": "Doo 3: Get Things Done", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "PT", - "ZH", - "ES", - "ZH" - ], - "fileSizeBytes": "3021396", - "sellerUrl": "https://www.getdooapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/doo-3-get-things-done/id1515371334?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Doo is a simple way to be more productive. Create a card for each of your tasks. The best cards are easy to start and have a clear goal. Pick a few tasks each day and snooze the rest. Doo helps you build consistent, sustainable habits that keep you focused. Over time, your completed cards become finished projects, without the stress and anxiety of an endless list.\n\nFEATURE HIGHLIGHTS\n• Be more productive with a unique, card-based interface\n• Separate cards into custom groups\n• Create open-ended, calendar, or location-based tasks\n• Create tasks within other apps with the share extension\n• Schedule tasks with custom intervals and alerts\n• Sync with iCloud across iOS and Mac devices\n• Collaborate on tasks with other Doo users (requires iCloud)\n• View upcoming tasks with the Doo Widget\n• Complete or snooze tasks from notifications\n• Run Doo from the dock or menu bar", - "currency": "USD", - "artistId": 1066322955, - "artistName": "Ciarlo Software, LLC", - "genres": [ - "Productivity" - ], - "price": 9.99, - "bundleId": "com.mciarlo.Everyminder.CiarloMacDoo3", - "version": "3.0.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple71/v4/af/ee/c4/afeec4b1-a276-3845-148d-78a0862331a5/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple62/v4/59/9f/22/599f22d1-b0c5-d6b0-8c67-7e59609d9a68/pr_source.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple41/v4/3b/5b/db/3b5bdbe0-ee09-7933-e6f1-24ee0d6a0f55/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/68/10/9c/68109c13-9add-40b9-5c7e-1bc2b4a1898c/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/ca/3b/1aca3b35-1082-082d-0a14-ba9643e4c683/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/ca/3b/1aca3b35-1082-082d-0a14-ba9643e4c683/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/ca/3b/1aca3b35-1082-082d-0a14-ba9643e4c683/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/new-technologies/id762050392?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "Oh! My Mind Mapping: Idea Flow", - "trackId": 1140048806, - "sellerName": "New Technologies, LLC.", - "releaseNotes": "This update \n• Includes bug fixes and feature enhancements.\n• Fixes several stability issues.\nThanks to everyone for your support! It really helps us out!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-09-03T19:18:06Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-14T07:52:27Z", - "trackCensoredName": "Oh! My Mind Mapping: Idea Flow", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "13457808", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/oh-my-mind-mapping-idea-flow/id1140048806?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Pick all your ideas together and create a clear mind map. Visual diagrams will help you keep in mind any flow of information! \n\nDIFFERENT PRE-INSTALLED TYPES OF VISUALIZATION\n- One-sided map;\n- Tree structured;\n- Fish-shaped;\n- Sun-shaped.\n\nSMART REPRESENTATION\n- Different layout structures vary depending on the purpose;\n- 3 elements to work with: arrows, stickers and images;\n- Applying images or icons to stickers;\n- Opportunity to interconnect stickers.\n\nVISUAL BRAINSTORMING RESULTS\n- Instant Saving & Sharing;\n- You can also print out your projects.\n\nWith user-friendly control and understandable functions of Oh! My Mind Mapping 2 the brainstorming process becomes easy and convenient!\n\nOur app offers subscription:\nhttps://newtech-ltd.com/privacy\nhttps://newtech-ltd.com/tos", - "currency": "USD", - "artistId": 762050392, - "artistName": "New Technologies", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.newtechnologies.OhMyMindMapping2lessia", - "version": "1.7.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple4/v4/98/2f/a7/982fa7f4-3dbe-7039-d2a0-b788eff5aaa0/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple4/v4/4b/23/ea/4b23eab3-063f-6ed0-03d1-0ea71f779bb6/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/5d/cb/a3/5dcba3bd-eb2e-3cc2-5c13-307fd6203b2c/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple2/v4/e2/2d/ca/e22dcadd-d7d5-3150-03c1-9f4a43c3b0f4/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/d2/f4/4c/d2f44c49-12e3-7bc7-e382-0cd9ec74d6fe/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/d2/f4/4c/d2f44c49-12e3-7bc7-e382-0cd9ec74d6fe/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/d2/f4/4c/d2f44c49-12e3-7bc7-e382-0cd9ec74d6fe/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "Antnotes", - "trackId": 808734429, - "sellerName": "Mykola Olshevskyi", - "releaseNotes": "- added option to disable gradient background\n- added option to create new notes in bottom left/right corners\n- changed delay for close/options buttons showing\n- some minor compatibility and UI fixes\n- fixed German localisation", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-03-08T02:52:03Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "$2.99", - "currentVersionReleaseDate": "2016-09-24T17:04:28Z", - "trackCensoredName": "Antnotes", - "languageCodesISO2A": [ - "EN", - "DE", - "RU", - "UK" - ], - "fileSizeBytes": "1014384", - "sellerUrl": "https://www.antlogic.com/apps/antnotes", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/antnotes/id808734429?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Antnotes are like paper notes: they are glued to your monitor, but from the other side of the screen.\n\nThis nice and handy application lives in the menu bar for faster access and has the following features:\n\n- customizable background, font and text color\n- snap to screen bounds and other notes\n- translucent notes\n- attach note to any application so it will be shown when this application is activated/hidden when switching to other application\n- automatically hide notes when inactive\n- pin note to desktop to make it stay atop of other windows\n- quick access via the menu bar icon\n- make new notes by dragging text, images, files to menu bar icon\n- integration with services: create new note from any text in any application\n- drag images and sounds to note contents\n- configurable global shortcuts to create new note or show/hide all notes\n- resizable\n- archive with all closed notes - do not lose your information by accidentally closing a note\n- smart position choosing for different display configurations\n\nWant more features? Just let us know, we'll consider almost anything unless it’s cooking, coffee making or walking your dog!\n\nVideo on how to use Antnotes: http://youtu.be/E7wKKeGZDFw ( or http://www.youtube.com/AntlogicCompany )\nFor more information visit our site: http://www.antlogic.com/ or Facebook page: http://facebook.com/AntlogicCompany\n\nIf you need support, have feature request or any complaints, you are welcome to our support forums: http://www.antlogic.com/forum/", - "currency": "USD", - "artistId": 364746702, - "artistName": "AntLogic", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 2.99, - "bundleId": "ua.com.AntLogic.Antnotes", - "version": "1.6.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple6/v4/47/e2/1a/47e21af8-c05e-57d5-98a4-3a596c897646/mzl.hflpafaz.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/e1/40/16/e1401647-5ee0-4819-20cd-2a4b6188b38c/mzl.mobmauoe.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple5/v4/3e/14/4e/3e144eb6-08f4-eb44-9465-c6cad39ef787/mzl.tnfvxdpb.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/43/b9/13/43b91374-695d-9ead-f634-2b7d545ca6bf/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/43/b9/13/43b91374-695d-9ead-f634-2b7d545ca6bf/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple49/v4/43/b9/13/43b91374-695d-9ead-f634-2b7d545ca6bf/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/appigo/id282769260?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.8", - "trackName": "Todo", - "trackId": 408975584, - "sellerName": "Appigo, Inc.", - "releaseNotes": "Easier to share lists with co-workers, and edit list information.\nBug fixes.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-06-03T01:41:59Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$14.99", - "currentVersionReleaseDate": "2016-01-14T21:39:56Z", - "trackCensoredName": "Todo", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "PT", - "RU", - "ZH", - "ES", - "ZH" - ], - "fileSizeBytes": "8661615", - "sellerUrl": "http://www.appigo.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/todo/id408975584?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Todo® empowers millions of people around the world to get things done. Todo’s beautiful and intuitive interface enables you to easily create and share tasks, checklists and projects. Whether you are managing a complex work project with a team, planning a family reunion or simply sharing a shopping list, Todo has what you need.\n\n\nKEY FEATURES\n· Automatically sync tasks across the computers and devices you use\n· Share Lists: Create lists and share them with your co-workers, family and friends\n· Collaboration: Add comments to tasks that will be seen by everyone in your shared task list\n· Stay Informed: Receive email notifications when someone in your shared list updates a task\n· Simply reply to task emails and your comments will automatically be added to that task\n\nSO MUCH MORE…\n· Geofence your tasks with Todo and your iPhone alerts you when you arrive\n· Share Tasks: Delegate tasks and get things done as a team\n· Manage Advanced Projects: Use checklists inside of your projects for better organization\n· Keep Tasks Updated: Tasks are updated at lightning speed to each of your devices \n· Drag and drop content from emails to quickly create tasks\n· Multiple reminder alerts\n· Organize with projects and checklists (subtasks)\n· Schedule repeating tasks\n· Starred tasks\n· Drag and drop sorting\n· Full task searching including notes\n· GTD support with contexts and tags\n\nTODO SYNCS WITH \n· Todo-Cloud (premium paid sync service)\n· iCloud (Note: iCloud is used for synchronizing tasks to other copies of the Todo app on iOS and Mac).\n· Dropbox\n· Toodledo.com\n· Wi-Fi\n\nMaximize Todo’s power with a Todo-Cloud Premium Account. Download the Todo app and try Todo-Cloud syncing service with a free 14 day trial. Todo is also available for iPhone, iPad, and iPod touch on the App Store. Additionally, you can always access your tasks online at www.todo-cloud.com, even without a premium account.\n\n\nEXCELLENT CUSTOMER SUPPORT\n\nWe value our customers and strive to answer every question, however, we are unable to reply directly to app reviews. If you are looking for help or have a specific question, please send us a message by visiting our Help Center. If you love to get things done, feel free to help others understand how this app has helped you by writing a review on the App Store. \n\nAppigo Help Center: http://help.appigo.com/\n\n\nSTAY IN THE KNOW\n\nBe sure to follow us online! We frequently publish updates about what's new and what's coming in the app on Facebook and Twitter.\n\nhttp://twitter.com/appigo\nhttp://www.facebook.com/appigo", - "currency": "USD", - "artistId": 282769260, - "artistName": "Appigo", - "genres": [ - "Productivity", - "Business" - ], - "price": 14.99, - "bundleId": "com.appigo.todomac", - "version": "3.0.7", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/2b/3b/ae/2b3baea6-3b6a-5575-9201-7b1f9374b59e/25d73a4f-71f0-4651-9a8d-d05a1438c099_iPhone-8Plus-1-final.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/40/95/c3/4095c368-0233-609c-cfbf-f7b8e54b1680/d66420dd-510c-4b78-9c73-fbf6f1732c69_iPhone-8Plus-2-final_3.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/57/f2/57/57f257ec-f627-c968-70f4-c65d1c6f8f13/8caf2452-0f43-4631-9561-8742fc682b83_iPhone-8Plus-3-final.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/b6/f4/a1/b6f4a184-95bc-ecae-6d7e-46155694ca2b/52661be5-0db9-4231-90d1-0673bfe42bb7_iPhone-8Plus-4-final.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/81/9a/73/819a739d-a936-3c80-aa89-3393c5fc5835/7044a241-d38d-4da6-abca-03afc47d06dc_iPhone-8Plus-5-final.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/ec/6e/32/ec6e32ff-d2ee-e5c5-adda-0a93c256a749/146a5b09-1775-498d-b16a-48b05b6d52be_iPad-Pro-1-final.png/576x768bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/ce/f6/1c/cef61c51-bd09-0a33-5a10-f33ac328a772/c2057935-2438-4efc-9d85-f8d72756376e_iPad-Pro-2-final_3.png/576x768bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/4e/1b/88/4e1b88b2-6f91-5996-5aa3-52bbb8fd3a71/c49adc41-3b71-4301-933d-671384616ede_iPad-Pro-3-final.png/576x768bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/08/3a/99/083a9917-7ac1-2503-dc51-b1f84a1cba7f/54b5f7cd-d35b-4bfe-a300-5e8621085081_iPad-Pro-4-final.png/576x768bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/2b/9d/d8/2b9dd8fc-afa4-2194-37a0-6490cbe9db63/2ea461a8-57cd-4bf8-b6fe-aeef6e9914f0_iPad-Pro-5-final.png/576x768bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b7/fd/ca/b7fdcac2-0b2c-8346-d321-42cc61292205/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b7/fd/ca/b7fdcac2-0b2c-8346-d321-42cc61292205/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b7/fd/ca/b7fdcac2-0b2c-8346-d321-42cc61292205/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/volodymyr-yahenskyi/id961335645?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.0", - "trackName": "Tally Counter & Habit Tracker", - "trackId": 1412716242, - "sellerName": "Volodymyr Yahenskyi", - "releaseNotes": "Thanks for using my app!\nThis release contains bug fixes and performance improvements.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-09-08T04:54:49Z", - "genreIds": [ - "6007", - "7009", - "6014", - "7004" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-29T14:09:17Z", - "trackCensoredName": "Tally Counter & Habit Tracker", - "languageCodesISO2A": [ - "EN", - "RU", - "UK" - ], - "fileSizeBytes": "11422720", - "sellerUrl": "https://yahenskyi.dev/tally/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.89655000000000040216718844021670520305633544921875, - "userRatingCountForCurrentVersion": 29, - "averageUserRating": 4.89655000000000040216718844021670520305633544921875, - "trackViewUrl": "https://apps.apple.com/us/app/tally-counter-habit-tracker/id1412716242?uo=4", - "trackContentRating": "4+", - "description": "Tally Counter & Habit Tracker will help you to count items, days, score points of games, count drinks you've been drinking, count training sessions, or anything else.\n\nFeatures:\n• Simple interface\n• Dark mode\n• Counting history\n• Total counters value\n• Last change timer\n• Colorful app icons\n• Reorder counters using a long press gesture\n• Rename each counter as you want\n• Assign different colors to each counter\n• Sound confirmation of counts\n• Haptic feedback on supported devices\n• Select the step size for your count\n• Reset, delete and export all counter data at once\n\nMultiple counters, as many as you need. To delete, reset or edit the counter, just swipe left on it. To increase or decrease the counter value, tap at any point on the ​left or right side of it. Long press to reorder counters.\n\nTally Premium benefits:\n• iCloud sync\n• Unlimited counters\n• Export to CSV/Excel\n\nTerms & Conditions: https://yahenskyi.dev/terms-conditions/\nPrivacy Policy: https://yahenskyi.dev/privacy-policy/", - "currency": "USD", - "artistId": 961335645, - "artistName": "Volodymyr Yahenskyi", - "genres": [ - "Productivity", - "Family", - "Games", - "Board" - ], - "price": 0.00, - "bundleId": "com.yahenskyi.tally", - "version": "1.6.4", - "wrapperType": "software", - "userRatingCount": 29 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple18/v4/5e/ae/76/5eae7683-6038-ba05-121b-4b6d77bc6a55/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple20/v4/6f/2d/83/6f2d8388-00eb-cb2b-6d10-905d823021c0/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/14/09/5c/14095c40-9a33-3685-cd67-7e0c8d5f6421/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple60/v4/b6/43/26/b6432664-84ce-ce21-c592-f6d2b137abdf/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/9a/5e/439a5e44-c71e-44e0-7b2e-535a1a953dec/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/9a/5e/439a5e44-c71e-44e0-7b2e-535a1a953dec/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/9a/5e/439a5e44-c71e-44e0-7b2e-535a1a953dec/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ping-lv/id1436949017?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.7", - "trackName": "iLove Sticky Notes", - "trackId": 1096650860, - "sellerName": "Ping Lv", - "releaseNotes": "1 Some minor improvements", - "primaryGenreId": 6012, - "primaryGenreName": "Lifestyle", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-05-23T02:32:02Z", - "genreIds": [ - "6012", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-05T19:39:54Z", - "trackCensoredName": "iLove Sticky Notes", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "988745", - "sellerUrl": "http://ilovemacapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/ilove-sticky-notes/id1096650860?mt=12&uo=4", - "trackContentRating": "4+", - "description": "iLove Sticky Notes is a very useful memo assistant for Mac users. With iLove Sticky Notes you can pin notes to your desktop which can help remind you of something urgent or important! You can customize the background color of your notes and it always stay in sight so you won't forget them.\n\nHow to use:\n1. Open iLove Sticky Notes.\n2. Click on the app icon on system tray to go into Edit Mode. \n3. Click on the top left \"Add\" button of note to create a new one, hit the top right \"cross\" button to delete the note.\n4. Click on the app icon on system tray to Quit Edit Mode.", - "currency": "USD", - "artistId": 1436949017, - "artistName": "Ping Lv", - "genres": [ - "Lifestyle", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.ilove.desktopnotes", - "version": "2.1.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple125/v4/5d/cf/06/5dcf06fc-2767-3b5e-ce1a-ae71d65d7949/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/20/8a/d6/208ad6df-1f33-2ec5-fb54-33dc416bebb8/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/5c/5f/0e5c5fb1-24e6-2d77-4ba3-97c62a95014e/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/5c/5f/0e5c5fb1-24e6-2d77-4ba3-97c62a95014e/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/0e/5c/5f/0e5c5fb1-24e6-2d77-4ba3-97c62a95014e/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/fiplab-ltd/id320240050?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "TaskTab: Simple To Do List", - "trackId": 1395414535, - "sellerName": "FIPLAB Ltd", - "releaseNotes": "- Bug fixes", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-07-23T03:22:17Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2019-09-06T19:50:30Z", - "trackCensoredName": "TaskTab: Simple To Do List", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "2261951", - "sellerUrl": "https://fiplab.com/apps/task-tab-for-mac", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/tasktab-simple-to-do-list/id1395414535?mt=12&uo=4", - "trackContentRating": "4+", - "description": "TaskTab places the focus squarely on your to do list. We've intentionally kept the app as simple as possible to allow you to quickly add, check off and manage your tasks without being distracted by pointless features that overly complicate the key purpose of a to do list app. The app lives in your menubar and is available at a click of a button or via its customizable hot key.\n\n- A beautifully designed native app for macOS that supports both light and dark mode. \n\n- You can easily import a to do list as well as export them to share with others.\n\n- You can optionally choose to show the number of remaining items in your menubar.\n\nWe've worked hard to make TaskTab as powerful and efficient as possible for you to use. We would love to hear your thoughts via email and make any improvements to future versions of this app. We intend to have an active development cycle powered by your feedback and support!", - "currency": "USD", - "artistId": 320240050, - "artistName": "FIPLAB Ltd", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.fiplab.tasktabmac", - "version": "1.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/d9/b6/7a/d9b67a64-4d35-ab91-6d9a-f31050c96b29/x5ijyUu7fLDtHIabWVSOW0-temp-upload.nuezlxgp.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple/v4/24/a4/fc/24a4fc65-960e-8c82-c129-bf0ef04b0fc6/mza_7830980125492921569.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/fb/ed/cf/fbedcfe1-9b32-a73c-41b2-8a17f98e9f75/mza_6440831072027067066.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/e1/a1/d5/e1a1d549-8810-7bce-9bae-4cb7de91b02d/mza_4557184267443726008.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/56/38/4c/56384c59-f7b4-2d9d-4dce-edbd66d63737/mza_5206003488144523712.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/c2/58/79/c25879b3-7495-f5d7-b111-c6e922979fa2/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/c2/58/79/c25879b3-7495-f5d7-b111-c6e922979fa2/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple/v4/c2/58/79/c25879b3-7495-f5d7-b111-c6e922979fa2/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/midnight-beep-softworks/id364896535?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6.6", - "trackName": "Inbox Classic", - "trackId": 528008223, - "sellerName": "Midnight Beep Softworks", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-06-11T21:16:59Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2012-06-11T21:16:59Z", - "trackCensoredName": "Inbox Classic", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "8862330", - "sellerUrl": "http://www.midnightbeep.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/inbox-classic/id528008223?mt=12&uo=4", - "trackContentRating": "4+", - "description": "This is the original version of Midnight Inbox that brings Getting Things Done (GTD) productivity to the desktop in true delicious style, now updated for the latest versions of Mac OS X with native support for 10.6 and 10.7! And now available for free!\n\nWe have new versions of Inbox for the iPhone and iPad for you to check out, and are working on a whole new desktop version of Midnight Inbox. See what's new with Inbox on our website: http://www.midnightbeep.com\n\nThe workflow.\nTop to bottom, Inbox provides an automated workflow of simple input, intuitive organizing, and contextual outputs. Just jump right to the part that best fits your situation: from collecting your thoughts, to concentrating on your work.\n\nAutomatic zero.\nGetting to the bottom of your inbox is what this app is all about. As things pile up you need to get them sorted and acted upon; Inbox makes this quick and easy by giving you shortcuts to the places where things need to be, and the assistance to get them done.\n\nGTD complete.\nMidnight Inbox is inspired by the Getting Things Done methodologies created by David Allen. If you've never heard of GTD, just let Inbox be your guide. It will help you be stress-free, following the GTD workflow for you, so you can concentrate on what really matters.\n\nPLEASE NOTE:\n• Inbox Classic is -not- compatible with current iOS versions (Inbox Mobile for iPhone, Inbox Touch for iPad) for data or syncing. The forthcoming version \"Midnight Inbox 2\" will bring full feature parity and iCloud-based syncing to the desktop version of Inbox, and import of Classic version's data.\n• Support for Mac OS 10.4 and 10.5 and PowerPC-based Macs is available in version 1.5.1 of Inbox Classic from our Web site.", - "currency": "USD", - "artistId": 364896535, - "artistName": "Midnight Beep Softworks", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.midnightbeep.InboxClassic", - "version": "1.6.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/16/c9/0d/16c90d93-849c-f870-5eed-d92500ca8ab8/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/68/77/5b/68775b4c-55b6-2dae-b4f8-358515f1ecfe/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/1b/ca/ad/1bcaad31-57fb-52fe-5b8b-2693d59ed311/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/1b/ca/ad/1bcaad31-57fb-52fe-5b8b-2693d59ed311/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/1b/ca/ad/1bcaad31-57fb-52fe-5b8b-2693d59ed311/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/busy-apps-fze/id409966805?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.11", - "trackName": "BusyContacts", - "trackId": 964258399, - "sellerName": "Busy Apps FZE", - "releaseNotes": "* Fixed a bug where column sizes in List view would not correctly restore for some users\n* Fixed a bug where new tags for a new contact syncing to a Google account would not sync correctly\n* Fixed a bug where manually selecting \"email\" from the share menu would open multiple copies of the compose email sheet\n* Ongoing stability and performance improvements", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-03-06T23:53:17Z", - "genreIds": [ - "6000", - "6002" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2020-06-10T23:58:36Z", - "trackCensoredName": "BusyContacts", - "languageCodesISO2A": [ - "NL", - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "ZH", - "ES" - ], - "fileSizeBytes": "12809192", - "sellerUrl": "https://www.busymac.com/busycontacts/index.html", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/busycontacts/id964258399?mt=12&uo=4", - "trackContentRating": "4+", - "description": "BusyContacts is a contact manager for macOS that makes creating, finding, and managing contacts faster and more efficient.\n\nBusyContacts brings to contact management the same power, flexibility, and sharing capabilities that BusyCal users have enjoyed with their calendars. What's more, BusyContacts integrates seamlessly with BusyCal forming a flexible, easy to use CRM solution that works the way you do.\n\nBusyContacts syncs with the built-in Contacts app on macOS and iOS and supports all leading cloud services, including iCloud, Google, Exchange, Facebook & Twitter.\n\nCUSTOMIZABLE VIEWS\n\nContacts can be displayed in two views: A single column list view, or a multi-column table view that allows you to control the columns displayed (e.g. company, last name, first name, email, phone, etc.) and the sort order.\n\nTAGS\n\nTags are an extremely flexible way to manage contacts in BusyContacts. You can assign multiple tags to each contact and a tag cloud allows you to easily filter the list of contacts by tag (e.g. family, client, prospect, coworker, etc.).\n\nACTIVITY LIST\n\nThe Activity List shows a chronological listing of activities associated with the selected contact including meetings, to dos and other calendar events (requires BusyCal), communication through email and messaging, and social network posts. Note: Email communication is only available on macOS 10.14 and below.\n\nBUSYCAL INTEGRATION\n\nBusyContacts integrates with BusyCal allowing you to link contacts to events and to dos in your calendar, providing flexible CRM capabilities for scheduling meetings, follow up tasks, and tracking past activities.\n\nSOCIAL NETWORK INTEGRATION\n\nBusyContacts syncs with leading social networks including Facebook, Twitter and LinkedIn, allowing you to integrate photos, birthdays and other information from social networks with your contacts.\n\nSMART FILTERS\n\nSmart Filters are a powerful tool for filtering contacts and creating saved searches that can be applied with a single click. You can create Smart Filters to display contacts that match certain conditions, such as a text string, tag, or birthdate. Or you can create Smart Filters to remember view settings such as columns displayed and sort order.\n\nSYNC\n\nBusyContacts syncs with all leading cloud services including iCloud, Google, Exchange, and other CardDAV servers, and syncs with the built-in Contacts app on macOS and iOS.\n\nSHARE\n\nBusyContacts allows you to share address books with other BusyContacts users with read-only or read/write privileges. Address Books can be shared through Exchange, Fruux, Kerio, over the LAN, and through other CardDAV servers that support sharing.", - "currency": "USD", - "artistId": 409966805, - "artistName": "Busy Apps FZE", - "genres": [ - "Business", - "Utilities" - ], - "price": 49.99, - "bundleId": "com.busymac.busycontacts", - "version": "1.4.8", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple1/v4/19/ed/39/19ed390e-0d46-20a9-3a07-959f5da8fef2/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple1/v4/1a/ad/11/1aad1160-0e74-6135-14bf-1d43839ee5b8/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple5/v4/24/2a/a4/242aa49d-ca64-c44a-46bd-b826a2114b56/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/05/3e/45/053e45b0-97a9-69bd-eb2d-1f5de77e8b2a/mzl.vcqfhklr.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/13/71/b3/1371b391-df3e-0928-cc3b-241b74f052d5/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/13/71/b3/1371b391-df3e-0928-cc3b-241b74f052d5/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/13/71/b3/1371b391-df3e-0928-cc3b-241b74f052d5/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/whetstone-apps/id460983180?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "DayMap", - "trackId": 460983177, - "sellerName": "Whetstone Apps, LLC", - "releaseNotes": "Support for dark system appearance. (e.g. Dark Mode).", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-11-17T07:39:46Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "$12.99", - "currentVersionReleaseDate": "2019-09-04T22:42:44Z", - "trackCensoredName": "DayMap", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "ZH" - ], - "fileSizeBytes": "1988675", - "sellerUrl": "https://www.daymapapp.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/daymap/id460983177?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Have you ever been confused by your day planner? Do you think and plan visually? We created DayMap because we believe visual people deserve an organizer tailored to their strengths.\n\nDayMap is a visual planning app which helps you plan your days and weeks for optimum productivity. The project outliner places your projects side by side in columns so that you can see more information with less scrolling.\n\nThe calendar located below the project outliner makes it easy to schedule important todo’s.\n\n- Sync your DayMap data between your Mac and iPhone (or any iOS Device) thanks to integrated iCloud sync support! (Requires iOS 8 or later and Mac OS X Yosemite or later)\n- System-wide keyboard shortcut to quickly add new tasks to inbox without having to switch apps.\n- Create deep hierarchies of tasks with subtasks.\n- View all your projects and tasks at a glance.\n\nDayMap helps you make the most of every day, every week.\n\nWe are interested in hearing what you think, so share your feedback with us on our website.", - "currency": "USD", - "artistId": 460983180, - "artistName": "Whetstone Apps", - "genres": [ - "Productivity", - "Business" - ], - "price": 12.99, - "bundleId": "com.whetstoneapps.daymap", - "version": "2.1.5", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "Watch4-Watch4", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/4a/8b/b1/4a8bb1a9-54fc-7214-4b40-7bb92fc9f2bc/pr_source.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/ec/5b/41/ec5b4185-2f49-1bb3-1441-637595930db9/pr_source.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/18/3b/26/183b2664-2c17-1bac-1e8b-5759df726eeb/pr_source.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/21/ab/f2/21abf2ea-672e-517a-9941-bd8057196b77/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/7f/a0/03/7fa00345-db22-bf9b-31b2-4d22490ca1f6/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/4a/6d/ab/4a6dab7d-1162-0235-6012-207038d00244/pr_source.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/63/35/7b/63357bc9-3e1c-7758-83d2-f36bb05ac12c/pr_source.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/11/c4/a4/11c4a431-474c-1c52-93df-2a2b948bc4e7/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/79/9c/bd/799cbdc4-b67f-7d7f-e54f-0b2b24ac185a/pr_source.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/87/c4/f6/87c4f65a-62c1-ea9d-036a-e77cf4921423/pr_source.png/552x414bb.png" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/8f/d7/bc8fd793-aad2-b60b-d174-44cba7754342/source/60x60bb.jpg", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/8f/d7/bc8fd793-aad2-b60b-d174-44cba7754342/source/512x512bb.jpg", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/8f/d7/bc8fd793-aad2-b60b-d174-44cba7754342/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/shihab-mehboob/id1012903170?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "13.0", - "trackName": "Allegory", - "trackId": 1470828583, - "sellerName": "Shihab Mehboob", - "releaseNotes": "- Added a new default app icon\n- Added cursor support for iPad\n- Pulling down when viewing a full-screen list will now dismiss the list\n- Improved iCloud sync\n- Improved accessibility and contrast across the app\n- Improved keyboard shortcut commands\n- Added more alternative app icons\n- Fixed various issues where the app may crash\n- Minor UI changes\n- Various bug fixes and improvements\n\nIf you have any questions or feedback, please get in touch. Allegory was created by an incredibly small team of one and I'd love to hear your thoughts. I'm available through email: shihab@allegoryapp.info or Twitter: @AllegoryApp or @JPEGuin.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-09-19T07:00:00Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-05-29T17:39:24Z", - "trackCensoredName": "Allegory", - "languageCodesISO2A": [ - "AR", - "NL", - "EN", - "FR", - "DE", - "HE", - "IT", - "PL", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "21512192", - "sellerUrl": "https://www.allegoryapp.info/press", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.07017000000000006565414878423325717449188232421875, - "userRatingCountForCurrentVersion": 57, - "averageUserRating": 4.07017000000000006565414878423325717449188232421875, - "trackViewUrl": "https://apps.apple.com/us/app/allegory/id1470828583?uo=4", - "trackContentRating": "4+", - "description": "Allegory is a powerful notes app with a beautiful iOS-centric UI. The minimal and focused writing experience, combined with an impressive set of powerful features, makes this the ideal choice for everyone. Allegory removes the complexity of most markdown editors, presenting a minimal UI that’s welcoming for casual writers and note-takers as well as more attuned authors, making it a great app for you to pick up regardless of your background and skill level.\n\n—\n\nWHY YOU'LL LOVE ALLEGORY:\n\nAllegory immediately launches to the clean and simple note editor screen, boosting your productivity and reducing friction between wanting to write down what you’ve just thought of, and actually getting it written down. Support for valid markdown as well as custom markdown makes it easier to create your best work.\n\nAllegory also includes a brilliant hand-crafted drawing area which lets you select from a variety of colors, long-holding on any of the tools brings them into focus with sliders to adjust their opacity and stroke size with gorgeous animations that strike awe.\n\nYou can bookmark and set due dates on any note from anywhere in the app regardless of where you are, which makes performing essential actions entirely seamless, further reducing friction for users. Seamless iCloud sync adds upon the notion of keeping your notes exactly where they need to be. Notes can be searched for within the app in ways that make sense to you, by words and dates, or by using language that comes naturally such as ‘today’, ‘yesterday’, and ‘last week’. A summary of your current note also provides further context on your writing by displaying character/word/line/paragraph counts, sentiment analysis to portray how positive or negative your writing is, quick links to open the specific note, and more.\n\nInspiration can strike anywhere, and that is why Allegory supports custom action extensions that allow creating notes and putting down ideas from other apps. You can also use Siri to do the same. Scan documents and text to convert it to notes, or add images within notes to provide more context. When you want to share your work with the world, use one of the many export options to create files that work best for you, from TXT and MD, to HTML and PDF. Or simply export as an image or plain text. It's built in ways that make sense to your workflow.\n\nTweak and adjust the app to make it look and behave the way that you want. From app icons and tints, to markdown settings and haptics, there's a settings option to make the app your own. URL schemes and Siri shortcuts allow you to perform useful actions in Allegory from anywhere. Allegory is available on your iPhone, your iPad, and your Apple Watch, and supports seamless iCloud sync to share notes without lifting a finger.\n\nAll the power of iOS is in your hands with Allegory. It is fully integrated with the latest technologies such as Dark Mode, a standalone Apple Watch app, Siri, Today Widget, Quick Actions, Action Extension, Context Menus, iCloud sync, Notifications, document scanning, biometric locks, custom haptics, Pencil support, keyboard shortcuts, URL schemes, custom app icons, iMessage stickers, fully accessible, and more.\n\n—\n\nGET IN TOUCH:\n\nIf you have any questions or feedback, please get in touch. Allegory was created by an incredibly small team of one and I'd love to hear your thoughts. I'm available through email shihab@allegoryapp.info or Twitter @AllegoryApp or @JPEGuin.\n\nhttps://www.allegoryapp.info/terms\nhttps://www.allegoryapp.info/privacy", - "currency": "USD", - "artistId": 1012903170, - "artistName": "Shihab Mehboob", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 0.00, - "bundleId": "com.shi.Allegory", - "version": "1.2.8", - "wrapperType": "software", - "userRatingCount": 57 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple/e4/aa/fe/mzl.rpignxpp.jpeg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple/5e/01/c1/mzl.rifpmnid.jpeg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/9f/ea/f4/mzl.jdgitdao.jpeg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple/6f/e8/97/mzl.grngihjg.jpeg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple/d8/a8/a1/mzl.seplyubx.jpeg/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/cf/5f/3a/cf5f3af7-75bb-38a4-7ac8-72267ee4e066/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/cf/5f/3a/cf5f3af7-75bb-38a4-7ac8-72267ee4e066/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple/v4/cf/5f/3a/cf5f3af7-75bb-38a4-7ac8-72267ee4e066/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/focus-home-interactive/id406173890?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "The Next Big Thing - Lite", - "trackId": 435972315, - "sellerName": "Focus Home Interactive SAS", - "primaryGenreId": 6014, - "primaryGenreName": "Games", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-06-02T08:38:51Z", - "genreIds": [ - "6014", - "7002", - "6016", - "7012" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2011-06-02T08:38:51Z", - "trackCensoredName": "The Next Big Thing - Lite", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "ES" - ], - "fileSizeBytes": "858856057", - "sellerUrl": "http://www.thenextbig-game.com", - "contentAdvisoryRating": "12+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/the-next-big-thing-lite/id435972315?mt=12&uo=4", - "trackContentRating": "12+", - "description": "The Next BIG Thing is the new, hilarious adventure gem from the creators of Runaway. A great adventure game in high definition, loaded with laughs, tributes, mysteries and wacky puzzles!\n \nThanks to a production worthy of a great animated movie, an awesome soundtrack, delightful dialogue and a great art style, The Next BIG Thing will make you live an unforgettable adventure which brilliantly takes players across the fantastic movie genres of a totally crazy Hollywood.\n \nHollywood\nWhat if horror movies' monsters were actually played by real monsters? And what if they were now forced to play in movies for kids, romantic comedies or even musicals? And what would happen if, eventually, they were to rebel?\n \nIn that context, Liz Allaire, talented journalist who can't count up to 4, and Dan Murray, a tough macho who can't stand beetles, attend the horror movies award ceremony… there is the starting point of an incredible story, full of twists and turns! Help Liz and Dan solve the numerous mysteries of an amazing adventure that you won't forget any time soon!\n \n- Solve numerous puzzles and mysteries\n \n- Meet wacky characters and monsters\n \n- Explore dozens of colorful places\n \n- A production worthy of a great animated movie\n \n- Movie-like musical score and English voice-over", - "currency": "USD", - "artistId": 406173890, - "artistName": "FOCUS HOME INTERACTIVE", - "genres": [ - "Games", - "Adventure", - "Entertainment", - "Puzzle" - ], - "price": 0.00, - "bundleId": "com.focushome.TNBTD", - "version": "1.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/b8/2c/8b/b82c8b3c-9b55-1c06-b735-80fb423912e7/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/fb/ef/b4/fbefb447-7011-d6d9-1064-8fad269e6cdc/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple122/v4/5e/7f/b1/5e7fb1a8-db6c-c17f-7522-87832c29ab28/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/39/fe/f8/39fef803-5fee-7074-e0f7-9e58a366aa59/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple122/v4/24/6e/4c/246e4c5e-3aa3-4dc3-2feb-7533c7676fc8/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/97/a8/e1/97a8e163-ae77-9fdb-67d0-bf1406869387/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/97/a8/e1/97a8e163-ae77-9fdb-67d0-bf1406869387/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/97/a8/e1/97a8e163-ae77-9fdb-67d0-bf1406869387/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/new-technologies/id762050392?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "iMap Builder - Mind Mapping", - "trackId": 1108898500, - "sellerName": "New Technologies, LLC.", - "releaseNotes": "We made some changes one can't see with a nude eye, which improve the app's performance and will help us to provide more good quality content in the future.", - "primaryGenreId": 6000, - "primaryGenreName": "Business", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-05-17T05:23:15Z", - "genreIds": [ - "6000", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-05-26T22:38:59Z", - "trackCensoredName": "iMap Builder - Mind Mapping", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "IT", - "JA", - "KO", - "PT", - "RU", - "ZH", - "ES" - ], - "fileSizeBytes": "26579691", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/imap-builder-mind-mapping/id1108898500?mt=12&uo=4", - "trackContentRating": "4+", - "description": "iMap Builder is a simple app designed for bringing your thoughts in order. Pick all your ideas together and create a clear mind map.\n\nUNIVERSAL DATABASE\n- Storage of any thought and ideas;\n- Images and diagrams adding;\n- Convenient built-in calendar;\n- Quick and easy tutorial;\n\nWORKFLOW ORGANIZATION\n- create several \"central\" blocks inside of your project;\n- add different colors to your subtasks;\n- set due dates for each block;\n- add icons or pictures to your blocks;\n\nSMART REPRESENTATION\n- 3 different templates to work with (1 basic hierarchy and 2 additional templates to unlock to organize your thoughts more creatively);\n- mark your tasks as done;\n- connect different elements of blocks into one \"cloud\";\n- make comments for any connections;\n- share your project in social media;\n\nNECESSARY TOOLS\n- search for necessary objects inside your mind map;\n- export to PNG file or print out your projects;\n- Fonts, colors, icon – everything is adjustable;\n- Delete, undo/redo, export – full set of operations.\n\nNo good idea will ever be lost with iMap Builder.\n\n\nPREMIUM ACCESS\n- The length of subscription is 1, 6 or 12 months.\n- Your subscription will be automatically renewed within 1 day before the current subscription ends. \n- Auto-renew option can be turned off in your iTunes Account Settings. \n- Payment will be charged to iTunes Account at confirmation of purchase. \n- No cancellation of the current subscription is allowed during active subscription period.\n\nPrivacy Policy: https://newtech-ltd.com/privacy\n\nTerms Of Use: https://newtech-ltd.com/tos", - "currency": "USD", - "artistId": 762050392, - "artistName": "New Technologies", - "genres": [ - "Business", - "Productivity" - ], - "price": 0.00, - "bundleId": "com.newtechnologies.iMapBuilderlessia", - "version": "2.2.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/91/39/5e/91395ebc-f4fe-74c6-c171-27b735574bca/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/b5/e8/53/b5e8534e-850c-07ba-896e-19d2b40e41e0/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/bc/9c/e9/bc9ce902-e5c8-e96c-c911-b37867105070/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/04/1e/cf/041ecfa3-2352-b6c9-0a84-fcb2fc5dfddb/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/07/2a/c7072a47-f560-ab3c-0444-19d6ac98c23e/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/fe/bf/a0/febfa032-a116-7597-c0db-d32b966c1fc9/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/fe/bf/a0/febfa032-a116-7597-c0db-d32b966c1fc9/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/fe/bf/a0/febfa032-a116-7597-c0db-d32b966c1fc9/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/dorian-brown/id1240209534?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15", - "trackName": "Guso - Quick Web Search", - "trackId": 1434234265, - "sellerName": "Dorian Brown", - "releaseNotes": "- Added new search feature, you can quickly search Amazon, Youtube, Pinterest, Stackoverflow, Reddit...\n- Improved performance and stability \n- Minor Bug fixes", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2018-08-30T12:01:47Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-04-24T22:40:06Z", - "trackCensoredName": "Guso - Quick Web Search", - "languageCodesISO2A": [ - "EN", - "ES" - ], - "fileSizeBytes": "5658895", - "contentAdvisoryRating": "17+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/guso-quick-web-search/id1434234265?mt=12&uo=4", - "trackContentRating": "17+", - "description": "Google at your fingertips\n\nTake your productivity and efficiency to the next level with Guso.\n\nKey Features:\n- Search Amazon, Youtube, Pinterest, Stackoverflow\n- Lightweight and extremely fast!\n- Instant search suggestions from Google\n- Keyboard shortcut to help you search faster\n- Support All Browsers (Safari, Chrome, Firefox, Internet...)\n- Operated via Menu Bar Icon\n- Beautiful & Intuitive User Interface\n- Dark Mode Support!\n\n\n \"Build for Google lovers\" \n\nIf you have any questions or feedback, please feel free to get in touch. Guso was created by an incredibly small team of one.\n\n\n\n\nNote: Guso is NOT affiliated with, funded, sponsored, or in any way associated with the prominent technology company known as \"Google\".", - "currency": "USD", - "artistId": 1240209534, - "artistName": "Dorian Brown", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.suji.googler", - "version": "1.0.12", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/3c/51/4a/3c514a48-7fd2-ddc4-c945-889cd3612867/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple5/v4/18/54/8d/18548da9-05b0-be27-7eb6-8c7484385d88/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/4a/9d/ff/4a9dff6d-7b3e-7f49-f534-1bf59f8bf122/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/e3/a7/45/e3a745b0-1ddd-1480-8b9c-3cf32ae5716a/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/6c/a9/cf/6ca9cfc2-69e9-9450-3b6e-83bf9c30ecfb/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/74/f6/77/74f677ae-f3b0-9fbf-839e-41fd8251f415/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/74/f6/77/74f677ae-f3b0-9fbf-839e-41fd8251f415/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple118/v4/74/f6/77/74f677ae-f3b0-9fbf-839e-41fd8251f415/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/karelia-software-llc/id404126469?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.9.5", - "trackName": "The Hit List", - "trackId": 432764806, - "sellerName": "Karelia Software LLC", - "releaseNotes": "• Mojave compatibility fixes\n• Improved handling of sync push notifications", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-05-10T07:00:00Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "$49.99", - "currentVersionReleaseDate": "2018-09-27T07:32:52Z", - "trackCensoredName": "The Hit List", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "5761638", - "sellerUrl": "http://www.karelia.com/products/the-hit-list/mac.html", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/the-hit-list/id432764806?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Powerful, flexible and simple task management that is a pleasure to use, The Hit List can handle personal tasks and professional projects. Use it with The Hit List for iPhone v2.3 (available on the iOS App Store) which includes The Hit List for Apple Watch and enjoy fast, reliable sync service included with the purchase of the app.\n\nIt is as easy to use as making lists.\n\nNeutralize chaos, and recapture your time and focus. Your life is complicated enough as it is. The tool to manage your life shouldn't be. The Hit List keeps things simple by not forcing you to learn a system. It can be as simple as just keeping a list of things to do as you would on a piece of paper. However, if you do use a task management system such as Getting Things Done by David Allen, The Hit List is flexible enough to support you.\n\nCapture and forget what you need to do later with The Hit List and get your \"now\" back, with confidence.\n\nThe Hit List includes sync service to sync with The Hit List for iPhone and The Hit List for Apple Watch (sold separately on the iOS App Store) or other Macs.", - "currency": "USD", - "artistId": 404126469, - "artistName": "Karelia Software LLC", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 49.99, - "bundleId": "com.potionfactory.TheHitList", - "version": "1.1.32", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple1/v4/4c/c2/59/4cc25958-545b-21be-4358-81949570b2c9/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple5/v4/ad/c7/0f/adc70f0a-beb9-92a9-5280-14b2d6ecd215/mzl.rfwynuee.tif/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple4/v4/b1/de/d9/b1ded952-9ca7-fdd5-fb5b-8c2a706843d2/mzl.kwhtbdtp.tif/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/32/97/39/32973957-855d-8d20-c0c2-04877a933a99/pr_source.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple3/v4/b4/32/c7/b432c76a-1def-d380-062d-928571cc50cd/mzl.bqwyjhwr.tif/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/d8/33/20/d8332020-6bbd-80be-fbe3-291dd78f78ea/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/d8/33/20/d8332020-6bbd-80be-fbe3-291dd78f78ea/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/d8/33/20/d8332020-6bbd-80be-fbe3-291dd78f78ea/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/delullo-software-llc/id409729333?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "All Things Money", - "trackId": 782925777, - "sellerName": "DeLullo Software, LLC", - "releaseNotes": "The All Things Money 1.4.0 update improves functionality and stability, and is recommended for all users.\n\nNew features include:\n\n- Enabled QFX files with transactions to be imported.\n- When importing a file, if transaction categories are not specified then they will be automatically set to the last category used by the payee.\n- Added stock market technical indicators for Aroon, Money Flow, Relative Strength, and Fast Stochastic.\n- Added the ability to simultaneously view weekly and daily stock technicals.\n\nStability improvements include:\n\n- Added support for importing files with international character encodings.\n- Fixed two bugs that affected printing functionality.\n- Fixed QIF exports, which incorrectly had a percent symbol in the date field.\n- Fixed international currency formatting of the payment and deposit columns.\n\nFor detailed release notes, please visit: http://www.delullosoftware.com/apps/AllThingsMoney/ReleaseNotes.pdf", - "primaryGenreId": 6015, - "primaryGenreName": "Finance", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2014-02-07T23:58:42Z", - "genreIds": [ - "6015", - "6000" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2015-09-15T13:07:44Z", - "trackCensoredName": "All Things Money", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "1456751", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/all-things-money/id782925777?mt=12&uo=4", - "trackContentRating": "4+", - "description": "All Things Money (ATM) was created with the mindset that good finance software doesn’t need to be expensive, to contain advertisements, or to log into your bank account. It should be comprehensive and provide interconnected services. It should also be efficient by supporting multiple input formats, auto-completing certain fields, and automatically logging recurring transactions. ATM delivers on these goals and more.\n\nIf you are curious, please give it a try. Download All Things Money Lite for free to determine whether ATM is a good fit for you. When you want to upgrade, use the export and import menu options to transfer data between apps.\n\nATM capabilities include the following.\n\nAccount Dashboard\n- The innovative layout gives perspective to all of your liquid assets.\n- Track bank, credit card, stock market, and U.S. savings bond accounts.\n- Calculate net worth.\n- Sort transactions.\n- Split transactions.\n- Add memos to transactions.\n- Import bank, credit card, and stock market transactions from QIF and CSV files.\n- Import bank and credit card transactions from OFX files. (At this time, stock market transactions are not supported via OFX imports.)\n- Import U.S. savings bonds from treasurydirect.gov HTML files.\n- ATM auto-fills payee names and categories.\n- The bank-account paradigm enables you to earmark money.\n\nBill Planner\n- Track payments and deposits.\n- Track automatic stock market trades that are typical of 401k-type accounts.\n- Elect to automatically move up bill dates to avoid weekends and U.S. holidays. \n- ATM supports the following payment intervals: daily, weekly, bi-weekly, semi-monthly, monthly, bi-monthly, quarterly, once every four months, semi-annually, annually, bi-annually.\n\nStay on Budget\n- Create a budget based on transactions from the prior year.\n- Update budget goals based on your expected bills and deposits.\n- Update actuals from account transactions.\n- ATM supports annual, monthly, and custom-period budgets.\n- ATM provides color-coded alerts to track spending excess.\n\nPlan for Retirement\n- Seed the retirement calculator with your account information.\n- Calculate how much money you will have when you retire.\n- Calculate the annuity that your retirement will generate.\n- ATM also includes a general-purpose, compound-interest calculator that has six unique equations and two combination options for expediency. This calculator enables reverse computations; it calculates how much money you need to retire based on your desired annuity, interest rate, and life expectancy.\n\nMonitor the Stock Market\n- Create a watch list that saves stock symbols and time intervals.\n- Display a stock’s price and volume alongside technical indicators like Bollinger Bands and Moving Average Convergence Divergence (MACD).\n- ATM supports daily and weekly stock charts.\n\nMortgage Analysis\n- Create an amortization table and track your mortgage balance as you pay down principal. \n- Compare the costs of buying and renting.\n\nInventory Tracking\n- Keep track of your collections.\n\nCreate Reports\n- Generate plots of net worth and stock prices.\n\nMinimum requirements: screen resolution of 1280 x 800 or higher.", - "currency": "USD", - "artistId": 409729333, - "artistName": "DeLullo Software, LLC", - "genres": [ - "Finance", - "Business" - ], - "price": 9.99, - "bundleId": "com.delullosoftware.AllThingsMoney", - "version": "1.4.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/35/ab/f3/35abf398-3d14-0260-8aa0-94c8fe338524/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/22/8a/50/228a50d9-e1c0-4c13-2dab-720277a5d135/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/34/3d/a0/343da0fc-14f2-ea17-60a0-18af8e8ef1d2/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/34/3d/a0/343da0fc-14f2-ea17-60a0-18af8e8ef1d2/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/34/3d/a0/343da0fc-14f2-ea17-60a0-18af8e8ef1d2/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/hypercritical-llc/id1493996621?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15", - "trackName": "SwitchGlass", - "trackId": 1498546559, - "sellerName": "Hypercritical LLC", - "releaseNotes": "• Minor layout adjustments to the Preferences and Exclude windows.\n• Updates to improve support for future operating systems.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2020-02-12T08:00:00Z", - "genreIds": [ - "6007" - ], - "formattedPrice": "$4.99", - "currentVersionReleaseDate": "2020-07-24T16:20:19Z", - "trackCensoredName": "SwitchGlass", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "2955121", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/switchglass/id1498546559?mt=12&uo=4", - "trackContentRating": "4+", - "description": "SwitchGlass adds a dedicated application switcher to your Mac. You can customize its appearance, size, and position on each attached display, including hiding it on selected displays. Use it to bring one or all of an app’s windows to the front, or as a drag-and-drop target to open files.\n\nThe following app switcher attributes can be customized individually for each attached display:\n\n• Visibility - Permanently hide, show, or auto-hide the app switcher. Auto-hide keeps the app switcher hidden until the mouse cursor touches the region of the screen edge where the app switcher is positioned.\n• Position - Move the app switcher to one of eight different positions on each screen.\n• Orientation - Make the app switcher horizontal or vertical in a given position.\n• Material - Change the appearance of the app switcher background to one of four possible choices (see screenshot above) including a mode where it automatically matches the system appearance (e.g, dark when dark mode is active, light otherwise).\n• Corner Style and Radius - Choose from three corner styles: square, circle, or “squircle” (a “continuous” rounded corner style). The corner radius can also be adjusted.\n• Icon Size - Adjust the size of app icons in the switcher from 16 to 256 points.\n• Icon Padding - Adjust the padding between icons from 0% to 100% of the configured icon size.\n• Margin - Adjust the amount of space around the app switcher.\n\nAdditionally, the following global settings may be customized:\n\n• App Sort Order - Sort apps by name or launch order, descending or ascending.\n• App Filtering - Show all apps or just those with windows on the current display.\n• Menu Bar Icon - Choose from three possible menu bar icons.\n• App Exclusions - Select apps that should never appear in the app switcher.\n\nHere’s how the app switcher works in its default configuration:\n\n• Click an app icon to bring all windows from that app to the front.\n• Hold down the Shift key while clicking an app icon to bring just one window from that app to the front.\n• Hold down the Option key while clicking an app icon to hide the current app before bringing the clicked app to the front. (Option-clicking the currently active app will hide it.)\n• Hold down the Option and Shift keys while clicking an app icon to hide the current app before bringing just one window from the clicked app to the front. (Option-Shift-clicking the currently active app will hide it.)\n• Right-click (or Control-click) an app icon to activate a context menu from which you can perform any of the supported actions (described below).\n• Drag one or more files or folders onto an app icon to open those items with the app.\n• Hold down the Command key while clicking on an app icon to reveal the app in the Finder.\n\nThe click and Shift-click actions are configurable. Each may be set to any one of these actions:\n\n• Show All Windows - Activate an app and bring all its window to the front.\n• Activate - Activate an app and bring a single window to the front.\n• Reopen - Activate an app in a way that triggers its “reopen” handler. Apps can choose to do whatever they want in response to a “reopen” action, but most bring a single window to the front, opening a new window (or un-minimizing a window from the Dock) if none are open.\n• Reopen and Show All - This is the same as “Reopen” except that SwitchGlass will also bring all the app’s windows to the front after sending the “reopen” action.\n\nFor more information, click the SwitchGlass menu bar icon and choose “Help”, or visit https://hypercritical.co/switchglass/#help", - "currency": "USD", - "artistId": 1493996621, - "artistName": "Hypercritical LLC", - "genres": [ - "Productivity" - ], - "price": 4.99, - "bundleId": "co.hypercritical.SwitchGlass", - "version": "1.4.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple125/v4/63/5c/bf/635cbf44-3c63-4737-28a7-b5290a7e32b5/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a6/a3/e6/a6a3e643-f2b1-1110-24a7-be0f1367c143/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/63/74/8f/63748fcc-365e-7a02-09d5-4107c770c6b7/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/46/98/bb/4698bb7e-13b7-684d-c0ab-e31400f65139/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/90/3c/17/903c1770-37ca-0c8a-e22d-bd43b703259a/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/20/d6/b1/20d6b1f9-d3c2-85af-274d-d3762879cf82/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/20/d6/b1/20d6b1f9-d3c2-85af-274d-d3762879cf82/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/20/d6/b1/20d6b1f9-d3c2-85af-274d-d3762879cf82/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/abdusodiq-saidov/id600067303?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.12", - "trackName": "Things for Work - Templates", - "trackId": 1275071331, - "sellerName": "Abdusodiq Saidov", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-04-23T04:19:28Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2019-04-23T04:19:28Z", - "trackCensoredName": "Things for Work - Templates", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "11704916", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/things-for-work-templates/id1275071331?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Writing a smart cover letter or resume is not always an easy task. For most people, writing a good resume and letter is tough, and it takes time. \n\nWouldn't it be wonderful if you could figure out how to make a resume that would get you an interview almost every time you applied for a job?\n\nResume is not a log of your job history. It is not a summary of your skills. It is not going to automatically get you a job if you don't prepare them in proper ways.\n\nThis app will help you to prepare better cover letter and resume step-by-step.\n\nIt comes with sample Cover Letters and Resumes.\n\nFeel Free to contact us if you need any other templates!\n\n*** Microsoft Word and/or Pages app is required to work with and open templates ***", - "currency": "USD", - "artistId": 600067303, - "artistName": "Abdusodiq Saidov", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.iconshots.VectorClliparts", - "version": "1.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/e3/c1/49/e3c1499b-0756-9e1e-51e3-ce92b9ce79b5/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/1f/89/e1/1f89e162-34a2-7bfd-41ef-975d13ca5d9b/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple5/v4/30/97/85/30978568-5806-42e3-b541-8c1182f8a152/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/67/7d/36/677d368d-fc13-3fe4-4f47-4cd91c1d026e/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple1/v4/3c/39/f0/3c39f04e-187e-4a97-10f7-2538625e7c6f/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/5e/a7/b1/5ea7b10b-dbee-262d-cb05-9a49883664dc/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/5e/a7/b1/5ea7b10b-dbee-262d-cb05-9a49883664dc/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/5e/a7/b1/5ea7b10b-dbee-262d-cb05-9a49883664dc/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/lucien-dupont/id292886202?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.9", - "trackName": "Check Off - A Task Manager", - "trackId": 458418895, - "sellerName": "Lucien Dupont", - "releaseNotes": "- fixed a crash when deleting a URL in the notes area. Thanks to Reza for reporting it!", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-08-31T07:11:42Z", - "genreIds": [ - "6007", - "6012" - ], - "formattedPrice": "$4.99", - "currentVersionReleaseDate": "2020-06-22T23:20:32Z", - "trackCensoredName": "Check Off - A Task Manager", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "6115501", - "sellerUrl": "http://www.chromedomesoftware.com/CheckOff/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/check-off-a-task-manager/id458418895?mt=12&uo=4", - "trackContentRating": "4+", - "description": "CheckOff is a task management application that runs as a stand alone application or a menu bar app. \n\nFeatures: \n\n• Your choice: runs in a menu bar mode so that your dock doesn't get cluttered up, or as a normal application.\n• Tasks can be placed within folders, and have labels attached to each task.\n• Tasks can be put inside other tasks\n• Tasks can have notes with plain text or rich text with fonts, colors, and sizes. \n• Notes can also contain links to websites or local files, allowing one click access to resources that can be used to complete the task. \n• Menu bar icon color can be customized.\n• Hot key support to open the main window.\n• Export your tasks as text, RTF, html and opml\n• Dark mode support for macOS Mojave and Catalina", - "currency": "USD", - "artistId": 292886202, - "artistName": "Lucien Dupont", - "genres": [ - "Productivity", - "Lifestyle" - ], - "price": 4.99, - "bundleId": "com.chromedomesoftware.checkoff", - "version": "5.8.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/e2/c3/1e/e2c31ea8-a561-5200-57bb-ca3826c0f670/mzl.jantsuya.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple4/v4/a8/30/32/a830321b-5e72-23e3-eb23-7160c4832b19/mzl.agzqdpdf.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple/v4/83/34/45/83344504-6351-a6c7-5df4-27856a80a0bb/mzl.hckkslpf.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/dc/f6/02/dcf602c6-df13-f41f-26ff-69c9fb6beca9/mzl.rrlwevnr.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple6/v4/d9/3d/94/d93d9406-9410-ef3b-8dae-28125d2dad8a/mzl.fkekfvts.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/28/51/cb/2851cb6d-1bfc-ec20-aa8d-28e2e366cc6d/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/28/51/cb/2851cb6d-1bfc-ec20-aa8d-28e2e366cc6d/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple7/v4/28/51/cb/2851cb6d-1bfc-ec20-aa8d-28e2e366cc6d/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/doit-im/id350974724?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.7", - "trackName": "Doit.im", - "trackId": 533391459, - "sellerName": "Xu Zhe", - "releaseNotes": "Fixed the bug that the background color of Smart Add popup is black.\nFixed the bug that repeat tasks will be created before the repeat strategy start time.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2012-10-23T19:25:14Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2015-07-09T22:51:34Z", - "trackCensoredName": "Doit.im", - "languageCodesISO2A": [ - "EN", - "JA", - "ZH" - ], - "fileSizeBytes": "3971688", - "sellerUrl": "http://doit.im", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/doit-im/id533391459?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Doit.im is the smartest way to manage your tasks with the implementation of Getting Things Done (GTD) methodology. It helps you efficiently handle your tasks, whether you are busy executives or smart staff.\n\nWe’ve redesigned the user interface to make it simpler and more convenient. The brand-new task view of Today and Next Actions makes our tasks more focused and organized. Doit.im supports for multi-platforms, including Web/iOS/Mac etc, with Mac version exclusive for the Pro members.\n\nFeatures:\n \n 1. Keep your tasks in sync with Doit.im Cloud to make you in control of your tasks anywhere, anytime.\n 2. Completely implement GTD theory.\n 3. Support multi-level views: goals, projects, tasks, subtasks.\n 4. Manually sort your goals, projects, next actions, subtasks and contexts.\n 5. Forward tasks to your companions and track the status of the tasks.\n 6. Support the customization of your avatar.\n 7. Support Daily Plan.\n 8. Support Daily Review and Statistics, which you can share in Twitter and Facebook.\n \n* Still use to do list? It's time to try GTD and enjoy the completely different upgrading!", - "currency": "USD", - "artistId": 350974724, - "artistName": "Doit.im", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.snoworange.Doitim-Mac", - "version": "4.2.3", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple118/v4/81/95/41/819541ad-9e99-a68d-680a-f6787be06dfb/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/ab/bb/6d/abbb6dd6-e56c-3617-40f2-689b337d7acd/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/5a/d1/05/5ad1059c-7e90-34a4-a257-f5c21c40c065/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/86/1d/bc/861dbc65-4574-6ae2-4e69-eda306db4a45/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/20/c0/52/20c052a4-00fc-db15-3859-1bee09157fd0/pr_source.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/4e/c2/71/4ec27160-f36d-dd23-66d8-234d77ff0f74/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/ca/2b/36/ca2b36b9-656f-c556-372b-14cf2504e020/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/63/19/9f/63199f56-2cf0-226f-d24a-a710e640329c/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/63/19/9f/63199f56-2cf0-226f-d24a-a710e640329c/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/63/19/9f/63199f56-2cf0-226f-d24a-a710e640329c/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/quantum-quinn/id284974414?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.13", - "trackName": "JotNow - Sticky Note Manager", - "trackId": 402371718, - "sellerName": "Christopher Bess", - "releaseNotes": "Thanks to everyone who reported bugs and unexpected issues!\n\n- Improved email integration, fixes email note bug\n- Fixed accent character input bug\n- Other bug fixes and enhancements", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-01-02T01:23:15Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "$2.99", - "currentVersionReleaseDate": "2019-07-05T19:09:03Z", - "trackCensoredName": "JotNow - Sticky Note Manager", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "3795543", - "sellerUrl": "https://www.quantumquinn.com/overview/jotnow", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/jotnow-sticky-note-manager/id402371718?mt=12&uo=4", - "trackContentRating": "4+", - "description": "It's the Stickies-like app you have been waiting for all this time…\n\nPlace notes in notebooks and further organize them with categories. Resize, dock, and collapse notes to keep them out of your way but still in plain sight. Even use your favorite color to add some style to your notes. Why write down notes when you can have JotNow store, search, and organize them?\n\nOrganize your thoughts and find them just as fast. Move between notes just as easy as you create them. Find and display notes with a click of a mouse or the press of a key. So much effort has been put on functionality and simplicity you will wonder how you ever used your computer without JotNow installed.\n\nVideos available at: http://jotnow.com/overview/jotnow\n\nNote Features:\n• Notes can float on top of other windows\n• Syncs note tasks reminders (due dates) with Reminders.app\n• All notes support rich text (RTF). Bold, italic, different font styles, etc.\n• Auto-saves, never worry about notes being saved\n• Note tasks, each note has it's own list of to-dos\n• Auto-indent lists\n• Email note\n• Custom note colors and note transparency, style your notes\n• Dock and collapse notes\n• Resizable (with scrollbars)\n• Dock notes to any part or edge of your screen (multi-monitors supported)\n• Manage notes without using the mouse\n• Search within notes\n• Swipe/flick notes left or right to dock them (trackpad only, two or three fingers)\n• Text-to-speech support for all notes (notes can be read aloud by your Mac)\n\nNote Manager Features:\n• Search Notes\n• Import and export notes (txt, rtf, etc)\n• Drag selected text or existing RTF and Text files to create notes\n• Create notes from empty searches\n• Find recent notes quickly using search operators (:today, :recent, :new)\n• No mouse required to manage notes (keyboard friendly)\n\nGeneral Features:\n• Easy to use\n• Hide dock icon\n• Quickly search for notes from anywhere (using Open Notes window)\n• Can be used without the mouse\n• Print directly from notes\n• Unlimited notes\n• Unlimited notebooks and tags\n• Global/system keyboard shortcuts (hot keys) allow access to JotNow from any application\n• Note trash, safely delete any note, you can see it later, if needed\n• Dock and Status Bar menus to access notes\n• Keyboard user friendly (many helpful shortcuts), you can command key features without touching your mouse\n• Stable and reliable, your notes are safe\n• Resource friendly, low memory footprint\nand much more...\n\nOnline documentation and videos are available in our support section.\nhttp://jotnow.com/overview/jotnow/support/\n\nSoli Deo Gloria", - "currency": "USD", - "artistId": 284974414, - "artistName": "Quantum Quinn", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 2.99, - "bundleId": "com.quantumquinn.JotNowDesktop", - "version": "1.8.2", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/c6/6e/4c/c66e4c72-28ed-8b4d-be7f-a91b41d89b30/ba6f5198-ca10-4672-96b0-e58af9e56c9c_MacOS-1_en.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/0f/92/90/0f9290b2-c11b-ca5d-e772-e891860270b4/1da0f90b-57fe-46b7-9f8d-db0272ab6e9c_MacOS-2_en.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/a1/61/d7/a161d7a1-e500-c169-d240-0bcc6a453b6e/5c43427a-7d9a-4394-b1e2-0a8b030c7e6e_MacOS-3_en.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/38/00/53/380053ec-128f-ba09-2971-6ef163f34c2c/2d5e948e-badb-46d0-81be-97102ecccb9a_MacOS-4_en.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/a8/7b/4c/a87b4c75-e493-635c-6e8b-68a44168c552/bbd91ce7-f028-4434-989c-e7583bc636f7_MacOS-5_en.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/58/33/62/583362d6-3e4d-5bd8-0241-8db2481456e4/e33d62f5-aafb-4281-9f81-610723031b52_MacOS-6_en.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/12/f8/e1/12f8e1cb-c20f-e1d3-3b98-5448130d9654/83d068bb-a3d9-4c96-a43d-830a7bafd964_MacOS-7_en.png/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/37/b4/86/37b48654-a9b8-1903-209e-b950400875d0/c777c2c3-ace7-46ee-b26b-ba88d9aa7507_MacOS-8_en.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/f5/bb/a4/f5bba445-fa45-e9ff-31c3-7184bc43cdc0/source/60x60bb.png", - "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/f5/bb/a4/f5bba445-fa45-e9ff-31c3-7184bc43cdc0/source/512x512bb.png", - "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/f5/bb/a4/f5bba445-fa45-e9ff-31c3-7184bc43cdc0/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/tarasov-mobile/id547656503?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.13.0", - "trackName": "Chaos Control™: GTD Task List", - "trackId": 1071244525, - "sellerName": "Dmitriy Tarasov", - "releaseNotes": "From now on projects with Due Dates are shown in the Daily Plan section of the app. You can switch off this feature in the Settings menu of the app if you prefer to see tasks only. \n+ various improvements and optimisations.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2016-01-20T20:29:38Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-09-01T21:01:42Z", - "trackCensoredName": "Chaos Control™: GTD Task List", - "languageCodesISO2A": [ - "EN", - "RU" - ], - "fileSizeBytes": "48546730", - "sellerUrl": "http://chaos-control.mobi/?utm_source=appstore&utm_medium=mac&utm_campaign=appstore_mac_ver_1", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/chaos-control-gtd-task-list/id1071244525?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Chaos Control is a task manager based on the best ideas of GTD (Getting Things Done) methodology created by David Allen. Whether you are running a business, launching an app, working on a project or simply planning your holiday trip, Chaos Control is a perfect tool to manage your goals, juggle your priorities, and organize your tasks to get things done. And the best part is, you can handle both heavyweight project planning and simple daily routine like shopping list management in one flexible app. Also, Chaos Control is available across all major mobile and desktop platforms with seamless sync.\n\nHERE IS HOW IT WORKS:\n\n1) MANAGE YOUR PROJECTS\nProject is a goal combined with a set of tasks you need to complete in order to achieve it. Create as many projects as you like to write down all the desired outcomes you have\n\n2) ORGANIZE YOUR GOALS\nCreate unlimited number of projects and group them by category using Folders\n\n3) USE GTD CONTEXTS\nOrganize tasks from different projects using flexible context lists. If you are familiar with GTD you would just love this feature\n\n4) PLAN YOUR DAY\nSet due dates for tasks and make plans for any particular day\n\n5) USE CHAOS BOX\nPut all the incoming tasks, notes and ideas into Chaos Box in order to process them later. It works similar to GTD inbox, but you can use it as a simple to-do list\n\n6) SYNC YOUR DATA\nChaos Control works on both desktop and mobile devices. Setup an account and sync your projects across all of your devices\n\nThis app is designed with creative people in mind. Designers, writers, developers, startup founders, entrepreneurs of all kinds and pretty much anyone with ideas and desire to make them happen. We combined the power of GTD with the convenient interface to help you with:\n• personal goal setting\n• task management\n• time management\n• planning your business and personal activities\n• building your routine\n• handling simple to do lists, checklists and shopping lists\n• catching your ideas and thoughts to process them later\n\nKEY FEATURES\n• Seamless cloud sync across all major mobile and desktop platforms\n• GTD-inspired Projects and Contexts supplemented with Folders, sub-folders and sub-contexts\n• Recurring tasks (daily, weekly, monthly and chosen days of the week)\n• Chaos Box - Inbox for your unstructured tasks, notes, memos, ideas and thoughts. Great tool for staying on track inspired by GTD ideas\n• Notes for tasks, projects, folders and contexts\n• Fast and smart search\n\nHave a productive day!\n\nTerms Of Use:\nhttp://chaos-control.mobi/toc.pdf", - "currency": "USD", - "artistId": 547656503, - "artistName": "Tarasov Mobile", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.tarasovmobile.ChaosControlMac", - "version": "1.10", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/6e/8d/56/6e8d56ea-494a-23ca-cfdd-0ba065d8e8f2/pr_source.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/6e/ed/49/6eed4931-c254-abba-7823-1e8ebd7d652c/pr_source.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/e8/77/3f/e8773fd7-7541-e9e4-2882-65c400434ad5/pr_source.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/68/62/71/686271c4-38df-ea7b-f95f-90fad57c6def/pr_source.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6d/a5/53/6da55373-9f5e-671f-eaf4-7f32ad79123b/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/a0/6c/07/a06c07bd-7542-f57e-92d0-49067c144111/pr_source.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/68/1a/4d/681a4d23-d0df-ea50-8afa-0b2959640395/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/93/4f/86/934f86be-be87-63a1-af42-b1183dca6f93/pr_source.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/1a/f4/77/1af4771a-e086-5085-d937-8458a2356e43/pr_source.png/392x696bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/40/7d/ba/407dba14-e345-9644-8c49-6ea8d07ee73f/pr_source.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/2a/3e/df/2a3edf37-3580-b911-ece9-b22afba71583/pr_source.png/552x414bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/48/43/f0/4843f065-030a-9721-5d08-971021f3c922/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/37/db/25/37db2594-602c-d411-1826-fc0446632cd3/pr_source.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/6c/8e/0a/6c8e0ab0-1519-474e-d7c4-c81e44be3925/pr_source.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c1/72/4e/c1724e7a-07e9-b9ff-7e1a-3dc3296a70dd/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e1/4d/6d/e14d6dee-571b-a277-5a6f-4e8a1966b235/pr_source.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/24/63/30/246330e7-7632-5f13-4288-ec4847c524fc/pr_source.png/552x414bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/7f/12/47/7f124738-f363-4d4d-e35c-05777ee3e472/pr_source.png/552x414bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/93/95/cf/9395cf14-4ae2-7303-61ac-d83a72c85af6/pr_source.png/552x414bb.png", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/94/cb/2d/94cb2d43-9999-3eda-22ff-8eadf003b561/pr_source.png/552x414bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/fa/9c/04/fa9c04ca-ff25-d039-2b39-c4a0585c05ee/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/fa/9c/04/fa9c04ca-ff25-d039-2b39-c4a0585c05ee/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/fa/9c/04/fa9c04ca-ff25-d039-2b39-c4a0585c05ee/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/tiger-ng/id356989572?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.4", - "trackName": "Book Generator", - "trackId": 1471872257, - "sellerName": "Tiger Ng", - "releaseNotes": "New features:\n1. Import Microsoft Word Document\n2. Import Google Docs\n3. Import Google Sheets\n4. Import Google Classroom's assignment submissions by students\n5. CSV: start from scratch & import from clipboard\n6. Markdown: start from scratch & import from clipboard\n7. Bug fix", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-07-25T07:00:00Z", - "genreIds": [ - "6007", - "6017" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2019-09-16T14:30:50Z", - "trackCensoredName": "Book Generator", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "16542720", - "sellerUrl": "https://getbookgenerator.blogspot.com/", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 1, - "userRatingCountForCurrentVersion": 1, - "averageUserRating": 1, - "trackViewUrl": "https://apps.apple.com/us/app/book-generator/id1471872257?uo=4", - "trackContentRating": "4+", - "description": "Book Generator is a new way to convert OneNote notebooks, Trello boards, Word Documents, Excel spreadsheets, Google Docs, Google Sheets, Google Classroom, CSV and Markdown files into EPUB or PDF.\n\nCore Features\n* OneNote: each notebook represents a book, each section represents a group, each page represents a chapter.\n* Trello: each board represents a book, each list represents a chapter, each card represents an element.\n* Word: each heading represents a page.\n* Excel: each row represents a page, each record represents an element.\n* Google Docs: each heading represents a page.\n* Google Sheets: each row represents a page, each record represents an element.\n* Google Classroom: each classwork represents a book, each assignment submission represents a page.\n* CSV: each row represents a page, each record represents an element.\n* Markdown: convert to HTML\n* Support image, video and audio attachment.\n\nBasic Editing Features\n* Edit cover image.\n* Edit book style.\n* Configure book information.\n* Delete chapter.\n* Reorder chapter.\n\nThe generated book can be imported into Creative Book Builder for further editing.", - "currency": "USD", - "artistId": 356989572, - "artistName": "Tiger Ng", - "genres": [ - "Productivity", - "Education" - ], - "price": 0.00, - "bundleId": "com.tigernghk.ios.bookgenerator", - "version": "1.2", - "wrapperType": "software", - "userRatingCount": 1 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/fb/ca/09/fbca09ec-b32e-57f0-1b1f-7d81d40a9151/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/98/82/fa/9882fa60-c5ee-1885-032b-a1c1b84e8b00/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/13/d4/c2/13d4c2b0-1a22-d904-e3e7-043f1e557fdd/pr_source.png/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/e1/d8/be/e1d8bee3-051b-6e5f-1e7d-27962dbf2def/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/16/eb/ab/16ebab10-9ef3-728e-7a07-1e61f51729dd/source/60x60bb.png", - "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/16/eb/ab/16ebab10-9ef3-728e-7a07-1e61f51729dd/source/512x512bb.png", - "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/16/eb/ab/16ebab10-9ef3-728e-7a07-1e61f51729dd/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/haibo-tang/id1501309035?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.15", - "trackName": "TomatoThings 2", - "trackId": 1501309036, - "sellerName": "Haibo Tang", - "primaryGenreId": 6002, - "primaryGenreName": "Utilities", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2020-03-03T08:00:00Z", - "genreIds": [ - "6002", - "6007" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-03-03T21:17:35Z", - "trackCensoredName": "TomatoThings 2", - "languageCodesISO2A": [ - "EN", - "ZH" - ], - "fileSizeBytes": "4494776", - "sellerUrl": "http://ihugo.cc", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/tomatothings-2/id1501309036?mt=12&uo=4", - "trackContentRating": "4+", - "description": "一款结合TODO和番茄工作法的工具。让您能够专注的进行工作。第一个版本包含一些基础的功能比如TODO,番茄工作法,统计。\n\n这是我个人的第一款应用。我想长期进行维护。所以您如果有任何想法,请通过邮件直接和我进行联系。\n\n联系方式: dusty@vip.qq.com", - "currency": "USD", - "artistId": 1501309035, - "artistName": "Haibo Tang", - "genres": [ - "Utilities", - "Productivity" - ], - "price": 0.00, - "bundleId": "cc.ihugo.app.TomatoThings2", - "version": "1.0", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple/5e/01/c1/mzl.ycuysdmk.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple/9f/ea/f4/mzl.kllojlbn.jpg/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple/d8/a8/a1/mzl.rdtnfjfa.jpg/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple/e4/aa/fe/mzl.qaehltfv.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple/6f/e8/97/mzl.vhjtxmra.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/9a/00/9a/9a009a64-82dc-85af-7f7d-92199ed9b97c/source/60x60bb.png", - "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/9a/00/9a/9a009a64-82dc-85af-7f7d-92199ed9b97c/source/512x512bb.png", - "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/9a/00/9a/9a009a64-82dc-85af-7f7d-92199ed9b97c/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/focus-home-interactive/id406173890?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6", - "trackName": "The Next Big Thing", - "trackId": 430203668, - "sellerName": "Focus Home Interactive SAS", - "releaseNotes": "Update 1.1: The game is now playable with voices in English, French and German.", - "primaryGenreId": 6014, - "primaryGenreName": "Games", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2011-06-02T07:00:00Z", - "genreIds": [ - "6014", - "7009", - "6016", - "7002" - ], - "formattedPrice": "$9.99", - "currentVersionReleaseDate": "2011-06-17T00:48:43Z", - "trackCensoredName": "The Next Big Thing", - "languageCodesISO2A": [ - "EN", - "FR", - "DE", - "ES" - ], - "fileSizeBytes": "5271779975", - "sellerUrl": "http://www.thenextbig-game.com/", - "contentAdvisoryRating": "12+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/the-next-big-thing/id430203668?mt=12&uo=4", - "trackContentRating": "12+", - "description": "The Next BIG Thing is the new, hilarious adventure gem from the creators of Runaway. A great adventure game in high definition, loaded with laughs, tributes, mysteries and wacky puzzles!\n \nThanks to a production worthy of a great animated movie, an awesome soundtrack, delightful dialogue and a great art style, The Next BIG Thing will make you live an unforgettable adventure which brilliantly takes players across the fantastic movie genres of a totally crazy Hollywood.\n \nHollywood\nWhat if horror movies' monsters were actually played by real monsters? And what if they were now forced to play in movies for kids, romantic comedies or even musicals? And what would happen if, eventually, they were to rebel?\n \nIn that context, Liz Allaire, talented journalist who can't count up to 4, and Dan Murray, a tough macho who can't stand beetles, attend the horror movies award ceremony… there is the starting point of an incredible story, full of twists and turns! Help Liz and Dan solve the numerous mysteries of an amazing adventure that you won't forget any time soon!\n \n- Solve numerous puzzles and mysteries\n \n- Meet wacky characters and monsters\n \n- Explore dozens of colorful places\n \n- A production worthy of a great animated movie\n \n- Movie-like musical score and English voice-over", - "currency": "USD", - "artistId": 406173890, - "artistName": "FOCUS HOME INTERACTIVE", - "genres": [ - "Games", - "Family", - "Entertainment", - "Adventure" - ], - "price": 9.99, - "bundleId": "com.focus-home.TNBT", - "version": "1.1", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/24/cb/ac/24cbac33-03a6-c33c-3c7e-f1d3acff2d74/pr_source.png/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple69/v4/e8/08/8f/e8088f7d-c688-7663-b738-efd7b64ce6ad/pr_source.png/800x500bb.jpg", - "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/f2/c3/32/f2c3323c-db7a-5015-5a36-8cb4ee8d6d5d/pr_source.png/800x500bb.jpg", - "https://is4-ssl.mzstatic.com/image/thumb/Purple49/v4/be/98/fd/be98fd4a-75e9-22e9-819b-c006d09badef/pr_source.png/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/a1/d0/12/a1d012c4-8ece-c61a-e6e7-4a27a3d740fa/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/a1/d0/12/a1d012c4-8ece-c61a-e6e7-4a27a3d740fa/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple62/v4/a1/d0/12/a1d012c4-8ece-c61a-e6e7-4a27a3d740fa/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/explain-3d/id1019479194?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.6.6", - "trackName": "Explain 3D How things work", - "trackId": 1020146983, - "sellerName": "ePic design s.r.o.", - "releaseNotes": "- added settings so you can adjust move, rotation and zoom speed\n- small performance fixes", - "primaryGenreId": 6017, - "primaryGenreName": "Education", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-07-30T05:32:31Z", - "genreIds": [ - "6017", - "6014", - "7015" - ], - "formattedPrice": "$3.99", - "currentVersionReleaseDate": "2016-09-03T02:14:31Z", - "trackCensoredName": "Explain 3D How things work", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "90413684", - "sellerUrl": "http://explain3d.com", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/explain-3d-how-things-work/id1020146983?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Explain3D success:\n***TOP 25 GEW - Global start up competition finalist 2013 (http://www.cnbc.com/id/101168347/page/6)***\n\nSimulations:\n- Car workshop - 4 stroke engine, Car clutch, Manual transmission, Disc brakes, Ignition system\n- Discover the Universe - ISS space station, Space shuttle, Solar system\n- Electricity around us - Hydroelectric dam, Wind turbine, Nuclear power plant\n- Transport simulations - Railroad switch, Submarine, Jet engine\n- Our homes - Lock and key, Elevator, Toilet, Desk Lamp\n- Tools - Jackhammer, Hand pump\n\nExplain3D is system of interactive simulations, that can help you to explain, how things work. Simulations are built in interactive 3D environment, which brings fun to education and helps understand the stuff. \n\nFeatures:\n- Interactive 3D environment\n- simple and intuitive interface\n- description of main parts of simulations / objects\n- nice 3D models and animations\n- great to make education more engaging and interesting", - "currency": "USD", - "artistId": 1019479194, - "artistName": "Explain 3D", - "genres": [ - "Education", - "Games", - "Simulation" - ], - "price": 3.99, - "bundleId": "com.Explain3D.HowThingsWork", - "version": "2.4", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "screenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/Purple18/v4/a8/55/f1/a855f17c-73a0-3266-561f-c1c2c7fd0dc7/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple30/v4/34/bc/d4/34bcd4cf-4143-9f6c-04d5-de3556bb75a8/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple60/v4/c9/ed/2f/c9ed2f0a-1064-2e6f-7e84-49f9e3d0d79e/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/4b/1c/fe/4b1cfee9-6a9b-4fe9-e9fa-41147ba4bc93/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple60/v4/ea/a4/37/eaa4378b-c323-7876-ea9b-992554c58208/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/a7/d1/61/a7d1618a-3af9-3143-d65d-7bf3778c6b28/source/60x60bb.png", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/a7/d1/61/a7d1618a-3af9-3143-d65d-7bf3778c6b28/source/512x512bb.png", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple118/v4/a7/d1/61/a7d1618a-3af9-3143-d65d-7bf3778c6b28/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/ithought-llc/id936777941?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.9", - "trackName": "Actio: The App for Action.", - "trackId": 994990008, - "sellerName": "iThought LLC", - "releaseNotes": "Bug fixes: Drag 'N Drop Bug & Minor refresh bug.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2015-06-24T18:33:48Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2017-12-05T23:51:13Z", - "trackCensoredName": "Actio: The App for Action.", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "29735536", - "sellerUrl": "http://www.iThought.biz", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/actio-the-app-for-action/id994990008?mt=12&uo=4", - "trackContentRating": "4+", - "description": "Actio: The App for Action\n\nActio is an action tracking app - great for project managers and home users alike. \n\nIt is built to be fast and flexible, intuitive to use and powerful enough for even the largest projects.\n\nYou can try the full version of Actio via in in-App free one month trial and then purchase either a full license (US$19.99*) or a monthly (US$0.99*) or yearly (US$9.99*) auto-renew subscription**. We hope you love using it.\n\nACTIO HELPS YOU:\n\nManage, organize and prioritise your actions\n• Create user-defined Flags.\n• Organize with user-definable Indexing.\n• Filter by Flag, Priority, Owner or item color to quickly focus on the important actions.\n• Sort by Index or Due Date. Hide unimportant actions.\n• Easily Promote and Demote actions into Projects and sub-Projects.\n• Define action owners in Actio, or copy from your Apple Contacts.\n\nGet stuff done quickly and efficiently\n• Fast mouse-free input of large scale projects with multiple levels of sub-projects.\n• Item and Note windows with full typographical control.\n• Unlimited Undo and Redo.\n\nTrack actions with robust detail\n• DueDate automatically colors actions, rolling them up to parent projects to easily see those with priority\n• Per-action customizable RAG (Red/Amber/Green) early warning periods.\n• Use multiple tracker views to show Lists, Details and Pageviews.\n• Rapid drill-down/up into sub-projects.\n• Drag & Drop and Drag & Duplicate actions to quickly reorganize.\n• Drag in Emails and WebPages to keep all your project information in one place.\n• Unlimited notes attached to every action.\n• Search across all title, detail and notes fields.\n• Receive Apple notifications when the state of each action item changes.\n\nGet your small business off the ground...\n• PageView window gives WYSIWYG display and control over included content. Printer ready.\n• Full-screen mode. Meeting ready.\n• File based to allow easy sharing of your project with others (e.g. via email).\n• Print Preview functions of all three views.\n\nOr reign in your priorities at home\n• DueDates, Repeating Dates and Anniversaries.\n• Check off important personal events, holidays and more\n\nUnlock Actio Unlimited via In-App-Purchase for:\n• All Free features\n• Unlimited actions and sub-actions (free version has 25 items).\n• Printing of all three views.\n• Multiple Open Projects (free version has one open project).\n• Project Tabs.\n• Drag and Drop between open Projects.\n• Drag and Duplicate between open Projects.\n• Search all open Projects.\n• Share and sync projects on file sharing servers (e.g. iCloud, Dropbox, GoogleDrive etc).\n• Save detail text to multiple formats (pdf, txt, html, doc etc)\n\nUse Actio for your day-to-day management to ensure that nothing falls through the cracks.\n\n-----------------\n* Denotes pricing on the US App Store. \n-----------------\n**Actio Unlimited Monthly and Yearly are auto-renewing subscriptions and automatically renew unless auto-renew is turned off at least 24-hours before the end of the current period. Subscriptions may be managed by the user in iTunes Account Settings.\n-----------------\nTerms of Use: www.iThought.biz/terms", - "currency": "USD", - "artistId": 936777941, - "artistName": "iThought LLC", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "biz.iThought.ActioMAS", - "version": "1.4.5", - "wrapperType": "software", - "userRatingCount": 0 - }, - { - "appletvScreenshotUrls": [], - "supportedDevices": [ - "iPadMini4-iPadMini4", - "iPadProSecondGen-iPadProSecondGen", - "iPhone11-iPhone11", - "iPad71-iPad71", - "iPadMiniRetinaCellular-iPadMiniRetinaCellular", - "iPhone8Plus-iPhone8Plus", - "iPhone6sPlus-iPhone6sPlus", - "iPadMini5-iPadMini5", - "iPadProFourthGen-iPadProFourthGen", - "iPhoneXS-iPhoneXS", - "iPadAir3Cellular-iPadAir3Cellular", - "iPadAir3-iPadAir3", - "iPadMini4Cellular-iPadMini4Cellular", - "iPadProCellular-iPadProCellular", - "MacDesktop-MacDesktop", - "iPadMini3-iPadMini3", - "iPhoneXR-iPhoneXR", - "iPhoneSE-iPhoneSE", - "iPad611-iPad611", - "iPhone7-iPhone7", - "iPad73-iPad73", - "iPad812-iPad812", - "iPadAir2Cellular-iPadAir2Cellular", - "iPhoneX-iPhoneX", - "iPadMini5Cellular-iPadMini5Cellular", - "iPadPro97-iPadPro97", - "iPad834-iPad834", - "iPadProSecondGenCellular-iPadProSecondGenCellular", - "iPhone5s-iPhone5s", - "iPad75-iPad75", - "iPadMini3Cellular-iPadMini3Cellular", - "iPad878-iPad878", - "iPhone6-iPhone6", - "iPadAir-iPadAir", - "iPadPro97Cellular-iPadPro97Cellular", - "iPadSeventhGen-iPadSeventhGen", - "iPodTouchSixthGen-iPodTouchSixthGen", - "iPhoneXSMax-iPhoneXSMax", - "iPad612-iPad612", - "iPadPro-iPadPro", - "iPodTouchSeventhGen-iPodTouchSeventhGen", - "iPhone11ProMax-iPhone11ProMax", - "iPadMiniRetina-iPadMiniRetina", - "iPad76-iPad76", - "iPadProFourthGenCellular-iPadProFourthGenCellular", - "iPadSeventhGenCellular-iPadSeventhGenCellular", - "iPhoneSESecondGen-iPhoneSESecondGen", - "iPad74-iPad74", - "iPhone6s-iPhone6s", - "iPhone7Plus-iPhone7Plus", - "iPadAir2-iPadAir2", - "iPad72-iPad72", - "iPhone6Plus-iPhone6Plus", - "iPadAirCellular-iPadAirCellular", - "iPhone8-iPhone8", - "iPad856-iPad856", - "iPhone11Pro-iPhone11Pro" - ], - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/0c/8f/c8/0c8fc884-c87a-7004-f44a-e3b333222e38/40f0748e-65b8-412e-b055-c74f457a8be7_i8-1.png/392x696bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/6d/12/99/6d12990d-2c4a-a09f-17a2-ab53b05004ba/91a4eb3f-6d59-480b-8926-a10b54dc200c_i8-2.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/cf/d0/76/cfd07658-e436-69be-dad1-f40a1d858855/97bc8ecd-7411-4eae-8a79-ccb18eecd5bf_i8-3.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/91/02/8a/91028ab6-979a-04ec-0e06-3cfc9957e754/33ef7be7-bd63-4d0e-a8e3-05bfed454285_i8-4.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/b9/a1/94/b9a194da-8062-7139-429a-c41336f46052/0a8e5d96-36ef-4e68-8460-060fb6532871_i8-5.png/392x696bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/83/91/27/8391279d-43e4-399b-373a-5ea2740da32b/0bd98917-79f5-436a-9e59-3aae11a966c4_i8-6.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/0b/8f/6e/0b8f6e54-2469-388c-6113-05e6c5a3692a/92d898e0-06ba-45b3-8ccd-3da8b20df8de_i8-7.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/5f/db/33/5fdb336c-82ed-fa17-a821-90027a51db67/8a478a41-3be5-4d73-a2aa-1e365a146e5f_i8-8.png/392x696bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/dd/9f/e4/dd9fe411-85c4-298b-60da-4b373ee5740c/05a10972-3a84-447c-9ea5-bf44268efefa_i8-9.png/392x696bb.png", - "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/25/2f/0c/252f0c7d-4c9f-d411-bf16-d415ecec293f/d1425972-f4d6-46d1-a868-f09a9f6572ef_i8-10.png/392x696bb.png" - ], - "ipadScreenshotUrls": [ - "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/bd/50/cf/bd50cf55-97ad-adec-55ec-e43ed2f26b44/594b53a3-48bd-4745-a1a3-d6457e03a426_ipad-v-1.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/69/01/0b/69010bd8-f9ce-05a4-80ac-beba32694866/861e0887-e2d3-40fe-bfba-041ab513fd50_ipad-v-2.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/91/16/ea/9116eaaa-3ded-38b1-a4d8-e1d89313b031/c156ce19-989c-4f5b-93bc-a9307fa0a9be_ipad-v-3.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/4a/cf/42/4acf4251-f8da-bdea-724c-5384be8ddb87/fb9c1e3d-856a-4fa4-9314-ea7ae0f790b4_ipad-v-4.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/ca/39/35/ca393565-6225-5729-18d2-3ea2d23abc52/4d9c971d-6096-459e-9ee2-f0af77ff6bfb_ipad-v-5.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/bc/c0/82/bcc0822b-ce04-33ff-d9bd-2380477b52a0/7795ceea-43bf-41c6-828e-5bd91d6bf4e7_ipad-v-6.png/552x414bb.png", - "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/d4/62/8e/d4628ebd-2504-6506-1e60-637beda2fcbb/ae1d6142-b2a1-45e3-8a3d-30984c075daf_ipad-v-7.png/552x414bb.png", - "https://is5-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/fa/d3/43/fad3436d-30c4-daff-dc10-5c7d4d5da752/f90faa3c-4351-43bc-be53-6da74403e829_ipad-v-8.png/552x414bb.png", - "https://is3-ssl.mzstatic.com/image/thumb/PurpleSource114/v4/63/e4/d0/63e4d099-5ba4-ac02-6ac6-c56969003f66/c46efc17-1b62-43a9-8974-0bede1a4766f_ipad-v-9.png/552x414bb.png" - ], - "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d1/2d/6f/d12d6fc4-f740-0c2e-99bc-f69f897e1302/source/60x60bb.jpg", - "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d1/2d/6f/d12d6fc4-f740-0c2e-99bc-f69f897e1302/source/512x512bb.jpg", - "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple124/v4/d1/2d/6f/d12d6fc4-f740-0c2e-99bc-f69f897e1302/source/100x100bb.jpg", - "artistViewUrl": "https://apps.apple.com/us/developer/easylife-srls/id1459685996?uo=4", - "isGameCenterEnabled": false, - "advisories": [], - "features": [ - "iosUniversal" - ], - "kind": "software", - "minimumOsVersion": "11.0", - "trackName": "easyPlanner 3 - Task manager", - "trackId": 1459685997, - "sellerName": "easyLife Srl Semplificata", - "releaseNotes": "Added \"Urgent\" quick button in task's page.\n\n• Fixed minor bugs.", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2019-04-30T21:02:33Z", - "genreIds": [ - "6007", - "6000" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-08-25T19:35:33Z", - "trackCensoredName": "easyPlanner 3 - Task manager", - "languageCodesISO2A": [ - "EN", - "FR", - "IT", - "RU", - "ZH" - ], - "fileSizeBytes": "90359808", - "sellerUrl": "https://www.easylife.biz", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 4.888889999999999957935870043002068996429443359375, - "userRatingCountForCurrentVersion": 9, - "averageUserRating": 4.888889999999999957935870043002068996429443359375, - "trackViewUrl": "https://apps.apple.com/us/app/easyplanner-3-task-manager/id1459685997?uo=4", - "trackContentRating": "4+", - "description": "easyPlanner is the innovative app that will manage for you the things to do using the tasks and projects and the things you need to remember using memos. You can manage your appointments and tasks on multiple levels (multi-levels) as your mind would! You can also monitor your progress with the Reports and see how productive you are. If you are not happy with what you are producing you can try to increase your productivity by using our FocusTimer, a tool that will allow you to maintain concentration and carry out more tasks simultaneously.\neasyPlanner aims to make your life easier and “to work” in place of your memory. Getting organized means having more free time for you and for those you love, dedicating the time you have saved to create unforgettable moments with the people of your life.\n\nMANAGE THE THINGS YOU HAVE TO DO AND THE THINGS YOU HAVE TO REMEMBER\u2028\n\nWe provide you tasks, projects and memos management in a single app.\n\nTASKS\n\neasyPlanner allows you to manage the “things you need to do” through our “Tasks”. Each task can contain infinite sub-tasks (multi-tasks) and can be grouped into folders. \n\nPROJECTS\n\nWhen a certain number of tasks are closely related to each other and they concern significant commitments, it is possible to manage them as a single project. Even the project, like the task, can contain infinite sub-tasks (multi-tasks).\n\nMEMOS\n\neasyPlanner allows you to manage the “things you need to remember” both in free format and in a structured format using memos.\n\nMONITOR YOUR REPETITIVE TASKS WITH REPORT\u2028\n\nThe \"Report\" function was born of the need to represent a set of complex data or complex tasks in the form of a table so that they can be printed and read more easily. Thanks to this function you will be able to graphically visualize a set of recurring tasks in a simple way and you will be able to monitor the progress of these tasks daily.\n\n\nINCREASE YOUR PRODUCTIVITY WITH FOCUS TIMER\n\nFocus Timer has been designed after a careful study based on several time management theories. It is a very advanced “time management” system which consists in dividing time into intervals, each of which is characterized by a task that you have to do. This division leads the subjects who use it to be much more efficient, to better manage their time and to constantly develop their mental abilities.\n\nAGENDA\n\u2028Thanks to the Agenda view easyPlanner will allow you to keep track of your schedule and your appointments at any time.\nAgenda will also integrate the Apple calendar. \nYou can add your events in the calendars directly from here in a practical and fast way.\nYou can decide which calendars to see and which to hide.\n\nICLOUD SYNC\n\nWith easyPlanner you can archive your photos, your documents, your tasks, etc. on ICloud and you can synchronize all your devices automatically and simultaneously. Regardless of the device you are using (IPhone or IPad), you will always have the latest version of your documents available.\n\nDELEGATE\n\nDelegate urgent tasks to other people by sharing them by email.\n\nAND SO MUCH MORE…\n\nYou will find out many more features trying our app or going to our website www.easyLife.biz/easyPlanner.\u2028\nCHOOSE THE VERSION THAT BEST SUITS YOU AND YOUR NEEDS\n\nTry easyPlanner – Task manager for free.\u2028You can use this version without time limits and in all its functions in order to appreciate all its features. However this version only allows 40 entries.\u2028If you start to get tight, upgrade to the easyPlanner – PRO version, available via in-app purchase or as a stand-alone version.\u2028easyPlanner – PRO Version allows you to get an unlimited number of entries, giving you the opportunity to always have everything you need with you and to take advantage of Reports over time. \nOne-time purchase. Without subscription. \n\nIf you have any question, any suggestions or if you want to tell us what you thing of easyPlanner you can contact us:\u2028\n\nwww.easylife.biz\nsupport@easylife.biz\n\nTerms of service: https://www.easylife.biz/terms/", - "currency": "USD", - "artistId": 1459685996, - "artistName": "easyLife Srls", - "genres": [ - "Productivity", - "Business" - ], - "price": 0.00, - "bundleId": "com.easyPlanner", - "version": "3.0.6", - "wrapperType": "software", - "userRatingCount": 9 - }, - { - "screenshotUrls": [ - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/e7/cb/a4/e7cba421-087e-0923-8e7f-3fb2fcff872c/pr_source.jpg/800x500bb.jpg", - "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/2f/2a/e9/2f2ae9f8-6954-dc16-81b8-cf44deb66ac0/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/e3/aa/5c/e3aa5ca0-2625-d0a3-6e30-8d22d058c810/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple123/v4/6b/5f/14/6b5f14e7-95a7-3c3d-7532-817d844d5f7a/pr_source.jpg/800x500bb.jpg", - "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/01/13/c701130f-ca88-ee48-6f49-dbbad63bcdd3/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/64/3a/4b/643a4b2d-3661-8203-7325-fc47d208f0d7/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/b7/7d/9c/b77d9c68-3707-078b-596e-53d82cc82528/pr_source.jpg/800x500bb.jpg", - "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/ad/f0/f8/adf0f884-e81f-567b-53ec-59d860132354/pr_source.jpg/800x500bb.jpg" - ], - "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/35/b8/48/35b84878-fe4d-61d1-99bb-d87ae71de6c7/source/60x60bb.png", - "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/35/b8/48/35b84878-fe4d-61d1-99bb-d87ae71de6c7/source/512x512bb.png", - "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/35/b8/48/35b84878-fe4d-61d1-99bb-d87ae71de6c7/source/100x100bb.png", - "artistViewUrl": "https://apps.apple.com/us/developer/%E6%9D%A8-%E7%94%B0/id1228008538?mt=12&uo=4", - "kind": "mac-software", - "minimumOsVersion": "10.10", - "trackName": "CleanTodo • Simplest Task List", - "trackId": 1228466824, - "sellerName": "杨 田", - "releaseNotes": "Thank you for downloading Clean Todo! Here's what's new: \n- Bug fixes and improvements. \n\nPlease contact us if you have any suggestions. support@cleantodo.com", - "primaryGenreId": 6007, - "primaryGenreName": "Productivity", - "isVppDeviceBasedLicensingEnabled": true, - "releaseDate": "2017-06-22T00:46:33Z", - "genreIds": [ - "6007", - "6002" - ], - "formattedPrice": "Free", - "currentVersionReleaseDate": "2020-04-25T21:38:32Z", - "trackCensoredName": "CleanTodo • Simplest Task List", - "languageCodesISO2A": [ - "EN" - ], - "fileSizeBytes": "3571392", - "contentAdvisoryRating": "4+", - "averageUserRatingForCurrentVersion": 0, - "userRatingCountForCurrentVersion": 0, - "averageUserRating": 0, - "trackViewUrl": "https://apps.apple.com/us/app/cleantodo-simplest-task-list/id1228466824?mt=12&uo=4", - "trackContentRating": "4+", - "description": "CleanTodo is a very simple app for managing your to-do list.\nThe app provides you two separate lists. One list records all your need to do. The other list only records your important tasks. \n\n\"The platform has a tidy, minimalist look that allows users to keep all of their tasks in one convenient place.\" - The American Genius\n\n\"On top of a simple operation, it also offers you the cleanest interface.\" - KOCPC,TW\n\n\"A simple to-do work list makes writing down important work items easy.\" - iPhone, iPad玩樂誌 Vol.49.\n\n\nThe Benefits of Clean Todo:\n\n• Because recording your tasks has become so simple, you will be happy to write things down and will never forget what you need to do.\n• You decide what you should do every day, rather than being forced to plan ahead and have your software urge you.\n• Avoid wasting time managing your to-do list and save time doing something meaningful.\n• Look at your weekly review and feel a great sense of accomplishment.\n• The simple and elegant software interface accentuates your fine taste.\n\nFeatures:\n\n- The cloud sync feature ensures your data remains exactly the same across devices.\n- No matter how many tasks you have, they are simply divided into to be done now and to be done later. This allows you to check your to-do list and place the important things into your \"Today\" list. Do the most important thing every day.\n- Use tags to distinguish between different categories of tasks. These tags are very easy to create and use.\n- Flip Card View: You can flip through and complete tasks one by one. This helps you focus on the present task.\n- You can create reminders that will be added directly to Calendar, which gives you accurate alerts even without Internet connection.\n- Weekly Review: helps you to check the tasks you’ve completed over the week and export a report easily.\n- Drag&Drop to reorder tasks.\n- Quickly add task, the default shortcut is Ctrl+Cmd+Z.\n\nUpgrade to premium membership to gain access to all the features. Once subscribed, all these features will be available to you for use on all platforms without additional costs.\n\nPremium Subscription: $12.99/Year\n\n• Subscription is auto-renewable which means that once purchased it will be auto-renewed every year until you cancel it 24 hours prior to the end of the current period. \n• Your subscription will be charged to your iTunes account at confirmation of purchase and will automatically renew unless auto-renew is turned off at least 24 hours before the end of the current period.\n• Current subscription may not be canceled during the active subscription period; however, you can manage your subscription and/or turn off auto-renewal by visiting your iTunes Account Settings after purchase.\n\nPrivacy Policy: https://www.cleantodo.com/privacy\nTerms & Conditions: https://www.cleantodo.com/terms", - "currency": "USD", - "artistId": 1228008538, - "artistName": "杨 田", - "genres": [ - "Productivity", - "Utilities" - ], - "price": 0.00, - "bundleId": "com.cleantodo.osx", - "version": "1.10", - "wrapperType": "software", - "userRatingCount": 0 - } - ] - } diff --git a/Tests/MasKitTests/Models/SearchResultListSpec.swift b/Tests/MasKitTests/Models/SearchResultListSpec.swift deleted file mode 100644 index 137c907..0000000 --- a/Tests/MasKitTests/Models/SearchResultListSpec.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// SearchResultListSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 9/2/20. -// Copyright © 2020 mas-cli. All rights reserved. -// - -import Foundation -import Nimble -import Quick - -@testable import MasKit - -public class SearchResultListSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("search result list") { - it("can parse bbedit") { - let data = Data(from: "search/bbedit.json") - let decoder = JSONDecoder() - let results = try decoder.decode(SearchResultList.self, from: data) - - expect(results.resultCount) == 1 - } - it("can parse things") { - let data = Data(from: "search/things.json") - let decoder = JSONDecoder() - let results = try decoder.decode(SearchResultList.self, from: data) - - expect(results.resultCount) == 50 - } - } - } -} diff --git a/Tests/MasKitTests/Nimble/ResultPredicates.swift b/Tests/MasKitTests/Nimble/ResultPredicates.swift deleted file mode 100644 index 209a3f6..0000000 --- a/Tests/MasKitTests/Nimble/ResultPredicates.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ResultPredicates.swift -// MasKitTests -// -// Created by Ben Chatelain on 12/27/18. -// Copyright © 2018 mas-cli. All rights reserved. -// - -import Nimble - -@testable import MasKit - -/// Nimble predicate for result enum success case, no associated value -func beSuccess() -> Predicate> { - Predicate.define("be ") { expression, message in - if case .success = try expression.evaluate() { - return PredicateResult(status: .matches, message: message) - } - return PredicateResult(status: .fail, message: message) - } -} - -/// Nimble predicate for result enum failure with associated error -func beFailure(test: @escaping (MASError) -> Void = { _ in }) -> Predicate> { - Predicate.define("be ") { expression, message in - if case .failure(let error) = try expression.evaluate() { - test(error) - return PredicateResult(status: .matches, message: message) - } - return PredicateResult(status: .fail, message: message) - } -} diff --git a/Tests/MasKitTests/OutputListener.swift b/Tests/MasKitTests/OutputListener.swift deleted file mode 100644 index 434655e..0000000 --- a/Tests/MasKitTests/OutputListener.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// OutputListener.swift -// MasKitTests -// -// Created by Ben Chatelain on 1/7/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -@testable import MasKit - -/// Test helper for monitoring strings written to stdout. Modified from: -/// https://stackoverflow.com/a/53569018 -class OutputListener { - /// Buffers strings written to stdout - var contents = "" - - init() { - printObserver = { [weak self] text in - strongify(self) { context in - context.contents += text - } - } - } - - deinit { - printObserver = nil - } -} diff --git a/Tests/MasKitTests/OutputListenerSpec.swift b/Tests/MasKitTests/OutputListenerSpec.swift deleted file mode 100644 index f793f19..0000000 --- a/Tests/MasKitTests/OutputListenerSpec.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// OutputListenerSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 1/8/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Nimble -import Quick - -@testable import MasKit - -public class OutputListenerSpec: QuickSpec { - override public func spec() { - beforeSuite { - MasKit.initialize() - } - describe("output listener") { - it("can intercept a single line written stdout") { - let output = OutputListener() - let expectedOutput = "hi there" - - print("hi there", terminator: "") - - expect(output.contents) == expectedOutput - } - it("can intercept multiple lines written stdout") { - let output = OutputListener() - let expectedOutput = """ - hi there - - """ - - print("hi there") - - expect(output.contents) == expectedOutput - } - } - } -} diff --git a/Tests/MasKitTests/Strongify.swift b/Tests/MasKitTests/Strongify.swift deleted file mode 100644 index 6ab81fd..0000000 --- a/Tests/MasKitTests/Strongify.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Strongify.swift -// MasKitTests -// -// Created by Ben Chatelain on 1/8/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -// https://medium.com/@merowing_/stop-weak-strong-dance-in-swift-3aec6d3563d4 - -func strongify(_ context: Context?, closure: (Context) -> Void) { - guard let strongContext = context else { return } - closure(strongContext) -} diff --git a/Tests/masTests/.swift-format b/Tests/masTests/.swift-format new file mode 100644 index 0000000..571f02f --- /dev/null +++ b/Tests/masTests/.swift-format @@ -0,0 +1,62 @@ +{ + "indentConditionalCompilationBlocks": false, + "indentation": { + "spaces": 4 + }, + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineBreakBetweenDeclarationAttributes": true, + "lineLength": 120, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "prioritizeKeepingFunctionOutputTogether": true, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true + }, + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "TrailingComma": false, + "version": 1 +} diff --git a/Tests/MasKitTests/.swiftlint.yml b/Tests/masTests/.swiftlint.yml similarity index 65% rename from Tests/MasKitTests/.swiftlint.yml rename to Tests/masTests/.swiftlint.yml index d6780ba..dc0cbcd 100644 --- a/Tests/MasKitTests/.swiftlint.yml +++ b/Tests/masTests/.swiftlint.yml @@ -1,6 +1,6 @@ # # .swiftlint.yml -# MasKitTests +# masTests # # https://github.com/realm/SwiftLint#configuration # @@ -9,4 +9,5 @@ disabled_rules: - force_cast - force_try - - function_body_length + - implicitly_unwrapped_optional + - no_magic_numbers diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift new file mode 100644 index 0000000..8885916 --- /dev/null +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -0,0 +1,30 @@ +// +// AccountSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +/// Deprecated test. +public class AccountSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#known-issues + describe("Account command") { + it("displays active account") { + expect { + try MAS.Account.parse([]).run() + } + .to(throwError(MASError.notSupported)) + } + } + } +} diff --git a/Tests/masTests/Commands/HomeSpec.swift b/Tests/masTests/Commands/HomeSpec.swift new file mode 100644 index 0000000..8d3faac --- /dev/null +++ b/Tests/masTests/Commands/HomeSpec.swift @@ -0,0 +1,54 @@ +// +// HomeSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-29. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +public class HomeSpec: QuickSpec { + override public func spec() { + let searcher = MockAppStoreSearcher() + let openCommand = MockOpenSystemCommand() + + beforeSuite { + MAS.initialize() + } + describe("home command") { + beforeEach { + searcher.reset() + } + it("fails to open app with invalid ID") { + expect { + try MAS.Home.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand) + } + .to(throwError()) + } + it("can't find app with unknown ID") { + expect { + try MAS.Home.parse(["999"]).run(searcher: searcher, openCommand: openCommand) + } + .to(throwError(MASError.noSearchResultsFound)) + } + it("opens app on MAS Preview") { + let mockResult = SearchResult( + trackId: 1111, + trackViewUrl: "mas preview url", + version: "0.0" + ) + searcher.apps[mockResult.trackId] = mockResult + expect { + try MAS.Home.parse([String(mockResult.trackId)]) + .run(searcher: searcher, openCommand: openCommand) + return openCommand.arguments + } + == [mockResult.trackViewUrl] + } + } + } +} diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift new file mode 100644 index 0000000..04b393c --- /dev/null +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -0,0 +1,68 @@ +// +// InfoSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class InfoSpec: QuickSpec { + override public func spec() { + let searcher = MockAppStoreSearcher() + + beforeSuite { + MAS.initialize() + } + describe("Info command") { + beforeEach { + searcher.reset() + } + it("fails to open app with invalid ID") { + expect { + try MAS.Info.parse(["--", "-999"]).run(searcher: searcher) + } + .to(throwError()) + } + it("can't find app with unknown ID") { + expect { + try MAS.Info.parse(["999"]).run(searcher: searcher) + } + .to(throwError(MASError.noSearchResultsFound)) + } + it("displays app details") { + let mockResult = SearchResult( + currentVersionReleaseDate: "2019-01-07T18:53:13Z", + fileSizeBytes: "1024", + formattedPrice: "$2.00", + minimumOsVersion: "10.14", + sellerName: "Awesome Dev", + trackId: 1111, + trackName: "Awesome App", + trackViewUrl: "https://awesome.app", + version: "1.0" + ) + searcher.apps[mockResult.trackId] = mockResult + expect { + try captureStream(stdout) { + try MAS.Info.parse([String(mockResult.trackId)]).run(searcher: searcher) + } + } + == """ + Awesome App 1.0 [$2.00] + By: Awesome Dev + Released: 2019-01-07 + Minimum OS: 10.14 + Size: 1 KB + From: https://awesome.app + + """ + } + } + } +} diff --git a/Tests/masTests/Commands/InstallSpec.swift b/Tests/masTests/Commands/InstallSpec.swift new file mode 100644 index 0000000..e2074c6 --- /dev/null +++ b/Tests/masTests/Commands/InstallSpec.swift @@ -0,0 +1,28 @@ +// +// InstallSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +public class InstallSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + xdescribe("install command") { + xit("installs apps") { + expect { + try MAS.Install.parse([]).run(appLibrary: MockAppLibrary()) + } + .toNot(throwError()) + } + } + } +} diff --git a/Tests/masTests/Commands/ListSpec.swift b/Tests/masTests/Commands/ListSpec.swift new file mode 100644 index 0000000..e9c02d6 --- /dev/null +++ b/Tests/masTests/Commands/ListSpec.swift @@ -0,0 +1,31 @@ +// +// ListSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-27. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class ListSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + describe("list command") { + it("lists apps") { + expect { + try captureStream(stderr) { + try MAS.List.parse([]).run(appLibrary: MockAppLibrary()) + } + } + == "Error: No installed apps found\n" + } + } + } +} diff --git a/Tests/masTests/Commands/LuckySpec.swift b/Tests/masTests/Commands/LuckySpec.swift new file mode 100644 index 0000000..e288739 --- /dev/null +++ b/Tests/masTests/Commands/LuckySpec.swift @@ -0,0 +1,31 @@ +// +// LuckySpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +public class LuckySpec: QuickSpec { + override public func spec() { + let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json") + let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) + + beforeSuite { + MAS.initialize() + } + xdescribe("lucky command") { + xit("installs the first app matching a search") { + expect { + try MAS.Lucky.parse(["Slack"]).run(appLibrary: MockAppLibrary(), searcher: searcher) + } + .toNot(throwError()) + } + } + } +} diff --git a/Tests/masTests/Commands/OpenSpec.swift b/Tests/masTests/Commands/OpenSpec.swift new file mode 100644 index 0000000..5f3ee72 --- /dev/null +++ b/Tests/masTests/Commands/OpenSpec.swift @@ -0,0 +1,62 @@ +// +// OpenSpec.swift +// masTests +// +// Created by Ben Chatelain on 2019-01-03. +// Copyright © 2019 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class OpenSpec: QuickSpec { + override public func spec() { + let searcher = MockAppStoreSearcher() + let openCommand = MockOpenSystemCommand() + + beforeSuite { + MAS.initialize() + } + describe("open command") { + beforeEach { + searcher.reset() + } + it("fails to open app with invalid ID") { + expect { + try MAS.Open.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand) + } + .to(throwError()) + } + it("can't find app with unknown ID") { + expect { + try MAS.Open.parse(["999"]).run(searcher: searcher, openCommand: openCommand) + } + .to(throwError(MASError.noSearchResultsFound)) + } + it("opens app in MAS") { + let mockResult = SearchResult( + trackId: 1111, + trackViewUrl: "fakescheme://some/url", + version: "0.0" + ) + searcher.apps[mockResult.trackId] = mockResult + expect { + try MAS.Open.parse([mockResult.trackId.description]) + .run(searcher: searcher, openCommand: openCommand) + return openCommand.arguments + } + == ["macappstore://some/url"] + } + it("just opens MAS if no app specified") { + expect { + try MAS.Open.parse([]).run(searcher: searcher, openCommand: openCommand) + return openCommand.arguments + } + == ["macappstore://"] + } + } + } +} diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift new file mode 100644 index 0000000..95ced8e --- /dev/null +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -0,0 +1,58 @@ +// +// OutdatedSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class OutdatedSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + describe("outdated command") { + it("displays apps with pending updates") { + let mockSearchResult = + SearchResult( + bundleId: "au.id.haroldchu.mac.Bandwidth", + currentVersionReleaseDate: "2024-09-02T00:27:00Z", + fileSizeBytes: "998130", + minimumOsVersion: "10.13", + price: 0, + sellerName: "Harold Chu", + sellerUrl: "https://example.com", + trackId: 490_461_369, + trackName: "Bandwidth+", + trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4", + version: "1.28" + ) + let searcher = MockAppStoreSearcher() + searcher.apps[mockSearchResult.trackId] = mockSearchResult + + let mockAppLibrary = MockAppLibrary() + mockAppLibrary.installedApps.append( + MockSoftwareProduct( + appName: mockSearchResult.trackName, + bundleIdentifier: mockSearchResult.bundleId, + bundlePath: "/Applications/Bandwidth+.app", + bundleVersion: "1.27", + itemIdentifier: NSNumber(value: mockSearchResult.trackId) + ) + ) + expect { + try captureStream(stdout) { + try MAS.Outdated.parse([]).run(appLibrary: mockAppLibrary, searcher: searcher) + } + } + == "490461369 Bandwidth+ (1.27 -> 1.28)\n" + } + } + } +} diff --git a/Tests/masTests/Commands/PurchaseSpec.swift b/Tests/masTests/Commands/PurchaseSpec.swift new file mode 100644 index 0000000..ac9db18 --- /dev/null +++ b/Tests/masTests/Commands/PurchaseSpec.swift @@ -0,0 +1,28 @@ +// +// PurchaseSpec.swift +// masTests +// +// Created by Maximilian Blochberger on 2020-03-21. +// Copyright © 2020 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +public class PurchaseSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + xdescribe("purchase command") { + xit("purchases apps") { + expect { + try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary()) + } + .toNot(throwError()) + } + } + } +} diff --git a/Tests/MasKitTests/Commands/ResetCommandSpec.swift b/Tests/masTests/Commands/ResetSpec.swift similarity index 51% rename from Tests/MasKitTests/Commands/ResetCommandSpec.swift rename to Tests/masTests/Commands/ResetSpec.swift index 6150274..96e8adb 100644 --- a/Tests/MasKitTests/Commands/ResetCommandSpec.swift +++ b/Tests/masTests/Commands/ResetSpec.swift @@ -1,6 +1,6 @@ // -// ResetCommandSpec.swift -// MasKitTests +// ResetSpec.swift +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,18 +9,19 @@ import Nimble import Quick -@testable import MasKit +@testable import mas -public class ResetCommandSpec: QuickSpec { +public class ResetSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("reset command") { it("resets the App Store state") { - let cmd = ResetCommand() - let result = cmd.run(ResetCommand.Options(debug: false)) - expect(result).to(beSuccess()) + expect { + try MAS.Reset.parse([]).run() + } + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift new file mode 100644 index 0000000..183e79f --- /dev/null +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -0,0 +1,49 @@ +// +// SearchSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class SearchSpec: QuickSpec { + override public func spec() { + let searcher = MockAppStoreSearcher() + + beforeSuite { + MAS.initialize() + } + describe("search command") { + beforeEach { + searcher.reset() + } + it("can find slack") { + let mockResult = SearchResult( + trackId: 1111, + trackName: "slack", + trackViewUrl: "mas preview url", + version: "0.0" + ) + searcher.apps[mockResult.trackId] = mockResult + expect { + try captureStream(stdout) { + try MAS.Search.parse(["slack"]).run(searcher: searcher) + } + } + == " 1111 slack (0.0)\n" + } + it("fails when searching for nonexistent app") { + expect { + try MAS.Search.parse(["nonexistent"]).run(searcher: searcher) + } + .to(throwError(MASError.noSearchResultsFound)) + } + } + } +} diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift new file mode 100644 index 0000000..ceea01c --- /dev/null +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -0,0 +1,30 @@ +// +// SignInSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +/// Deprecated test. +public class SignInSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#known-issues + describe("signin command") { + it("signs in") { + expect { + try MAS.SignIn.parse(["", ""]).run() + } + .to(throwError(MASError.notSupported)) + } + } + } +} diff --git a/Tests/MasKitTests/Commands/SignOutCommandSpec.swift b/Tests/masTests/Commands/SignOutSpec.swift similarity index 50% rename from Tests/MasKitTests/Commands/SignOutCommandSpec.swift rename to Tests/masTests/Commands/SignOutSpec.swift index 6a6bd61..7aeaa01 100644 --- a/Tests/MasKitTests/Commands/SignOutCommandSpec.swift +++ b/Tests/masTests/Commands/SignOutSpec.swift @@ -1,6 +1,6 @@ // -// SignOutCommandSpec.swift -// MasKitTests +// SignOutSpec.swift +// masTests // // Created by Ben Chatelain on 2018-12-28. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,18 +9,19 @@ import Nimble import Quick -@testable import MasKit +@testable import mas -public class SignOutCommandSpec: QuickSpec { +public class SignOutSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("signout command") { it("signs out") { - let cmd = SignOutCommand() - let result = cmd.run(SignOutCommand.Options()) - expect(result).to(beSuccess()) + expect { + try MAS.SignOut.parse([]).run() + } + .toNot(throwError()) } } } diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift new file mode 100644 index 0000000..7ea7c44 --- /dev/null +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -0,0 +1,88 @@ +// +// UninstallSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-27. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class UninstallSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + xdescribe("uninstall command") { + let appID: AppID = 12345 + let app = MockSoftwareProduct( + appName: "Some App", + bundleIdentifier: "com.some.app", + bundlePath: "/tmp/Some.app", + bundleVersion: "1.0", + itemIdentifier: NSNumber(value: appID) + ) + let mockLibrary = MockAppLibrary() + + context("dry run") { + let uninstall = try! MAS.Uninstall.parse(["--dry-run", String(appID)]) + + beforeEach { + mockLibrary.reset() + } + it("can't remove a missing app") { + expect { + try uninstall.run(appLibrary: mockLibrary) + } + .to(throwError(MASError.notInstalled(appID: appID))) + } + it("finds an app") { + mockLibrary.installedApps.append(app) + expect { + try captureStream(stdout) { + try uninstall.run(appLibrary: mockLibrary) + } + } + == "==> 'Some App' '/tmp/Some.app'\n==> (not removed, dry run)\n" + } + } + context("wet run") { + let uninstall = try! MAS.Uninstall.parse([String(appID)]) + + beforeEach { + mockLibrary.reset() + } + it("can't remove a missing app") { + expect { + try uninstall.run(appLibrary: mockLibrary) + } + .to(throwError(MASError.notInstalled(appID: appID))) + } + it("removes an app") { + mockLibrary.installedApps.append(app) + expect { + try captureStream(stdout) { + try uninstall.run(appLibrary: mockLibrary) + } + } + .toNot(throwError()) + } + it("fails if there is a problem with the trash command") { + var brokenApp = app + brokenApp.bundlePath = "/dev/null" + mockLibrary.installedApps.append(brokenApp) + expect { + try captureStream(stdout) { + try uninstall.run(appLibrary: mockLibrary) + } + } + .to(throwError(MASError.uninstallFailed(error: nil))) + } + } + } + } +} diff --git a/Tests/masTests/Commands/UpgradeSpec.swift b/Tests/masTests/Commands/UpgradeSpec.swift new file mode 100644 index 0000000..6630777 --- /dev/null +++ b/Tests/masTests/Commands/UpgradeSpec.swift @@ -0,0 +1,32 @@ +// +// UpgradeSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class UpgradeSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + describe("upgrade command") { + it("finds no upgrades") { + expect { + try captureStream(stderr) { + try MAS.Upgrade.parse([]) + .run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher()) + } + } + == "Warning: Nothing found to upgrade\n" + } + } + } +} diff --git a/Tests/masTests/Commands/VendorSpec.swift b/Tests/masTests/Commands/VendorSpec.swift new file mode 100644 index 0000000..1834cb3 --- /dev/null +++ b/Tests/masTests/Commands/VendorSpec.swift @@ -0,0 +1,55 @@ +// +// VendorSpec.swift +// masTests +// +// Created by Ben Chatelain on 2019-01-03. +// Copyright © 2019 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +public class VendorSpec: QuickSpec { + override public func spec() { + let searcher = MockAppStoreSearcher() + let openCommand = MockOpenSystemCommand() + + beforeSuite { + MAS.initialize() + } + describe("vendor command") { + beforeEach { + searcher.reset() + } + it("fails to open app with invalid ID") { + expect { + try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand) + } + .to(throwError()) + } + it("can't find app with unknown ID") { + expect { + try MAS.Vendor.parse(["999"]).run(searcher: searcher, openCommand: openCommand) + } + .to(throwError(MASError.noSearchResultsFound)) + } + it("opens vendor app page in browser") { + let mockResult = SearchResult( + sellerUrl: "https://awesome.app", + trackId: 1111, + trackViewUrl: "https://apps.apple.com/us/app/awesome/id1111?mt=12&uo=4", + version: "0.0" + ) + searcher.apps[mockResult.trackId] = mockResult + expect { + try MAS.Vendor.parse([String(mockResult.trackId)]) + .run(searcher: searcher, openCommand: openCommand) + return openCommand.arguments + } + == [mockResult.sellerUrl] + } + } + } +} diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift new file mode 100644 index 0000000..ec66b55 --- /dev/null +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -0,0 +1,31 @@ +// +// VersionSpec.swift +// masTests +// +// Created by Ben Chatelain on 2018-12-28. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class VersionSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + describe("version command") { + it("displays the current version") { + expect { + try captureStream(stdout) { + try MAS.Version.parse([]).run() + } + } + == "\(Package.version)\n" + } + } + } +} diff --git a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift new file mode 100644 index 0000000..3b72ee8 --- /dev/null +++ b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift @@ -0,0 +1,78 @@ +// +// ITunesSearchAppStoreSearcherSpec.swift +// masTests +// +// Created by Ben Chatelain on 1/4/19. +// Copyright © 2019 mas-cli. All rights reserved. +// + +import Nimble +import Quick + +@testable import mas + +public class ITunesSearchAppStoreSearcherSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + describe("url string") { + it("contains the app name") { + expect { + ITunesSearchAppStoreSearcher().searchURL(for: "myapp", inCountry: "US")?.absoluteString + } + == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp" + } + it("contains the encoded app name") { + expect { + ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString + } + == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App" + } + } + describe("store") { + context("when searched") { + it("can find slack") { + let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json") + let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) + + expect { + try searcher.search(for: "slack").wait() + } + .to(haveCount(39)) + } + } + + context("when lookup used") { + it("can find slack") { + let appID: AppID = 803_453_959 + let networkSession = MockFromFileNetworkSession(responseFile: "lookup/slack.json") + let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession)) + + var result: SearchResult? + do { + result = try searcher.lookup(appID: appID).wait() + } catch { + let maserror = error as! MASError + if case .jsonParsing(let nserror) = maserror { + fail("\(maserror) \(nserror!)") + } + } + + guard let result else { + fatalError("lookup result was nil") + } + + expect(result.trackId) == appID + expect(result.bundleId) == "com.tinyspeck.slackmacgap" + expect(result.price) == 0 + expect(result.sellerName) == "Slack Technologies, Inc." + expect(result.sellerUrl) == "https://slack.com" + expect(result.trackName) == "Slack" + expect(result.trackViewUrl) == "https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4" + expect(result.version) == "3.3.3" + } + } + } + } +} diff --git a/Tests/masTests/Controllers/MockAppLibrary.swift b/Tests/masTests/Controllers/MockAppLibrary.swift new file mode 100644 index 0000000..c992abc --- /dev/null +++ b/Tests/masTests/Controllers/MockAppLibrary.swift @@ -0,0 +1,28 @@ +// +// MockAppLibrary.swift +// masTests +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +@testable import mas + +class MockAppLibrary: AppLibrary { + var installedApps: [SoftwareProduct] = [] + + func uninstallApps(atPaths appPaths: [String]) throws { + // Special case for testing where we pretend the trash command failed + if appPaths.contains("/dev/null") { + throw MASError.uninstallFailed(error: nil) + } + } +} + +/// Members not part of the AppLibrary protocol that are only for test state management. +extension MockAppLibrary { + /// Clears out the list of installed apps. + func reset() { + installedApps = [] + } +} diff --git a/Tests/masTests/Controllers/MockAppStoreSearcher.swift b/Tests/masTests/Controllers/MockAppStoreSearcher.swift new file mode 100644 index 0000000..1df0ef6 --- /dev/null +++ b/Tests/masTests/Controllers/MockAppStoreSearcher.swift @@ -0,0 +1,31 @@ +// +// MockAppStoreSearcher.swift +// masTests +// +// Created by Ben Chatelain on 1/4/19. +// Copyright © 2019 mas-cli. All rights reserved. +// + +import PromiseKit + +@testable import mas + +class MockAppStoreSearcher: AppStoreSearcher { + var apps: [AppID: SearchResult] = [:] + + func search(for searchTerm: String) -> Promise<[SearchResult]> { + .value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 }) + } + + func lookup(appID: AppID) -> Promise { + guard let result = apps[appID] else { + return Promise(error: MASError.noSearchResultsFound) + } + + return .value(result) + } + + func reset() { + apps = [:] + } +} diff --git a/Tests/MasKitTests/Controllers/MasAppLibrarySpec.swift b/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift similarity index 65% rename from Tests/MasKitTests/Controllers/MasAppLibrarySpec.swift rename to Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift index 9409879..c4e50ca 100644 --- a/Tests/MasKitTests/Controllers/MasAppLibrarySpec.swift +++ b/Tests/masTests/Controllers/SoftwareMapAppLibrarySpec.swift @@ -1,6 +1,6 @@ // -// MasAppLibrarySpec.swift -// MasKitTests +// SoftwareMapAppLibrarySpec.swift +// masTests // // Created by Ben Chatelain on 3/1/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,30 +9,29 @@ import Nimble import Quick -@testable import MasKit +@testable import mas -public class MasAppLibrarySpec: QuickSpec { +public class SoftwareMapAppLibrarySpec: QuickSpec { override public func spec() { - let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps)) + let library = SoftwareMapAppLibrary(softwareMap: MockSoftwareMap(products: apps)) beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("mas app library") { it("contains all installed apps") { - expect(library.installedApps.count) == apps.count + expect(library.installedApps).to(haveCount(apps.count)) expect(library.installedApps.first!.appName) == myApp.appName } it("can locate an app by bundle id") { - let app = library.installedApp(forBundleId: "com.example")! - expect(app.bundleIdentifier) == myApp.bundleIdentifier + expect(library.installedApp(forBundleID: "com.example")!.bundleIdentifier) == myApp.bundleIdentifier } } } } // MARK: - Test Data -let myApp = SoftwareProductMock( +let myApp = MockSoftwareProduct( appName: "MyApp", bundleIdentifier: "com.example", bundlePath: "/Applications/MyApp.app", @@ -42,8 +41,8 @@ let myApp = SoftwareProductMock( var apps: [SoftwareProduct] = [myApp] -// MARK: - SoftwareMapMock -struct SoftwareMapMock: SoftwareMap { +// MARK: - MockSoftwareMap +struct MockSoftwareMap: SoftwareMap { var products: [SoftwareProduct] = [] func allSoftwareProducts() -> [SoftwareProduct] { diff --git a/Tests/MasKitTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift similarity index 79% rename from Tests/MasKitTests/Errors/MASErrorTestCase.swift rename to Tests/masTests/Errors/MASErrorTestCase.swift index cdefffd..027a196 100644 --- a/Tests/MasKitTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -1,6 +1,6 @@ // // MASErrorTestCase.swift -// mas-tests +// masTests // // Created by Ben Chatelain on 2/11/18. // Copyright © 2018 Andrew Naylor. All rights reserved. @@ -9,20 +9,22 @@ import Foundation import XCTest -@testable import MasKit +@testable import mas class MASErrorTestCase: XCTestCase { private let errorDomain = "MAS" - var error: MASError! - var nserror: NSError! + private var error: MASError! + private var nserror: NSError! /// Convenience property for setting the value which will be use for the localized description - /// value of the next NSError created. Only used when the NSError does not have a user info + /// value of the next NSError created. + /// + /// Only used when the NSError does not have a user info /// entry for localized description. - var localizedDescription: String { + private var localizedDescription: String { get { "dummy value" } set { - NSError.setUserInfoValueProvider(forDomain: errorDomain) { (_: Error, _: String) -> Any? in + NSError.setUserInfoValueProvider(forDomain: errorDomain) { _, _ in newValue } } @@ -30,7 +32,7 @@ class MASErrorTestCase: XCTestCase { override func setUp() { super.setUp() - MasKit.initialize() + MAS.initialize() nserror = NSError(domain: errorDomain, code: 999) localizedDescription = "foo" } @@ -57,8 +59,8 @@ class MASErrorTestCase: XCTestCase { } func testAlreadySignedIn() { - error = .alreadySignedIn - XCTAssertEqual(error.description, "Already signed in") + error = .alreadySignedIn(asAppleID: "person@example.com") + XCTAssertEqual(error.description, "Already signed in as person@example.com") } func testPurchaseFailed() { @@ -107,12 +109,12 @@ class MASErrorTestCase: XCTestCase { } func testNotInstalled() { - error = .notInstalled - XCTAssertEqual(error.description, "Not installed") + error = .notInstalled(appID: 123) + XCTAssertEqual(error.description, "No apps installed with app ID 123") } func testUninstallFailed() { - error = .uninstallFailed + error = .uninstallFailed(error: nil) XCTAssertEqual(error.description, "Uninstall failed") } @@ -122,7 +124,7 @@ class MASErrorTestCase: XCTestCase { } func testJsonParsing() { - error = .jsonParsing(error: nil) - XCTAssertEqual(error.description, "Unable to parse response JSON") + error = .jsonParsing(data: nil) + XCTAssertEqual(error.description, "Received empty response") } } diff --git a/Tests/MasKitTests/Extensions/Bundle+JSON.swift b/Tests/masTests/Extensions/Bundle+JSON.swift similarity index 84% rename from Tests/MasKitTests/Extensions/Bundle+JSON.swift rename to Tests/masTests/Extensions/Bundle+JSON.swift index 1159243..19c2c0d 100644 --- a/Tests/MasKitTests/Extensions/Bundle+JSON.swift +++ b/Tests/masTests/Extensions/Bundle+JSON.swift @@ -1,6 +1,6 @@ // // Bundle+JSON.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -10,7 +10,8 @@ import Foundation extension Data { /// Unsafe initializer for loading data from string paths. - /// - Parameter file: Relative path within the JSON folder + /// + /// - Parameter fileName: Relative path within the JSON folder init(from fileName: String) { let fileURL = Bundle.url(for: fileName)! try! self.init(contentsOf: fileURL, options: .mappedIfSafe) @@ -25,11 +26,12 @@ extension Bundle { static func url(for fileName: String) -> URL? { // The Swift Package Manager places resources in a separate bundle from the executable. // https://forums.swift.org/t/swift-5-3-spm-resources-in-tests-uses-wrong-bundle-path/37051 - let bundleURL = Bundle(for: NetworkSessionMock.self) + let bundleURL = Bundle(for: MockNetworkSession.self) .bundleURL .deletingLastPathComponent() - .appendingPathComponent("mas_MasKitTests.bundle") - guard let bundle = Bundle(url: bundleURL), + .appendingPathComponent("mas_masTests.bundle") + guard + let bundle = Bundle(url: bundleURL), let url = bundle.url(for: fileName) else { fatalError("Unable to load file \(fileName)") diff --git a/Tests/MasKitTests/Extensions/String+FileExtension.swift b/Tests/masTests/Extensions/String+FileExtension.swift similarity index 96% rename from Tests/MasKitTests/Extensions/String+FileExtension.swift rename to Tests/masTests/Extensions/String+FileExtension.swift index 1c08969..6f8649a 100644 --- a/Tests/MasKitTests/Extensions/String+FileExtension.swift +++ b/Tests/masTests/Extensions/String+FileExtension.swift @@ -1,6 +1,6 @@ // // String+FileExtension.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift b/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift similarity index 70% rename from Tests/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift rename to Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift index 96b56df..da30e13 100644 --- a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift +++ b/Tests/masTests/ExternalCommands/MockOpenSystemCommand.swift @@ -1,6 +1,6 @@ // -// OpenSystemCommandMock.swift -// MasKitTests +// MockOpenSystemCommand.swift +// masTests // // Created by Ben Chatelain on 1/4/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,12 +8,12 @@ import Foundation -@testable import MasKit +@testable import mas -class OpenSystemCommandMock: ExternalCommand { +class MockOpenSystemCommand: ExternalCommand { // Stub out protocol logic var succeeded = true - var arguments: [String]? + var arguments: [String] = [] // unused var binaryPath = "/dev/null" @@ -21,8 +21,6 @@ class OpenSystemCommandMock: ExternalCommand { var stdoutPipe = Pipe() var stderrPipe = Pipe() - init() {} - func run(arguments: String...) throws { self.arguments = arguments } diff --git a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandSpec.swift b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift similarity index 62% rename from Tests/MasKitTests/ExternalCommands/OpenSystemCommandSpec.swift rename to Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift index 9e479fc..9b8c1f5 100644 --- a/Tests/MasKitTests/ExternalCommands/OpenSystemCommandSpec.swift +++ b/Tests/masTests/ExternalCommands/OpenSystemCommandSpec.swift @@ -1,6 +1,6 @@ // // OpenSystemCommandSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 2/24/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,22 +9,20 @@ import Nimble import Quick -@testable import MasKit +@testable import mas public class OpenSystemCommandSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("open system command") { context("binary path") { it("defaults to the macOS open command") { - let cmd = OpenSystemCommand() - expect(cmd.binaryPath) == "/usr/bin/open" + expect(OpenSystemCommand().binaryPath) == "/usr/bin/open" } it("can be overridden") { - let cmd = OpenSystemCommand(binaryPath: "/dev/null") - expect(cmd.binaryPath) == "/dev/null" + expect(OpenSystemCommand(binaryPath: "/dev/null").binaryPath) == "/dev/null" } } } diff --git a/Tests/MasKitTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift similarity index 70% rename from Tests/MasKitTests/Formatters/AppListFormatterSpec.swift rename to Tests/masTests/Formatters/AppListFormatterSpec.swift index 91b6faf..1b92397 100644 --- a/Tests/MasKitTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -1,6 +1,6 @@ // // AppListFormatterSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 8/23/2020. // Copyright © 2020 mas-cli. All rights reserved. @@ -9,46 +9,44 @@ import Nimble import Quick -@testable import MasKit +@testable import mas -public class AppListsFormatterSpec: QuickSpec { +public class AppListFormatterSpec: QuickSpec { override public func spec() { // static func reference let format = AppListFormatter.format(products:) var products: [SoftwareProduct] = [] beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("app list formatter") { beforeEach { products = [] } it("formats nothing as empty string") { - let output = format(products) - expect(output) == "" + expect(format(products)).to(beEmpty()) } it("can format a single product") { - let product = SoftwareProductMock( + let product = MockSoftwareProduct( appName: "Awesome App", bundleIdentifier: "", bundlePath: "", bundleVersion: "19.2.1", itemIdentifier: 12345 ) - let output = format([product]) - expect(output) == "12345 Awesome App (19.2.1)" + expect(format([product])) == "12345 Awesome App (19.2.1)" } it("can format two products") { products = [ - SoftwareProductMock( + MockSoftwareProduct( appName: "Awesome App", bundleIdentifier: "", bundlePath: "", bundleVersion: "19.2.1", itemIdentifier: 12345 ), - SoftwareProductMock( + MockSoftwareProduct( appName: "Even Better App", bundleIdentifier: "", bundlePath: "", @@ -56,8 +54,8 @@ public class AppListsFormatterSpec: QuickSpec { itemIdentifier: 67890 ), ] - let output = format(products) - expect(output) == "12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)" + expect(format(products)) + == "12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)" } } } diff --git a/Tests/MasKitTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift similarity index 66% rename from Tests/MasKitTests/Formatters/SearchResultFormatterSpec.swift rename to Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 1fe7fb7..2db3e52 100644 --- a/Tests/MasKitTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -1,6 +1,6 @@ // // SearchResultFormatterSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/14/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -9,81 +9,77 @@ import Nimble import Quick -@testable import MasKit +@testable import mas -public class SearchResultsFormatterSpec: QuickSpec { +public class SearchResultFormatterSpec: QuickSpec { override public func spec() { // static func reference let format = SearchResultFormatter.format(results:includePrice:) var results: [SearchResult] = [] beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("search results formatter") { beforeEach { results = [] } it("formats nothing as empty string") { - let output = format(results, false) - expect(output) == "" + expect(format(results, false)).to(beEmpty()) } it("can format a single result") { let result = SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ) - let output = format([result], false) - expect(output) == " 12345 Awesome App (19.2.1)" + expect(format([result], false)) == " 12345 Awesome App (19.2.1)" } it("can format a single result with price") { let result = SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ) - let output = format([result], true) - expect(output) == " 12345 Awesome App $ 9.87 (19.2.1)" + expect(format([result], true)) == " 12345 Awesome App (19.2.1) $9.87" } it("can format a two results") { results = [ SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ), SearchResult( - price: 0.01, + formattedPrice: "$0.01", trackId: 67890, trackName: "Even Better App", version: "1.2.0" ), ] - let output = format(results, false) - expect(output) == " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)" + expect(format(results, false)) + == " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)" } it("can format a two results with prices") { results = [ SearchResult( - price: 9.87, + formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1" ), SearchResult( - price: 0.01, + formattedPrice: "$0.01", trackId: 67890, trackName: "Even Better App", version: "1.2.0" ), ] - let output = format(results, true) - expect(output) - == " 12345 Awesome App $ 9.87 (19.2.1)\n 67890 Even Better App $ 0.01 (1.2.0)" + expect(format(results, true)) + == " 12345 Awesome App (19.2.1) $9.87\n 67890 Even Better App (1.2.0) $0.01" } } } diff --git a/Tests/MasKitTests/JSON/lookup/fantastical.json b/Tests/masTests/JSON/lookup/fantastical.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/fantastical.json rename to Tests/masTests/JSON/lookup/fantastical.json diff --git a/Tests/MasKitTests/JSON/lookup/notability.json b/Tests/masTests/JSON/lookup/notability.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/notability.json rename to Tests/masTests/JSON/lookup/notability.json diff --git a/Tests/MasKitTests/JSON/lookup/slack.json b/Tests/masTests/JSON/lookup/slack.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/slack.json rename to Tests/masTests/JSON/lookup/slack.json diff --git a/Tests/MasKitTests/JSON/lookup/things.json b/Tests/masTests/JSON/lookup/things.json similarity index 100% rename from Tests/MasKitTests/JSON/lookup/things.json rename to Tests/masTests/JSON/lookup/things.json diff --git a/Tests/MasKitTests/JSON/search/bbedit.json b/Tests/masTests/JSON/search/bbedit.json similarity index 100% rename from Tests/MasKitTests/JSON/search/bbedit.json rename to Tests/masTests/JSON/search/bbedit.json diff --git a/Tests/MasKitTests/JSON/search/bear.json b/Tests/masTests/JSON/search/bear.json similarity index 100% rename from Tests/MasKitTests/JSON/search/bear.json rename to Tests/masTests/JSON/search/bear.json diff --git a/Tests/MasKitTests/JSON/search/deliveries.json b/Tests/masTests/JSON/search/deliveries.json similarity index 100% rename from Tests/MasKitTests/JSON/search/deliveries.json rename to Tests/masTests/JSON/search/deliveries.json diff --git a/Tests/MasKitTests/JSON/search/fantastical.json b/Tests/masTests/JSON/search/fantastical.json similarity index 100% rename from Tests/MasKitTests/JSON/search/fantastical.json rename to Tests/masTests/JSON/search/fantastical.json diff --git a/Tests/MasKitTests/JSON/search/mojave.json b/Tests/masTests/JSON/search/mojave.json similarity index 100% rename from Tests/MasKitTests/JSON/search/mojave.json rename to Tests/masTests/JSON/search/mojave.json diff --git a/Tests/MasKitTests/JSON/search/nonexistent.json b/Tests/masTests/JSON/search/nonexistent.json similarity index 100% rename from Tests/MasKitTests/JSON/search/nonexistent.json rename to Tests/masTests/JSON/search/nonexistent.json diff --git a/Tests/MasKitTests/JSON/search/notability.json b/Tests/masTests/JSON/search/notability.json similarity index 100% rename from Tests/MasKitTests/JSON/search/notability.json rename to Tests/masTests/JSON/search/notability.json diff --git a/Tests/MasKitTests/JSON/search/slack.json b/Tests/masTests/JSON/search/slack.json similarity index 100% rename from Tests/MasKitTests/JSON/search/slack.json rename to Tests/masTests/JSON/search/slack.json diff --git a/Tests/MasKitTests/JSON/search/things-3.json b/Tests/masTests/JSON/search/things-3.json similarity index 100% rename from Tests/MasKitTests/JSON/search/things-3.json rename to Tests/masTests/JSON/search/things-3.json diff --git a/Tests/MasKitTests/JSON/search/things-that-go-bump.json b/Tests/masTests/JSON/search/things-that-go-bump.json similarity index 98% rename from Tests/MasKitTests/JSON/search/things-that-go-bump.json rename to Tests/masTests/JSON/search/things-that-go-bump.json index c413b22..f2e873c 100644 --- a/Tests/MasKitTests/JSON/search/things-that-go-bump.json +++ b/Tests/masTests/JSON/search/things-that-go-bump.json @@ -19,6 +19,9 @@ "trackName": "Things That Go Bump", "trackId": 1472954003, "sellerName": "Tinybop Inc.", + "price": 0.99, + "fileSizeBytes": "12345678", + "formattedPrice": "$0.99", "releaseNotes": "* BOOM *, this is a BIG update. The house spawns a game room, complete with video games you can ENTER INTO. It's fun and a little bit weird! Try it! \n»-(¯`·.·´¯)->", "primaryGenreId": 6014, "primaryGenreName": "Games", diff --git a/Tests/masTests/JSON/search/things.json b/Tests/masTests/JSON/search/things.json new file mode 100644 index 0000000..2c0bfb7 --- /dev/null +++ b/Tests/masTests/JSON/search/things.json @@ -0,0 +1,871 @@ +{ + "resultCount": 50, + "results": [ + { + "screenshotUrls": [ + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/6f/4c/e5/6f4ce5d6-7caa-d1eb-bbc9-86558e97d2ba/pr_source.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/92/b4/f8/92b4f8f5-f133-abd8-db17-135ac27bb1fa/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/72/63/63/726363b9-45ff-f93e-975c-fb69836eaf1a/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/29/fa/63/29fa63e3-3cb2-8b8a-8541-31fa9b7ef27f/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/da/17/5f/da175f95-c2cd-e5df-8cbc-d800d6770c64/pr_source.png/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/f1/82/37/f182376c-4f25-6dbb-c6a8-5e6c1c617620/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/60x60bb.png", + "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/512x512bb.png", + "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/69/3b/12/693b12e6-67d5-8252-7607-3438e420bbaa/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/cultured-code-gmbh-co-kg/id284971784?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.13.0", + "trackName": "Things 3", + "trackId": 904280696, + "sellerName": "Cultured Code GmbH & Co. KG", + "releaseNotes": "• Moved the database file to a new location (now at /Library/Group Containers/).\n• Increased the clickable area of items in the sidebar.\n• Improved the formatting of years in Japanese.\n• Fixed some crashes that could occur when hitting Cmd+[ or ] in Quick Entry while the When popover was visible.\n• Updated the crash reporter.\n• Some sync improvements.\n\n\nNEW IN 3.12\n\nWe’re excited to release Things 3.12 – a big update for our Watch app!\n\nWe’ve entirely rebuilt its foundation to allow it to sync and operate without your phone being nearby. We’ve also taken this opportunity to add some often-requested features to the app. For more information about this release, please visit our blog: thingsapp.com\n\nThere are no huge changes in this release for Mac, but there’s one great new feature you should know about: you can now edit the Tags or Deadlines of collapsed to-dos – even for multiple to-dos at once – by hitting Cmd+Shift+T or D. It’s super convenient :)", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2017-05-18T16:42:04Z", + "genreIds": [ + "6007", + "6000" + ], + "formattedPrice": "$49.99", + "currentVersionReleaseDate": "2020-08-04T07:57:44Z", + "trackCensoredName": "Things 3", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "IT", + "JA", + "RU", + "ZH", + "ES", + "ZH" + ], + "fileSizeBytes": "17474797", + "sellerUrl": "https://culturedcode.com/things/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/things-3/id904280696?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Get things done! The award-winning Things app helps you plan your day, manage your projects, and make real progress toward your goals.\n\nBest of all, it’s easy to use. Within the hour, you’ll have everything off your mind and neatly organized—from routine tasks to your biggest life goals—and you can start focusing on what matters today.\n\n“Things offers the best combination of design and functionality of any app we tested, with nearly all the features of other power user applications and a delightful interface that never gets in the way of your work.”\n—Wirecutter, The New York Times\n\n\nKEY FEATURES\n\n• Your To-Dos\nYour basic building block is the almighty To-Do—each a small step toward a great accomplishment. You can add notes, tag it, schedule it, and break it down into smaller steps.\n\n• Your Projects\nCreate a Project for any big goal, then add the to-dos to reach it. Use headings to structure your list as you outline your plan. There’s also a place to jot down your notes, and a deadline to keep you on schedule.\n\n• Your Areas\nCreate an Area for each sphere of your life, such as Work, Family, Finance, and so on. This keeps everything neatly organized, and helps you see the big picture as you set your plans in motion.\n\n• Your Plan\nEverything on your schedule is neatly laid out in the Today and Upcoming lists, which show your to-dos and calendar events. Each morning, see what you planned for Today and decide what you want to do. The rest is down to you :)\n\n\nMORE THINGS TO LOVE\n\nAs you dive deeper, you’ll find Things packed with helpful features. Here are just a few:\n\n• Reminders — set a time and Things will remind you.\n• Repeaters — automatically repeat to-dos on a schedule you set.\n• This Evening — a special place for your evening plans.\n• Calendar integration — see your events and to-dos together.\n• Tags — categorize your to-dos and quickly filter lists.\n• Quick Entry — create to-dos from anywhere, as soon as the thought hits you.\n• Quick Find — instantly locate to-dos, headings, or tags.\n• Type Travel — jump from list to list with your keyboard; just start typing!\n• Mail to Things — forward an email to Things; now it’s a to-do.\n• And much more!\n\n\nMADE FOR MAC\n\nThings is tailored to the Mac with deep system integrations as well. A great example is Quick Entry with Autofill: a shortcut that grabs content from other apps and adds it to Things for you, such as a link to a website or an email you want to get back to.\n\nYou can also enjoy a beautiful dark mode at sunset, connect your calendars, enable a Things widget, use your Mac’s Touch Bar, import from Reminders—Things can do it all! There’s even AppleScript support if you need powerful automation.\n\n\nSTAY PRODUCTIVE ON THE GO\n\nThings has full-featured apps for iPhone, iPad, and Apple Watch as well (sold separately). All your devices sync seamlessly via our free Things Cloud service. It’s great to have everything at your fingertips when you need it!\n\n\nAWARD-WINNING DESIGN\n\nMade in Stuttgart, with two Apple Design Awards to its name, Things is a fine example of German engineering: designed, not only to look fantastic, but to be perfectly functional as well. Every detail is thoughtfully considered, then polished to perfection.\n\n“It’s like the unicorn of productivity tools: deep enough for serious work, surprisingly easy to use, and gorgeous enough to enjoy staring at.”\n—Apple\n\n\nGET THINGS TODAY\n\nWhatever it is you want to accomplish in life, Things can help you get there. Install the app today and see what you can do!\n\nVisit our website now and get a free 15-day trial for your Mac: thingsapp.com\n\nIf you have any questions, please get in touch. We provide professional support and will be glad to help you!", + "currency": "USD", + "artistId": 284971784, + "artistName": "Cultured Code GmbH & Co. KG", + "genres": [ + "Productivity", + "Business" + ], + "price": 49.99, + "bundleId": "com.culturedcode.ThingsMac", + "version": "3.12.6", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c9/5d/af/c95daf17-c405-56f0-90f5-9411828e44d2/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/94/32/3b/94323b37-f81b-7ba8-a280-b951e7e840de/pr_source.jpg/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/82/f1/35/82f1356d-1e68-8f9d-3967-566e256f9265/pr_source.jpg/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/dc/ad/83/dcad839b-7705-e4c0-180a-2f97cb68054d/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/21/50/6a/21506a09-c48c-d25e-dfa5-c0d6aa4cdd9d/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/09/64/61/096461c1-f392-ec7d-13dd-2caa927d8244/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/appest-limited/id434073155?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.12", + "trackName": "TickTick: Things & Tasks To Do", + "trackId": 966085870, + "sellerName": "Appest Limited", + "releaseNotes": "- Bug fixes and improvements.\n\nRecent Updates:\n- Customizable Section in List View.\n- Tag names can be capitalized.\n- The number of Pomos can now be estimated beforehand.\n- Lists under different folders can share the same name.\n- New city themes! Los Angeles and Cairo.\n\nThanks for using TickTick! We'll bring regular updates to give you more pleasant experience with performance and stability.\nWe'll read all reviews in App Store and evaluate your feedbacks carefully. Any issues encountered during the use, you may write to us via Avatar -> Feedback & Suggestions -> Submit feedback, we will get back to you asap.\nTickTick team with love.", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2016-03-04T06:37:31Z", + "genreIds": [ + "6007" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-08-27T01:27:34Z", + "trackCensoredName": "TickTick: Things & Tasks To Do", + "languageCodesISO2A": [ + "EN", + "ZH" + ], + "fileSizeBytes": "24698702", + "sellerUrl": "https://ticktick.com", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/ticktick-things-tasks-to-do/id966085870?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Design exclusively for macOS, TickTick is your daily must-have to-do & task list to get all things done.\nTickTick can be accessed on more than 10 different platforms including Mac, iPhone, iPad, Apple Watch which enables you to manage tasks on all your devices/Web.\n\nKey features: \n- Add task via shortcut (Command+Shift+A)\n- Instant reminder\n- Set priority levels to tasks\n- Set flexible recurring tasks \n- Create checklists within tasks \n- Sort tasks by order/date/name/priority \n- Sync all your tasks across all devices \n\nTickTick is free but you can also upgrade to Premium account for full access of premium features for $2.99 a month or $27.99 a year through an auto-renewing subscription.\n\nPremium Features: \n- Grid view and Timeline view of calendar\n- Duration\n- Custom Smart List\n- Description for checklist\n- Reminders for sub-tasks\n- More lists and tasks (299 lists, 999 tasks in each list, 199 subtasks in each task)\n- Add at most 5 reminders to each task\n- Share a task list up to 19 members for better task collaboration\n- Upload up to 99 attachments every day\n\nSubscriptions for Premium account will be charged to your credit card through your iTunes account. Your subscription will automatically renew unless cancelled at least 24-hours before the end of the current period. You will not be able to cancel a subscription during the active period. You can manage your subscriptions in the Account Settings after purchase. \n\nHow TickTick makes you productive: \n- Get all things done \n- Never miss a schedule\n- Make work more productive \n- Keep life on track \n\nConnect with us: \nFacebook: https://www.facebook.com/TickTickApp\nTwitter: https://twitter.com/TickTickTeam @TickTickTeam\nHelp Center: https://help.ticktick.com/\n\nPrivacy Policy: https://www.ticktick.com/about/privacy\nTerms of Use: https://www.ticktick.com/about/tos", + "currency": "USD", + "artistId": 434073155, + "artistName": "Appest Limited", + "genres": [ + "Productivity" + ], + "price": 0.00, + "bundleId": "com.TickTick.task.mac", + "version": "3.7.11", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is2-ssl.mzstatic.com/image/thumb/Purple1/v4/79/5c/63/795c63aa-698c-1c6c-b6da-e7ebba718d01/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple30/v4/15/4d/40/154d4071-4a6f-dcd7-0d15-2e495f6f4710/mzm.mvtkjcyn.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple2/v4/e0/31/dc/e031dc74-ce06-afe3-fd8e-8693f6c7c50c/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple1/v4/fc/8d/23/fc8d2367-725d-11dd-6da9-816a7780a1d9/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple71/v4/ff/4d/6b/ff4d6b03-2f12-e12d-9bb3-b3607bcd8ad8/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.6", + "trackName": "Simple Antnotes", + "trackId": 846599902, + "sellerName": "Mykola Olshevskyi", + "releaseNotes": "- added option to disable gradient background\n- added option to create new notes in bottom left/right corners\n- changed delay for close/options buttons showing\n- some minor compatibility and UI fixes\n- fixed German localisation", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2014-03-28T12:49:14Z", + "genreIds": [ + "6007", + "6002" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2016-09-24T17:06:52Z", + "trackCensoredName": "Simple Antnotes", + "languageCodesISO2A": [ + "EN", + "DE", + "RU", + "UK" + ], + "fileSizeBytes": "1002100", + "sellerUrl": "https://www.antlogic.com/apps/antnotes", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/simple-antnotes/id846599902?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Antnotes are like paper notes: they are glued to your monitor, but from the other side of the screen.\n\nThis nice and handy application lives in the menu bar for faster access and has the following features:\n\n- customizable background, font and text color\n- pin note to desktop to make it stay atop of other windows\n- translucent notes\n- make new notes by dragging text, images and files to the menu bar icon\n- drag images and sounds to note contents\n- automatically hide notes when inactive\n- quick access via menu bar icon\n- configurable global shortcuts to create new note or show/hide all notes\n- integration with services: create new note from any text in any application\n- snap to screen bounds and other notes\n- archive with all closed notes - do not lose your information by accidentally closing a note\n- smart position choosing for different display configurations\n\nWant more features? Let us know, or check out our Antnotes application!\n\nVisit our support forums: https://www.antlogic.com/forum/", + "currency": "USD", + "artistId": 364746702, + "artistName": "AntLogic", + "genres": [ + "Productivity", + "Utilities" + ], + "price": 0.00, + "bundleId": "ua.com.AntLogic.SimpleAntnotes", + "version": "1.6.1", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "appletvScreenshotUrls": [], + "supportedDevices": [ + "iPadMini4-iPadMini4", + "iPadProSecondGen-iPadProSecondGen", + "iPhone11-iPhone11", + "iPad71-iPad71", + "iPadMiniRetinaCellular-iPadMiniRetinaCellular", + "iPhone8Plus-iPhone8Plus", + "iPhone6sPlus-iPhone6sPlus", + "iPadMini5-iPadMini5", + "iPadProFourthGen-iPadProFourthGen", + "iPhoneXS-iPhoneXS", + "iPadAir3Cellular-iPadAir3Cellular", + "iPadAir3-iPadAir3", + "iPadMini4Cellular-iPadMini4Cellular", + "iPadProCellular-iPadProCellular", + "MacDesktop-MacDesktop", + "iPadMini3-iPadMini3", + "iPhoneXR-iPhoneXR", + "iPhoneSE-iPhoneSE", + "iPad611-iPad611", + "iPhone7-iPhone7", + "iPad73-iPad73", + "iPad812-iPad812", + "iPadAir2Cellular-iPadAir2Cellular", + "iPhoneX-iPhoneX", + "iPadMini5Cellular-iPadMini5Cellular", + "iPadPro97-iPadPro97", + "iPad834-iPad834", + "iPadProSecondGenCellular-iPadProSecondGenCellular", + "iPhone5s-iPhone5s", + "iPad75-iPad75", + "iPadMini3Cellular-iPadMini3Cellular", + "iPad878-iPad878", + "iPhone6-iPhone6", + "iPadAir-iPadAir", + "iPadPro97Cellular-iPadPro97Cellular", + "iPadSeventhGen-iPadSeventhGen", + "iPodTouchSixthGen-iPodTouchSixthGen", + "iPhoneXSMax-iPhoneXSMax", + "iPad612-iPad612", + "iPadPro-iPadPro", + "iPodTouchSeventhGen-iPodTouchSeventhGen", + "iPhone11ProMax-iPhone11ProMax", + "iPadMiniRetina-iPadMiniRetina", + "iPad76-iPad76", + "iPadProFourthGenCellular-iPadProFourthGenCellular", + "iPadSeventhGenCellular-iPadSeventhGenCellular", + "iPhoneSESecondGen-iPhoneSESecondGen", + "iPad74-iPad74", + "iPhone6s-iPhone6s", + "iPhone7Plus-iPhone7Plus", + "iPadAir2-iPadAir2", + "iPad72-iPad72", + "iPhone6Plus-iPhone6Plus", + "iPadAirCellular-iPadAirCellular", + "Watch4-Watch4", + "iPhone8-iPhone8", + "iPad856-iPad856", + "iPhone11Pro-iPhone11Pro" + ], + "screenshotUrls": [ + "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/2b/ce/8f/2bce8ffa-545b-050c-1dd9-2aeef532facd/pr_source.png/406x228bb.png", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/fb/36/14/fb36142e-17ba-fdab-90c6-e8f9d3c080ef/pr_source.png/406x228bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/4e/e2/de/4ee2de74-d0ef-010b-19f6-63755aa0175c/pr_source.png/406x228bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/46/8e/bd/468ebdd3-73a9-ec6a-b4da-6931ce887cff/pr_source.png/406x228bb.png", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/a4/9c/fa/a49cfa14-69e0-f1cf-3924-6ff878027b2d/pr_source.png/406x228bb.png", + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/c1/94/cc/c194ccb6-c15a-c0a5-47a6-3ddd625fd98d/pr_source.png/406x228bb.png" + ], + "ipadScreenshotUrls": [ + "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/3f/23/5e/3f235e16-c049-8ee8-ebdc-3d52f25f2636/pr_source.png/552x414bb.png", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/80/48/1d/80481dff-e404-721c-920e-4688f860cf27/pr_source.png/552x414bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/58/a2/c9/58a2c970-1bd3-6f4d-1bdc-502f75faaa6a/pr_source.png/552x414bb.png", + "https://is2-ssl.mzstatic.com/image/thumb/Purple123/v4/2c/a6/06/2ca606eb-8b40-219a-34c5-626f79b7e593/pr_source.png/552x414bb.png", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/c7/d7/04/c7d70441-51bd-1417-c7bf-a5d2702380e4/pr_source.png/552x414bb.png", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/87/e0/75/87e075fd-a979-6151-5744-56ab76ac8f18/pr_source.png/552x414bb.png" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/60x60bb.jpg", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/512x512bb.jpg", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b3/ce/e9/b3cee939-9c28-6e05-f600-2e1b9419e0d2/source/100x100bb.jpg", + "artistViewUrl": "https://apps.apple.com/us/developer/volodymyr-yahenskyi/id961335645?uo=4", + "isGameCenterEnabled": false, + "advisories": [], + "features": [ + "iosUniversal" + ], + "kind": "software", + "minimumOsVersion": "11.0", + "trackName": "Random: Lists & Decision Maker", + "trackId": 1128190780, + "sellerName": "Volodymyr Yahenskyi", + "releaseNotes": "• Fixed crash when adding items to a new list\n• Fixed lists sync on Apple Watch\n\nThanks for using the Random!\nThis release also contains bug fixes and performance improvements.", + "primaryGenreId": 6012, + "primaryGenreName": "Lifestyle", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2016-07-05T22:00:04Z", + "genreIds": [ + "6012", + "7009", + "6014", + "7004" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-08-29T19:21:51Z", + "trackCensoredName": "Random: Lists & Decision Maker", + "languageCodesISO2A": [ + "EN", + "RU", + "UK" + ], + "fileSizeBytes": "76392448", + "sellerUrl": "https://yahenskyi.dev/random/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 4.6104900000000004212097337585873901844024658203125, + "userRatingCountForCurrentVersion": 1525, + "averageUserRating": 4.6104900000000004212097337585873901844024658203125, + "trackViewUrl": "https://apps.apple.com/us/app/random-lists-decision-maker/id1128190780?uo=4", + "trackContentRating": "4+", + "description": "Need a random number? Or can’t you decide what to do? Random is a powerful app that will solve all such problems.\n\nFeatures:\n• Number generator (from a range 0 - 999999999)\n• Letter generator\n• Dice roller (roll up to 4 regular dices in one go)\n• A custom item from a list generator\n• Yes or No \n• Coin flipper\n• Card generator\n• Rock-Paper-Scissors\n• Map Point\n\nGenerate a new random number simply by tapping a ​randomize button or by touching the Apple Watch screen. For those who want a bit of additional exercise, shaking your iOS device will also result in a new random response.\n\nUse Force Touch for setting the minimum or maximum values in your Apple Watch app. Same for the number of dices​, cards, and selection of lists.\n\nRandom Premium subscription benefits:\n• Sync: Get access to your data from all your devices.\n• Themes: Customize the app with various themes and background images.\n• No advertising.\n\nIf you decide to get Random Premium subscription, your purchase will be charged to your iTunes account. 1 month costs $2.99 and 1 year costs $11.99. Active subscriptions will be auto-renewed 24 hours before the expiry date. You can manage subscriptions from Account in iTunes after subscribing, you’ll also be able to cancel the auto-renewing subscription from there at any time. Any unused portion of the free trial period will be forfeited if you purchase a subscription to Random Premium before your trial expires.\n\nTerms & Conditions: https://yahenskyi.dev/terms-conditions/\nPrivacy Policy: https://yahenskyi.dev/privacy-policy/", + "currency": "USD", + "artistId": 961335645, + "artistName": "Volodymyr Yahenskyi", + "genres": [ + "Lifestyle", + "Family", + "Games", + "Board" + ], + "price": 0.00, + "bundleId": "com.yahenskyi.random", + "version": "2.2.10", + "wrapperType": "software", + "userRatingCount": 1525 + }, + { + "screenshotUrls": [ + "https://is2-ssl.mzstatic.com/image/thumb/Purple71/v4/0e/74/bb/0e74bb9a-5ac2-5d5f-516a-5f1c12e95328/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/36/54/f0/3654f064-4013-95e6-2683-c89ab8e51102/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple71/v4/1a/75/86/1a758637-9db5-007c-595b-b724e9083321/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/b0/7b/ed/b07bed5e-d977-6655-7a6a-d35a90901fba/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/any-case-solutions/id1396419026?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.10", + "trackName": "Task Planner - To Do List", + "trackId": 1063681909, + "sellerName": "Any Case Solutions, OOO", + "releaseNotes": "We’ve updated the app! In the new version:\n- less bugs;\n- minor changes in the interface;\n- some general improvements.\nYour opinion is important to us! Please, leave your feedback - we will gladly consider all your wishes and suggestions.", + "primaryGenreId": 6000, + "primaryGenreName": "Business", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2016-01-07T00:04:36Z", + "genreIds": [ + "6000", + "6007" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-07-17T23:48:12Z", + "trackCensoredName": "Task Planner - To Do List", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "IT", + "JA", + "KO", + "PT", + "RU", + "ZH", + "ES" + ], + "fileSizeBytes": "27930644", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/task-planner-to-do-list/id1063681909?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Plan Your Tasks is a productivity tool that allows you to capture your ideas and duties in one place. \nManage everything you have to do while working with many different tasks!\n\nEasy task management - create, organize, and prioritize tasks;\n- Set notifications;\n- Add comments;\n- Sort tasks by categories;\n- Track due dates.\n\nNew approach to agenda\n- Build-in calendar;\n- Coherent tutorial mode;\n- Magic Trackpad 2 support.\n\nCapture all your flash ideas and duties in the calendar and manage your to dos while working with many tasks more effectively.\n\n\nPrivacy Policy: https://anycasesolutions.com/privacy\nTerms Of Use: https://anycasesolutions.com/tos", + "currency": "USD", + "artistId": 1396419026, + "artistName": "Any Case Solutions", + "genres": [ + "Business", + "Productivity" + ], + "price": 0.00, + "bundleId": "com.newtechnologies.iPlanTasksinapp", + "version": "2.1.2", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/69/a0/58/69a0583d-02fd-1d37-cb33-19b80578e9e5/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/09/be/29/09be2981-4d08-a021-423a-29cc212c1b59/pr_source.jpg/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple7/v4/28/5a/f4/285af4d8-37e6-118a-ff28-a4211eeb1122/pr_source.jpg/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple3/v4/50/4d/52/504d520c-4f8c-b011-d1e0-18addb5700a8/pr_source.jpg/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple3/v4/f2/6e/07/f26e0760-efb1-0353-3ebd-9fd2803f4b3d/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/60x60bb.png", + "artworkUrl512": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/512x512bb.png", + "artworkUrl100": "https://is5-ssl.mzstatic.com/image/thumb/Purple69/v4/ac/6e/9a/ac6e9aea-8f4b-66bd-6046-c1735f27806f/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/realmac-software/id310591643?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.10", + "trackName": "Clear – Tasks, Reminders & To-Do Lists", + "trackId": 504544917, + "sellerName": "Realmac Software Limited", + "releaseNotes": "Thanks for using Clear! Just two small enhancements in today’s update:\n\n- We’ve tweaked (increased) the delay before “Click to Clear” appears.\n- We’ve ensured compatibility with OS X El Capitan.\n\nStay productive, and follow @realmacsoftware on Twitter for the latest news!", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2012-11-08T08:00:00Z", + "genreIds": [ + "6007", + "6012" + ], + "formattedPrice": "$9.99", + "currentVersionReleaseDate": "2015-08-19T14:05:32Z", + "trackCensoredName": "Clear – Tasks, Reminders & To-Do Lists", + "languageCodesISO2A": [ + "EN" + ], + "fileSizeBytes": "13109875", + "sellerUrl": "http://impending.com/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/clear-tasks-reminders-to-do-lists/id504544917?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Over 2.5 million people de-clutter their lives with Clear, so stop stalling and start organizing your daily routine.\n\nClear is the revolutionary to-do and reminders app that makes you more productive. Just start typing to add to-dos, and once you start organizing your life with Clear you’ll wonder how you ever managed without it.\n\n- Simple gestural design that allows you to focus on your to-dos. Designed for the Magic Trackpad, but works great with a mouse too!\n- Full keyboard navigation. Just start typing to create to-dos.\n- Use separate lists to organize every aspect of your life.\n- iCloud sync built-in so you can be productive everywhere.\n- Set reminders so you’ll never forget important tasks.\n- Personalize your Clear lists with themes and make them your own.\n- Syncs with Clear for iOS (available separately on the App Store).\n\nClear is built by a small team, dedicated to bringing you frequent free feature updates. We’d love to know how we can make you even more productive, so get in touch via the App Store “Support” link, or tweet us @UseClear.\n\nClear for Mac and Clear for iOS are not affiliated with or endorsed by CLEAR Wireless.", + "currency": "USD", + "artistId": 310591643, + "artistName": "Realmac Software", + "genres": [ + "Productivity", + "Lifestyle" + ], + "price": 9.99, + "bundleId": "com.realmacsoftware.clear.mac", + "version": "1.1.7", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/cd/bd/44/cdbd44af-06eb-21d6-a793-43dae1077c47/pr_source.jpg/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/e9/8f/17/e98f17c6-787b-b180-6f8d-fb8385ceedd3/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/43/12/a2/4312a25b-f773-9c1a-ddd4-2515d948cc27/pr_source.jpg/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/97/0d/b4/970db444-bbd9-ed77-439e-001bce006e17/pr_source.jpg/800x500bb.jpg" + ], + "artworkUrl60": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/60x60bb.png", + "artworkUrl512": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/512x512bb.png", + "artworkUrl100": "https://is1-ssl.mzstatic.com/image/thumb/Purple113/v4/6b/f0/58/6bf058c1-90ab-5bdf-7c06-18de305efd6d/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/shenzhen-tomato-software-technology-co-ltd/id966057212?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.12", + "trackName": "Focus To-Do: Pomodoro & Tasks", + "trackId": 1258530160, + "sellerName": "Shenzhen Tomato Software Technology Co., Ltd.", + "releaseNotes": "1.Support new languages\n2.Bug fix", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2017-08-02T03:45:26Z", + "genreIds": [ + "6007", + "6002" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-05-03T04:38:29Z", + "trackCensoredName": "Focus To-Do: Pomodoro & Tasks", + "languageCodesISO2A": [ + "CS", + "EN", + "FR", + "DE", + "ID", + "IT", + "JA", + "KO", + "PL", + "PT", + "RO", + "RU", + "ZH", + "ES", + "ZH", + "TR", + "VI" + ], + "fileSizeBytes": "12135791", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/focus-to-do-pomodoro-tasks/id1258530160?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Focus To-Do combines Pomodoro Timer with Task Management, it is a science-based app that will motivate you to stay focused and get things done. \n\nIt brings Pomodoro Technique and To Do List into one place, you can capture and organize tasks into your todo lists, start focus timer and focus on work & study, set reminders for important tasks and errands, check the time spent at work. \n\nIt's the ultimate app for managing Tasks, Reminders, Lists, Calendar events, Grocery lists, checklist, helping you focus on work & study and tracking your working hours.\n\nFocus To-Do syncs between your phone and computer, so you can access your lists from anywhere.\n\nHow it works:\n 1. Pick a task you need to accomplish.\n 2. Set a timer for 25 minutes, keep focused and start working.\n 3. When the pomodoro timer rings, take a 5 minute break.\n \nKey Features:\n\n- Pomodoro Timer:Stay focused and get more things done.\n Pause and resume Pomodoro\n Customizable pomodoro/breaks lengths\n Notification before the end of a Pomodoro\n Support for short and long breaks\n Skip a break after the end of a Pomodoro\n Continuous Mode\n \n- Tasks Management: Task Organizer, Schedule Planner, Reminder, Habit Tracker, Time Tracker\n Tasks and projects: Organise your day with Focus To-Do and complete your to do, study, work, homework or housework you need to get done.\n Recurring tasks: Build lasting habits with powerful recurring due dates like \"Every Monday\".\n Reminders: Setting a Reminder ensures you never forget important things ever again, you can set up recurring due dates to remind you each and every time. \n Sub-tasks: Break down your task into smaller, actionable items or add a checklist .\n Task Priority: Highlight your day’s most important To-Do with color-coded priority levels.\n Estimated Pomodoro Number: Estimate the workload or set a goal.\n Note: Record more detailed about the task.\n\n- Report: Detailed statistics of your time distribution, tasks completed.\n Support the calculation of the total time of Focus Time.\n Gantt Chart of the Focus Time.\n Statistics on completed To Do. \n Statistics on time distribution of project.\n Trend chart of the completed To Do and the focus time.\n\n- All-Platform synchronization: View and manage your goals wherever you are for better goal achieving.\n Support seamless synchronization within iPhone、Mac、iPad、Apple Watch and other platforms.\n \n- Various Reminding:\n Focus Timer finished alarm, vibration reminding.\n Various white noise to help you focus on work & study.\n\nContact Us: focustodo@163.com, reply within 24 hours.\nWebsite: http://www.focustodo.cn\nPomodoro ™ and Pomodoro Technique ® are registered trademarks of Francesco Cirillo. This app is not affiliated with Francesco Cirillo.\n\nUsers have been focused on our app for 200 million hours, join us and we help you to be focused and increase your productivity, reduce procrastination and anxiety.", + "currency": "USD", + "artistId": 966057212, + "artistName": "Shenzhen Tomato Software Technology Co., Ltd.", + "genres": [ + "Productivity", + "Utilities" + ], + "price": 0.00, + "bundleId": "com.macpomodoro", + "version": "6.3", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "appletvScreenshotUrls": [], + "supportedDevices": [ + "iPadMini4-iPadMini4", + "iPadProSecondGen-iPadProSecondGen", + "iPhone11-iPhone11", + "iPad71-iPad71", + "iPadMiniRetinaCellular-iPadMiniRetinaCellular", + "iPhone8Plus-iPhone8Plus", + "iPhone6sPlus-iPhone6sPlus", + "iPadMini5-iPadMini5", + "iPadProFourthGen-iPadProFourthGen", + "iPhoneXS-iPhoneXS", + "iPadAir3Cellular-iPadAir3Cellular", + "iPadAir3-iPadAir3", + "iPadMini4Cellular-iPadMini4Cellular", + "iPadProCellular-iPadProCellular", + "MacDesktop-MacDesktop", + "iPadMini3-iPadMini3", + "iPhoneXR-iPhoneXR", + "iPhoneSE-iPhoneSE", + "iPad611-iPad611", + "iPhone7-iPhone7", + "iPad73-iPad73", + "iPad812-iPad812", + "iPadAir2Cellular-iPadAir2Cellular", + "iPhoneX-iPhoneX", + "iPadMini5Cellular-iPadMini5Cellular", + "iPadPro97-iPadPro97", + "iPad834-iPad834", + "iPadProSecondGenCellular-iPadProSecondGenCellular", + "iPhone5s-iPhone5s", + "iPad75-iPad75", + "iPadMini3Cellular-iPadMini3Cellular", + "iPad878-iPad878", + "iPhone6-iPhone6", + "iPadAir-iPadAir", + "iPadPro97Cellular-iPadPro97Cellular", + "iPadSeventhGen-iPadSeventhGen", + "iPodTouchSixthGen-iPodTouchSixthGen", + "iPhoneXSMax-iPhoneXSMax", + "iPad612-iPad612", + "iPadPro-iPadPro", + "iPodTouchSeventhGen-iPodTouchSeventhGen", + "iPhone11ProMax-iPhone11ProMax", + "iPadMiniRetina-iPadMiniRetina", + "iPad76-iPad76", + "iPadProFourthGenCellular-iPadProFourthGenCellular", + "iPadSeventhGenCellular-iPadSeventhGenCellular", + "iPhoneSESecondGen-iPhoneSESecondGen", + "iPad74-iPad74", + "iPhone6s-iPhone6s", + "iPhone7Plus-iPhone7Plus", + "iPadAir2-iPadAir2", + "iPad72-iPad72", + "iPhone6Plus-iPhone6Plus", + "iPadAirCellular-iPadAirCellular", + "Watch4-Watch4", + "iPhone8-iPhone8", + "iPad856-iPad856", + "iPhone11Pro-iPhone11Pro" + ], + "screenshotUrls": [ + "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/95/33/5f/95335f94-26d3-3567-93ac-77d60ab821dd/pr_source.png/392x696bb.png", + "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/03/53/b9/0353b9b1-ef2a-7ff1-a1b8-4124867af41b/pr_source.png/392x696bb.png", + "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/ef/63/ce/ef63ce41-371a-2508-b101-fb99e9c7758f/pr_source.png/392x696bb.png" + ], + "ipadScreenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/43/19/bb/4319bb4b-5700-0f6b-2c19-7bd386bf186c/pr_source.jpg/552x414bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/5d/51/1a/5d511a30-7fab-fd18-6967-c0caf9674d55/pr_source.jpg/552x414bb.jpg" + ], + "artworkUrl60": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/60x60bb.jpg", + "artworkUrl512": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/512x512bb.jpg", + "artworkUrl100": "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/42/50/53/425053d8-2b26-c28a-72db-40323cc62aeb/source/100x100bb.jpg", + "artistViewUrl": "https://apps.apple.com/us/developer/kevin-reutter/id1273424431?uo=4", + "isGameCenterEnabled": true, + "advisories": [], + "features": [ + "gameCenter", + "iosUniversal" + ], + "kind": "software", + "minimumOsVersion": "13.0", + "trackName": "Planny 3 - Smart To Do List", + "trackId": 1289070327, + "sellerName": "Kevin Reutter", + "releaseNotes": "Stay tuned! Planny 4 ships in a few week and will be a free update with many great features!\n\n• SwiftUI \nNow Planny uses SwiftUI in some parts of the app. SwiftUI is an innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift. Over time more and more of the app will be created with SwiftUI to avoid crashes and improve performance. \n\n• Advanced Cursor Support\nWhen using a Trackpad on iPadOS or on the Mac, specific Elements become larger when you come closer to make clicking easier\n\n• Alternative App icons\nChoose the icon color you’d like in settings (iOS for iPhone only)\n\n• New Onboarding Experience\nA new tutorial shows the key features \n\n• New Purchase View\nThe purchase view is now much simpler. Feel free to subscribe :) \n\n• Fixed deadlines on macOS\n• Direct Deadlines now support days and time \n• Fixed issues with overdue tasks \n\nDo you have any wishes for Planny 4? Feel free to submit ideas on the website!", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2017-10-13T19:16:40Z", + "genreIds": [ + "6007", + "6002" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-07-30T17:37:31Z", + "trackCensoredName": "Planny 3 - Smart To Do List", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "IT", + "RU", + "ZH", + "ES", + "TR" + ], + "fileSizeBytes": "47687680", + "sellerUrl": "https://www.kevinreutter.de/planny-3/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 4.3897300000000001318767317570745944976806640625, + "userRatingCountForCurrentVersion": 331, + "averageUserRating": 4.3897300000000001318767317570745944976806640625, + "trackViewUrl": "https://apps.apple.com/us/app/planny-3-smart-to-do-list/id1289070327?uo=4", + "trackContentRating": "4+", + "description": "++ Planny was part of Apples favorite Apps from October ++\n\nPlanny is all new and has been rethought from the ground up.\n\nPlanny is your new friend helping you to be more productive. Planny learned everything important from common to do list apps but combines them with intelligence and gamification. In the morning and during the day Planny intelligently recommends tasks and also reminds you if you tend to forget them. You earn productivity points for adding and completing tasks, and also lose them if you shift tasks or forget them. Users can compare their productivity with friends over the week. \n\nPlanny also features all the important features like deadlines, lists / projects, tagging, location based reminders, notes and attachments, routines and more. \n\nKey features\n• Daily list to focus on today's tasks\n• Assistant for creating a productive daily plan\n• Daily review of the last day\n• Routines to train your habits\n• Deadlines and reminders\n• Smart reminders if you tend to forget your tasks\n• Notes for your tasks\n• Weekly productivity ranking of your contacts\n• Rewards\n• Dark mode\n• Lists\n• Siri support\n• Advanced Apple Watch app\n\nPlanny Premium offers additional features like:\n• Calendar view\n• Teamwork with your friends\n• Add Photos from your library to tasks\n• Add Photos from your camera to tasks\n• Location based reminders\n• iCloud sync\n• iCloud backup \n• FaceID Unlock\n• More than 2 lists\n• Printing\n• Sketches\n• Review your recent days\n• Tagging\n\n+++ Planny Premium - Unlock all features and use Planny on iPhone, iPad and Apple Watch (Mac soon) - And get free feature updates over time! +++ \n\nA Planny Premium subscription unlocks all features. Note that iCloud features require an iCloud-Account. \n\nPlanny offers two auto-renewing subscriptions\n\nPremium 3 Months\n$6,99 / 3 Months (may differ in your country & currency)\n\nPremium Annual\n$19,99 / Year (may differ in your country & currency)\n\nPayment will be charged to iTunes Account at confirmation of purchase\nSubscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period\nAccount will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal\n\nSubscriptions may be managed by the user and auto-renewal may be turned off by going to the user's Account Settings after purchase\n\nWhen your subscription is cancelled and expires, all the features of Planny Pro won't be available any longer. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable.\n\nPrivacy policy for Planny: http://kevinreutter.de/privacy\nTerms of use / Conditions: http://kevinreutter.de/privacy", + "currency": "USD", + "artistId": 1273424431, + "artistName": "Kevin Reutter", + "genres": [ + "Productivity", + "Utilities" + ], + "price": 0.00, + "bundleId": "com.kevinreutter.Callisto", + "version": "3.4.2", + "wrapperType": "software", + "userRatingCount": 331 + }, + { + "screenshotUrls": [ + "https://is4-ssl.mzstatic.com/image/thumb/Purple/v4/4f/ff/f9/4ffff968-2932-48af-431f-fd1b086026cf/mzl.srudbvwp.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/2a/4f/9f/2a4f9fad-c1a7-fa80-56d7-fea2d3beaa0a/mzl.dcyubghz.png/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple6/v4/56/3a/a7/563aa771-8288-e21a-cfe8-e28e77ffad83/mzl.lzjpfyct.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple4/v4/1e/8d/4e/1e8d4eaa-17f7-5295-1f36-975b62164d19/mzl.yufjavxy.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple4/v4/fb/9b/85/fb9b851d-6ef5-792f-0945-ff2f1a78ce7a/mzl.gycjiioz.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/60x60bb.png", + "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/512x512bb.png", + "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple3/v4/6b/67/f2/6b67f2d4-2603-ec03-504c-fd408d3577d7/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/antlogic/id364746702?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.6.6", + "trackName": "To-do Lists", + "trackId": 416993121, + "sellerName": "Mykola Olshevskyi", + "releaseNotes": "fixed accidentally broken compatibility for Mac OS 10.6-10.7", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2011-03-01T03:09:22Z", + "genreIds": [ + "6007", + "6000" + ], + "formattedPrice": "$4.99", + "currentVersionReleaseDate": "2015-04-16T19:07:37Z", + "trackCensoredName": "To-do Lists", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "RU", + "UK" + ], + "fileSizeBytes": "2095731", + "sellerUrl": "http://www.antlogic.com/apps/todo-lists/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/to-do-lists/id416993121?mt=12&uo=4", + "trackContentRating": "4+", + "description": "To-do Lists provides simple but powerful interface for tasks management.\n\nTo-do Lists features:\n- Quick, one-click tasks addition/removal.\n- Rich-text editing, in-text links support.\n- Seamless iCloud Reminders synchronization.\n- DropBox synchronization between computers and To-do Lists Mobile for iOS\n- Import/export of to-do lists via text files.\n- Printing of to-do lists or mailing them directly from the application.\n- Backup and restore of whole to-do database.\n- Full drag'n'drop support (make new to-do from web link, file, document, e-mail, or any other text by simply dropping them on to-do list).\n- System services support (make new to-do from any text in any application).\n- Rolled-up, translucent or floating to-do lists.\n- Customized background color, text color, font and checkbox appearance.\n- Reminders.\n- Quick-access icon in system menu.\n\nTo-do Lists usage video:\nhttp://youtu.be/5KB-4sYcelo (or http://youtube.com/AntlogicCompany )\n\nFor more information, visit our site at http://www.antlogic.com/\nor Facebook page:\nhttp://facebook.com/AntlogicCompany\n\nIf you have any problems or questions using To-do Lists - visit our support forums at http://www.antlogic.com/forum/", + "currency": "USD", + "artistId": 364746702, + "artistName": "AntLogic", + "genres": [ + "Productivity", + "Business" + ], + "price": 4.99, + "bundleId": "ua.com.AntLogic.ToDoLists", + "version": "1.7.7", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/a8/e9/42/a8e942ec-8eea-03b3-ea37-cc6e2837fb5e/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/5f/62/5c/5f625c38-c559-3b8d-5042-94e241735ef1/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/aa/a1/e7/aaa1e746-e660-2b6e-6833-d751e7879752/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple113/v4/c0/6f/7d/c06f7de4-17b9-7475-8778-22a97c13cdce/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple113/v4/84/3d/8d/843d8de0-6257-7bf0-6a66-6f3ce41af803/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple123/v4/ad/0d/28/ad0d28c6-ff1d-c394-266a-fdff0b8e9cc6/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple123/v4/36/28/a3/3628a3e9-6073-0ce4-17d7-9d9a5f479c64/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple113/v4/a9/ac/b9/a9acb983-76e2-d76d-fbc8-c35388dcee48/pr_source.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple113/v4/8f/41/90/8f4190f5-ebb8-370b-c8a6-afb47c56140d/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple123/v4/09/37/83/09378396-de11-2185-24f4-360be20dbcac/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/60x60bb.png", + "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/512x512bb.png", + "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/71/6f/f0/716ff030-f8ec-536c-41ca-f5116ae1f497/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/the-omni-group/id281731738?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.14", + "trackName": "OmniFocus 3", + "trackId": 1346203938, + "sellerName": "The Omni Group", + "releaseNotes": "OmniFocus 3.9.2 is a minor update focused on bug fixes.\n\n• Omni Automation — OmniFocus now recognizes simple plug-ins that use the .omnifocusjs file extention.\n• First Run — Improved reliability of the first run flow.\n• Notice Bar — Fixed bugs related to the Trial Mode & Free Viewer notice bars.\n\nIf you have any feedback or questions, we’d love to hear from you! The Omni Group offers free tech support; you can email omnifocus@omnigroup.com, call 1–800–315–6664 or 1–206–523–4152, or tweet @OmniFocus.\n\nIf OmniFocus empowers you, we would appreciate an App Store review. Your review will help other people find OmniFocus and make them more productive too.", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2018-09-24T12:28:36Z", + "genreIds": [ + "6007", + "6000" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-08-27T17:54:49Z", + "trackCensoredName": "OmniFocus 3", + "languageCodesISO2A": [ + "NL", + "EN", + "FR", + "DE", + "IT", + "JA", + "KO", + "PT", + "RU", + "ZH", + "ES" + ], + "fileSizeBytes": "64931473", + "sellerUrl": "https://www.omnigroup.com/omnifocus/", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/omnifocus-3/id1346203938?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Two-week free trial! OmniFocus Standard and Pro are in-app purchases, with discounts for people who bought earlier versions of OmniFocus for Mac through the Mac App Store. Or you can get OmniFocus for iOS, Mac, and web for just one price with the OmniFocus Subscription. Download the app for details.\n\nUse OmniFocus to accomplish more every day. Create projects and tasks, organize them with tags, focus on what you can do right now — and get stuff done.\n\nOmniFocus — now celebrating 10 years as the trusted, gold-standard to-do list app — brings unrivaled power and flexibility to your Mac, making it easy to work the way you want to work.\n\nOmniFocus manages everything in your busy life. Use projects to organize tasks naturally, and then add tags to organize across projects. Easily enter tasks when you’re on the go, and process them when you have time. Tap the Forecast view — which shows both tasks and calendar events — to get a handle on your day. Use the Review perspective to keep your projects and tasks on track.\n\nThen let our free syncing system make sure your data is the same on every Mac. (And on OmniFocus for iOS and Web, available separately.) Because your data is encrypted, it’s safe in the cloud.\n\nSTANDARD FEATURES (VIA IN-APP PURCHASE)\n\n• NEW: Tags add a powerful additional organizing tool. Create tags for people, energy levels, priorities, locations, and more.\n• NEW: The Forecast view shows your tasks and calendar events in order, so you can better see what’s coming up in your day.\n• NEW: Enhanced repeating tasks are easier than ever to set up — and they work with real-world examples such as the first weekday of the month.\n• NEW: The Modern, fresh-but-familiar design helps you focus on your content.\n• Inbox is where you quickly add tasks — save them when you think of them, and organize them later.\n• Syncing supports end-to-end encryption so that your data is safe wherever it’s stored, on our server or yours.\n• Notes can be attached to your tasks, so you have all the information you need.\n• Attachments — graphics, video, audio, whatever you want — add richness to your tasks.\n• View Options let you customize each perspective by deciding what it should show and how it should filter your tasks.\n• The Review perspective takes you through your projects and tasks — so you stay on track.\n• OmniFocus Mail Drop adds tasks via email and works with services like IFTTT and Zapier (if you’re using our free syncing server).\n• The Today Widget shows you your most important items — you don’t even have to switch to the app to know what’s up.\n• Support for TaskPaper Text and omnifocus:///add and /paste lets you automate using URLs.\n\nPro features make OmniFocus even more powerful:\n\nPRO FEATURES (VIA IN-APP PURCHASE)\n\n• Custom perspectives help you create new ways to see your data by filtering and grouping projects and tags. NEW: The filtering rules are simpler to use while being more powerful than ever, letting you combine rules with “all,” “any,” and “none.” You can also choose any image to use as your custom perspective’s icon, and a custom tint color to go with it.\n• NEW: Today’s Forecast can include items with a specific tag, and you can reorder those tasks however you choose, so you can plan your day better.\n• The customizable sidebar lets you organize your perspectives the way you want to, for super-fast access.\n• The Today Widget shows a perspective of your choice in Notification Center.\n• AppleScript support opens up a world of automation, using Apple’s Mac scripting language.\n\nDownload OmniFocus right now and start your free trial! The app includes a manual, and there’s plenty more documentation on the website.\n\nSUPPORT\n\nIf you have feedback or questions, our Support Humans would love to hear from you! Send email to omnifocus@omnigroup.com, call us at at 1-800-315-6664 or +1-206-523-4152, or reach us on Twitter at @omnifocus.\n\n\nSubscription Terms of Service: https://www.omnigroup.com/legal", + "currency": "USD", + "artistId": 281731738, + "artistName": "The Omni Group", + "genres": [ + "Productivity", + "Business" + ], + "price": 0.00, + "bundleId": "com.omnigroup.OmniFocus3.MacAppStore", + "version": "3.9.2", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/1a/72/1d/1a721d98-fbc4-ed9e-2aae-ef9d5b538693/pr_source.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a2/b1/10/a2b110fd-aa90-286a-658b-2abd85bd1c68/mzl.menowpkq.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/26/f3/32/26f3322f-8ef9-6171-2864-715f571300e6/mzl.qxibkqwt.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/a4/1b/12/a41b12dc-6e1d-74ff-7ad3-1d6888c31462/mzl.fdlcjqnh.png/800x500bb.jpg", + "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/83/83/24/83832412-85d1-78ff-a9ab-86a37b31121d/mzl.ateekpxr.png/800x500bb.jpg", + "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/e6/7c/c7/e67cc703-d274-a1fc-88af-b5a8ce9cbfd8/mzl.grtjmgef.png/800x500bb.jpg", + "https://is4-ssl.mzstatic.com/image/thumb/Purple124/v4/18/a8/f2/18a8f211-61b5-7b7e-b343-784b260de31d/mzl.ulsghntx.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/60x60bb.png", + "artworkUrl512": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/512x512bb.png", + "artworkUrl100": "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/5a/cf/6c/5acf6c83-c496-d5fb-2445-96ef44f13a82/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/masterbuilders/id896347016?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.14", + "trackName": "Focus - Time Management", + "trackId": 777233759, + "sellerName": "Masterbuilders", + "releaseNotes": "Subscription status is now properly unlocked on all devices.", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2013-12-19T19:16:50Z", + "genreIds": [ + "6007", + "6017" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-02-12T20:37:50Z", + "trackCensoredName": "Focus - Time Management", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "JA", + "ZH", + "ES" + ], + "fileSizeBytes": "24637530", + "sellerUrl": "https://www.focusapp.io", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/focus-time-management/id777233759?mt=12&uo=4", + "trackContentRating": "4+", + "description": "Meet Focus: the best time manager for iPhone, iPad, Apple Watch and Mac. Focus is the most elegant and professional way to get more wore done, working in highly efficient work sessions, one task at a time.\n\n“[…] a tool that can genuinely make people more productive\" – MacStories.net\n\n“[…] a must-have for anyone who finds themselves easily getting distracted or forgetting to take occasional breaks.\" – iDownloadBlog.com\n\n======================\nFEATURES\n======================\n\nFOCUS SESSIONS\nFocus Sessions are a highly efficient way to work. Focus for 25 minutes, then take a short break to relax your mind. After four sessions, take a 15 to 20 minute break. This method maximizes energy, stimulates creativity and promotes a sense of achievement.\n\nTASK MANAGER\nFocus includes a lightweight task manager that lets you organize the things you want to work on intuitively. By working on one task at a time, you won’t be distracted and can focus all your attention towards completing that goal. That way you’ll be perfectly organized on your path to success.\n\nIN-DEPTH STATISTICS\nCheck what you’ve already done! Focus keeps track of your work and offers in-depth and motivating statistics. See your daily, weekly and monthly activity so you don’t lose sight of the big picture. \n\nFOCUS EVERYWHERE\nSeamlessly use Focus on your Mac, iPad, iPhone, and Apple Watch. Sync across your devices using iCloud; use Handoff to pick up your current work on another device and get up-to-the-second data with iCloud Push. You can also use the Today widget to quickly glance at your progress, import tasks using the handy Action extension, and more.\n\nFOCUS & APPLE WATCH: A PERFECT FIT\nUsing Focus on your wrist is a natural fit. The independent Apple Watch app is made for for easy and lightweight interactions that lets you control sessions and track your progress throughout the day. With the Focus complication, you can customize your watch face to see your current progress at a glance.\n\nBEAUTIFUL INTERFACE\nThe name says it all: Focus draws your attention to the most important things. It’s designed to be unobtrusive, accessible and easy-to-use. You’ll intuitively master its collection of features just by using them.\n\n======================\nSUBSCRIPTION PRICING\n======================\n\nFocus offers two subscription options: \nFocus Monthly at $4.99/ month \nFocus Yearly at $39.99/ year\n\nThe subscription unlocks all features on all devices (Mac, iPhone, iPad and Apple Watch).\n\nTRY IT FREE \nFocus Monthly comes with a 3-day free trial period, Focus Yearly with a 7-day free trial period. If you cancel before the end of the trial, you will not be charged for the subscription.\n\nSUBSCRIPTION TERMS\nPayment will be charged to your Apple ID account at the confirmation of purchase or after the free trial period if offered. \n\nYou subscription will automatically renew unless it is canceled at least 24 hours before the end of the current period. Your account will be charged 24 hours prior to the end of the current period. \n\nYou can manage and cancel your subscriptions by going to your account settings in the App Store after purchase. Any unused portion of a free trial will be forfeited when you purchase a subscription\n\n======================\nCONTACT\n======================\n\nIf you have any questions or ideas, please write us at hello@masterbuilders.io\n\nTwitter: @focusappio\nhttps://www.masterbuilders.io\n\n\n\nPrivacy Policy: https://www.masterbuilders.io/privacy\nTerms of Service: https://www.masterbuilders.io/terms", + "currency": "USD", + "artistId": 896347016, + "artistName": "Masterbuilders", + "genres": [ + "Productivity", + "Education" + ], + "price": 0.00, + "bundleId": "com.malteundjan.focus-osx", + "version": "6.2.3", + "wrapperType": "software", + "userRatingCount": 0 + }, + { + "screenshotUrls": [ + "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/35/03/5a/35035a62-e2da-2f4b-6ece-63475bd7cd02/pr_source.png/800x500bb.jpg", + "https://is3-ssl.mzstatic.com/image/thumb/Purple124/v4/b5/ac/1f/b5ac1fe2-431d-e45d-63e0-57ddbfbd525f/pr_source.png/800x500bb.jpg", + "https://is5-ssl.mzstatic.com/image/thumb/Purple114/v4/6c/18/ee/6c18eeff-ca66-2b01-82f3-f81576336ab7/pr_source.png/800x500bb.jpg" + ], + "artworkUrl60": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/60x60bb.png", + "artworkUrl512": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/512x512bb.png", + "artworkUrl100": "https://is3-ssl.mzstatic.com/image/thumb/Purple114/v4/12/30/fb/1230fb0c-42fd-1a80-9379-29be0ba0f612/source/100x100bb.png", + "artistViewUrl": "https://apps.apple.com/us/developer/niklas-behrens/id969210609?mt=12&uo=4", + "kind": "mac-software", + "minimumOsVersion": "10.10", + "trackName": "1Focus: Website & App Blocker", + "trackId": 969210610, + "sellerName": "Niklas Behrens", + "releaseNotes": "- Allows updating 1Focus while blocking is active\n- Fixed toolbar overflow on macOS High Sierra\n- Improved status item width\n- Fixed quick start menu starting wrong task\n- Other bug fixes", + "primaryGenreId": 6007, + "primaryGenreName": "Productivity", + "isVppDeviceBasedLicensingEnabled": true, + "releaseDate": "2015-03-15T05:54:46Z", + "genreIds": [ + "6007", + "6017" + ], + "formattedPrice": "Free", + "currentVersionReleaseDate": "2020-07-18T23:51:25Z", + "trackCensoredName": "1Focus: Website & App Blocker", + "languageCodesISO2A": [ + "EN", + "FR", + "DE", + "JA", + "KO", + "RU", + "ZH", + "ES" + ], + "fileSizeBytes": "8338821", + "sellerUrl": "https://onefocusapp.com", + "contentAdvisoryRating": "4+", + "averageUserRatingForCurrentVersion": 0, + "userRatingCountForCurrentVersion": 0, + "averageUserRating": 0, + "trackViewUrl": "https://apps.apple.com/us/app/1focus-website-app-blocker/id969210610?mt=12&uo=4", + "trackContentRating": "4+", + "description": "1Focus creates an oasis for focused work by disabling access to specific websites and apps. Use it to schedule a bit of automated self-restraint when you find yourself clicking away from what really needs to get done. Ideal for students, freelancers and writers.\n\n\"If you find yourself on Facebook or checking your email every five minutes, you need 1Focus.\" – Pagoda Technologies\n\n\"1Focus is one of the best apps for tuning out the diversions that are most distracting for you.\" – Tyler Horvath, CEO of Tyton Media\n\n\nFREE FEATURES\n\n• Block websites in Google Chrome, Safari, Opera, Microsoft Edge and Brave\n• Block apps (e.g. email, games)\n• Block internet access by blocking web browsers\n• You cannot cancel active blocks once you close the 1Focus window\n• Create your own task presets (up to 2)\n• Dark Mode\n\n\n1FOCUS PRO\n\n• Schedule recurring block events (e.g. Mon - Fri)\n• Work break timer\n• Unlimited task presets\n• Block all websites/apps except specific ones\n• Suspend blocking for a limited time\n• Block URL keywords (e.g. *gaming*)\n• Block popular websites by category (e.g. Social Media)\n\nTry it free for 14 days. $1.99/month or $9.99/year after.\n\nPrices may vary by location. Subscriptions are charged to your iTunes Account. They automatically renew unless you cancel them in your Account Settings at least 24 hours before the end of the current period. Your Account is charged for renewal within 24 hours prior to the end of the current period. Terms of use: https://onefocusapp.com/terms\n\n\nCUSTOMER SUPPORT\n\nDo you have any questions or suggestions?\nonefocusapp.com/support", + "currency": "USD", + "artistId": 969210609, + "artistName": "Niklas Behrens", + "genres": [ + "Productivity", + "Education" + ], + "price": 0.00, + "bundleId": "com.onefocusapp.OneFocus", + "version": "3.4.4", + "wrapperType": "software", + "userRatingCount": 0 + } + ] +} diff --git a/Tests/MasKitTests/JSON/search/tweetbot.json b/Tests/masTests/JSON/search/tweetbot.json similarity index 100% rename from Tests/MasKitTests/JSON/search/tweetbot.json rename to Tests/masTests/JSON/search/tweetbot.json diff --git a/Tests/MasKitTests/Models/SoftwareProductMock.swift b/Tests/masTests/Models/MockSoftwareProduct.swift similarity index 70% rename from Tests/MasKitTests/Models/SoftwareProductMock.swift rename to Tests/masTests/Models/MockSoftwareProduct.swift index c12bfa6..ca03bc7 100644 --- a/Tests/MasKitTests/Models/SoftwareProductMock.swift +++ b/Tests/masTests/Models/MockSoftwareProduct.swift @@ -1,6 +1,6 @@ // -// SoftwareProductMock.swift -// MasKitTests +// MockSoftwareProduct.swift +// masTests // // Created by Ben Chatelain on 12/27/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -8,9 +8,9 @@ import Foundation -@testable import MasKit +@testable import mas -struct SoftwareProductMock: SoftwareProduct { +struct MockSoftwareProduct: SoftwareProduct { var appName: String var bundleIdentifier: String var bundlePath: String diff --git a/Tests/masTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift new file mode 100644 index 0000000..7895b1c --- /dev/null +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -0,0 +1,35 @@ +// +// SearchResultListSpec.swift +// masTests +// +// Created by Ben Chatelain on 9/2/20. +// Copyright © 2020 mas-cli. All rights reserved. +// + +import Foundation +import Nimble +import Quick + +@testable import mas + +public class SearchResultListSpec: QuickSpec { + override public func spec() { + beforeSuite { + MAS.initialize() + } + describe("search result list") { + it("can parse bbedit") { + expect( + try JSONDecoder().decode(SearchResultList.self, from: Data(from: "search/bbedit.json")).resultCount + ) + == 1 + } + it("can parse things") { + expect( + try JSONDecoder().decode(SearchResultList.self, from: Data(from: "search/things.json")).resultCount + ) + == 50 + } + } + } +} diff --git a/Tests/MasKitTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift similarity index 52% rename from Tests/MasKitTests/Models/SearchResultSpec.swift rename to Tests/masTests/Models/SearchResultSpec.swift index 563946b..9462456 100644 --- a/Tests/MasKitTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -1,6 +1,6 @@ // // SearchResultSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 9/2/20. // Copyright © 2020 mas-cli. All rights reserved. @@ -10,20 +10,21 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class SearchResultSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("search result") { it("can parse things") { - let data = Data(from: "search/things-that-go-bump.json") - let decoder = JSONDecoder() - let result = try decoder.decode(SearchResult.self, from: data) - - expect(result.bundleId) == "uikitformac.com.tinybop.thingamabops" + expect( + try JSONDecoder() + .decode(SearchResult.self, from: Data(from: "search/things-that-go-bump.json")) + .bundleId + ) + == "uikitformac.com.tinybop.thingamabops" } } } diff --git a/Tests/MasKitTests/Models/SoftwareProductSpec.swift b/Tests/masTests/Models/SoftwareProductSpec.swift similarity index 61% rename from Tests/MasKitTests/Models/SoftwareProductSpec.swift rename to Tests/masTests/Models/SoftwareProductSpec.swift index ba1262f..cd5e15a 100644 --- a/Tests/MasKitTests/Models/SoftwareProductSpec.swift +++ b/Tests/masTests/Models/SoftwareProductSpec.swift @@ -1,6 +1,6 @@ // // SoftwareProductSpec.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 9/30/21. // Copyright © 2018 mas-cli. All rights reserved. @@ -10,15 +10,15 @@ import Foundation import Nimble import Quick -@testable import MasKit +@testable import mas public class SoftwareProductSpec: QuickSpec { override public func spec() { beforeSuite { - MasKit.initialize() + MAS.initialize() } describe("software product") { - let app = SoftwareProductMock( + let app = MockSoftwareProduct( appName: "App", bundleIdentifier: "", bundlePath: "", @@ -26,10 +26,10 @@ public class SoftwareProductSpec: QuickSpec { itemIdentifier: 111 ) - let currentApp = SearchResult(kind: "mac-software", version: "1.0.0") - let appUpdate = SearchResult(kind: "mac-software", version: "2.0.0") - let higherOs = SearchResult(kind: "mac-software", minimumOsVersion: "99.0.0", version: "3.0.0") - let updateIos = SearchResult(kind: "software", minimumOsVersion: "99.0.0", version: "3.0.0") + let currentApp = SearchResult(version: "1.0.0") + let appUpdate = SearchResult(version: "2.0.0") + let higherOs = SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0") + let updateIos = SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0") it("is not outdated when there is no new version available") { expect(app.isOutdatedWhenComparedTo(currentApp)) == false @@ -37,11 +37,11 @@ public class SoftwareProductSpec: QuickSpec { it("is outdated when there is a new version available") { expect(app.isOutdatedWhenComparedTo(appUpdate)) == true } - it("is not outdated when the new version requires a higher OS version") { + it("is not outdated when the new version of mac-software requires a higher OS version") { expect(app.isOutdatedWhenComparedTo(higherOs)) == false } - it("ignores minimum iOS version") { - expect(app.isOutdatedWhenComparedTo(updateIos)) == true + it("is not outdated when the new version of software requires a higher OS version") { + expect(app.isOutdatedWhenComparedTo(updateIos)) == false } } } diff --git a/Tests/MasKitTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/MockFromFileNetworkSession.swift similarity index 59% rename from Tests/MasKitTests/Network/NetworkSessionMockFromFile.swift rename to Tests/masTests/Network/MockFromFileNetworkSession.swift index 52c5d12..893f15e 100644 --- a/Tests/MasKitTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/MockFromFileNetworkSession.swift @@ -1,6 +1,6 @@ // -// NetworkSessionMockFromFile.swift -// MasKitTests +// MockFromFileNetworkSession.swift +// masTests // // Created by Ben Chatelain on 2019-01-05. // Copyright © 2019 mas-cli. All rights reserved. @@ -10,7 +10,7 @@ import Foundation import PromiseKit /// Mock NetworkSession for testing with saved JSON response payload files. -class NetworkSessionMockFromFile: NetworkSessionMock { +class MockFromFileNetworkSession: MockNetworkSession { /// Path to response payload file relative to test bundle. private let responseFile: String @@ -21,18 +21,13 @@ class NetworkSessionMockFromFile: NetworkSessionMock { self.responseFile = responseFile } - /// Loads data from a file. - /// - /// - Parameters: - /// - url: unused - /// - completionHandler: Closure which is delivered either data or an error. override func loadData(from _: URL) -> Promise { - guard let fileURL = Bundle.url(for: responseFile) - else { fatalError("Unable to load file \(responseFile)") } + guard let fileURL = Bundle.url(for: responseFile) else { + fatalError("Unable to load file \(responseFile)") + } do { - let data = try Data(contentsOf: fileURL, options: .mappedIfSafe) - return .value(data) + return .value(try Data(contentsOf: fileURL, options: .mappedIfSafe)) } catch { print("Error opening file: \(error)") return Promise(error: error) diff --git a/Tests/MasKitTests/Network/NetworkSessionMock.swift b/Tests/masTests/Network/MockNetworkSession.swift similarity index 63% rename from Tests/MasKitTests/Network/NetworkSessionMock.swift rename to Tests/masTests/Network/MockNetworkSession.swift index 4be4b1b..504ff32 100644 --- a/Tests/MasKitTests/Network/NetworkSessionMock.swift +++ b/Tests/masTests/Network/MockNetworkSession.swift @@ -1,6 +1,6 @@ // -// NetworkSessionMock -// MasKitTests +// MockNetworkSession +// masTests // // Created by Ben Chatelain on 11/13/18. // Copyright © 2018 mas-cli. All rights reserved. @@ -9,20 +9,15 @@ import Foundation import PromiseKit -@testable import MasKit +@testable import mas /// Mock NetworkSession for testing. -class NetworkSessionMock: NetworkSession { +class MockNetworkSession: NetworkSession { // Properties that enable us to set exactly what data or error // we want our mocked URLSession to return for any request. var data: Data? var error: Error? - /// Immediately passes data and error to completion handler. - /// - /// - Parameters: - /// - url: unused - /// - completionHandler: Closure which is delivered either data or an error. func loadData(from _: URL) -> Promise { guard let data else { return Promise(error: error ?? MASError.noData) diff --git a/Tests/MasKitTests/Network/NetworkManagerTests.swift b/Tests/masTests/Network/NetworkManagerTests.swift similarity index 90% rename from Tests/MasKitTests/Network/NetworkManagerTests.swift rename to Tests/masTests/Network/NetworkManagerTests.swift index 1d73534..6f3e520 100644 --- a/Tests/MasKitTests/Network/NetworkManagerTests.swift +++ b/Tests/masTests/Network/NetworkManagerTests.swift @@ -1,6 +1,6 @@ // // NetworkManagerTests.swift -// MasKitTests +// masTests // // Created by Ben Chatelain on 1/5/19. // Copyright © 2019 mas-cli. All rights reserved. @@ -8,17 +8,17 @@ import XCTest -@testable import MasKit +@testable import mas class NetworkManagerTests: XCTestCase { - override public func setUp() { + override func setUp() { super.setUp() - MasKit.initialize() + MAS.initialize() } func testSuccessfulAsyncResponse() throws { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) // Create data and tell the session to always return it @@ -35,7 +35,7 @@ class NetworkManagerTests: XCTestCase { func testSuccessfulSyncResponse() throws { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) // Create data and tell the session to always return it @@ -52,7 +52,7 @@ class NetworkManagerTests: XCTestCase { func testFailureAsyncResponse() { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) session.error = MASError.noData @@ -73,7 +73,7 @@ class NetworkManagerTests: XCTestCase { func testFailureSyncResponse() { // Setup our objects - let session = NetworkSessionMock() + let session = MockNetworkSession() let manager = NetworkManager(session: session) session.error = MASError.noData diff --git a/contrib/completion/mas.fish b/contrib/completion/mas.fish index 592218a..de9576f 100644 --- a/contrib/completion/mas.fish +++ b/contrib/completion/mas.fish @@ -46,7 +46,7 @@ complete -c mas -n "__fish_seen_subcommand_from help" -xa "list" complete -c mas -n "__fish_use_subcommand" -f -a lucky -d "Install the first result from the Mac App Store" complete -c mas -n "__fish_seen_subcommand_from help" -xa "lucky" ### open -complete -c mas -n "__fish_use_subcommand" -f -a open -d "Opens app page in AppStore.app" +complete -c mas -n "__fish_use_subcommand" -f -a open -d "Opens app page in 'App Store.app'" complete -c mas -n "__fish_seen_subcommand_from help" -xa "open" ### outdated complete -c mas -n "__fish_use_subcommand" -f -a outdated -d "Lists pending updates from the Mac App Store" diff --git a/docs/sample.swift b/docs/sample.swift index 793d947..17fc5d4 100644 --- a/docs/sample.swift +++ b/docs/sample.swift @@ -74,7 +74,7 @@ case let .success(data): print("The response returned successfully \(data)") case let .failure(error): - print("An error occured: \(error)") + print("An error occurred: \(error)") } // MARK: Organization @@ -90,11 +90,14 @@ private extension MyClass { guard let singleTest = somethingFailable() else { return } guard statementThatShouldBeTrue else { return } -// If there is one long expression to guard or multiple expressions -// move else to next line -guard let oneItem = somethingFailable(), +// If a guard clause requires multiple lines, chop down, then start `else` new line +// In this case, always chop down else clause. +guard + let oneItem = somethingFailable(), let secondItem = somethingFailable2() -else { return } +else { + return +} // If the return in else is long, move to next line guard let something = somethingFailable() else { diff --git a/docs/style.md b/docs/style.md index e4c48cc..c0487ba 100644 --- a/docs/style.md +++ b/docs/style.md @@ -5,7 +5,9 @@ - Use `script/lint` to look for these before committing. - Note that [two trailing spaces](https://gist.github.com/shaunlebron/746476e6e7a4d698b373) is intentional in markdown documents to create a line break like `
`, so these should _not_ be removed. -- End each file with a [newline character](https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789). +- End each file with a [newline character]( + https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789 + ). ## Swift @@ -14,13 +16,13 @@ - Avoid [force unwrapping optionals](https://blog.timac.org/2017/0628-swift-banning-force-unwrapping-optionals/) with `!` in production code - Production code is what gets shipped with the app. Basically, everything under the - [`mas-cli/`](https://github.com/mas-cli/mas/tree/main/mas-cli) folder. + [`Sources/mas`](https://github.com/mas-cli/mas/tree/main/Sources/mas) folder. - However, force unwrapping is **encouraged** in tests for less code and tests _should_ break when any expected conditions aren't met. - Prefer `struct`s over `class`es wherever possible - Default to marking classes as `final` - Prefer protocol conformance to class inheritance -- Break long lines after 120 characters +- Break lines at 120 characters - Use 4 spaces for indentation - Use `let` whenever possible to make immutable variables - Name all parameters in functions and enum cases @@ -28,10 +30,7 @@ with `!` in production code - Let the compiler infer the type whenever possible - Group computed properties below stored properties - Use a blank line above and below computed properties -- Group methods into specific extensions for each level of access control +- Group functions into separate extensions for each level of access control - When capitalizing acronyms or initialisms, follow the capitalization of the first letter. - When using `Void` in function signatures, prefer `()` for arguments and `Void` for return types. -- Prefer strong `IBOutlet` references. - Avoid evaluating a weak reference multiple times in the same scope. Strongify first, then use the strong reference. -- Prefer to name `IBAction` and target/action methods using a verb describing the action it will trigger, instead - of the user action (e.g., `edit:` instead of `editTapped:`) diff --git a/script/bootstrap b/script/bootstrap index e6aac8e..8af2285 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/bootstrap # mas @@ -6,6 +6,13 @@ # Installs development dependencies and builds project dependencies. # +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + function usage { echo "Usage: bootstrap [-f]" echo " -f option enables frozen mode" @@ -26,35 +33,23 @@ while getopts "f" opt; do esac done -main() { - script/clean +script/clean - echo "==> 👢 Bootstrapping" +printf $'==> 👢 Bootstrapping (%s)\n' "$(script/version)" - # Install Homebrew tools - if [[ "${frozen}" == "-f" ]]; then - # --no-lock Don't touch Brewfile.lock.json - brew bundle install --no-lock --no-upgrade --verbose - else - # Allow upgrades - rm -f Brewfile.lock.json - brew bundle install --verbose - fi +# Install Homebrew tools +if [[ "${frozen}" == "-f" ]]; then + # --no-lock Don't touch Brewfile.lock.json + brew bundle install --no-lock --no-upgrade --verbose +else + # Allow upgrades + rm -f Brewfile.lock.json + brew bundle install --verbose +fi - if [[ ! -x "$(command -v mise)" ]]; then - brew install mise - fi - mise settings set experimental true - mise install --verbose - mise list --verbose - - # Already installed on GitHub Actions runner. - if [[ ! -x "$(command -v swiftlint)" ]]; then - brew install swiftlint - fi - - # Generate Package.swift - script/version -} - -main +if [[ ! -x "$(command -v mise)" ]]; then + brew install mise +fi +mise settings set experimental true +mise install --verbose +mise list --verbose diff --git a/script/build b/script/build index ed9ea03..baacc02 100755 --- a/script/build +++ b/script/build @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/build # mas @@ -6,9 +6,16 @@ # Builds the Swift Package. # +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + # Build for the host architecture by default. ARCH=() -if [[ "$1" == '--universal' ]]; then +if [[ "${#}" -gt 1 && "${1}" == '--universal' ]]; then ARCH=( --arch arm64 --arch x86_64 @@ -21,9 +28,9 @@ if [[ "$(swift build --help)" =~ manifest-cache ]]; then CACHE=(--manifest-cache none) fi -echo "==> 🏗️ Building mas ($(script/version))" +echo "==> 🏗️ Building mas ($(script/version --write))" swift build \ --configuration release \ - "${ARCH[@]}" \ + "${ARCH[@]+"${ARCH[@]}"}" \ --disable-sandbox \ - "${CACHE[@]}" + "${CACHE[@]+"${CACHE[@]}"}" diff --git a/script/format b/script/format index f1f6265..b942cb2 100755 --- a/script/format +++ b/script/format @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/format # mas @@ -6,38 +6,46 @@ # Linting checks for development and CI. # # Automatically formats and fixes style violations using various tools. -# Additionally runs `lint` to report any remaining style violations. # # Please keep in sync with script/lint. # -echo "==> 🚨 Formatting mas" +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" -for LINTER in markdownlint prettier shfmt swiftformat swiftlint yamllint; do +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +printf $'==> 🚨 Formatting mas\n' + +for LINTER in markdownlint prettier shfmt swift-format swiftformat swiftlint yamllint; do if [[ ! -x "$(command -v ${LINTER})" ]]; then - echo "error: ${LINTER} is not installed. Run 'script/bootstrap' or 'brew install ${LINTER}'." + printf $'error: %s is not installed. Run \'script/bootstrap\' or \'brew install %s\'.\n' "${LINTER}" "${LINTER}" exit 1 fi done -echo "--> 🕊️ Swift" for SOURCE in Package.swift Sources Tests; do - swiftformat ${SOURCE} - swift run swift-format format --in-place --recursive ${SOURCE} - swiftlint lint --fix --strict ${SOURCE} + printf -- $'--> 🕊 %s swift-format\n' "${SOURCE}" + swift-format format --in-place --recursive "${SOURCE}" + printf -- $'--> 🕊 %s swiftformat\n' "${SOURCE}" + swiftformat "${SOURCE}" + printf -- $'--> 🕊 %s swiftlint\n' "${SOURCE}" + swiftlint --fix --strict "${SOURCE}" done -echo "--> 〽️ Markdown" -markdownlint --config .markdownlint.json --fix .github . - -echo "--> 🖊 YAML" -# shellcheck disable=SC2046 -prettier --write $(yamllint --list-files .) - -echo "--> 📜 Bash" +printf -- $'--> 📜 Bash shfmt\n' shfmt \ --write \ --list \ --indent 2 \ --case-indent \ contrib/ script/ + +printf -- $'--> 〽️ Markdown\n' +markdownlint --config .markdownlint.json --fix .github . + +printf -- $'--> 🖊 YAML\n' +# shellcheck disable=SC2046 +prettier --write $(yamllint --list-files .) diff --git a/script/lint b/script/lint index c95292d..d655a52 100755 --- a/script/lint +++ b/script/lint @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -u # # script/lint # mas @@ -10,37 +10,67 @@ # Please keep in sync with script/format. # -echo "==> 🚨 Linting mas" +set -o pipefail -for LINTER in git markdownlint periphery shfmt swiftformat swiftlint yamllint; do - if [[ ! -x "$(command -v ${LINTER})" ]]; then - echo "error: ${LINTER} is not installed. Run 'script/bootstrap' or 'brew install ${LINTER}'." +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +printf $'==> 🚨 Linting mas (%s)\n' "$(script/version --write)" + +for linter in git markdownlint periphery shellcheck shfmt swift-format swiftformat swiftlint yamllint; do + if [[ ! -x "$(command -v ${linter})" ]]; then + printf $'error: %s is not installed. Run \'script/bootstrap\' or \'brew install %s\'.\n' "${linter}" "${linter}" exit 1 fi done -echo "--> 🕊️ Swift" -for SOURCE in Package.swift Sources Tests; do - swiftformat --lint ${SOURCE} - swift run swift-format lint --recursive ${SOURCE} - swiftlint lint --strict ${SOURCE} +exit_code=0 +for source in Package.swift Sources Tests; do + printf -- $'--> 🦅 %s swift-format\n' "${source}" + swift-format lint --strict --recursive "${source}" + ((exit_code |= "${?}")) + printf -- $'--> 🦅 %s swiftformat\n' "${source}" + script -q /dev/null swiftformat --lint --strict "${source}" | + (grep -vxE '(?:\^D\x08{2})?Running SwiftFormat\.{3}\r|\(lint mode - no files will be changed\.\)\r|Reading (?:config|swift-version) file at .*|\x1b\[32mSwiftFormat completed in \d+\.\d+s\.\x1b\[0m\r|0/\d+ files require formatting\.\r' || true) + ((exit_code |= "${?}")) + printf -- $'--> 🦅 %s swiftlint\n' "${source}" + swiftlint --strict --quiet "${source}" 2> \ + >((grep -vxF $'warning: Configuration option \'allow_multiline_func\' in \'opening_brace\' rule is deprecated. Use the option \'ignore_multiline_function_signatures\' instead.' || true) >&2) + ((exit_code |= "${?}")) done -periphery scan -echo "--> 🌳 Git" -git diff --check - -echo "--> 〽️ Markdown" -markdownlint --config .markdownlint.json .github . - -echo "--> 🖊 YAML" -yamllint . - -echo "--> 📜 Bash" +printf -- $'--> 🐚 Bash shellcheck\n' shellcheck --shell=bash script/* +((exit_code |= "${?}")) + +printf -- $'--> 📜 Bash shfmt\n' shfmt \ --diff \ --list \ --indent 2 \ --case-indent \ contrib/ script/ +((exit_code |= "${?}")) + +printf -- $'--> 〽️ Markdown\n' +markdownlint --config .markdownlint.json .github . +((exit_code |= "${?}")) + +printf -- $'--> 🌳 Git\n' +PAGER='cat' git diff --check +((exit_code |= "${?}")) + +printf -- $'--> 🖊 YAML\n' +yamllint . +((exit_code |= "${?}")) + +printf -- $'--> 🌀 Periphery\n' +script -q /dev/null periphery scan --strict --quiet --disable-update-check | + (grep -vxE '(?:\x1b\[0;1;32m|\^D\x08{2})\* (?:\x1b\[0;0m\x1b\[0;1m)?No unused code detected\.(?:\x1b\[0;0m)?\r' || true) +((exit_code |= "${?}")) + +exit "${exit_code}" diff --git a/script/test b/script/test index 01188ef..7c5f41c 100755 --- a/script/test +++ b/script/test @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/test # mas @@ -6,5 +6,13 @@ # Runs mas tests. # -echo "==> ✅ Testing" +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +printf $'==> ✅ Testing mas (%s)\n' "$(script/version --write)" + swift test diff --git a/script/uninstall b/script/uninstall index 043e5d6..e672d28 100755 --- a/script/uninstall +++ b/script/uninstall @@ -15,4 +15,4 @@ fi echo "==> 🔥 Uninstalling mas from $PREFIX" -trash "$PREFIX/bin/mas" || true +trash -F "$PREFIX/bin/mas" || true diff --git a/script/version b/script/version index 604e784..a11a32d 100755 --- a/script/version +++ b/script/version @@ -1,4 +1,4 @@ -#!/bin/bash -ue +#!/bin/bash -eu # # script/version # mas @@ -6,10 +6,25 @@ # Displays the current marketing version of mas. # -if git describe >/dev/null 2>&1; then - last_tag=$(git describe --abbrev=0 --tags 2>/dev/null || true) - # Use MAS_VERSION environment varible unless unset - MAS_VERSION=${MAS_VERSION:-$last_tag} +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +if [[ -z "${MAS_VERSION:-}" ]]; then + # Use last tag if MAS_VERSION environment variable is unset or empty + MAS_VERSION=$(git describe --abbrev=0 --tags 2>/dev/null || true) +fi +echo "${MAS_VERSION#v}" + +if [[ "${#}" -ge 1 && "${1}" == '--write' ]]; then + # Write new version into swift package + cat <Sources/mas/Package.swift +/// Generated by \`script/version\`. +enum Package { + static let version = "${MAS_VERSION#v}" +} +EOF fi -MAS_VERSION=${MAS_VERSION#v} -echo "${MAS_VERSION}" diff --git a/script/version_bump b/script/version_bump index d3a52b7..4f4f5b2 100755 --- a/script/version_bump +++ b/script/version_bump @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash -eu # # script/version_bump # mas @@ -6,10 +6,16 @@ # Increments the marketing version of mas. # -PROJECT_PATH="$(git rev-parse --show-toplevel)" -LOCAL_MAS_FORMULA_PATH="${PROJECT_PATH}/Homebrew/mas.rb" -LOCAL_TAP_FORMULA_PATH="${PROJECT_PATH}/Homebrew/mas-tap.rb" -SWIFT_PACKAGE="${PROJECT_PATH}/Sources/MasKit/Package.swift" +mas_dir="$(readlink -fn "$(dirname "${BASH_SOURCE:-"${0}"}")/..")" + +if ! cd -- "${mas_dir}"; then + printf $'Error: Could not cd into mas directory: %s\n' "${mas_dir}" >&2 + exit 1 +fi + +LOCAL_MAS_FORMULA_PATH="Homebrew/mas.rb" +LOCAL_TAP_FORMULA_PATH="Homebrew/mas-tap.rb" +SWIFT_PACKAGE="Sources/mas/Package.swift" function usage { echo "Usage: version_bump v0.0 [sha1_hash]" @@ -23,12 +29,10 @@ if [[ $# -lt 1 ]]; then fi # arg 1 - version tag -if test -n "${1}"; then - MAS_VERSION="${1}" -fi +MAS_VERSION="${1}" # arg 2 - revision (commit hash) -if test -n "${2}"; then +if [[ "${#}" -ge 2 ]]; then REVISION="${2}" else REVISION=$(git rev-parse "${MAS_VERSION}") @@ -39,7 +43,7 @@ echo "REVISION: ${REVISION}" # Write new version into swift package cat <"${SWIFT_PACKAGE}" -// Generated by: script/version +/// Generated by \`script/version_bump\`. enum Package { static let version = "${MAS_VERSION#v}" }