├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── build.yml │ ├── release_github.yml │ ├── release_github_non-appcast.yml │ ├── test_build.yml │ ├── test_build_debug.yml │ ├── update_airlift_binary.yml │ ├── update_csv2notion_neo_binary.yml │ └── update_pagemaker.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Distribution ├── dmg-builds │ ├── build-marker-data-dmg.json │ ├── dmg-background.png │ ├── dmg-background@2x.png │ ├── entitlements.plist │ ├── marker-data-dmg-icon.icns │ ├── sparkle │ │ ├── generate_appcast │ │ ├── generate_appcast_script.py │ │ └── sign_update │ └── uninstaller │ │ └── include │ │ ├── Uninstall Marker Data.scpt │ │ ├── applet.icns │ │ └── entitlements.plist └── version.txt ├── LICENSE ├── Press Kit ├── Marker Data - Press Release.pdf └── press-kit.zip ├── README.md ├── SDK ├── Workflow_Extensions_1.0.2.dmg └── Workflow_Extensions_1.0.3.dmg ├── SECURITY.md ├── Source └── Marker Data │ ├── Marker Data.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Marker Data.xcscheme │ ├── Marker Data │ ├── ApplicationDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-1024@1x.png │ │ │ ├── Icon-128@1x.png │ │ │ ├── Icon-128@2x.png │ │ │ ├── Icon-16@1x.png │ │ │ ├── Icon-16@2x.png │ │ │ ├── Icon-256@1x.png │ │ │ ├── Icon-256@2x 1.png │ │ │ ├── Icon-256@2x.png │ │ │ ├── Icon-32@1x.png │ │ │ └── Icon-32@2x.png │ │ ├── AppIconSingle.imageset │ │ │ ├── Contents.json │ │ │ └── Icon-1024@1x.png │ │ ├── Contents.json │ │ └── Export Profile Icons │ │ │ ├── AirtableLogo.imageset │ │ │ ├── AirtableLogo.png │ │ │ └── Contents.json │ │ │ ├── CompressorLogo.imageset │ │ │ ├── CompressorLogo.png │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── ExcelLogo.imageset │ │ │ ├── Contents.json │ │ │ └── ExcelLogo.png │ │ │ ├── MarkdownLogo.imageset │ │ │ ├── Contents.json │ │ │ └── MarkdownLogo.png │ │ │ ├── MusicLogo.imageset │ │ │ ├── Contents.json │ │ │ └── MusicLogo.png │ │ │ ├── NotionLogo.imageset │ │ │ ├── Contents.json │ │ │ └── NotionLogo.png │ │ │ ├── NumbersLogo.imageset │ │ │ ├── Contents.json │ │ │ └── NumbersLogo.png │ │ │ ├── ResolveLogo.imageset │ │ │ ├── Contents.json │ │ │ └── ResolveLogo.png │ │ │ └── YouTubeLogo.imageset │ │ │ ├── Contents.json │ │ │ └── YouTubeLogo.png │ ├── FCP Share Destination │ │ ├── Install View │ │ │ ├── InstallShareDestinationView.swift │ │ │ └── ShareDestinationInstaller.swift │ │ ├── Objective-C Code │ │ │ ├── Document Controller │ │ │ │ ├── DocumentController.h │ │ │ │ └── DocumentController.m │ │ │ └── Scripting │ │ │ │ ├── Asset.h │ │ │ │ ├── Asset.m │ │ │ │ ├── Document.h │ │ │ │ ├── Document.m │ │ │ │ ├── FCPXMetadataKeys.h │ │ │ │ ├── FCPXMetadataKeys.m │ │ │ │ ├── MakeCommand.h │ │ │ │ ├── MakeCommand.m │ │ │ │ ├── MediaAssetHelperKeys.h │ │ │ │ ├── MediaAssetHelperKeys.m │ │ │ │ ├── Object.h │ │ │ │ ├── Object.m │ │ │ │ ├── ScriptingSupportCategories.h │ │ │ │ └── ScriptingSupportCategories.m │ │ └── OpenEventHandler.swift │ ├── Marker_Data.entitlements │ ├── Marker_DataApp.swift │ ├── Models │ │ ├── Color Swatch │ │ │ ├── ColorPaletteRenderer.swift │ │ │ ├── ColorsExtractorService │ │ │ │ ├── ColorExtractMethod.swift │ │ │ │ ├── ColorMood.swift │ │ │ │ ├── ColorsExtractorService.swift │ │ │ │ └── DeltaEFormulaExtension.swift │ │ │ ├── ImageRenderService │ │ │ │ ├── ImageMergeOperation.swift │ │ │ │ ├── ImageRenderService.swift │ │ │ │ └── ImageRenderServiceError.swift │ │ │ ├── Other │ │ │ │ ├── ColorPaletteFileFormat.swift │ │ │ │ └── ImageStrip.swift │ │ │ └── Settings Model │ │ │ │ └── ColorSwatchSettingsModel.swift │ │ ├── Configurations │ │ │ └── ConfigurationsViewModel.swift │ │ ├── Database │ │ │ ├── DatabaseManager.swift │ │ │ └── Profile Models │ │ │ │ ├── Airtable │ │ │ │ └── AirtableDBModel.swift │ │ │ │ ├── DatabasePlatform.swift │ │ │ │ ├── DatabaseProfileModel.swift │ │ │ │ ├── Dropbox │ │ │ │ ├── DropboxInfo.swift │ │ │ │ └── DropboxSetupModel.swift │ │ │ │ └── Notion │ │ │ │ └── NotionDBModel.swift │ │ ├── Errors │ │ │ ├── ConfigurationErrors.swift │ │ │ ├── DatabaseErrors.swift │ │ │ └── ExtractError.swift │ │ ├── Extract │ │ │ ├── DatabaseUploader.swift │ │ │ ├── ExportExitStatus.swift │ │ │ ├── ExportProcess.swift │ │ │ ├── Extraction Model │ │ │ │ ├── ExtractionModel.swift │ │ │ │ └── ExtractionModel_EventHandlers.swift │ │ │ ├── ExtractionResult.swift │ │ │ └── ProgressViewModel.swift │ │ ├── Other │ │ │ ├── MainViews.swift │ │ │ ├── UnifiedExportProfile.swift │ │ │ └── WindowSize.swift │ │ ├── Queue │ │ │ ├── ExtractInfo.swift │ │ │ ├── QueueError.swift │ │ │ ├── QueueInstance.swift │ │ │ ├── QueueModel.swift │ │ │ └── QueueStatus.swift │ │ ├── Roles │ │ │ ├── RoleModel.swift │ │ │ ├── RolesManager+DropDelegate.swift │ │ │ └── RolesManager.swift │ │ └── Settings │ │ │ ├── MarkersExtractorModelExtensions.swift │ │ │ ├── SettingsContainer.swift │ │ │ ├── SettingsModels.swift │ │ │ ├── SettingsStore.swift │ │ │ └── SettingsVersioningManager.swift │ ├── Pagemaker │ │ ├── PagemakerPDFExportHandler.swift │ │ ├── PagemakerUIDelegate.swift │ │ ├── PagemakerView.swift │ │ └── WebViewStateManager.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Resources │ │ ├── DefaultConfiguration.json │ │ ├── Marker Data H.264.fcpxdest │ │ ├── Marker Data Source.fcpxdest │ │ ├── OSAScriptingDefinition.sdef │ │ ├── Pagemaker.html │ │ ├── airlift │ │ ├── csv2notion_neo │ │ └── entitlements.plist │ ├── Utilities │ │ ├── Extensions │ │ │ ├── ArrayExtension.swift │ │ │ ├── BundleExtension.swift │ │ │ ├── ColorExtension.swift │ │ │ ├── DominantColorAlgorithmExtension.swift │ │ │ ├── EmptyOrIntFormatStyle.swift │ │ │ ├── ExportProfileFormatExtrension.swift │ │ │ ├── NSImageExtension.swift │ │ │ ├── NotificationNameExtension.swift │ │ │ ├── RoleExtension.swift │ │ │ ├── StringExtension.swift │ │ │ ├── TaskExtension.swift │ │ │ ├── URLExtension.swift │ │ │ ├── UTTypeExtension.swift │ │ │ └── ViewExtensions.swift │ │ ├── Notifications │ │ │ ├── NotificationFrequency.swift │ │ │ └── NotificationManager.swift │ │ ├── Other │ │ │ ├── DeepCopy.swift │ │ │ ├── DeminiaturizeAllWindows.swift │ │ │ ├── DicitionaryEncoder.swift │ │ │ ├── LibraryFolders.swift │ │ │ ├── Links.swift │ │ │ ├── LogManager.swift │ │ │ ├── SidebarSelectionSwitcher.swift │ │ │ ├── UserDefaultsArray.swift │ │ │ └── WalkDirectory.swift │ │ └── Shell │ │ │ ├── Shell.swift │ │ │ ├── ShellArgumentList.swift │ │ │ └── ShellError.swift │ └── Views │ │ ├── Components │ │ ├── BigButtonStyle.swift │ │ ├── ColorPickerForm.swift │ │ ├── ColorPickerOpacitySliderForm.swift │ │ ├── LabeledFormElement.swift │ │ ├── LabeledTextboxStepperForm.swift │ │ ├── PulsingIcon.swift │ │ └── ResizedImage.swift │ │ ├── Detail Views │ │ ├── AboutView.swift │ │ ├── Configurations │ │ │ ├── ConfigurationContextMenuView.swift │ │ │ ├── ConfigurationSettingsView.swift │ │ │ └── Configurations_AddSheet.swift │ │ ├── Database │ │ │ ├── Create Sheet │ │ │ │ ├── AirtableFormView.swift │ │ │ │ ├── CreateDBProfileSheet.swift │ │ │ │ ├── DropboxSetupView.swift │ │ │ │ ├── NotionFormView.swift │ │ │ │ └── PlatformInfoTextField.swift │ │ │ └── DatabaseSettingsView.swift │ │ ├── General Settings │ │ │ ├── FileSettingsView.swift │ │ │ ├── GeneralSettingsView.swift │ │ │ ├── NotificationSettingsView.swift │ │ │ ├── RolesSettingsView.swift │ │ │ └── UpdateSettingsView.swift │ │ ├── Image │ │ │ ├── ImageExtractionSettingsView.swift │ │ │ ├── ImageSettingsView.swift │ │ │ └── SwatchSettingsView.swift │ │ ├── Label │ │ │ ├── GeneralLabelSettingsView.swift │ │ │ ├── LabelSettingsView.swift │ │ │ └── OverlaySettingsView.swift │ │ └── QueueView.swift │ │ ├── Main │ │ ├── ContentView.swift │ │ └── ExtractView.swift │ │ ├── Menu Bar Commands │ │ ├── AppCommands.swift │ │ ├── ConfigurationCommands.swift │ │ ├── EditCommands.swift │ │ ├── FileCommands.swift │ │ └── HelpCommands.swift │ │ ├── Onboarding │ │ ├── OnboardingFeature.swift │ │ ├── OnboardingPageView.swift │ │ ├── OnboardingPages.swift │ │ └── OnboardingView.swift │ │ └── Other │ │ ├── CheckForUpdatesView.swift │ │ ├── ExportDestinationPicker.swift │ │ ├── ExportProfilePicker.swift │ │ ├── FailedExtractionsView.swift │ │ ├── HelpButton.swift │ │ └── PickerViews.swift │ ├── Marker-Data-Info.plist │ └── Workflow Extension │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024@1x.png │ │ ├── Icon-128@1x.png │ │ ├── Icon-128@2x.png │ │ ├── Icon-16@1x.png │ │ ├── Icon-16@2x.png │ │ ├── Icon-256@1x.png │ │ ├── Icon-256@2x 1.png │ │ ├── Icon-256@2x.png │ │ ├── Icon-32@1x.png │ │ └── Icon-32@2x.png │ ├── AppIconSingle.imageset │ │ ├── Contents.json │ │ └── Icon-1024@1x.png │ └── Contents.json │ ├── Info.plist │ ├── WorkflowExtension-Bridging-Header.h │ ├── WorkflowExtension.entitlements │ ├── WorkflowExtensionView.swift │ └── WorkflowExtensionViewController.swift ├── appcast.xml └── assets ├── macos_badge_noborder.png └── marker_data_app_icon.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [IAmVigneswaran, TheAcharya, milanvarady, orchetect] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a Bug for Marker Data 3 | title: "Bug: " 4 | labels: "bug" 5 | body: 6 | - type: input 7 | attributes: 8 | label: Marker Data Version? 9 | description: Which version of Marker Data are you using? 10 | placeholder: "1.2.0" 11 | validations: 12 | required: true 13 | - type: input 14 | attributes: 15 | label: macOS Version 16 | description: Which macOS version are you using? 17 | placeholder: "e.g. macOS Sonoma 14.7" 18 | validations: 19 | required: true 20 | - type: input 21 | attributes: 22 | label: Final Cut Pro Version 23 | description: Which Final Cut Pro version are you using? 24 | placeholder: "e.g. 11.0" 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Bug Description 30 | description: A clear description of the bug and how to reproduce it. Please attach FCPXMLD if necessary. 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Log excerpt 36 | description: Please attach log excerpt 37 | render: shell -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Workflow Discussions 4 | url: https://github.com/TheAcharya/MarkerData/discussions 5 | about: Questions about specific workflow. 6 | - name: I need help setting up or troubleshooting 7 | url: https://github.com/TheAcharya/MarkerData/discussions 8 | about: Questions not answered in the documentation. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for Marker Data 3 | title: "Feature Request: " 4 | labels: ["feature request"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Description 9 | description: A clear and detailed description of the feature request here. 10 | placeholder: "Enter the detailed description here" 11 | validations: 12 | required: true 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: macos-15 9 | 10 | steps: 11 | - name: Checkout Repository 12 | uses: actions/checkout@v4 13 | 14 | - name: List Xcode Installations 15 | run: sudo ls -1 /Applications | grep "Xcode" 16 | 17 | - name: Select Xcode 16.3 18 | run: sudo xcode-select -s /Applications/Xcode_16.3.0.app/Contents/Developer 19 | 20 | - name: Change to Project Directory 21 | run: cd Source/Marker\ Data 22 | 23 | - name: Prepare Directories 24 | run: | 25 | PARENT=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 26 | mkdir -p "$PARENT/dist/dmg-builds/app-build" 27 | mkdir -p "$PARENT/dist/dmg-builds/sdk" 28 | 29 | - name: Copy Workflow Extensions SDK 30 | run: | 31 | mkdir -p dist/dmg-builds/sdk 32 | cp -R ./SDK/Workflow_Extensions_1.0.3.dmg ./dist/dmg-builds/sdk 33 | 34 | - name: Mount Workflow Extensions DMG 35 | run: | 36 | hdiutil attach ./dist/dmg-builds/sdk/Workflow_Extensions_1.0.3.dmg 37 | 38 | - name: Install Workflow Extensions SDK 39 | run: | 40 | sudo installer -pkg /Volumes/WorkflowExtensionsSDK/WorkflowExtensionsSDK.pkg -target / 41 | 42 | - name: View Volume Directory 43 | run: | 44 | ls /Volumes 45 | 46 | - name: View Workflow Extensions SDK DMG Directory 47 | run: | 48 | ls /Volumes/WorkflowExtensionsSDK 49 | 50 | - name: Unmount Workflow Extensions SDK DMG 51 | run: | 52 | hdiutil detach /Volumes/WorkflowExtensionsSDK 53 | 54 | - name: Check Installed Workflow Extensions SDK Directory 55 | run: | 56 | ls /Library/Developer/SDKs 57 | 58 | - name: Build Marker Data 59 | run: | 60 | PARENT=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) 61 | PROJECT_PATH="Source/Marker Data/Marker Data.xcodeproj" 62 | SCHEME="Marker Data" 63 | CONFIGURATION="Release" 64 | DESTINATION="platform=macOS,arch=arm64" 65 | BUILD_FOLDER="$PARENT/dist/dmg-builds/app-build" 66 | 67 | xcodebuild -project "$PROJECT_PATH" -scheme "$SCHEME" -configuration "$CONFIGURATION" -destination "$DESTINATION" -derivedDataPath "$BUILD_FOLDER" clean build ONLY_ACTIVE_ARCH=NO EXCLUDED_ARCHS="x86_64" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -allowProvisioningUpdates | grep -v "Workflow Extension isn't code signed but requires entitlements" | grep -v "Marker Data isn't code signed but requires entitlements" | xcbeautify && exit ${PIPESTATUS[0]} 68 | -------------------------------------------------------------------------------- /.github/workflows/update_airlift_binary.yml: -------------------------------------------------------------------------------- 1 | name: update_airlift_binary 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update_binary: 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Get latest release info 15 | id: get_release 16 | run: | 17 | latest_release_tag=$(curl -sSL https://api.github.com/repos/TheAcharya/Airlift/releases/latest | jq -r '.tag_name | ltrimstr("v")') 18 | latest_release_url=$(curl -sSL https://api.github.com/repos/TheAcharya/Airlift/releases/latest | jq -r '.assets[] | select(.name | endswith("macos_arm64.zip")) | .browser_download_url') 19 | 20 | echo "Latest release tag: $latest_release_tag" 21 | echo "Latest release URL: $latest_release_url" 22 | 23 | curl -L -o airlift.zip $latest_release_url 24 | 25 | unzip airlift.zip -d extracted_files 26 | 27 | cp -R ./extracted_files/dist/airlift "./Source/Marker Data/Marker Data/Resources" 28 | 29 | git status 30 | git config user.name "GitHub Actions" 31 | git config user.email "actions@github.com" 32 | git add Source 33 | git commit -m "Updated Airlift Binary Version $latest_release_tag" 34 | git push 35 | -------------------------------------------------------------------------------- /.github/workflows/update_csv2notion_neo_binary.yml: -------------------------------------------------------------------------------- 1 | name: update_csv2notion-neo_binary 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update_binary: 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Get latest release info 15 | id: get_release 16 | run: | 17 | latest_release_tag=$(curl -sSL https://api.github.com/repos/TheAcharya/csv2notion-neo/releases/latest | jq -r '.tag_name | ltrimstr("v")') 18 | latest_release_url=$(curl -sSL https://api.github.com/repos/TheAcharya/csv2notion-neo/releases/latest | jq -r '.assets[] | select(.name | contains("macos_arm64.zip")) | .browser_download_url') 19 | 20 | echo "Latest release tag: $latest_release_tag" 21 | echo "Latest release URL: $latest_release_url" 22 | 23 | curl -L -o csv2notion-neo.zip $latest_release_url 24 | 25 | unzip csv2notion-neo.zip -d extracted_files 26 | 27 | cp -R ./extracted_files/dist/csv2notion_neo "./Source/Marker Data/Marker Data/Resources" 28 | 29 | git status 30 | git config user.name "GitHub Actions" 31 | git config user.email "actions@github.com" 32 | git add Source 33 | git commit -m "Updated CSV2Notion Neo Binary Version $latest_release_tag" 34 | git push 35 | -------------------------------------------------------------------------------- /.github/workflows/update_pagemaker.yml: -------------------------------------------------------------------------------- 1 | name: update_pagemaker 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update_pagemaker: 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Get latest release info 15 | id: get_release 16 | run: | 17 | # Get latest release info 18 | API_RESPONSE=$(curl -sSL -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/TheAcharya/MarkerData-Pagemaker/releases/latest) 19 | 20 | # Extract tag name and zipball URL 21 | LATEST_TAG=$(echo "$API_RESPONSE" | jq -r '.tag_name') 22 | ZIPBALL_URL=$(echo "$API_RESPONSE" | jq -r '.zipball_url') 23 | 24 | # Fallback to direct URL if API fails 25 | if [[ "$LATEST_TAG" == "null" || "$ZIPBALL_URL" == "null" ]]; then 26 | LATEST_TAG="v1.0.5" 27 | ZIPBALL_URL="https://github.com/TheAcharya/MarkerData-Pagemaker/archive/refs/tags/v1.0.5.zip" 28 | fi 29 | 30 | # Clean version number (remove v prefix) 31 | VERSION_NUMBER=$(echo "$LATEST_TAG" | sed 's/^v//') 32 | 33 | echo "VERSION=$VERSION_NUMBER" >> $GITHUB_ENV 34 | echo "ZIPBALL_URL=$ZIPBALL_URL" >> $GITHUB_ENV 35 | 36 | - name: Download and extract latest release 37 | run: | 38 | # Create temp directory for extraction 39 | mkdir -p temp_extract 40 | 41 | # Download the zipball 42 | curl -L -o pagemaker_source.zip "${{ env.ZIPBALL_URL }}" 43 | 44 | # Extract the zip file 45 | unzip -q pagemaker_source.zip -d temp_extract 46 | 47 | # Find the extracted directory and copy the file 48 | extract_dir=$(find temp_extract -type d -depth 1 | head -n 1) 49 | cp "$extract_dir/Pagemaker.html" "./Source/Marker Data/Marker Data/Resources/Pagemaker.html" 50 | 51 | # Clean up 52 | rm -rf temp_extract pagemaker_source.zip 53 | 54 | - name: Commit and push changes 55 | run: | 56 | # Check if there are changes to commit 57 | if git diff --quiet "./Source/Marker Data/Marker Data/Resources/Pagemaker.html"; then 58 | echo "No changes to Pagemaker.html, skipping commit" 59 | exit 0 60 | fi 61 | 62 | git config user.name "GitHub Actions" 63 | git config user.email "actions@github.com" 64 | git add "./Source/Marker Data/Marker Data/Resources/Pagemaker.html" 65 | git commit -m "Updated Pagemaker Version ${{ env.VERSION }}" 66 | git push -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | .nova 4 | *.dmg 5 | Package.resolved 6 | xcuserdata/ 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 1.2.0 (7) 4 | 5 | **🎉 Released:** 6 | - 12th May 2025 7 | 8 | **Marker Data** no longer officially supports macOS Ventura starting with version 1.2.0. 9 | 10 | **🔨 Improvements:** 11 | - Introducing [Pagemaker](https://markerdata.theacharya.co/user-guide/pagemaker/) – a new feature that allows users to create PDFs directly within Marker Data 12 | - Swatch analysis now provides percentage progress, including completion status for each processed image (#122) 13 | - Updated Notion Module CSV2Notion Neo to version 1.3.5 14 | - Increased Notion Module's upload threads 15 | 16 | --- 17 | 18 | ### 1.1.4 (6) 19 | 20 | **🎉 Released:** 21 | - 29th March 2025 22 | 23 | **🐞 Bug Fix:** 24 | - Removed unintended exposure of the XML Path column in certain Extraction Profile 25 | 26 | --- 27 | 28 | ### 1.1.3 (5) 29 | 30 | **🎉 Released:** 31 | - 20th February 2025 32 | 33 | **🔨 Improvements:** 34 | - Updated [Troubleshooting](https://markerdata.theacharya.co/troubleshooting/) guide to include Module Status 35 | 36 | **🐞 Bug Fix:** 37 | - Fixed a critical bug that caused Marker Data's Workflow Extension to crash in macOS Sequoia (#117) 38 | 39 | --- 40 | 41 | ### 1.1.2 (4) 42 | 43 | **🎉 Released:** 44 | - 17th February 2025 45 | 46 | **🔨 Improvements:** 47 | - Updated Notion Module CSV2Notion Neo to version 1.3.4 48 | - Updated Workflow Extensions SDK to 1.0.3 49 | - Internal dependencies updates 50 | - Codebase updates for Xcode 16.2 51 | - Complete codebase updates and refactors for Swift 6 strict concurrency compatibility 52 | 53 | **🐞 Bug Fix:** 54 | - Fixed a critical bug in the Notion module that prevented Marker Data's Data Set uploads due to Notion API changes 55 | - Markers placed on transitions are now extracted correctly 56 | - YouTube Chapters Extraction Profile now formats output timestamps consistently formatted as `HH:MM:SS` 57 | - YouTube Chapters Extraction Profile now inserts initial chapter marker at `00:00:00` if one does not exist 58 | 59 | --- 60 | 61 | ### 1.1.1 (3) 62 | 63 | **🎉 Released:** 64 | - 14th November 2024 65 | 66 | **Marker Data** is now exclusively build and optimised for Apple Silicon only. 67 | 68 | **🔨 Improvements:** 69 | - Added support and compatibility for FCPXML v1.13 (Final Cut Pro 11) 70 | - Added support and compatibility for frame rates `90p`, `100p` and `120p` 71 | 72 | --- 73 | 74 | ### 1.1.0 (2) 75 | 76 | **🎉 Released:** 77 | - 11th November 2024 78 | 79 | **Marker Data** is now exclusively build and optimised for Apple Silicon only. 80 | 81 | **🔨 Improvements:** 82 | - Application bundle size has been reduced 83 | - User can now Assign Shortcut to Configurations (#47) 84 | - Codebase updates for better compatibility with Xcode 16 85 | - Updated Notion Module CSV2Notion Neo to version 1.3.3 86 | - Updated Airtable Module Airlift to version 1.1.4 87 | 88 | **🐞 Bug Fix:** 89 | - Fixed a critical bug in the Notion module that prevented Marker Data's Data Set uploads when Notion Database URL is not provided 90 | 91 | --- 92 | 93 | ### 1.0.0 (1) 94 | 95 | **🎉 Released:** 96 | - 11th July 2024 97 | 98 | This is the first public release of **Marker Data**! 99 | 100 |

101 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Marker Data 2 | 3 | This file contains general guidelines but is subject to change and evolve over time. 4 | 5 | ## Code Contributions 6 | 7 | Before contributing, it is encouraged to post an Issue to discuss a bug or new feature prior to implementing it. Once implemented on your fork, Pull Requests are welcome for features that benefit the core functionality of the application. 8 | 9 | Code Owners & Maintainers reserve the right to revise or reject contributions if they are not deemed fit. 10 | 11 | ### Languages 12 | 13 | We kindly request that all pull requests be submitted in English. Pull requests submitted in other languages will unfortunately have to be declined. 14 | 15 | ### Code Formatting 16 | 17 | Code formatting is not strictly enforced but is a courtesy we would like contributors to employ. 18 | 19 | [SwiftFormat](https://github.com/nicklockwood/SwiftFormat) is used to format `*.swift` files. 20 | 21 | ```bash 22 | cd 23 | swiftformat . 24 | ``` 25 | 26 | ## Releases 27 | 28 | Publishing releases and tags should be left to Code Owners & Maintainers. 29 | 30 | For Code Owners & Maintainers, the following release specification is used: 31 | 32 | 1. Ensure package dependencies are set to version numbers and not branch names. 33 | 2. Ensure dependant binaries are update to date. 34 | 3. Perform the following file modifications: 35 | - Update the version number string literal in `Source/Marker Data/Marker Data.xcodeproj/project.pbxproj` under `MARKETING_VERSION`. 36 | - Update the build number string literal in `Source/Marker Data/Marker Data.xcodeproj/project.pbxproj` under `MARKETING_VERSION`. 37 | - Update root `CHANGELOG.md` 38 | - with a condensed bullet-point list of changes/fixes/improvements according to its established format 39 | - where possible, reference the Issue/PR number(s) or commit(s) where each change was made 40 | - Update `https://markerdata.theacharya.co/release-notes/` with identical notes from `CHANGELOG.md`. 41 | - Update `https://markerdata.theacharya.co/release-notes-appcast.html` with identical notes from `CHANGELOG.md`. 42 | 4. Commit the changes made in Step 3 using the new version number (ie: `1.0.0`) as the commit message, and push to main. 43 | 5. Update the version number of `Distribution/version.txt`. 44 | 6. Create [Test Build](https://github.com/TheAcharya/MarkerData/actions/workflows/test_build.yml) for internal and private (closed) beta test. 45 | 7. Make GitHub Release using [Release GitHub](https://github.com/TheAcharya/MarkerData/actions/workflows/release_github_sparkle.yml). 46 | 8. Update latest GitHub Release Notes from `CHANGELOG.md` accordingly. 47 | -------------------------------------------------------------------------------- /Distribution/dmg-builds/build-marker-data-dmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Marker Data", 3 | "icon": "marker-data-dmg-icon.icns", 4 | "icon-size": 125, 5 | "background": "dmg-background.png", 6 | "window": { 7 | "position": { "x": 200, "y": 200 }, 8 | "size": { "width": 750, "height": 450 } 9 | }, 10 | "contents": [ 11 | { "x": 375, "y": 230, "type": "link", "path": "/Applications" }, 12 | { "x": 145, "y": 230, "type": "file", "path": "latest-build/Marker Data.app" }, 13 | { "x": 605, "y": 230, "type": "file", "path": "latest-build/Uninstall Marker Data.app" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Distribution/dmg-builds/dmg-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/dmg-background.png -------------------------------------------------------------------------------- /Distribution/dmg-builds/dmg-background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/dmg-background@2x.png -------------------------------------------------------------------------------- /Distribution/dmg-builds/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.automation.apple-events 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | com.apple.security.temporary-exception.files.home-relative-path.read-write 14 | 15 | /Library/Application Support/Marker Data/preferences.json 16 | 17 | com.apple.security.scripting-targets 18 | 19 | com.apple.FinalCut 20 | 21 | com.apple.FinalCut.library.inspection 22 | 23 | com.apple.FinalCutTrial 24 | 25 | com.apple.FinalCut.library.inspection 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Distribution/dmg-builds/marker-data-dmg-icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/marker-data-dmg-icon.icns -------------------------------------------------------------------------------- /Distribution/dmg-builds/sparkle/generate_appcast: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/sparkle/generate_appcast -------------------------------------------------------------------------------- /Distribution/dmg-builds/sparkle/generate_appcast_script.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from datetime import datetime, timedelta, timezone 3 | import sys 4 | import re 5 | 6 | pattern = r'sparkle:edSignature="([^"]+)" length="([^"]+)"' 7 | 8 | BUNDLE_VERSION = sys.argv[1] 9 | BUNDLE_BUILD = sys.argv[2] 10 | EDSA = sys.argv[3] 11 | 12 | URL = f"https://github.com/TheAcharya/MarkerData/releases/download/v{BUNDLE_VERSION}/Marker-Data_v{BUNDLE_VERSION}.dmg" 13 | 14 | match = re.search(pattern, EDSA) 15 | 16 | if match: 17 | # Extract values 18 | ed_signature = match.group(1) 19 | length = match.group(2) 20 | 21 | # Print the extracted values 22 | print("edSignature:", ed_signature) 23 | print("length:", length) 24 | else: 25 | print("No match found.") 26 | 27 | # Get current time in UTC 28 | utc_now = datetime.now(timezone.utc) 29 | # Calculate Singapore time (UTC +8 hours) 30 | sgt_now = utc_now + timedelta(hours=8) 31 | # Format the time 32 | pub_date = sgt_now.strftime('%a, %d %b %Y %H:%M:%S +0800') 33 | 34 | # Define the new item content 35 | ET.register_namespace('sparkle', 'http://www.andymatuschak.org/xml-namespaces/sparkle') 36 | new_item_content = f''' 37 | 38 | Version {BUNDLE_VERSION} 39 | https://markerdata.theacharya.co 40 | {pub_date} 41 | {BUNDLE_BUILD} 42 | {BUNDLE_VERSION} 43 | 13.0 44 | https://markerdata.theacharya.co/release-notes-appcast.html 45 | https://markerdata.theacharya.co/release-notes/ 46 | 47 | 48 | ''' 49 | 50 | # Parse the existing XML file 51 | tree = ET.parse('./appcast.xml') 52 | root = tree.getroot() 53 | 54 | # Find the last item in the channel 55 | channel = root.find('.//channel') 56 | 57 | # Append the new item after the last item 58 | channel.insert(1, ET.fromstring(new_item_content)) 59 | 60 | # Write the modified XML back to the file with manual XML declaration 61 | with open('./appcast.xml', 'wb') as f: 62 | f.write('\n'.encode()) 63 | f.write(ET.tostring(root, encoding='utf-8')) 64 | -------------------------------------------------------------------------------- /Distribution/dmg-builds/sparkle/sign_update: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/sparkle/sign_update -------------------------------------------------------------------------------- /Distribution/dmg-builds/uninstaller/include/Uninstall Marker Data.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/uninstaller/include/Uninstall Marker Data.scpt -------------------------------------------------------------------------------- /Distribution/dmg-builds/uninstaller/include/applet.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Distribution/dmg-builds/uninstaller/include/applet.icns -------------------------------------------------------------------------------- /Distribution/dmg-builds/uninstaller/include/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | 10 | -------------------------------------------------------------------------------- /Distribution/version.txt: -------------------------------------------------------------------------------- 1 | v1.2.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 The Acharya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Press Kit/Marker Data - Press Release.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Press Kit/Marker Data - Press Release.pdf -------------------------------------------------------------------------------- /Press Kit/press-kit.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Press Kit/press-kit.zip -------------------------------------------------------------------------------- /SDK/Workflow_Extensions_1.0.2.dmg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/SDK/Workflow_Extensions_1.0.2.dmg -------------------------------------------------------------------------------- /SDK/Workflow_Extensions_1.0.3.dmg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/SDK/Workflow_Extensions_1.0.3.dmg -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.2.0 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If you find a Security vulnerability, please create [a new issue](https://github.com/TheAcharya/MarkerData/issues) on GitHub. A fix will be issued as soon as possible. 12 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data.xcodeproj/xcshareddata/xcschemes/Marker Data.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/ApplicationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationDelegate.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 13/01/2024. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | import Sparkle 11 | 12 | class ApplicationDelegate: NSObject, NSApplicationDelegate, SPUUpdaterDelegate { 13 | var updaterController: SPUStandardUpdaterController! 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil) 17 | updaterController.updater.checkForUpdatesInBackground() 18 | } 19 | 20 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 21 | return true 22 | } 23 | 24 | // Notify SwiftUI view if update is available 25 | func bestValidUpdate(in appcast: SUAppcast, for updater: SPUUpdater) -> SUAppcastItem? { 26 | // Compare current build number with the best available one 27 | if let buildNumberString = appcast.items.first?.versionString, 28 | let bestAvaiableBuildNumber = Int(buildNumberString), 29 | let currentBuildNumber = Int(Bundle.main.buildNumber) { 30 | 31 | if bestAvaiableBuildNumber > currentBuildNumber { 32 | // Notify SwiftUI view about the update 33 | NotificationCenter.default.post(name: .updateAvailable, object: nil) 34 | } 35 | } 36 | 37 | return SUAppcastItem.empty() 38 | } 39 | 40 | // For some reason this function is not called consistently so we use the bestValidUpdate instead 41 | // func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { 42 | // // Notify SwiftUI view about the update 43 | // NotificationCenter.default.post(name: .updateAvailable, object: nil) 44 | // } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemIndigoColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-16@1x.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32@1x.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128@1x.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256@1x.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-256@2x 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-256@2x.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024@1x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-1024@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-128@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-16@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-256@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-256@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-256@2x 1.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-32@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIconSingle.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-1024@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/AppIconSingle.imageset/Icon-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/AppIconSingle.imageset/Icon-1024@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/AirtableLogo.imageset/AirtableLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/AirtableLogo.imageset/AirtableLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/AirtableLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AirtableLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/CompressorLogo.imageset/CompressorLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/CompressorLogo.imageset/CompressorLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/CompressorLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "CompressorLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/ExcelLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ExcelLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/ExcelLogo.imageset/ExcelLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/ExcelLogo.imageset/ExcelLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/MarkdownLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MarkdownLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/MarkdownLogo.imageset/MarkdownLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/MarkdownLogo.imageset/MarkdownLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/MusicLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MusicLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/MusicLogo.imageset/MusicLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/MusicLogo.imageset/MusicLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/NotionLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "NotionLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/NotionLogo.imageset/NotionLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/NotionLogo.imageset/NotionLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/NumbersLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "NumbersLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/NumbersLogo.imageset/NumbersLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/NumbersLogo.imageset/NumbersLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/ResolveLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ResolveLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/ResolveLogo.imageset/ResolveLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/ResolveLogo.imageset/ResolveLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/YouTubeLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "YouTubeLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/YouTubeLogo.imageset/YouTubeLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Assets.xcassets/Export Profile Icons/YouTubeLogo.imageset/YouTubeLogo.png -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Install View/ShareDestinationInstaller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareDestinationInstaller.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 16/01/2024. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | struct ShareDestinationInstaller { 12 | static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ShareDestinationInstaller") 13 | 14 | public static func install() async throws { 15 | Self.logger.notice("Start install Share Destination") 16 | 17 | guard let fcpxdestSourceURL = Bundle.main.url(forResource: "Marker Data Source", withExtension: "fcpxdest"), 18 | let fcpxdestH264 = Bundle.main.url(forResource: "Marker Data H.264", withExtension: "fcpxdest") else { 19 | 20 | throw ShareDestinationInstallError.failedToLocateFCPXDEST 21 | } 22 | 23 | for url in [fcpxdestSourceURL, fcpxdestH264] { 24 | let installScript = """ 25 | tell application "Final Cut Pro" 26 | activate 27 | open POSIX file "\(url.path(percentEncoded: false))" 28 | end tell 29 | """ 30 | let script = NSAppleScript(source: installScript) 31 | var errorInfo: NSDictionary? = nil 32 | let result = script?.executeAndReturnError(&errorInfo) 33 | 34 | if result == nil { 35 | Self.logger.error("Failed to install Share Destination, error info: \(errorInfo, privacy: .public)") 36 | throw ShareDestinationInstallError.nilResult 37 | } 38 | } 39 | } 40 | } 41 | 42 | enum ShareDestinationInstallError: Error { 43 | case failedToLocateFCPXDEST 44 | case nilResult 45 | } 46 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Document Controller/DocumentController.h: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentController.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #import 11 | 12 | @interface DocumentController : NSDocumentController 13 | 14 | - (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Document Controller/DocumentController.m: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentController.m 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #import "DocumentController.h" 9 | #import "Document.h" 10 | 11 | @implementation DocumentController 12 | 13 | // ------------------------------------------------------------ 14 | // Read from URL: 15 | // ------------------------------------------------------------ 16 | - (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError { 17 | NSLog(@"[ShareDestinationKit] INFO - readFromURL triggered!"); 18 | return YES; 19 | } 20 | 21 | // ------------------------------------------------------------ 22 | // Write to URL: 23 | // ------------------------------------------------------------ 24 | - (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError { 25 | NSLog(@"[ShareDestinationKit] INFO - writeToURL triggered!"); 26 | return YES; 27 | } 28 | 29 | // ------------------------------------------------------------ 30 | // Open Document with Contents of URL: 31 | // ------------------------------------------------------------ 32 | - (void)openDocumentWithContentsOfURL:(NSURL *)url display:(BOOL)displayDocument completionHandler:(void (^)(NSDocument *document, BOOL documentWasAlreadyOpen, NSError *error))completionHandler 33 | { 34 | NSLog(@"[ShareDestinationKit] INFO - openDocumentWithContentsOfURL triggered!"); 35 | 36 | NSError *theErr = nil; 37 | NSString *documentType = [self typeForContentsOfURL:url error:&theErr]; 38 | 39 | if (theErr != nil) { 40 | NSLog(@"[ShareDestinationKit] ERROR in openDocumentWithContentsOfURL: %@", theErr.localizedDescription); 41 | } 42 | 43 | if ( documentType == nil ) { 44 | completionHandler(nil, NO, theErr); 45 | return; 46 | } 47 | 48 | if ( [documentType isEqualToString:@"Asset Media File"] || [documentType isEqualToString:@"Asset Description File"] ) { 49 | 50 | NSLog(@"[ShareDestinationKit] INFO - It's an Asset Media File or Asset Description File!"); 51 | 52 | Document *currentDocument = [self currentDocument]; 53 | 54 | if ( currentDocument == nil ) { 55 | NSArray *documents = [self documents]; 56 | 57 | if ( [documents count] > 0 ) { 58 | currentDocument = [documents objectAtIndex:0]; 59 | } else { 60 | currentDocument = [self openUntitledDocumentAndDisplay:YES error:&theErr]; 61 | } 62 | } 63 | 64 | NSUInteger assetIndex = [currentDocument addURL:url content:YES metadata:nil dataOptions:nil]; 65 | 66 | completionHandler([currentDocument.assets objectAtIndex:assetIndex], NO, theErr); 67 | } else { 68 | NSLog(@"[ShareDestinationKit] INFO - It's NOT an Asset Media File or Asset Description File, so calling the super method..."); 69 | [super openDocumentWithContentsOfURL:url display:displayDocument completionHandler:completionHandler]; 70 | } 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/Asset.h: -------------------------------------------------------------------------------- 1 | // 2 | // Asset.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #import 11 | #import 12 | 13 | #import "Object.h" 14 | 15 | // ------------------------------------------------------------ 16 | // Metadata Keys: 17 | // ------------------------------------------------------------ 18 | extern const NSString* kMetadataKeyManagedAsset; 19 | extern const NSString* kMetadataKeyPreparedAsset; 20 | extern const NSString* kMetadataKeyExpirationDate; 21 | 22 | @interface Asset : Object 23 | 24 | + (BOOL)isMediaExtension:(NSString*)extension; 25 | + (BOOL)isDescExtension:(NSString*)extension; 26 | 27 | // ------------------------------------------------------------ 28 | // init and dealloc: 29 | // ------------------------------------------------------------ 30 | - (instancetype)init; 31 | - (instancetype)init:(NSURL*)url; 32 | - (instancetype)init:(NSString*)assetName at:(NSURL*)location media:(NSString*)mediaExt desc:(NSString*)descExt; 33 | - (void)dealloc; 34 | 35 | // ------------------------------------------------------------ 36 | // Properties: 37 | // ------------------------------------------------------------ 38 | @property (nonatomic, readonly) NSURL* principalURL; 39 | @property (nonatomic, readonly) NSURL* folderLocation; 40 | @property (nonatomic, readwrite) NSString* mediaExtension; 41 | @property (nonatomic, readwrite) NSString* descExtension; 42 | @property (nonatomic, readonly) BOOL hasMedia; 43 | @property (nonatomic, readonly) BOOL hasDescription; 44 | 45 | // ------------------------------------------------------------ 46 | // A dictionary that contains base name, folder location, 47 | // media extension, and description extension: 48 | // ------------------------------------------------------------ 49 | @property (nonatomic, readonly) NSDictionary *locationInfo; 50 | 51 | @property (nonatomic, readonly) NSURL *mediaFile; 52 | @property (nonatomic, readonly) NSURL *descFile; 53 | 54 | @property (nonatomic, readwrite) NSDictionary *metadata; 55 | @property (nonatomic, readwrite) NSDictionary *dataOptions; 56 | 57 | - (void)loadMedia; 58 | - (void)loadDescription; 59 | 60 | // ------------------------------------------------------------ 61 | // Metadata & Data Options: 62 | // ------------------------------------------------------------ 63 | - (void)addMetadata:(id)value forKey:(const NSString*)key; 64 | 65 | - (void)setDataOption:(id)option forKey:(NSString*)key; 66 | - (id)dataOptionForKey:(NSString*)key; 67 | 68 | // ------------------------------------------------------------ 69 | // Convenience Properties: 70 | // ------------------------------------------------------------ 71 | @property (readonly) NSString* duration; 72 | @property (readonly) CGSize frameSize; 73 | @property (readonly) NSString* frameDuration; 74 | 75 | @property (readonly) NSString* episodeID; 76 | @property (readonly) NSNumber* episodeNumber; 77 | 78 | @property (readonly) BOOL mediaLoaded; 79 | @property (readonly) BOOL descriptionLoaded; 80 | @property (readonly) BOOL hasRoles; 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/FCPXMetadataKeys.h: -------------------------------------------------------------------------------- 1 | // 2 | // FCPXMetadataKeys.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #import 11 | 12 | extern const NSString* kFCPXMetadataKeyDescription; 13 | 14 | extern const NSString* kFCPXShareMetadataKeyEpisodeID; 15 | extern const NSString* kFCPXShareMetadataKeyEpisodeNumber; 16 | 17 | extern const NSString* kFCPXShareMetadataKeyShareID; 18 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/FCPXMetadataKeys.m: -------------------------------------------------------------------------------- 1 | // 2 | // FCPXMetadataKeys.m 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #import "FCPXMetadataKeys.h" 9 | 10 | const NSString* kFCPXMetadataKeyDescription = @"com.apple.quicktime.description"; 11 | 12 | const NSString* kFCPXShareMetadataKeyEpisodeID = @"com.apple.proapps.share.episodeID"; 13 | const NSString* kFCPXShareMetadataKeyEpisodeNumber = @"com.apple.proapps.share.episodeNumber"; 14 | 15 | const NSString* kFCPXShareMetadataKeyShareID = @"com.apple.proapps.share.id"; 16 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/MakeCommand.h: -------------------------------------------------------------------------------- 1 | // 2 | // MakeCommand.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #import 11 | 12 | @interface MakeCommand : NSCreateCommand 13 | 14 | - (id)performDefaultImplementation; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/MediaAssetHelperKeys.h: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAssetHelperKeys.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #import 11 | 12 | // ------------------------------------------------------------ 13 | // Bundle plist keys: 14 | // ------------------------------------------------------------ 15 | extern NSString* kMediaAssetProtocolInfoKey; 16 | 17 | // ------------------------------------------------------------ 18 | // Asset location dictionary keys: 19 | // ------------------------------------------------------------ 20 | extern NSString* kMediaAssetLocationFolderKey; // Destination folder 21 | extern NSString* kMediaAssetLocationBasenameKey; // Base Name 22 | extern NSString* kMediaAssetLocationHasMediaKey; // Boolean indicating need for media export 23 | extern NSString* kMediaAssetLocationHasDescriptionKey; // Boolean indicating need for xml export 24 | 25 | // ------------------------------------------------------------ 26 | // Data option dictionary keys: 27 | // ------------------------------------------------------------ 28 | extern NSString* kMediaAssetDataOptionAvailableMetadataSetsKey; // NSArray of available metadata view set names 29 | extern NSString* kMediaAssetDataOptionMetadataSetKey; // NSString of desired metadata view set 30 | 31 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/MediaAssetHelperKeys.m: -------------------------------------------------------------------------------- 1 | // 2 | // MediaAssetHelperKeys.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #import "MediaAssetHelperKeys.h" 9 | 10 | // ------------------------------------------------------------ 11 | // CONSTANTS: 12 | // ------------------------------------------------------------ 13 | 14 | // ------------------------------------------------------------ 15 | // The info plist key that indicate the application supports 16 | // the protocol: 17 | // ------------------------------------------------------------ 18 | const NSString* kMediaAssetProtocolInfoKey = @"com.apple.proapps.MediaAssetProtocol"; 19 | 20 | // ------------------------------------------------------------ 21 | // The asset location dictionary keys: 22 | // ------------------------------------------------------------ 23 | const NSString* kMediaAssetLocationFolderKey = @"folder"; 24 | const NSString* kMediaAssetLocationBasenameKey = @"basename"; 25 | const NSString* kMediaAssetLocationHasMediaKey = @"hasMedia"; 26 | const NSString* kMediaAssetLocationHasDescriptionKey = @"hasDescription"; 27 | 28 | // ------------------------------------------------------------ 29 | // The asset data option dictionary keys: 30 | // ------------------------------------------------------------ 31 | const NSString* kMediaAssetDataOptionAvailableMetadataSetsKey = @"availableMetadataSets"; 32 | const NSString* kMediaAssetDataOptionMetadataSetKey = @"metadataSet"; 33 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/Objective-C Code/Scripting/ScriptingSupportCategories.h: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptingSupportCategories.h 3 | // ShareDestinationKit 4 | // 5 | // Created by Chris Hocking on 10/12/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #import 11 | 12 | @interface NSDictionary (UserDefinedRecord) 13 | 14 | // ------------------------------------------------------------ 15 | // AppleEvent record descriptor (typeAERecord) with arbitrary 16 | // keys: 17 | // ------------------------------------------------------------ 18 | +(NSDictionary*)scriptingUserDefinedRecordWithDescriptor:(NSAppleEventDescriptor*)desc; 19 | -(NSAppleEventDescriptor*)scriptingUserDefinedRecordDescriptor; 20 | 21 | @end 22 | 23 | @interface NSArray (UserList) 24 | 25 | // ------------------------------------------------------------ 26 | // AppleEvent list descriptor (typeAEList): 27 | // ------------------------------------------------------------ 28 | +(NSArray*)scriptingUserListWithDescriptor:(NSAppleEventDescriptor*)desc; 29 | -(NSAppleEventDescriptor*)scriptingUserListDescriptor; 30 | 31 | @end 32 | 33 | @interface NSAppleEventDescriptor (GenericObject) 34 | 35 | // ------------------------------------------------------------ 36 | // AppleEvent descriptor that may be a record, a list, or 37 | // other object. This is necessary to handle a list or a record 38 | // contained in another list or record. 39 | // ------------------------------------------------------------ 40 | +(NSAppleEventDescriptor*)descriptorWithObject:(id)object; 41 | -(id)objectValue; 42 | 43 | @end 44 | 45 | @interface NSAppleEventDescriptor (URLValue) 46 | 47 | // ------------------------------------------------------------ 48 | // AppleEvenf file URL (typeFileURL) descriptor: 49 | // ------------------------------------------------------------ 50 | +(NSAppleEventDescriptor*)descriptorWithURL:(NSURL*)url; 51 | -(NSURL*)urlValue; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/FCP Share Destination/OpenEventHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenEventHandler.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 16/01/2024. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | @MainActor 12 | class OpenEventHandler: NSObject { 13 | static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "OpenEventHandler") 14 | 15 | override init() { 16 | super.init() 17 | NotificationCenter.default.addObserver(self, selector: #selector(self.setupHandler), name: .FCPShareStart, object: nil) 18 | self.setupHandler() 19 | } 20 | 21 | @objc func setupHandler() { 22 | // Do not under any circumstances remove the "DispatchQueque.main.async" 23 | // If it's not here the events are not set up correctly for some reason 24 | // and the file URL is not recieved. 25 | DispatchQueue.main.async { 26 | // Setup an Apple Event hander for the "Open" event 27 | NSAppleEventManager.shared().setEventHandler(self, 28 | andSelector: #selector(self.handleOpen(event:replyEvent:)), 29 | forEventClass: AEEventClass(kCoreEventClass), 30 | andEventID: AEEventID(kAEOpen)) 31 | 32 | Self.logger.notice("Open event handler setup DONE") 33 | } 34 | } 35 | 36 | @objc func handleOpen(event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) { 37 | guard let descriptorList = event.paramDescriptor(forKeyword: keyDirectObject) else { return } 38 | 39 | if descriptorList.numberOfItems == 0 { 40 | Self.logger.error("Open event triggered but no files received") 41 | return 42 | } 43 | 44 | for index in 1...descriptorList.numberOfItems { 45 | if let fileDescriptor = descriptorList.atIndex(index), 46 | let urlString = fileDescriptor.stringValue, 47 | let url = URL(string: urlString) { 48 | 49 | let logger = Logger() 50 | logger.notice("Open event has received file at URL: \(url, privacy: .public)") 51 | 52 | NotificationCenter.default.post(name: .openFile, object: nil, userInfo: ["url": url]) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Marker_Data.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.cs.disable-library-validation 8 | 9 | com.apple.security.scripting-targets 10 | 11 | com.apple.FinalCut 12 | 13 | com.apple.FinalCut.library.inspection 14 | 15 | com.apple.FinalCutTrial 16 | 17 | com.apple.FinalCut.library.inspection 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/ColorsExtractorService/ColorExtractMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorMood.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 01.09.2023. 6 | // 7 | 8 | import Foundation 9 | import DominantColors 10 | 11 | /// Предустановка цветового отделения по различным сценариям 12 | enum ColorExtractMethod: Int, CaseIterable { 13 | case averageColor 14 | case averageAreaColor 15 | case dominationColor 16 | 17 | var name: String { 18 | switch self { 19 | case .averageColor: 20 | return "Average Color" 21 | case .averageAreaColor: 22 | return "Area average color" 23 | case .dominationColor: 24 | return "Domination colors" 25 | } 26 | } 27 | } 28 | 29 | extension ColorExtractMethod: CustomStringConvertible { 30 | var description: String { 31 | switch self { 32 | case .averageColor: 33 | return "Finds the dominant colors of an image by using using a k-means clustering algorithm." 34 | case .averageAreaColor: 35 | return "Finds the dominant colors of an image by using using a area average algorithm." 36 | case .dominationColor: 37 | return "Finds the dominant colors of an image by iterating, grouping and sorting its pixels and using the difference between the colors." 38 | } 39 | } 40 | } 41 | 42 | extension ColorExtractMethod: Hashable, Equatable { 43 | func hash(into hasher: inout Hasher) { 44 | hasher.combine(self.name) 45 | } 46 | 47 | static func == (lhs: Self, rhs: Self) -> Bool { 48 | lhs.name == rhs.name 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/ColorsExtractorService/ColorMood.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorMood.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import Foundation 9 | @preconcurrency import DominantColors 10 | 11 | struct ColorMood: Sendable { 12 | var formula: DeltaEFormula 13 | var method: ColorExtractMethod 14 | var quality: DominantColorQuality 15 | var isExcludeWhite: Bool 16 | var isExcludeBlack: Bool 17 | var isExcludeGray: Bool 18 | 19 | var options: [DominantColors.Options] { 20 | var options = [DominantColors.Options]() 21 | if isExcludeBlack { 22 | options.append(.excludeBlack) 23 | } 24 | if isExcludeWhite { 25 | options.append(.excludeWhite) 26 | } 27 | if isExcludeGray { 28 | options.append(.excludeGray) 29 | } 30 | return options 31 | } 32 | 33 | init(formula: DeltaEFormula, excludeBlack: Bool, excludeWhite: Bool, excludeGray: Bool, quality: DominantColorQuality) { 34 | self.method = .dominationColor 35 | self.formula = formula 36 | self.isExcludeBlack = excludeBlack 37 | self.isExcludeWhite = excludeWhite 38 | self.isExcludeGray = excludeGray 39 | self.quality = quality 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/ColorsExtractorService/ColorsExtractorService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // class AverageColorsService.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 01.09.2023. 6 | // 7 | 8 | import Foundation 9 | import CoreImage 10 | @preconcurrency import DominantColors 11 | 12 | struct ColorsExtractorService { 13 | static func extract(from cgImage: CGImage, method: ColorExtractMethod, count: Int = 8, formula: DeltaEFormula = .CIE76, quality: DominantColorQuality, options: [DominantColors.Options] = []) async throws -> [CGColor] { 14 | return try await withCheckedThrowingContinuation({ continuation in 15 | DispatchQueue.global(qos: .utility).async { 16 | switch method { 17 | case .averageColor: 18 | do { 19 | let colors = try DominantColors.kMeansClusteringColors(image: cgImage, count: count) 20 | continuation.resume(returning: colors) 21 | } catch let error { 22 | continuation.resume(throwing: error) 23 | } 24 | case .averageAreaColor: 25 | do { 26 | let colors = try DominantColors.averageColors(image: cgImage, count: count) 27 | continuation.resume(returning: colors) 28 | } catch let error { 29 | continuation.resume(throwing: error) 30 | } 31 | case .dominationColor: 32 | do { 33 | let colors = try DominantColors.dominantColors( 34 | image: cgImage, 35 | quality: quality, 36 | algorithm: formula, 37 | maxCount: count, 38 | options: options, 39 | sorting: .darkness 40 | ) 41 | continuation.resume(returning: colors) 42 | } catch let error { 43 | continuation.resume(throwing: error) 44 | } 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/ColorsExtractorService/DeltaEFormulaExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeltaEFormulaExtension.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 04.09.2023. 6 | // 7 | 8 | import DominantColors 9 | 10 | extension DeltaEFormula: Codable {} 11 | 12 | extension DeltaEFormula { 13 | var name: String { 14 | switch self { 15 | case .euclidean: 16 | return "Euclidean" 17 | case .CIE76: 18 | return "CIE76" 19 | case .CIE94: 20 | return "CIE94" 21 | case .CIEDE2000: 22 | return "CIEDE2000" 23 | case .CMC: 24 | return "CMC" 25 | } 26 | } 27 | } 28 | 29 | extension DeltaEFormula: @retroactive CustomStringConvertible { 30 | public var description: String { 31 | switch self { 32 | case .euclidean: 33 | return "Euclidean algorithm calculates difference in RGB colour space." 34 | case .CIE76: 35 | return "CIE76 algorithm calculates difference in Lab colour space." 36 | case .CIE94: 37 | return "CIE94 algorithm is an improvement of CIE76, it calculates the difference in the Lab colour space." 38 | case .CIEDE2000: 39 | return "CIEDE2000 algorithm is the most accurate colour comparison algorithm in the Lab colour space." 40 | case .CMC: 41 | return "CMC algorithm calculates the difference in the HCL (Hue, Chroma, Luminance) colour space." 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/ImageRenderService/ImageRenderService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageService.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 31.08.2023. 6 | // 7 | 8 | import SwiftUI 9 | import OSLog 10 | 11 | class ImageRenderService { 12 | private static let exportImageStripFormat: ColorPaletteFileFormat = .jpeg 13 | private static let exportImageStripCompressionFactor: Double = 0.0 14 | 15 | static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ImageRenderService") 16 | 17 | private var taskGroup: TaskGroup? = nil 18 | 19 | // MARK: - Functions 20 | 21 | func export( 22 | imageStrips: [ImageStrip], 23 | stripHeight: CGFloat, 24 | colorsCount: Int, 25 | paletteStripOnly: Bool, 26 | progress: ProgressViewModel 27 | ) async { 28 | await progress.setProcesses(urls: imageStrips.map(\.url)) 29 | 30 | await withTaskGroup(of: Void.self) { group in 31 | self.taskGroup = group 32 | 33 | for imageStrip in imageStrips { 34 | group.addTask { 35 | await Self.addMergeOperation( 36 | imageStrip: imageStrip, 37 | stripHeight: stripHeight, 38 | colorsCount: colorsCount, 39 | paletteStripOnly: paletteStripOnly 40 | ) 41 | 42 | await progress.markProcessAsFinished(url: imageStrip.url) 43 | } 44 | } 45 | } 46 | } 47 | 48 | func stop() { 49 | self.taskGroup?.cancelAll() 50 | } 51 | 52 | // MARK: - Private functions 53 | 54 | static func addMergeOperation(imageStrip: ImageStrip, stripHeight: CGFloat, colorsCount: Int, paletteStripOnly: Bool) async { 55 | guard 56 | let nsImage = imageStrip.nsImage(), 57 | let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil), 58 | let exportURL = imageStrip.exportURL 59 | else { 60 | Self.logger.error("Failed to load CGImage from ImageStrip at: \(imageStrip.url)") 61 | return 62 | } 63 | 64 | let mergeOperation = ImageMergeOperation( 65 | colors: imageStrip.colors, 66 | cgImage: cgImage, 67 | stripHeight: stripHeight, 68 | paletteStripOnly: paletteStripOnly, 69 | colorsCount: colorsCount, 70 | colorMood: imageStrip.colorMood, 71 | format: Self.exportImageStripFormat, 72 | compressionFactor: Float(Self.exportImageStripCompressionFactor) 73 | ) 74 | 75 | if let jpegData = await mergeOperation.performMerge() { 76 | do { 77 | try save(jpeg: jpegData, to: exportURL) 78 | } catch let error { 79 | Self.logger.error("Failed to save palette image. Error: \(error)") 80 | } 81 | } 82 | } 83 | 84 | static func writeImage(jpeg data: Data, to url: URL) throws { 85 | try data.write(to: url, options: .atomic) 86 | } 87 | 88 | static func save(jpeg data: Data, to url: URL) throws { 89 | try writeImage(jpeg: data, to: url) 90 | url.stopAccessingSecurityScopedResource() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/ImageRenderService/ImageRenderServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageServiceError.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 31.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ImageRenderServiceError: Error { 11 | case stripRender 12 | case mergeImageWithStrip 13 | case map(errorDescription: String?, recoverySuggestion: String?) 14 | case colorsIsEmpty 15 | } 16 | 17 | extension ImageRenderServiceError: LocalizedError { 18 | var errorDescription: String? { 19 | let comment = "Image service error" 20 | switch self { 21 | case .stripRender: 22 | return NSLocalizedString("Unable to render stripe image", comment: comment) 23 | case .mergeImageWithStrip: 24 | return NSLocalizedString("Failed to merge image and strip", comment: comment) 25 | case .map(let errorDescription, _): 26 | return NSLocalizedString(errorDescription ?? "Unknown error", comment: comment) 27 | case .colorsIsEmpty: 28 | return NSLocalizedString("Strip colors not found or could not be createde", comment: comment) 29 | } 30 | } 31 | 32 | var recoverySuggestion: String? { 33 | let comment = "Image service error" 34 | switch self { 35 | case .stripRender, .mergeImageWithStrip: 36 | return NSLocalizedString("Try again.", comment: comment) 37 | case .map(_, let recoverySuggestion): 38 | return NSLocalizedString(recoverySuggestion ?? "Unknown reaction", comment: comment) 39 | case .colorsIsEmpty: 40 | return NSLocalizedString("Try exporting the image separately", comment: comment) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/Other/ColorPaletteFileFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPaletteFileFormat.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/04/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ColorPaletteFileFormat: String, CaseIterable, Identifiable { 11 | case png, jpeg, tiff 12 | 13 | var fileExtension: String { 14 | switch self { 15 | case .png: 16 | "png" 17 | case .jpeg: 18 | "jpeg" 19 | case .tiff: 20 | "tiff" 21 | } 22 | } 23 | 24 | var pixelFormat: String { 25 | switch self { 26 | case .png: 27 | "yuvj420p" 28 | case .jpeg: 29 | "yuvj420p" 30 | case .tiff: 31 | "rgba" 32 | } 33 | } 34 | 35 | var id: String { self.rawValue } 36 | } 37 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/Other/ImageStrip.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageStrip.swift 3 | // GrabShot 4 | // 5 | // Created by Denis Dmitriev on 29.08.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImageStrip: Sendable, Hashable, Identifiable { 11 | let id: UUID 12 | let url: URL 13 | let ending = ".Strip" 14 | let imageExtension = "jpg" 15 | 16 | lazy var size: CGSize = { 17 | if let nsImage = nsImage() { 18 | return CGSize(width: nsImage.size.width, height: nsImage.size.height) 19 | } else { 20 | return .zero 21 | } 22 | }() 23 | 24 | var title: String 25 | 26 | var exportTitle: String { 27 | title 28 | .appending(ending) 29 | .appending(".") 30 | .appending(imageExtension) 31 | } 32 | 33 | var exportURL: URL? 34 | 35 | var colors = [Color]() 36 | var colorMood: ColorMood 37 | 38 | init(url: URL, colors: [Color] = [Color](), exportDirectory: URL, colorMood: ColorMood) { 39 | self.id = UUID() 40 | self.url = url 41 | self.colors = colors 42 | self.exportURL = exportDirectory 43 | self.colorMood = colorMood 44 | self.title = url.deletingPathExtension().lastPathComponent 45 | } 46 | 47 | func nsImage() -> NSImage? { 48 | do { 49 | let data = try Data(contentsOf: url) 50 | let nsImage = NSImage(data: data) 51 | return nsImage 52 | } catch let error { 53 | print(error.localizedDescription) 54 | return nil 55 | } 56 | } 57 | 58 | func hash(into hasher: inout Hasher) { 59 | hasher.combine(id) 60 | } 61 | 62 | static func == (lhs: ImageStrip, rhs: ImageStrip) -> Bool { 63 | lhs.id == rhs.id 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Color Swatch/Settings Model/ColorSwatchSettingsModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorSwatchSettingsModel.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 20/04/2024. 6 | // 7 | 8 | import Foundation 9 | @preconcurrency import DominantColors 10 | 11 | struct ColorSwatchSettingsModel: Sendable, Codable, Hashable, Equatable { 12 | var enableSwatch: Bool 13 | var algorithm: DeltaEFormula 14 | var accuracy: DominantColorQuality 15 | var excludeBlack: Bool 16 | var excludeWhite: Bool 17 | var excludeGray: Bool 18 | 19 | static func defaults() -> ColorSwatchSettingsModel { 20 | ColorSwatchSettingsModel( 21 | enableSwatch: false, 22 | algorithm: .CIE76, 23 | accuracy: .high, 24 | excludeBlack: false, 25 | excludeWhite: false, 26 | excludeGray: false 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Configurations/ConfigurationsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationsViewModel.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 20/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | @MainActor 11 | class ConfigurationsViewModel: ObservableObject { 12 | var settings: SettingsContainer? = nil 13 | 14 | @Published var showAlert = false 15 | @Published var alertTitle = "" 16 | @Published var alertMessage = "" 17 | 18 | public func add(saveAs name: String) async { 19 | do { 20 | try await settings?.saveCurrentAs(name: name) 21 | } catch { 22 | showAlert("Couldn't create configuration", message: error.localizedDescription) 23 | } 24 | } 25 | 26 | public func remove(name: String) async { 27 | do { 28 | try await settings?.removeConfiguration(name: name) 29 | } catch { 30 | showAlert("Failed to remove configuration", message: error.localizedDescription) 31 | } 32 | } 33 | 34 | public func makeActive(_ store: SettingsStore?, ignoreChanges: Bool = false) { 35 | guard let storeUnwrapped = store else { 36 | showAlert("Failed to get configuration") 37 | return 38 | } 39 | 40 | do { 41 | try settings?.load(storeUnwrapped) 42 | } catch { 43 | showAlert("Failed to load configuration", message: error.localizedDescription) 44 | } 45 | } 46 | 47 | public func duplicateConfiguration(store: SettingsStore?) async { 48 | guard let storeUnwrapped = store else { 49 | showAlert("Failed to get configuration") 50 | return 51 | } 52 | 53 | do { 54 | try await settings?.duplicateStore(store: storeUnwrapped, as: storeUnwrapped.name + " copy") 55 | settings?.objectWillChange.send() 56 | } catch { 57 | showAlert("Failed to duplicate configuration") 58 | } 59 | } 60 | 61 | public func updateCurrent() async { 62 | do { 63 | try await settings?.store.saveAsConfiguration() 64 | await settings?.checkForUnsavedChanges() 65 | } catch { 66 | showAlert("Failed to update active configuration", message: error.localizedDescription) 67 | } 68 | } 69 | 70 | public func discardChanges() { 71 | do { 72 | try settings?.discardChanges() 73 | } catch { 74 | showAlert("Failed to discard changes", message: error.localizedDescription) 75 | } 76 | } 77 | 78 | public func rename(store: SettingsStore?, to newName: String) async { 79 | guard let jsonURL = store?.jsonURL, 80 | let loadedStore = try? settings?.loadStoreFromDisk(at: jsonURL) else { 81 | showAlert("Failed to rename") 82 | return 83 | } 84 | 85 | do { 86 | try await settings?.duplicateStore(store: loadedStore, as: newName, setAsCurrent: true) 87 | try await settings?.removeConfiguration(name: loadedStore.name) 88 | } catch { 89 | showAlert("Failed to rename", message: error.localizedDescription) 90 | } 91 | } 92 | 93 | private func showAlert(_ title: String, message: String = "") { 94 | self.showAlert = true 95 | self.alertTitle = title 96 | self.alertMessage = message 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Database/Profile Models/Airtable/AirtableDBModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirtableDBModel.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 05/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | final class AirtableDBModel: DatabaseProfileModel { 11 | @Published var token: String 12 | @Published var baseID: String 13 | @Published var tableID: String 14 | @Published var renameKeyColumn: String 15 | 16 | init() { 17 | self.token = "" 18 | self.baseID = "" 19 | self.tableID = "" 20 | self.renameKeyColumn = "" 21 | 22 | super.init(name: "", plaform: .airtable) 23 | } 24 | 25 | override func validate() throws { 26 | if self.name.isEmpty { 27 | throw AirtableValidationError.emptyName 28 | } 29 | if self.baseID.isEmpty { 30 | throw AirtableValidationError.emptyBaseID 31 | } 32 | if self.renameKeyColumn == "Marker ID" { 33 | throw NotionValidationError.illegalRenameKeyColumn 34 | } 35 | } 36 | 37 | // MARK: Encoding & Decoding 38 | 39 | required init(from decoder: Decoder) throws { 40 | let container = try decoder.container(keyedBy: CodingKeys.self) 41 | 42 | self.token = try container.decode(String.self, forKey: .token) 43 | self.baseID = try container.decode(String.self, forKey: .baseID) 44 | self.tableID = try container.decode(String.self, forKey: .tableID) 45 | self.renameKeyColumn = try container.decode(String.self, forKey: .renameKeyColumn) 46 | 47 | // Parent's properties 48 | let name = try container.decode(String.self, forKey: .name) 49 | let plaform = try container.decode(DatabasePlatform.self, forKey: .platform) 50 | 51 | super.init(name: name, plaform: plaform) 52 | } 53 | 54 | enum CodingKeys: CodingKey { 55 | case name 56 | case platform 57 | case token 58 | case baseID 59 | case tableID 60 | case renameKeyColumn 61 | } 62 | 63 | override func encode(to encoder: Encoder) throws { 64 | var container = encoder.container(keyedBy: CodingKeys.self) 65 | 66 | // Parent's properties 67 | try container.encode(self.name, forKey: .name) 68 | try container.encode(self.plaform, forKey: .platform) 69 | 70 | try container.encode(self.token, forKey: .token) 71 | try container.encode(self.baseID, forKey: .baseID) 72 | try container.encode(self.tableID, forKey: .tableID) 73 | try container.encode(self.renameKeyColumn, forKey: .renameKeyColumn) 74 | } 75 | 76 | override func copy() -> AirtableDBModel? { 77 | return deepCopy(of: self) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Database/Profile Models/DatabasePlatform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabasePlatform.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 05/02/2024. 6 | // 7 | 8 | import Foundation 9 | import MarkersExtractor 10 | 11 | enum DatabasePlatform: String, Codable, CaseIterable, Identifiable { 12 | case notion = "Notion" 13 | case airtable = "Airtable" 14 | 15 | var id: Self { self } 16 | 17 | var asExportProfile: ExportProfileFormat { 18 | switch self { 19 | case .notion: 20 | ExportProfileFormat.notion 21 | case .airtable: 22 | ExportProfileFormat.airtable 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Database/Profile Models/DatabaseProfileModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseProfileModel.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 22/11/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A parent class for Notion and Airtable datbase models 11 | class DatabaseProfileModel: ObservableObject, Equatable, Identifiable, Codable, Hashable { 12 | var name: String 13 | let plaform: DatabasePlatform 14 | 15 | init(name: String, plaform: DatabasePlatform) { 16 | self.name = name 17 | self.plaform = plaform 18 | } 19 | 20 | static func == (lhs: DatabaseProfileModel, rhs: DatabaseProfileModel) -> Bool { 21 | lhs.name == rhs.name 22 | } 23 | 24 | var id: String { 25 | name 26 | } 27 | 28 | func getJSONURL() -> URL { 29 | switch self.plaform { 30 | case .notion: 31 | return URL.notionProfilesFolder 32 | .appendingPathComponent(name, conformingTo: .json) 33 | case .airtable: 34 | return URL.airtableProfilesFolder 35 | .appendingPathComponent(name, conformingTo: .json) 36 | } 37 | } 38 | 39 | func validate() throws { 40 | if name.isEmpty { 41 | throw DatabaseValidationError.emptyCredentials 42 | } 43 | } 44 | 45 | func copy() -> DatabaseProfileModel? { 46 | return deepCopy(of: self) 47 | } 48 | 49 | func hash(into hasher: inout Hasher) { 50 | hasher.combine(name) 51 | hasher.combine(plaform) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Database/Profile Models/Dropbox/DropboxInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropboxInfo.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DropboxInfo: Codable { 11 | let appKey: String 12 | let refreshToken: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case appKey = "app_key" 16 | case refreshToken = "refresh_token" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Database/Profile Models/Notion/NotionDBModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotionDBModel.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 05/02/2024. 6 | // 7 | 8 | import Foundation 9 | import MarkersExtractor 10 | 11 | final class NotionDBModel: DatabaseProfileModel { 12 | @Published var workspaceName: String 13 | @Published var token: String 14 | @Published var databaseURL: String 15 | @Published var renameKeyColumn: String 16 | @Published var mergeOnlyColumns: [ExportField] 17 | 18 | init() { 19 | self.workspaceName = "" 20 | self.token = "" 21 | self.databaseURL = "" 22 | self.renameKeyColumn = "" 23 | self.mergeOnlyColumns = [] 24 | 25 | super.init(name: "", plaform: .notion) 26 | } 27 | 28 | override func validate() throws { 29 | if self.name.isEmpty { 30 | throw NotionValidationError.emptyName 31 | } 32 | if self.workspaceName.isEmpty { 33 | throw NotionValidationError.emptyWorkspaceName 34 | } 35 | if self.token.isEmpty { 36 | throw NotionValidationError.noToken 37 | } 38 | if self.renameKeyColumn == "Marker ID" { 39 | throw NotionValidationError.illegalRenameKeyColumn 40 | } 41 | } 42 | 43 | // MARK: Encoding & Decoding 44 | 45 | required init(from decoder: Decoder) throws { 46 | let container = try decoder.container(keyedBy: CodingKeys.self) 47 | 48 | self.workspaceName = try container.decode(String.self, forKey: .workspaceName) 49 | self.token = try container.decode(String.self, forKey: .token) 50 | self.databaseURL = try container.decode(String.self, forKey: .databaseURL) 51 | self.renameKeyColumn = try container.decode(String.self, forKey: .renameKeyColumn) 52 | self.mergeOnlyColumns = try container.decode([ExportField].self, forKey: .mergeOnly) 53 | 54 | // Parent's properties 55 | let name = try container.decode(String.self, forKey: .name) 56 | let plaform = try container.decode(DatabasePlatform.self, forKey: .platform) 57 | 58 | super.init(name: name, plaform: plaform) 59 | } 60 | 61 | enum CodingKeys: CodingKey { 62 | case name 63 | case platform 64 | case workspaceName 65 | case token 66 | case databaseURL 67 | case renameKeyColumn 68 | case mergeOnly 69 | } 70 | 71 | override func encode(to encoder: Encoder) throws { 72 | var container = encoder.container(keyedBy: CodingKeys.self) 73 | 74 | // Parent's properties 75 | try container.encode(self.name, forKey: .name) 76 | try container.encode(self.plaform, forKey: .platform) 77 | 78 | try container.encode(self.workspaceName, forKey: .workspaceName) 79 | try container.encode(self.token, forKey: .token) 80 | try container.encode(self.databaseURL, forKey: .databaseURL) 81 | try container.encode(self.renameKeyColumn, forKey: .renameKeyColumn) 82 | try container.encode(self.mergeOnlyColumns, forKey: .mergeOnly) 83 | } 84 | 85 | override func copy() -> NotionDBModel? { 86 | return deepCopy(of: self) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Errors/ConfigurationErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationErrors.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 10/10/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ConfigurationSaveError: Error { 11 | case nameTooLong 12 | case jsonSerializationError 13 | case fileCreationError 14 | case nameAlreadyExists 15 | case illegalName 16 | case duplicationError 17 | } 18 | 19 | enum ConfigurationLoadError: Error { 20 | case fileDoesntExists 21 | case emptyConfigurationName 22 | case jsonParseError 23 | } 24 | 25 | enum StoreLocateError: Error { 26 | case storeNotFound 27 | } 28 | 29 | extension ConfigurationSaveError: LocalizedError { 30 | public var errorDescription: String? { 31 | switch self { 32 | case .nameTooLong: 33 | "Name too long" 34 | case .jsonSerializationError: 35 | "Couldn't serialize configurations" 36 | case .fileCreationError: 37 | "Couldn't create configuration file" 38 | case .nameAlreadyExists: 39 | "A configuration with the same name already exists" 40 | case .illegalName: 41 | "Configuration name not allowed" 42 | case .duplicationError: 43 | "Failed to duplicate configuration" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Errors/ExtractError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtractError.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 25/12/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ExtractError: Error { 11 | case invalidExportDestination 12 | case exportResultisNil 13 | case settingsReadError 14 | case unifiedExportProfileReadError 15 | case userCancel 16 | case conflictingNamingAndSource 17 | } 18 | 19 | extension ExtractError: LocalizedError { 20 | public var errorDescription: String? { 21 | switch self { 22 | case .invalidExportDestination: 23 | "Invalid export destination" 24 | case .settingsReadError: 25 | "Failed to read export settings" 26 | case .unifiedExportProfileReadError: 27 | "Couldn't read export profile" 28 | case .userCancel: 29 | "User initiated cancel" 30 | case .exportResultisNil: 31 | "Failed to get export result" 32 | case .conflictingNamingAndSource: 33 | "Incompatible Settings Detected - The Naming Mode is set to Notes, which conflicts with Marker Source when set to Marker and Captions or Captions. Please adjust your settings to resolve this conflict." 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Extract/ExportExitStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportExitStatus.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 02/01/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ExportExitStatus { 11 | case none 12 | case success 13 | case failed 14 | } 15 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Extract/ExportProcess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportProcess.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 05/12/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class ExportProcess { 12 | var progress: Progress 13 | var url: URL 14 | var isFinished: Bool = false 15 | 16 | init(url: URL) { 17 | self.url = url 18 | self.progress = Progress(totalUnitCount: 100) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Extract/ExtractionResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtractionResult.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 25/11/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ExtractionFailure: Codable, Identifiable, Hashable { 11 | let url: URL 12 | let exitStatus: ExportFailPhase 13 | let errorMessage: String 14 | 15 | var id: URL { 16 | return url 17 | } 18 | } 19 | 20 | enum ExportFailPhase: String, Codable { 21 | case failedToExtract = "Extract error" 22 | case failedToUpload = "Upload error" 23 | } 24 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Other/MainViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViews.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 05/11/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Views selectable in the sidebar 11 | enum MainViews: String, CaseIterable, Identifiable { 12 | case extract 13 | case queue 14 | case general 15 | case image 16 | case label 17 | case configurations 18 | case databases 19 | case about 20 | 21 | var id: String { 22 | self.rawValue 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Other/UnifiedExportProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedExportProfile.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 04/01/2024. 6 | // 7 | 8 | import Foundation 9 | import MarkersExtractor 10 | 11 | /// Holds both extract profile and database profile 12 | /// 13 | /// Selected profile is saved to application support 14 | struct UnifiedExportProfile: Codable, Hashable, Identifiable, Equatable { 15 | let displayName: String 16 | let extractProfile: ExportProfileFormat 17 | let databaseProfileName: String 18 | let exportProfileType: ExportProfileType 19 | 20 | var id: Self { 21 | self 22 | } 23 | 24 | /// Returns the no upload extraction proifles as a list of ``UnifiedExportProfile`` 25 | public static var noUploadProfiles: [UnifiedExportProfile] { 26 | let unifiledProfiles = ExportProfileFormat.allCasesInUIOrder.map { exportFormat in 27 | UnifiedExportProfile( 28 | displayName: exportFormat.extractOnlyName, 29 | extractProfile: exportFormat.self, 30 | databaseProfileName: "", 31 | exportProfileType: .extractOnly 32 | ) 33 | } 34 | 35 | return unifiledProfiles 36 | } 37 | 38 | /// Icon name 39 | public var iconImageName: String { 40 | switch self.extractProfile { 41 | case .airtable: 42 | return "AirtableLogo" 43 | case .csv: 44 | return "NumbersLogo" 45 | case .midi: 46 | return "MusicLogo" 47 | case .notion: 48 | return "NotionLogo" 49 | case .tsv: 50 | return "NumbersLogo" 51 | case .youtube: 52 | return "YouTubeLogo" 53 | case .xlsx: 54 | return "ExcelLogo" 55 | case .json: 56 | return "" 57 | } 58 | } 59 | } 60 | 61 | enum ExportProfileType: String, Codable { 62 | case extractOnly 63 | case extractAndUpload 64 | } 65 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Other/WindowSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowSize.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 05/11/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct WindowSize { 11 | static let fullWidth: CGFloat = 900 12 | static let fullHeight: CGFloat = 500 13 | static let sidebarWidth: CGFloat = 200 14 | static var detailWidth: CGFloat { Self.fullWidth - Self.sidebarWidth } 15 | } 16 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Queue/ExtractInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtractInfo.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 13/02/2024. 6 | // 7 | 8 | import Foundation 9 | import MarkersExtractor 10 | 11 | struct ExtractInfo: Sendable, Codable, Identifiable { 12 | let jsonURL: URL 13 | let creationDate: Date 14 | let profile: DatabasePlatform 15 | 16 | var id: URL { 17 | jsonURL 18 | } 19 | 20 | init(jsonURL: URL, profile: DatabasePlatform) { 21 | self.jsonURL = jsonURL 22 | self.creationDate = Date() 23 | self.profile = profile 24 | } 25 | 26 | init?(exportResult: ExportResult) { 27 | guard let profile: DatabasePlatform = switch exportResult.profile { 28 | case .notion: 29 | DatabasePlatform.notion 30 | case .airtable: 31 | DatabasePlatform.airtable 32 | default: 33 | nil 34 | } else { 35 | return nil 36 | } 37 | 38 | self.profile = profile 39 | 40 | guard let jsonURL = exportResult.jsonManifestPath else { 41 | return nil 42 | } 43 | 44 | self.jsonURL = jsonURL 45 | 46 | self.creationDate = Date() 47 | } 48 | 49 | public func save(to url: URL) throws { 50 | let encoder = JSONEncoder() 51 | encoder.outputFormatting = .prettyPrinted 52 | 53 | let data = try encoder.encode(self) 54 | 55 | try data.write(to: url) 56 | } 57 | } 58 | 59 | enum ExtractInfoError: Error { 60 | case invalidProfile 61 | case invalidJSONPath 62 | } 63 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Queue/QueueError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueueError.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 13/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum QueueError: Error { 11 | case missingOutputDirectory 12 | } 13 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Queue/QueueInstance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueueInstance.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 14/02/2024. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | @MainActor 12 | class QueueInstance: ObservableObject, Identifiable, Sendable { 13 | public let name: String 14 | private let folderURL: URL 15 | let extractInfo: ExtractInfo 16 | let uploader = DatabaseUploader() 17 | let availableDatabaseProfiles: [DatabaseProfileModel] 18 | 19 | @Published var uploadDestination: DatabaseProfileModel? = nil 20 | @Published var status: QueueStatus = .idle 21 | 22 | var creationDateFormatted: String { 23 | extractInfo.creationDate.formatted() 24 | } 25 | 26 | static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "QueueInstance") 27 | 28 | init(extractInfo: ExtractInfo, folderURL: URL, databaseProfiles: [DatabaseProfileModel]) { 29 | self.name = extractInfo.jsonURL.deletingPathExtension().lastPathComponent 30 | self.extractInfo = extractInfo 31 | self.availableDatabaseProfiles = databaseProfiles.filter { $0.plaform == extractInfo.profile } 32 | self.folderURL = folderURL 33 | self.uploader.uploadProgress.showDockProgress = false 34 | } 35 | 36 | public func upload() async throws { 37 | guard let uploadDestinationUnwrapped = self.uploadDestination else { 38 | return 39 | } 40 | 41 | self.status = .uploading 42 | 43 | try await self.uploader.uploadToDatabase( 44 | url: extractInfo.jsonURL, 45 | databaseProfile: uploadDestinationUnwrapped 46 | ) 47 | 48 | self.status = .success 49 | } 50 | 51 | public func deleteFolder() async { 52 | // Return if no upload destination was selected 53 | if self.uploadDestination == nil { 54 | return 55 | } 56 | 57 | do { 58 | Self.logger.notice("Upload done. Deleting folder: \(self.folderURL)") 59 | try self.folderURL.trashOrDelete() 60 | } catch { 61 | Self.logger.error("Failed to delete folder after queue upload: \(self.folderURL.path(percentEncoded: false))") 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Queue/QueueStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueueStatus.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 14/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum QueueStatus { 11 | case idle 12 | case uploading 13 | case success 14 | case failed 15 | } 16 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Roles/RoleModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoleModel.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 27/01/2024. 6 | // 7 | 8 | import Foundation 9 | import DAWFileKit 10 | 11 | struct RoleModel: Identifiable, Codable, Hashable, Equatable { 12 | let role: FinalCutPro.FCPXML.AnyRole 13 | var enabled: Bool 14 | 15 | var id: String { 16 | return self.role.rawValue 17 | } 18 | 19 | var displayName: String { 20 | var name = self.role.rawValue 21 | 22 | if let captionRole = FinalCutPro.FCPXML.CaptionRole(rawValue: self.role.rawValue) { 23 | name = "\(captionRole.role) (\(captionRole.captionFormat))" 24 | } 25 | 26 | return name 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Roles/RolesManager+DropDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RolesManager+DropDelegate.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 28/04/2024. 6 | // 7 | 8 | import SwiftUI 9 | import MarkersExtractor 10 | 11 | extension RolesManager: DropDelegate { 12 | nonisolated func performDrop(info: DropInfo) -> Bool { 13 | Task { @MainActor in 14 | self.loadingInProgress = true 15 | } 16 | 17 | let providers = info.itemProviders( 18 | for: [.fcpxml, .fileURL] 19 | ) 20 | 21 | for provider in providers { 22 | // Load FCPXML 23 | if provider.hasRepresentationConforming(toTypeIdentifier: "com.apple.finalcutpro.xml") { 24 | _ = provider.loadDataRepresentation(for: .fcpxml) { data, error in 25 | Task { 26 | defer { 27 | Task { @MainActor in 28 | self.loadingInProgress = false 29 | } 30 | } 31 | 32 | guard let dataUnwrapped = data else { 33 | return 34 | } 35 | 36 | if let extractedRoles = await self.getRoles(fcpxml: FCPXMLFile(fileContents: dataUnwrapped)) { 37 | await self.setRoles(extractedRoles) 38 | } 39 | } 40 | } 41 | } 42 | 43 | // Load FCPXMLD 44 | if provider.canLoadObject(ofClass: URL.self) { 45 | // Load the file URL from the provider 46 | let _ = provider.loadObject(ofClass: URL.self) { url, error in 47 | Task { 48 | defer { 49 | Task { @MainActor in 50 | self.loadingInProgress = false 51 | } 52 | } 53 | 54 | guard let urlUnwrapped = url else { 55 | return 56 | } 57 | 58 | if !urlUnwrapped.conformsToType([.fcpxmld]) { 59 | Self.logger.warning("File doesn't conform to FCPXMLD") 60 | return 61 | } 62 | 63 | if let extractedRoles = await self.getRoles(fcpxml: try FCPXMLFile(at: urlUnwrapped)) { 64 | await self.setRoles(extractedRoles) 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | return true 72 | } 73 | 74 | nonisolated private func getRoles(fcpxml: FCPXMLFile) async -> [RoleModel]? { 75 | do { 76 | let rolesExtractor = RolesExtractor(fcpxml: fcpxml) 77 | 78 | let roles = try await rolesExtractor.extract() 79 | 80 | let roleModels = roles.map { RoleModel(role: $0, enabled: true) } 81 | 82 | return roleModels 83 | } catch { 84 | Self.logger.error("Failed to extract roles") 85 | return nil 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Models/Settings/MarkersExtractorModelExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkersExtractorModelExtensions.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 18/02/2024. 6 | // 7 | 8 | import Foundation 9 | import MarkersExtractor 10 | 11 | extension ExportFolderFormat: Codable { 12 | var displayName: String { 13 | switch self { 14 | case .short: 15 | "Short" 16 | case .medium: 17 | "Medium" 18 | case .long: 19 | "Long" 20 | } 21 | } 22 | } 23 | 24 | extension MarkerIDMode: Codable { 25 | var displayName: String { 26 | switch self { 27 | case .timelineNameAndTimecode: 28 | "Timeline and Timecode" 29 | case .name: 30 | "Name" 31 | case .notes: 32 | "Notes" 33 | } 34 | } 35 | } 36 | 37 | extension MarkerLabelProperties.AlignHorizontal: Codable { 38 | var displayName: String { 39 | switch self { 40 | case .left: 41 | "Left" 42 | case .center: 43 | "Center" 44 | case .right: 45 | "Right" 46 | } 47 | } 48 | } 49 | 50 | extension MarkerLabelProperties.AlignVertical: Codable { 51 | var displayName: String { 52 | switch self { 53 | case .top: 54 | "Top" 55 | case .center: 56 | "Center" 57 | case .bottom: 58 | "Bottom" 59 | } 60 | } 61 | } 62 | 63 | extension ExportField: Codable {} 64 | 65 | extension MarkersSource: Codable {} 66 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Pagemaker/PagemakerPDFExportHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PagemakerPDFExportHandler.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 2025.05.02. 6 | // 7 | 8 | import Foundation 9 | import WebKit 10 | import OSLog 11 | import UniformTypeIdentifiers 12 | 13 | @MainActor 14 | class PagemakerPDFExportHandler: NSObject, WKScriptMessageHandler { 15 | static let shared = PagemakerPDFExportHandler() 16 | static let logger = Logger(subsystem: "\(Bundle.main.bundleIdentifier!).Pagemaker", category: String(describing: PagemakerPDFExportHandler.self)) 17 | 18 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 19 | guard message.name == "exportPDF", 20 | let dict = message.body as? [String: Any], 21 | let pdfDataUri = dict["pdfData"] as? String, 22 | let filename = dict["filename"] as? String else { 23 | Self.logger.error("Pagemaker: Invalid message received: \(message)") 24 | return 25 | } 26 | 27 | guard let dataRange = pdfDataUri.range(of: ";base64,"), 28 | let pdfData = Data(base64Encoded: String(pdfDataUri[dataRange.upperBound...])) else { 29 | Self.logger.error("Pagemaker: Failed to decode PDF data") 30 | return 31 | } 32 | 33 | Task { 34 | await savePDF(pdfData: pdfData, filename: filename) 35 | } 36 | } 37 | 38 | private func savePDF(pdfData: Data, filename: String) async { 39 | // Show save dialog 40 | let savePanel = NSSavePanel() 41 | savePanel.allowedContentTypes = [UTType.pdf] 42 | savePanel.nameFieldStringValue = filename 43 | 44 | let response = await savePanel.beginSheetModal(for: NSApp.keyWindow!) 45 | 46 | if response == .OK, let saveURL = savePanel.url { 47 | do { 48 | try pdfData.write(to: saveURL) 49 | } catch { 50 | Self.logger.error("Pagemaker: Failed to save PDF: \(error.localizedDescription)") 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Pagemaker/PagemakerUIDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PagemakerUIDelegate.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 2025.05.03. 6 | // 7 | 8 | import Foundation 9 | import WebKit 10 | 11 | @MainActor 12 | class PagemakerUIDelegate: NSObject, WKUIDelegate { 13 | static let shared = PagemakerUIDelegate() 14 | 15 | // Folder picker functionality 16 | func webView( 17 | _ webView: WKWebView, 18 | runOpenPanelWith parameters: WKOpenPanelParameters, 19 | initiatedByFrame frame: WKFrameInfo 20 | ) async -> [URL]? { 21 | return try? await selectFolders(parameters: parameters) 22 | } 23 | 24 | private func selectFolders(parameters: WKOpenPanelParameters) async throws -> [URL]? { 25 | let panel = NSOpenPanel() 26 | panel.canChooseFiles = false 27 | panel.canChooseDirectories = true 28 | panel.allowsMultipleSelection = parameters.allowsMultipleSelection 29 | panel.message = "Select a folder extracted from Marker Data" 30 | panel.prompt = "Select Folder" 31 | 32 | let response = await panel.beginSheetModal(for: NSApp.keyWindow!) 33 | 34 | return response == .OK ? panel.urls : nil 35 | } 36 | 37 | // Handle JavaScript alert dialogs 38 | func webView( 39 | _ webView: WKWebView, 40 | runJavaScriptAlertPanelWithMessage message: String, 41 | initiatedByFrame frame: WKFrameInfo, 42 | ) async { 43 | let alert = NSAlert() 44 | alert.messageText = "Alert" 45 | alert.informativeText = message 46 | alert.alertStyle = .informational 47 | alert.addButton(withTitle: "OK") 48 | 49 | await alert.beginSheetModal(for: NSApp.keyWindow!) 50 | } 51 | 52 | // Handle JavaScript confirm dialogs 53 | func webView( 54 | _ webView: WKWebView, 55 | runJavaScriptConfirmPanelWithMessage message: String, 56 | initiatedByFrame frame: WKFrameInfo 57 | ) async -> Bool { 58 | let alert = NSAlert() 59 | alert.messageText = "Confirm" 60 | alert.informativeText = message 61 | alert.alertStyle = .warning 62 | alert.addButton(withTitle: "OK") 63 | alert.addButton(withTitle: "Cancel") 64 | 65 | await alert.beginSheetModal(for: NSApp.keyWindow!) 66 | return true 67 | } 68 | 69 | // Handle JavaScript prompt dialogs 70 | func webView( 71 | _ webView: WKWebView, 72 | runJavaScriptTextInputPanelWithPrompt prompt: String, 73 | defaultText: String?, 74 | initiatedByFrame frame: WKFrameInfo 75 | ) async -> String? { 76 | let alert = NSAlert() 77 | alert.messageText = "Prompt" 78 | alert.informativeText = prompt 79 | alert.alertStyle = .informational 80 | alert.addButton(withTitle: "OK") 81 | alert.addButton(withTitle: "Cancel") 82 | 83 | let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) 84 | input.stringValue = defaultText ?? "" 85 | input.placeholderString = prompt 86 | 87 | alert.accessoryView = input 88 | 89 | let response = await alert.beginSheetModal(for: NSApp.keyWindow!) 90 | return response == .alertFirstButtonReturn ? input.stringValue : nil 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Pagemaker/WebViewStateManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewStateManager.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 2025.05.02. 6 | // 7 | 8 | import Foundation 9 | import WebKit 10 | 11 | /// State manager to track WebView loading state and handle external links 12 | @MainActor 13 | class WebViewStateManager: NSObject, ObservableObject, WKNavigationDelegate { 14 | @Published var isLoading = true 15 | private var loadingTimeout: Task? 16 | private var baseURL: URL? 17 | 18 | // Called when page starts loading 19 | func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { 20 | isLoading = true 21 | 22 | // Store the base URL for the first load 23 | if baseURL == nil { 24 | baseURL = webView.url 25 | } 26 | 27 | // Set timeout 28 | loadingTimeout?.cancel() 29 | loadingTimeout = Task { 30 | do { 31 | try await Task.sleep(for: .seconds(10)) 32 | if !Task.isCancelled { 33 | isLoading = false 34 | } 35 | } catch {} 36 | } 37 | } 38 | 39 | // Called when page finishes loading 40 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 41 | loadingTimeout?.cancel() 42 | isLoading = false 43 | } 44 | 45 | // Called when page fails to load 46 | func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { 47 | loadingTimeout?.cancel() 48 | isLoading = false 49 | } 50 | 51 | // Called when initial loading fails 52 | func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { 53 | loadingTimeout?.cancel() 54 | isLoading = false 55 | } 56 | 57 | // Handle decision for navigation actions - this is where we intercept external links 58 | func webView( 59 | _ webView: WKWebView, 60 | decidePolicyFor navigationAction: WKNavigationAction 61 | ) async -> WKNavigationActionPolicy { 62 | guard let url = navigationAction.request.url else { 63 | return .allow 64 | } 65 | 66 | // Check if this is our PDF export (handled separately) 67 | if url.pathExtension.lowercased() == "pdf" { 68 | return .cancel 69 | } 70 | 71 | // If it's a local file URL within our app bundle or the same as our base URL, allow it 72 | if url.isFileURL, 73 | url.absoluteString.contains(Bundle.main.bundleURL.absoluteString) || 74 | url.absoluteString == baseURL?.absoluteString { 75 | return .allow 76 | } 77 | 78 | // For external links: if it's a link click or form submission 79 | if navigationAction.navigationType == .linkActivated || 80 | navigationAction.navigationType == .formSubmitted { 81 | 82 | // Open in default browser 83 | NSWorkspace.shared.open(url) 84 | 85 | // Cancel the navigation in the WebView 86 | return .cancel 87 | } 88 | 89 | // Allow all other navigation 90 | return .allow 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Resources/DefaultConfiguration.json: -------------------------------------------------------------------------------- 1 | { 2 | "exportFolderURL": "", 3 | "selectedFolderFormat": 1, 4 | "selectedImageMode": 0, 5 | "enabledSubframes": false, 6 | "enabledNoMedia": false, 7 | "selectedIDNamingMode": 0, 8 | "selectedMarkersSource": "markers", 9 | "overrideImageSize": 0, 10 | "selectedImageSizePercent": 100, 11 | "imageWidth": 1920, 12 | "imageHeight": 1080, 13 | "selectedJPEGImageQuality": 100, 14 | "selectedGIFFPS": 10, 15 | "selectedGIFLength": 2, 16 | "selectedFontNameType": 3, 17 | "selectedFontStyleType": 0, 18 | "selectedFontSize": 30, 19 | "selectedStrokeSize": 6, 20 | "isStrokeSizeAuto": true, 21 | "selectedFontColor": "#FFFFFF", 22 | "selectedFontColorOpacity": 100, 23 | "selectedStrokeColor": "#000000", 24 | "selectedHorizontalAlignment": 0, 25 | "selectedVerticalAlignment": 0, 26 | "selectedOverlays": [], 27 | "copyrightText": "", 28 | "hideLabelNames": false, 29 | "unifiedExportProfile": { 30 | "displayName": "CSV", 31 | "extractProfile": "csv", 32 | "databaseProfileName": "", 33 | "exportProfileType": "extractOnly" 34 | }, 35 | "roles": [] 36 | } 37 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Resources/Marker Data H.264.fcpxdest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Resources/Marker Data H.264.fcpxdest -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Resources/Marker Data Source.fcpxdest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Resources/Marker Data Source.fcpxdest -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Resources/airlift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Resources/airlift -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Resources/csv2notion_neo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Marker Data/Resources/csv2notion_neo -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Resources/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | com.apple.security.cs.allow-unsigned-executable-memory 9 | 10 | com.apple.security.cs.disable-library-validation 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/06/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | subscript(safe index: Int) -> Element? { 12 | return indices.contains(index) ? self[index] : nil 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/BundleExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/10/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | var appName: String { 12 | object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? 13 | object(forInfoDictionaryKey: "CFBundleName") as? String ?? 14 | "" 15 | } 16 | 17 | var version: String { 18 | return infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown version" 19 | } 20 | 21 | var buildNumber: String { 22 | return infoDictionary?["CFBundleVersion"] as? String ?? "?" 23 | } 24 | 25 | var safeBundleID: String { 26 | return Bundle.main.bundleIdentifier ?? "co.theacharya.MarkerData" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 07/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | 11 | extension Color: Codable { 12 | /// Init from a hex string. I.e #FFFFFF for white 13 | init(hex: String) { 14 | let hex = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) 15 | var rgbValue: UInt64 = 0 16 | Scanner(string: hex).scanHexInt64(&rgbValue) 17 | let red = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0 18 | let green = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0 19 | let blue = CGFloat(rgbValue & 0x0000FF) / 255.0 20 | self.init(red: Double(red), green: Double(green), blue: Double(blue)) 21 | } 22 | 23 | /// Hex string representation. I.e #FFFFFF for white 24 | var hex: String { 25 | let components = self.components() 26 | let red = Int(components.red * 255.0) 27 | let green = Int(components.green * 255.0) 28 | let blue = Int(components.blue * 255.0) 29 | return String(format: "#%02X%02X%02X", red, green, blue) 30 | } 31 | 32 | private func components() -> (red: CGFloat, green: CGFloat, blue: CGFloat) { 33 | guard let color = NSColor(self).usingColorSpace(.sRGB) else { 34 | // Return black as default 35 | return (0, 0, 0) 36 | } 37 | 38 | let red = color.redComponent 39 | let green = color.greenComponent 40 | let blue = color.blueComponent 41 | return (red, green, blue) 42 | } 43 | 44 | public func isEqual(to color: Color, tolerance: CGFloat = 0.1) -> Bool { 45 | let (red1, green1, blue1) = components() 46 | let (red2, green2, blue2) = color.components() 47 | 48 | return abs(red1 - red2) <= tolerance && 49 | abs(green1 - green2) <= tolerance && 50 | abs(blue1 - blue2) <= tolerance 51 | } 52 | 53 | static let darkPurple = Color(#colorLiteral(red: 0.2784313725, green: 0.03137254902, blue: 0.5843137255, alpha: 1)) 54 | 55 | public func encode(to encoder: Encoder) throws { 56 | var container = encoder.singleValueContainer() 57 | try container.encode(self.hex) 58 | } 59 | 60 | public init(from decoder: Decoder) throws { 61 | let container = try decoder.singleValueContainer() 62 | let hex = try container.decode(String.self) 63 | 64 | self.init(hex: hex) 65 | } 66 | 67 | static func == (lhs: Color, rhs: Color) -> Bool { 68 | return lhs.isEqual(to: rhs) 69 | } 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/DominantColorAlgorithmExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DominantColorAlgorithmExtension.swift 3 | // ImageColors 4 | // 5 | // Created by Denis Dmitriev on 19.09.2023. 6 | // 7 | 8 | import Foundation 9 | import DominantColors 10 | 11 | extension DominantColorAlgorithm { 12 | var title: String { 13 | switch self { 14 | case .areaAverage: 15 | return "Area average" 16 | case .iterative: 17 | return "Iterative" 18 | case .kMeansClustering: 19 | return "Means Clustering" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/EmptyOrIntFormatStyle.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Parses either an integer or an empty string as `Int?`. 4 | struct EmptyOrIntParseStrategy: ParseStrategy { 5 | static let formatter: NumberFormatter = { 6 | let formatter = NumberFormatter() 7 | formatter.numberStyle = .decimal 8 | 9 | return formatter 10 | }() 11 | 12 | func parse(_ value: String) throws -> Int? { 13 | if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { 14 | // Empty string 15 | return nil 16 | } 17 | 18 | let number = Self.formatter.number(from: value) 19 | let int = number?.intValue 20 | return int 21 | 22 | } 23 | } 24 | 25 | struct EmptyOrIntFormatStyle: ParseableFormatStyle { 26 | static var formatter: NumberFormatter { 27 | EmptyOrIntParseStrategy.formatter 28 | } 29 | 30 | var parseStrategy: EmptyOrIntParseStrategy { 31 | EmptyOrIntParseStrategy() 32 | } 33 | 34 | func format(_ value: Int?) -> String { 35 | guard let int = value else { 36 | return "" 37 | } 38 | 39 | let nsNumber = NSNumber(value: int) 40 | let string = Self.formatter.string(from: nsNumber) 41 | 42 | return string ?? "" 43 | } 44 | } 45 | 46 | extension FormatStyle where Self == EmptyOrIntFormatStyle { 47 | static var emptyOrInt: EmptyOrIntFormatStyle { 48 | EmptyOrIntFormatStyle() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/ExportProfileFormatExtrension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportProfileFormatExtrension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 04/12/2023. 6 | // 7 | 8 | import Foundation 9 | import MarkersExtractor 10 | 11 | extension ExportProfileFormat: Codable { 12 | static var allCasesInUIOrder: [ExportProfileFormat] { 13 | let inUIOrder = [Self.csv, Self.tsv, Self.xlsx, Self.midi, Self.youtube, Self.notion, Self.airtable] 14 | assert(inUIOrder.count == Self.allCases.count - 1, "ExportProfileFormat.allCasesInUIOrder has invalid number of elements") 15 | return inUIOrder 16 | } 17 | 18 | public var extractOnlyName: String { 19 | switch self { 20 | case .airtable: 21 | return "Airtable (No Upload)" 22 | case .midi: 23 | return "MIDI" 24 | case .notion: 25 | return "Notion (No Upload)" 26 | case .csv: 27 | return "CSV" 28 | case .tsv: 29 | return "TSV" 30 | case .youtube: 31 | return "YouTube Chapters" 32 | case .xlsx: 33 | return "Excel" 34 | case .json: 35 | return "JSON" 36 | } 37 | } 38 | 39 | public static var allExtractOnlyNames: [String] { 40 | Self.allCases.map( { $0.extractOnlyName }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/NSImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImageExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/01/2024. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | extension NSImage { 12 | func scalePreservingAspectRatio(targetSize: NSSize) -> NSImage { 13 | let widthRatio = targetSize.width / size.width 14 | let heightRatio = targetSize.height / size.height 15 | 16 | let scaleFactor = min(widthRatio, heightRatio) 17 | 18 | let scaledImageSize = NSSize( 19 | width: size.width * scaleFactor, 20 | height: size.height * scaleFactor 21 | ) 22 | 23 | let newImage = NSImage(size: scaledImageSize) 24 | newImage.lockFocus() 25 | self.draw( 26 | in: NSRect(origin: .zero, size: scaledImageSize), 27 | from: NSRect(origin: .zero, size: self.size), 28 | operation: .copy, 29 | fraction: 1.0 30 | ) 31 | newImage.unlockFocus() 32 | 33 | return newImage 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/NotificationNameExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationNameExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 26/05/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | static let openFile = Notification.Name("OpenFile") 12 | static let workflowExtensionFileReceived = Notification.Name("WorkflowExtensionFileReceived") 13 | static let rolesChanged = Notification.Name("RolesChanged") 14 | static let FCPShareStart = Notification.Name("FCPShareStart") 15 | static let updateAvailable = Notification.Name("updateAvailable") 16 | } 17 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/RoleExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoleExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 25/01/2024. 6 | // 7 | 8 | import Foundation 9 | import DAWFileKit 10 | 11 | extension FinalCutPro.FCPXML.AnyRole: Codable {} 12 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/StringExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 2025.02.13. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func cleanTerminalOutput() -> String { 12 | // Regex to match ANSI escape codes 13 | let ansiEscapePattern = "\\u001B\\[[0-9;]*[a-zA-Z]" 14 | // Match backspaces with preceding characters 15 | let backspacePattern = ".\\u{08}" 16 | // Regex for other unwanted characters 17 | let unwantedPattern = "[\\r\\f]" 18 | 19 | let cleaned = self 20 | .replacingOccurrences(of: ansiEscapePattern, with: "", options: .regularExpression) 21 | .replacingOccurrences(of: backspacePattern, with: "", options: .regularExpression) 22 | .replacingOccurrences(of: unwantedPattern, with: "", options: .regularExpression) 23 | 24 | return cleaned 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/TaskExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/04/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Task where Failure == Error { 11 | /// Performs an async task in a sync context. 12 | /// 13 | /// - Note: This function blocks the thread until the given operation is finished. The caller is responsible for managing multithreading. 14 | static func synchronous(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) { 15 | let semaphore = DispatchSemaphore(value: 0) 16 | 17 | Task(priority: priority) { 18 | defer { semaphore.signal() } 19 | return try await operation() 20 | } 21 | 22 | semaphore.wait() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/UTTypeExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UTTypeExtension.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 07/10/2023. 6 | // 7 | 8 | import Foundation 9 | import UniformTypeIdentifiers 10 | 11 | extension UTType { 12 | public static let fcpxml = UTType("com.apple.finalcutpro.xml")! 13 | public static let fcpxmld = UTType("com.apple.finalcutpro.xmld")! 14 | } 15 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Extensions/ViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func eraseToAnyView() -> AnyView { 12 | AnyView(self) 13 | } 14 | 15 | func modify( 16 | @ViewBuilder _ content: (Self) -> Content 17 | ) -> some View { 18 | content(self) 19 | } 20 | 21 | func overlayHelpButton(url: URL) -> some View { 22 | self.modifier(OverlayHelpButton(url: url)) 23 | } 24 | 25 | /** 26 | 27 | ``` 28 | self.alignmentGuide(.formControlAlignment) { d in 29 | d[.leading] 30 | } 31 | ``` 32 | */ 33 | func formControlLeadingAlignmentGuide() -> some View { 34 | self.alignmentGuide(.formControlAlignment) { d in 35 | d[.leading] 36 | } 37 | } 38 | 39 | func onHover(isHovering: Binding) -> some View { 40 | self.onHover { hovering in 41 | isHovering.wrappedValue = hovering 42 | } 43 | } 44 | 45 | /// Applies the given transform if the given condition evaluates to `true`. 46 | /// - Parameters: 47 | /// - condition: The condition to evaluate. 48 | /// - transform: The transform to apply to the source `View`. 49 | /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. 50 | @ViewBuilder func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View { 51 | if condition() { 52 | transform(self) 53 | } else { 54 | self 55 | } 56 | } 57 | } 58 | 59 | struct OverlayHelpButton: ViewModifier { 60 | 61 | @Environment(\.openURL) var openURL 62 | 63 | let url: URL 64 | 65 | func body(content: Content) -> some View { 66 | content 67 | .frame(maxHeight: .infinity) 68 | .overlay(alignment: .bottomTrailing) { 69 | HelpButton { 70 | self.openURL(url) 71 | } 72 | .padding([.trailing, .bottom], 10) 73 | } 74 | } 75 | 76 | } 77 | 78 | extension Scene { 79 | 80 | func modify( 81 | @SceneBuilder _ content: (Self) -> Content 82 | ) -> some Scene { 83 | content(self) 84 | } 85 | 86 | } 87 | 88 | extension HorizontalAlignment { 89 | private enum ControlAlignment: AlignmentID { 90 | static func defaultValue(in context: ViewDimensions) -> CGFloat { 91 | return context[HorizontalAlignment.center] 92 | } 93 | } 94 | static let formControlAlignment = HorizontalAlignment(ControlAlignment.self) 95 | } 96 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Notifications/NotificationFrequency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationFrequency.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/01/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NotificationFrequency: Int, CaseIterable, Identifiable, Codable { 11 | case never = 0 12 | case onlyOnCompletion = 1 13 | case allSteps = 2 14 | 15 | var displayName: String { 16 | switch self { 17 | case .never: 18 | "Never" 19 | case .onlyOnCompletion: 20 | "Only on Completion" 21 | case .allSteps: 22 | "All Steps" 23 | } 24 | } 25 | 26 | var id: Int { 27 | self.rawValue 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Notifications/NotificationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationManager.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/01/2024. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | import OSLog 11 | 12 | struct NotificationManager { 13 | private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationManager") 14 | 15 | static func sendNotification(taskFinished: Bool, title: String, body: String = "") async { 16 | let frequencyInt = UserDefaults.standard.integer(forKey: "notificationFrequency") 17 | guard let notificationFrequency = NotificationFrequency(rawValue: frequencyInt) else { 18 | Self.logger.error("Failed to read notification frequency settings") 19 | return 20 | } 21 | 22 | // Check if we should send notifiction 23 | // else return 24 | switch notificationFrequency { 25 | case .never: 26 | return 27 | case .onlyOnCompletion: 28 | if !taskFinished { 29 | return 30 | } 31 | case .allSteps: 32 | break 33 | } 34 | 35 | let center = UNUserNotificationCenter.current() 36 | let settings = await center.notificationSettings() 37 | 38 | guard (settings.authorizationStatus == .authorized) || 39 | (settings.authorizationStatus == .provisional) else { 40 | 41 | // Ask for authorization 42 | do { 43 | try await center.requestAuthorization(options: [.alert, .sound, .badge]) 44 | } catch { 45 | logger.error("Failed to request notification authorization. Error: \(error.localizedDescription)") 46 | } 47 | 48 | return 49 | } 50 | 51 | let content = UNMutableNotificationContent() 52 | 53 | content.title = title 54 | content.body = body 55 | content.sound = .default 56 | 57 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: .leastNonzeroMagnitude, repeats: false) 58 | let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) 59 | 60 | do { 61 | try await UNUserNotificationCenter.current().add(request) 62 | } catch { 63 | logger.error("Failed to send notication. Error: \(error.localizedDescription)") 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/DeepCopy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepCopy.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 06/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | func deepCopy(of object: T) -> T? { 11 | do { 12 | let json = try JSONEncoder().encode(object) 13 | return try JSONDecoder().decode(T.self, from: json) 14 | } catch let error { 15 | print("Failed to deep copy \(object.self). \(error.localizedDescription)") 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/DeminiaturizeAllWindows.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeminiaturizeAllWindows.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 29/05/2024. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | @MainActor 12 | func deminiaturizeAllWindows() { 13 | for window in NSApplication.shared.windows { 14 | if window.title == "Colors" { 15 | return 16 | } 17 | 18 | window.deminiaturize(nil) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/DicitionaryEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DicitionaryEncoder.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/04/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | class DictionaryEncoder { 11 | private let encoder = JSONEncoder() 12 | 13 | var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy { 14 | set { encoder.dateEncodingStrategy = newValue } 15 | get { return encoder.dateEncodingStrategy } 16 | } 17 | 18 | var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy { 19 | set { encoder.dataEncodingStrategy = newValue } 20 | get { return encoder.dataEncodingStrategy } 21 | } 22 | 23 | var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy { 24 | set { encoder.nonConformingFloatEncodingStrategy = newValue } 25 | get { return encoder.nonConformingFloatEncodingStrategy } 26 | } 27 | 28 | var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy { 29 | set { encoder.keyEncodingStrategy = newValue } 30 | get { return encoder.keyEncodingStrategy } 31 | } 32 | 33 | func encode(_ value: T) throws -> [String: Any] where T : Encodable { 34 | let data = try encoder.encode(value) 35 | if let dict = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] { 36 | return dict 37 | } else { 38 | return [:] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/Links.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Links.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 02/12/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Links { 11 | static let queueHelpURL = URL(string: "https://markerdata.theacharya.co/user-guide/queue/")! 12 | static let generalSettingsURL = URL(string: "https://markerdata.theacharya.co/user-guide/general/")! 13 | static let imageSettingsURL = URL(string: "https://markerdata.theacharya.co/user-guide/image/")! 14 | static let labelSettingsURL = URL(string: "https://markerdata.theacharya.co/user-guide/label/")! 15 | static let configurationSettingsURL = URL(string: "https://markerdata.theacharya.co/user-guide/configurations/")! 16 | static let databaseSettingsURL = URL(string: "https://markerdata.theacharya.co/user-guide/databases/")! 17 | 18 | static let airtableHelpURL = URL(string: "https://markerdata.theacharya.co/databases/airtable-prerequisite/")! 19 | static let notionHelpURL = URL(string: "https://markerdata.theacharya.co/databases/notion-prerequisite/")! 20 | 21 | static let dropboxAppConsole = URL(string: "https://www.dropbox.com/developers/apps")! 22 | 23 | // Notion & Airtable Template Links 24 | static let notionTemplateURL = URL(string: "https://markerdata.theacharya.co/user-guide/databases/#notion-template")! 25 | static let airtableTemplateURL = URL(string: "https://markerdata.theacharya.co/user-guide/databases/#airtable-template")! 26 | } 27 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/LogManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogManager.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 07/01/2024. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | struct LogManager { 12 | public static func export() async { 13 | do { 14 | let store = try OSLogStore(scope: .currentProcessIdentifier) 15 | let date = Date.now.addingTimeInterval(-24 * 3600) 16 | let position = store.position(date: date) 17 | 18 | var entries = try store 19 | .getEntries(at: position) 20 | .compactMap { $0 as? OSLogEntryLog } 21 | .filter { $0.subsystem == Bundle.main.bundleIdentifier! } 22 | .map { "[\($0.date.formatted())] [\($0.category)] \($0.composedMessage)" } 23 | 24 | if entries.isEmpty { 25 | entries.append("No available logs!") 26 | entries.append("Note: logs are dropped when the app is closed.") 27 | entries.append("Logs are only recorded from the instance of the application that is currently running.") 28 | } 29 | 30 | let url = URL.logsFolder 31 | .appendingPathComponent("markerdata_log.txt", conformingTo: .plainText) 32 | 33 | try entries.joined(separator: "\n").write(to: url, atomically: true, encoding: String.Encoding.utf8) 34 | } catch { 35 | print("\(error.localizedDescription)") 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/SidebarSelectionSwitcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarSelectionSwitcher.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 03/02/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /// Recieves events from the FCP Share Destination and Workflow Extension 12 | /// and sets the sidebar selection to the Extract Panel so we can see the progress 13 | final class SidebarSelectionSwitcher { 14 | @Binding var sidebarSelection: MainViews 15 | 16 | init(sidebarSelection: Binding) { 17 | self._sidebarSelection = sidebarSelection 18 | 19 | NotificationCenter.default.addObserver( 20 | self, 21 | selector: #selector(switchToExtactView), 22 | name: .openFile, 23 | object: nil) 24 | 25 | DistributedNotificationCenter.default.addObserver( 26 | self, 27 | selector: #selector(switchToExtactView), 28 | name: .workflowExtensionFileReceived, 29 | object: nil) 30 | } 31 | 32 | @objc func switchToExtactView() { 33 | self.sidebarSelection = .extract 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/UserDefaultsArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsArray.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 2024.11.01. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | struct UserDefaultsArray { 12 | private let key: String 13 | private let defaultValue: [T] 14 | 15 | // Add this initializer specifically for arrays 16 | init(wrappedValue defaultValue: [T], _ key: String) { 17 | self.key = key 18 | self.defaultValue = defaultValue 19 | } 20 | 21 | var wrappedValue: [T] { 22 | get { 23 | guard let data = UserDefaults.standard.data(forKey: key) else { return defaultValue } 24 | return (try? JSONDecoder().decode([T].self, from: data)) ?? defaultValue 25 | } 26 | set { 27 | if let data = try? JSONEncoder().encode(newValue) { 28 | UserDefaults.standard.set(data, forKey: key) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Other/WalkDirectory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WalkDirectory.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/04/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | @Sendable 11 | func walkDirectory(at url: URL, options: FileManager.DirectoryEnumerationOptions) -> AsyncStream { 12 | AsyncStream { continuation in 13 | Task { 14 | let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: nil, options: options) 15 | 16 | while let fileURL = enumerator?.nextObject() as? URL { 17 | continuation.yield(fileURL) 18 | } 19 | 20 | continuation.finish() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Shell/ShellArgumentList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShellArgumentList.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 11/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ShellArgumentList { 11 | var executablePath: URL 12 | var parameters: [any ShellOption] 13 | 14 | init(executablePath: URL, parameters: [any ShellOption]) { 15 | self.executablePath = executablePath 16 | self.parameters = parameters 17 | } 18 | 19 | init(executablePath: URL) { 20 | self.executablePath = executablePath 21 | self.parameters = [] 22 | } 23 | 24 | mutating func append(_ parameter: any ShellOption) { 25 | self.parameters.append(parameter) 26 | } 27 | 28 | func getCommand() -> String { 29 | let executablePathString = executablePath.path(percentEncoded: false).quoted 30 | 31 | let commands: [String] = self.parameters.map { 32 | $0.toString() 33 | } 34 | let commandsString = commands.joined(separator: " ") 35 | 36 | return "\(executablePathString) \(commandsString)" 37 | } 38 | } 39 | 40 | protocol ShellOption { 41 | func toString() -> String 42 | } 43 | 44 | struct ShellArgument: ShellOption { 45 | let argument: String 46 | 47 | init(_ argument: String) { 48 | self.argument = argument 49 | } 50 | 51 | init(url: URL) { 52 | self.argument = url.path(percentEncoded: false).quoted 53 | } 54 | 55 | func toString() -> String { 56 | return if !self.argument.hasPrefix(#"""#) && !self.argument.hasSuffix(#"""#) { 57 | self.argument.quoted 58 | } else { 59 | self.argument 60 | } 61 | } 62 | } 63 | 64 | struct ShellFlag: ShellOption { 65 | let flag: String 66 | 67 | init(_ flag: String) { 68 | self.flag = flag 69 | } 70 | 71 | func toString() -> String { 72 | return self.flag 73 | } 74 | } 75 | 76 | struct ShellParameter: ShellOption { 77 | let option: String 78 | let value: String 79 | 80 | init(for option: String, value: String) { 81 | self.option = option 82 | self.value = value 83 | } 84 | 85 | init(for option: String, url: URL) { 86 | self.option = option 87 | self.value = url.path(percentEncoded: false) 88 | } 89 | 90 | func toString() -> String { 91 | // Add quotes to value 92 | let valueQuoted = if !self.value.hasPrefix(#"""#) && !self.value.hasSuffix(#"""#) { 93 | self.value.quoted 94 | } else { 95 | self.value 96 | } 97 | 98 | return "\(option) \(valueQuoted)" 99 | } 100 | } 101 | 102 | struct ShellRawArgument: ShellOption { 103 | let argument: String 104 | 105 | init(_ argument: String) { 106 | self.argument = argument 107 | } 108 | 109 | func toString() -> String { 110 | return argument 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Utilities/Shell/ShellError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShellError.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 2025.02.13. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ShellError: LocalizedError { 11 | case askpassNotFound 12 | case askpassChecksumMismatch 13 | case outputDecodingFailed 14 | case coundtGetHomeDirectory 15 | case nonZeroExit(command: String, exitCode: Int32, output: String) 16 | 17 | var errorDescription: String? { 18 | switch self { 19 | case .askpassNotFound: 20 | return "askpass script not found" 21 | case .askpassChecksumMismatch: 22 | return "Script checksum mismatch. The file has been modified." 23 | case .outputDecodingFailed: 24 | return "Failed to decode command output as UTF-8" 25 | case .coundtGetHomeDirectory: 26 | return "Failed to get home directory" 27 | case .nonZeroExit(let command, let exitCode, let output): 28 | return "Failed to run shell command.\nCommand: \(command) (exit code: \(exitCode))\nOutput: \(output)" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/BigButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BigButtonStyle.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/06/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BigButtonStyle: ButtonStyle { 11 | var color: Color 12 | var minWidth: CGFloat = 0 13 | 14 | func makeBody(configuration: Configuration) -> some View { 15 | configuration.label 16 | .frame(minWidth: minWidth) 17 | .padding(8) 18 | .font(.system(size: 18, weight: .semibold)) 19 | .background(color) 20 | .clipShape(RoundedRectangle(cornerRadius: 6)) 21 | .scaleEffect(configuration.isPressed ? 0.8 : 1.0) // Optional: Add a scale effect for press feedback 22 | .animation(.spring(), value: configuration.isPressed) // Optional: Animate the scale effect 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/ColorPickerForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPickerForm.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 13/11/2023. 6 | // 7 | 8 | import SwiftUI 9 | import ColorWellKit 10 | 11 | struct ColorPickerForm: View { 12 | @Binding var color: Color 13 | 14 | var body: some View { 15 | HStack { 16 | Text("Color:") 17 | 18 | ColorWell(selection: $color, supportsOpacity: false) 19 | .colorWellStyle(.minimal) 20 | .formControlLeadingAlignmentGuide() 21 | } 22 | } 23 | } 24 | 25 | #Preview { 26 | ColorPickerForm(color: .constant(.white)) 27 | } 28 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/ColorPickerOpacitySliderForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | import ColorWellKit 10 | 11 | /// Only use in a Form 12 | struct ColorPickerOpacitySliderForm: View { 13 | @Binding var color: Color 14 | @Binding var opacity: Double 15 | 16 | var body: some View { 17 | HStack { 18 | ColorWell(selection: $color, supportsOpacity: false) 19 | .colorWellStyle(.minimal) 20 | 21 | Slider(value: $opacity, in: 0...100) 22 | .frame(width: 75) 23 | 24 | Text("\(Int(opacity))%") 25 | } 26 | } 27 | } 28 | 29 | struct ColorPickerOpacitySliderForm_Previews: PreviewProvider { 30 | @State static private var color = Color.red 31 | @State static private var opacity: Double = 100 32 | 33 | static let verticalSpacing: CGFloat = 10 34 | 35 | static var previews: some View { 36 | ColorPickerOpacitySliderForm( 37 | color: $color, 38 | opacity: $opacity 39 | ) 40 | .padding() 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/LabeledFormElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabeledFormElement.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 01/05/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A container for attaching a centered label to a form view. 11 | struct LabeledFormElement: View { 12 | let label: String 13 | @ViewBuilder let content: Content 14 | 15 | init(_ label: String, @ViewBuilder content: () -> Content) { 16 | // Add ":" to label if necessary 17 | self.label = label.hasSuffix(":") ? label : "\(label):" 18 | self.content = content() 19 | } 20 | 21 | var body: some View { 22 | HStack { 23 | Text(label) 24 | 25 | HStack { 26 | self.content 27 | } 28 | .labelsHidden() 29 | .formControlLeadingAlignmentGuide() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/LabeledTextboxStepperForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Only designed for use in forms 11 | struct LabeledTextboxStepperForm: View where 12 | Value: Strideable, 13 | Label: View, 14 | Format: ParseableFormatStyle, 15 | Format.FormatOutput == String, 16 | Format.FormatInput == Value 17 | { 18 | 19 | @Binding var value: Value 20 | 21 | let range: ClosedRange 22 | let label: () -> Label 23 | let format: Format 24 | let textFieldWidth: CGFloat? 25 | 26 | init( 27 | value: Binding, 28 | in range: ClosedRange, 29 | format: Format, 30 | textFieldWidth: CGFloat? = nil, 31 | @ViewBuilder label: @escaping () -> Label 32 | ) { 33 | self._value = value 34 | self.range = range 35 | self.format = format 36 | self.textFieldWidth = textFieldWidth 37 | self.label = label 38 | } 39 | 40 | var body: some View { 41 | HStack { 42 | label() 43 | TextField( 44 | "", 45 | value: $value, 46 | format: format 47 | ) 48 | .multilineTextAlignment(.center) 49 | .textFieldStyle(.roundedBorder) 50 | .formControlLeadingAlignmentGuide() 51 | .frame(width: textFieldWidth) 52 | 53 | Stepper( 54 | "", 55 | value: $value, 56 | in: range 57 | ) 58 | .padding(.leading, -10) 59 | } 60 | 61 | } 62 | } 63 | 64 | extension LabeledTextboxStepperForm where Label == Text { 65 | 66 | init( 67 | label: String, 68 | value: Binding, 69 | in range: ClosedRange, 70 | format: Format, 71 | textFieldWidth: CGFloat? = nil 72 | ) { 73 | self.init( 74 | value: value, 75 | in: range, 76 | format: format, 77 | textFieldWidth: textFieldWidth, 78 | label: { Text(label) } 79 | ) 80 | } 81 | 82 | } 83 | 84 | struct LabeledTextboxStepperForm_Previews: PreviewProvider { 85 | 86 | @State static var value: Double = 100 87 | 88 | static let range: ClosedRange = 0...300 89 | 90 | static var previews: some View { 91 | Form { 92 | LabeledTextboxStepperForm( 93 | label: "amount:", 94 | value: $value, 95 | in: range, 96 | format: .number, 97 | textFieldWidth: 100 98 | ) 99 | .padding() 100 | } 101 | .frame(width: 400) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/PulsingIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulsingIcon.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 16/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PulsingIcon: View { 11 | let icon: String 12 | let iconSize: CGFloat 13 | let tint: Color 14 | let duration: TimeInterval 15 | let ringDiameter: CGFloat 16 | let ringMaxScale: CGFloat 17 | 18 | init( 19 | icon: String, 20 | iconSize: CGFloat = 32, 21 | tint: Color = .accentColor, 22 | duration: TimeInterval = 2, 23 | ringDiameter: CGFloat = 20, 24 | ringMaxScale: CGFloat = 4) { 25 | 26 | self.icon = icon 27 | self.iconSize = iconSize 28 | self.tint = tint 29 | self.duration = duration 30 | self.ringDiameter = ringDiameter 31 | self.ringMaxScale = ringMaxScale 32 | } 33 | 34 | @State private var scale: CGFloat = 1 35 | @State private var shadowScale: CGFloat = 1 36 | @State private var opacity: Double = 1 37 | 38 | var body: some View { 39 | ZStack { 40 | tint 41 | .opacity(opacity) 42 | .frame(width: self.ringDiameter, height: self.ringDiameter) 43 | .cornerRadius(100) 44 | .scaleEffect(shadowScale) 45 | .onAppear { 46 | withAnimation( 47 | .easeOut(duration: self.duration) 48 | .repeatForever(autoreverses: false) 49 | ) { 50 | shadowScale = self.ringMaxScale 51 | opacity = 0 52 | } 53 | } 54 | 55 | 56 | Image(systemName: self.icon) 57 | .font(.system(size: self.iconSize)) 58 | .symbolRenderingMode(.multicolor) 59 | .tint(tint) 60 | } 61 | } 62 | } 63 | 64 | #Preview { 65 | VStack { 66 | PulsingIcon(icon: "folder.circle.fill") 67 | } 68 | .frame(width: 300, height: 300) 69 | } 70 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Components/ResizedImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResizedImage.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 21/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Image resized to a specific size 11 | struct ResizedImage: View { 12 | let resourceName: String 13 | let width: Int 14 | let height: Int 15 | 16 | init(_ resourceName: String, width: Int, height: Int) { 17 | self.resourceName = resourceName 18 | self.width = width 19 | self.height = height 20 | } 21 | 22 | var body: some View { 23 | if let image = NSImage(named: resourceName) { 24 | let imageResized = image.scalePreservingAspectRatio(targetSize: NSSize(width: width, height: height)) 25 | Image(nsImage: imageResized) 26 | } else { 27 | // Default questionmark if image is not found 28 | Image(systemName: "questionmark.square") 29 | } 30 | } 31 | } 32 | 33 | #Preview { 34 | ResizedImage("NotionLogo", width: 32, height: 32) 35 | } 36 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AboutView: View { 11 | var body: some View { 12 | VStack(spacing: 20) { 13 | Image("AppIconSingle") 14 | .resizable() 15 | .frame(width: 200, height: 200) 16 | 17 | Text("Marker Data") 18 | .font(.largeTitle) 19 | .bold() 20 | 21 | Text("Version \(Bundle.main.version) (\(Bundle.main.buildNumber))") 22 | Text("Copyright © 2025 The Acharya. All rights reserved.") 23 | Link("Acknowledgments & Credits", destination: URL(string: "https://markerdata.theacharya.co/credits/")!) 24 | Link("The Acharya Technology", destination: URL(string: "https://tech.theacharya.co")!) 25 | } 26 | .navigationTitle("About") 27 | } 28 | } 29 | 30 | #Preview { 31 | AboutView() 32 | .padding() 33 | .frame(width: 500, height: 500) 34 | } 35 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/Configurations/Configurations_AddSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configurations_AddSheet.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 18/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | import ButtonKit 10 | 11 | extension ConfigurationSettingsView { 12 | func addOrRenameConfigurationModal(rename: Bool = false) -> some View { 13 | func doAction() async { 14 | if rename { 15 | await confModel.rename(store: selectedStore, to: configurationNameText) 16 | configurationNameText.removeAll() 17 | showRenameConfigurationSheet = false 18 | } else { 19 | await confModel.add(saveAs: configurationNameText) 20 | showAddConfigurationSheet = false 21 | } 22 | } 23 | 24 | return VStack(alignment: .leading) { 25 | Text("\(rename ? "Rename" : "Add") Configuration") 26 | .font(.system(size: 18, weight: .bold)) 27 | 28 | HStack { 29 | Text("Configuration Name:") 30 | 31 | TextField("Configuration Name", text: $configurationNameText) 32 | .onChange(of: configurationNameText) { newName in 33 | // Limit characters to 50 34 | configurationNameText = String(newName.prefix(50)) 35 | } 36 | .onSubmit { 37 | Task { 38 | await doAction() 39 | } 40 | } 41 | } 42 | 43 | HStack { 44 | Spacer() 45 | 46 | Button("Cancel", role: .cancel) { 47 | configurationNameText.removeAll() 48 | showAddConfigurationSheet = false 49 | showRenameConfigurationSheet = false 50 | } 51 | 52 | AsyncButton("Save") { 53 | await doAction() 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/Database/Create Sheet/AirtableFormView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirtableFormView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 07/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AirtableFormView: View { 11 | @ObservedObject var profileModel: AirtableDBModel 12 | 13 | var body: some View { 14 | VStack { 15 | airtableFields 16 | 17 | Divider() 18 | .padding(.vertical, 8) 19 | 20 | DropboxSetupView() 21 | } 22 | } 23 | 24 | var airtableFields: some View { 25 | VStack { 26 | PlatformInfoTextField( 27 | title: "Airtable Token", 28 | prompt: "Personal access token", 29 | text: $profileModel.token, 30 | isRequired: true, 31 | secureField: true 32 | ) 33 | 34 | PlatformInfoTextField( 35 | title: "Airtable Base ID", 36 | prompt: "Base IDs begin with \"app\"", 37 | text: $profileModel.baseID, 38 | isRequired: true, 39 | secureField: true 40 | ) 41 | 42 | PlatformInfoTextField( 43 | title: "Airtable Table ID", 44 | prompt: "Table IDs begin with \"tbl\"", 45 | text: $profileModel.tableID, 46 | isRequired: true, 47 | secureField: true 48 | ) 49 | 50 | PlatformInfoTextField( 51 | title: "Rename Key Column", 52 | prompt: "Different key column name in Notion (Default is \"Marker ID\")", 53 | text: $profileModel.renameKeyColumn, 54 | isRequired: false, 55 | secureField: false 56 | ) 57 | } 58 | } 59 | } 60 | 61 | #Preview { 62 | AirtableFormView(profileModel: AirtableDBModel()) 63 | .padding() 64 | .preferredColorScheme(.dark) 65 | } 66 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/Database/Create Sheet/DropboxSetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropboxSetupView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DropboxSetupView: View { 11 | @State var appKey = "" 12 | 13 | @StateObject var dropboxSetupModel = DropboxSetupModel() 14 | 15 | @State var showAppKeyError = false 16 | 17 | var body: some View { 18 | VStack(alignment: .leading) { 19 | Text("Dropbox") 20 | .font(.title2) 21 | 22 | HStack { 23 | PlatformInfoTextField( 24 | title: "Dropbox App Key", 25 | prompt: "App key", 26 | text: $appKey, 27 | isRequired: true, 28 | secureField: false 29 | ) 30 | .disabled(dropboxSetupModel.authRequestStatus != .notInitiated) 31 | 32 | switch dropboxSetupModel.authRequestStatus { 33 | case .notInitiated: 34 | Button("Continue") { 35 | Task { 36 | do { 37 | try await dropboxSetupModel.saveAppKeyAndLaunchTerminal(appKey) 38 | } catch { 39 | showAppKeyError = true 40 | dropboxSetupModel.authRequestStatus = .notInitiated 41 | } 42 | } 43 | } 44 | .disabled(appKey.isEmpty) 45 | case .inProgress: 46 | ProgressView() 47 | .controlSize(.small) 48 | .padding(.horizontal, 5) 49 | 50 | Button { 51 | dropboxSetupModel.authRequestStatus = .notInitiated 52 | } label: { 53 | Label("Start Over", systemImage: "arrow.clockwise") 54 | } 55 | case .success: 56 | Label("Success", systemImage: "checkmark.circle") 57 | .foregroundColor(.green) 58 | } 59 | } 60 | .alert("Failed to save app key", isPresented: $showAppKeyError) {} 61 | 62 | Text("Clicking **Continue** will launch the Terminal. Follow the on-screen instructions for the rest of the setup process.") 63 | .fontWeight(.thin) 64 | 65 | Link(destination: Links.dropboxAppConsole) { 66 | Label("Open Dropbox App Console", systemImage: "rectangle.portrait.and.arrow.right") 67 | } 68 | .padding(.bottom) 69 | 70 | Text(dropboxSetupModel.setupComplete ? "Dropbox configured" : "Dropbox setup incomplete") 71 | .foregroundColor(dropboxSetupModel.setupComplete ? .green : .red) 72 | } 73 | } 74 | } 75 | 76 | #Preview { 77 | DropboxSetupView() 78 | .preferredColorScheme(.dark) 79 | .padding() 80 | } 81 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/Database/Create Sheet/PlatformInfoTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlatformInfoTextField.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 06/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | import PasswordField 10 | 11 | struct PlatformInfoTextField: View { 12 | let title: String 13 | let prompt: LocalizedStringKey 14 | @Binding var text: String 15 | let isRequired: Bool 16 | let secureField: Bool 17 | 18 | var body: some View { 19 | HStack { 20 | Group { 21 | Text(title) + 22 | Text(" (\(isRequired ? "Required" : "Optional"))").fontWeight(.thin) + 23 | Text(":") 24 | } 25 | .padding(.trailing, -12) 26 | 27 | if secureField { 28 | PasswordField("", text: $text) { isInputVisible in 29 | // Visibility toggle button 30 | Button { 31 | isInputVisible.wrappedValue = isInputVisible.wrappedValue.toggled() 32 | } label: { 33 | Image(systemName: isInputVisible.wrappedValue ? "eye.slash" : "eye") 34 | } 35 | .buttonStyle(.plain) 36 | } 37 | .visibilityControlPosition(.inlineOutside) 38 | .textFieldStyle(.roundedBorder) 39 | } else { 40 | TextField(prompt, text: $text) 41 | .padding(.leading, 8) 42 | .textFieldStyle(.roundedBorder) 43 | } 44 | 45 | PasteButton(payloadType: String.self) { strings in 46 | guard let first = strings.first else { return } 47 | 48 | Task { 49 | await MainActor.run { 50 | text = first 51 | } 52 | } 53 | } 54 | .buttonStyle(.plain) 55 | .labelStyle(.iconOnly) 56 | 57 | Button { 58 | text = "" 59 | } label: { 60 | Image(systemName: "trash") 61 | } 62 | .buttonStyle(.plain) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/General Settings/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | import Sparkle 10 | 11 | struct GeneralSettingsView: View { 12 | let updater: SPUUpdater 13 | 14 | @Environment(\.openURL) var openURL 15 | @EnvironmentObject var settings: SettingsContainer 16 | 17 | var body: some View { 18 | TabView { 19 | FileSettingsView() 20 | .tabItem { Label("File", systemImage: "folder") } 21 | 22 | RolesSettingsView() 23 | .padding() 24 | .padding(.bottom) 25 | .tabItem { Label("Roles", systemImage: "movieclapper") } 26 | 27 | NotificationSettingsView() 28 | .tabItem { Label("Notifications", systemImage: "bell.badge") } 29 | 30 | UpdateSettingsView(updater: updater) 31 | .tabItem { Label("Updates", systemImage: "arrow.clockwise") } 32 | } 33 | .padding(.top) 34 | .overlayHelpButton(url: Links.generalSettingsURL) 35 | .navigationTitle("General Settings") 36 | 37 | } 38 | } 39 | 40 | #Preview { 41 | let settings = SettingsContainer() 42 | let databaseManager = DatabaseManager(settings: settings) 43 | 44 | return GeneralSettingsView(updater: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil).updater) 45 | .preferredColorScheme(.dark) 46 | .environmentObject(settings) 47 | .environmentObject(databaseManager) 48 | .padding() 49 | } 50 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/General Settings/NotificationSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationSettingsView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 25/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NotificationSettingsView: View { 11 | @EnvironmentObject var settings: SettingsContainer 12 | 13 | var body: some View { 14 | VStack(alignment: .formControlAlignment) { 15 | Spacer() 16 | 17 | Text("Progress Reporting") 18 | .font(.headline) 19 | 20 | LabeledFormElement("Notification Frequency") { 21 | Picker("", selection: $settings.store.notificationFrequency) { 22 | ForEach(NotificationFrequency.allCases) { frequency in 23 | Text(frequency.displayName) 24 | .tag(frequency) 25 | } 26 | } 27 | .frame(width: 250) 28 | } 29 | 30 | LabeledFormElement("Show Progress on Dock Icon") { 31 | Toggle("", isOn: $settings.store.showDockProgress) 32 | } 33 | 34 | Spacer() 35 | 36 | HStack { 37 | // Open preferences button 38 | Button { 39 | if let notificationSettingsURL = URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension") { 40 | NSWorkspace.shared.open(notificationSettingsURL) 41 | } 42 | } label: { 43 | Label("Open macOS Notification Settings", systemImage: "bell.badge") 44 | } 45 | .buttonStyle(.link) 46 | 47 | Spacer() 48 | } 49 | .padding() 50 | } 51 | } 52 | } 53 | 54 | #Preview { 55 | let settings = SettingsContainer() 56 | let databaseManager = DatabaseManager(settings: settings) 57 | 58 | return NotificationSettingsView() 59 | .preferredColorScheme(.dark) 60 | .environmentObject(settings) 61 | .environmentObject(databaseManager) 62 | .padding() 63 | } 64 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/General Settings/UpdateSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateSettingsView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 29/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | import Sparkle 10 | 11 | struct UpdateSettingsView: View { 12 | private let updater: SPUUpdater 13 | 14 | @State private var automaticallyChecksForUpdates: Bool 15 | @State private var automaticallyDownloadsUpdates: Bool 16 | 17 | init(updater: SPUUpdater) { 18 | self.updater = updater 19 | self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates 20 | self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates 21 | } 22 | 23 | 24 | var body: some View { 25 | VStack { 26 | CheckForUpdatesView(updater: updater) { 27 | Label("Check for Updates...", systemImage: "arrow.uturn.down") 28 | } 29 | 30 | Text("Current app version: \(Bundle.main.version) (\(Bundle.main.buildNumber))") 31 | .font(.system(.body, weight: .light)) 32 | .foregroundColor(.secondary) 33 | 34 | Spacer() 35 | .frame(height: 20) 36 | 37 | Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates) 38 | .onChange(of: automaticallyChecksForUpdates) { newValue in 39 | updater.automaticallyChecksForUpdates = newValue 40 | } 41 | 42 | Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates) 43 | .disabled(!automaticallyChecksForUpdates) 44 | .onChange(of: automaticallyDownloadsUpdates) { newValue in 45 | updater.automaticallyDownloadsUpdates = newValue 46 | } 47 | } 48 | } 49 | } 50 | 51 | #Preview { 52 | UpdateSettingsView(updater: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil).updater) 53 | } 54 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/Image/ImageSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImageSettingsView: View { 11 | var body: some View { 12 | TabView { 13 | ImageExtractionSettingsView() 14 | .tabItem { 15 | Text("Extraction") 16 | } 17 | 18 | SwatchSettingsView() 19 | .tabItem { 20 | Text("Swatch") 21 | } 22 | } 23 | .padding(.top) 24 | .overlayHelpButton(url: Links.imageSettingsURL) 25 | .navigationTitle("Image Settings") 26 | } 27 | } 28 | 29 | #Preview { 30 | let settings = SettingsContainer() 31 | 32 | return ImageSettingsView() 33 | .environmentObject(settings) 34 | } 35 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Detail Views/Label/LabelSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LabelSettingsView: View { 11 | @EnvironmentObject var settings: SettingsContainer 12 | 13 | enum Section: String, CaseIterable, Identifiable { 14 | case general, token 15 | 16 | var id: String { self.rawValue } 17 | 18 | } 19 | 20 | @State private var section = Section.token 21 | 22 | var body: some View { 23 | TabView { 24 | GeneralLabelSettingsView() 25 | .tabItem { 26 | Text("Appearance") 27 | } 28 | 29 | OverlaySettingsView() 30 | .tabItem { 31 | Text("Overlays") 32 | } 33 | } 34 | .padding(.top) 35 | .overlayHelpButton(url: Links.labelSettingsURL) 36 | } 37 | 38 | } 39 | 40 | #Preview { 41 | let settings = SettingsContainer() 42 | 43 | return LabelSettingsView() 44 | .environmentObject(settings) 45 | } 46 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Menu Bar Commands/AppCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCommands.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 16/01/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import Sparkle 11 | 12 | struct AppCommands: Commands { 13 | @Binding var sidebarSelection: MainViews 14 | @Binding var updateAvailable: Bool 15 | let updaterController: SPUStandardUpdaterController 16 | 17 | var body: some Commands { 18 | CommandGroup(replacing: .appInfo) { 19 | Button("About Marker Data") { 20 | deminiaturizeAllWindows() 21 | sidebarSelection = .about 22 | } 23 | } 24 | 25 | CommandGroup(before: .systemServices) { 26 | CheckForUpdatesView(updater: updaterController.updater) { 27 | Text("Check for Updates...") 28 | } 29 | .if(updateAvailable) { view in 30 | view 31 | .badge("Update Available") 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Menu Bar Commands/ConfigurationCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationCommands.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 20/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConfigurationCommands: Commands { 11 | @ObservedObject var settings: SettingsContainer 12 | @Binding var sidebarSelection: MainViews 13 | 14 | var body: some Commands { 15 | CommandMenu("Configurations") { 16 | Button("Update Active Configuration") { 17 | Task { 18 | do { 19 | try await settings.store.saveAsConfiguration() 20 | await settings.checkForUnsavedChanges() 21 | } catch { 22 | print("Failed to update configuration from Menu Bar Command") 23 | } 24 | } 25 | } 26 | .disabled(settings.isDefaultActive || !settings.unsavedChanges) 27 | .keyboardShortcut("s", modifiers: .command) 28 | 29 | Button("Discard Changes") { 30 | do { 31 | try settings.discardChanges() 32 | } catch { 33 | print("Failed to discard changes from Menu Bar Command") 34 | } 35 | } 36 | .disabled(!settings.unsavedChanges) 37 | .keyboardShortcut("z", modifiers: .command) 38 | 39 | Divider() 40 | 41 | Button("Open Configurations Panel") { 42 | deminiaturizeAllWindows() 43 | sidebarSelection = .configurations 44 | } 45 | 46 | Button("Open Configuration Folder in Finder") { 47 | NSWorkspace.shared.open(URL.configurationsFolder) 48 | } 49 | 50 | Divider() 51 | 52 | Text("Select Configuration") 53 | 54 | ForEach(settings.configurations) { store in 55 | Button { 56 | do { 57 | try settings.load(store) 58 | } catch { 59 | print("Failed to load config from menu bar") 60 | } 61 | } label: { 62 | if settings.isStoreActive(store) { 63 | Label(store.name, systemImage: "checkmark") 64 | } else { 65 | Text(store.name) 66 | } 67 | } 68 | .labelStyle(.titleAndIcon) 69 | .if(settings.keyboardShortcuts.contains(store.name)) { view in 70 | let shortcutIndex = settings.keyboardShortcuts.firstIndex(of: store.name) ?? 0 71 | let shortcut = KeyEquivalent("\(shortcutIndex + 1)".first ?? "0") 72 | 73 | return view 74 | .keyboardShortcut(shortcut, modifiers: .command) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Menu Bar Commands/EditCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditCommands.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 13/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EditCommands: Commands { 11 | var body: some Commands { 12 | // Remove Undo & Redo 13 | CommandGroup(replacing: .undoRedo) { } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Menu Bar Commands/FileCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileCommands.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 20/01/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct FileCommands: Commands { 12 | @Environment(\.openWindow) var openWindow 13 | 14 | var body: some Commands { 15 | CommandGroup(after: .importExport) { 16 | Button("Open Pagemaker") { 17 | openWindow(id: "pagemaker") 18 | } 19 | .keyboardShortcut("p", modifiers: .command) 20 | 21 | Divider() 22 | 23 | Button("Install FCP Share Destination...") { 24 | Task { 25 | do { 26 | try await ShareDestinationInstaller.install() 27 | } catch { 28 | print("Failed to install FCP Share Destination from menu bar") 29 | } 30 | } 31 | } 32 | 33 | Divider() 34 | 35 | Button("Show Cache") { 36 | NSWorkspace.shared.open(URL.FCPExportCacheFolder) 37 | } 38 | 39 | Button("Clean Cache") { 40 | LibraryFolders.deleteCache() 41 | } 42 | .keyboardShortcut("k", modifiers: .command) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Menu Bar Commands/HelpCommands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelpCommands.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 20/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | 11 | struct HelpCommands: Commands { 12 | var body: some Commands { 13 | //Add Help And Debug Menu Buttons 14 | CommandGroup(replacing: .help) { 15 | Link("Marker Data Website", destination: URL(string: "https://markerdata.theacharya.co")!) 16 | Link("Keyboard Shortcuts", destination: URL(string: "https://markerdata.theacharya.co/user-guide/keyboard-shortcuts/")!) 17 | Link("Troubleshooting", destination: URL(string: "https://markerdata.theacharya.co/troubleshooting/")!) 18 | 19 | Divider() 20 | 21 | Link("Send Feedback", destination: URL(string: "https://github.com/TheAcharya/MarkerData/issues")!) 22 | Link("Discussions", destination: URL(string: "https://github.com/TheAcharya/MarkerData/discussions")!) 23 | 24 | Divider() 25 | 26 | Link("Release Notes", destination: URL(string: "https://markerdata.theacharya.co/release-notes/")!) 27 | Link("Source Code", destination: URL(string: "https://github.com/TheAcharya/MarkerData")!) 28 | Link("Sponsor Marker Data", destination: URL(string: "https://github.com/sponsors/TheAcharya")!) 29 | Link("About Marker Data", destination: URL(string: "https://markerdata.theacharya.co/credits/")!) 30 | 31 | Divider() 32 | 33 | Button("Open Logs") { 34 | Task { 35 | // Open log folder in Finder 36 | NSWorkspace.shared.open(URL.logsFolder) 37 | 38 | await LogManager.export() 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Onboarding/OnboardingFeature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingFeature.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/06/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct OnboardingFeature: Identifiable { 11 | let icon: String 12 | let title: String 13 | let description: String 14 | 15 | var id: String { 16 | self.icon + self.title + self.description 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Onboarding/OnboardingPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingPageView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/06/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OnboardingPageView: View { 11 | let title: String 12 | let features: [OnboardingFeature] 13 | 14 | var body: some View { 15 | VStack { 16 | Text(title) 17 | .font(.system(size: 30, weight: .bold)) 18 | .padding(.vertical, 15) 19 | 20 | VStack(alignment: .leading, spacing: 20) { 21 | ForEach(features) { feature in 22 | featureView(feature) 23 | } 24 | } 25 | } 26 | } 27 | 28 | func featureView(_ feature: OnboardingFeature) -> some View { 29 | HStack { 30 | Image(systemName: feature.icon) 31 | .foregroundStyle(.accent) 32 | .font(.system(size: 32)) 33 | 34 | VStack(alignment: .leading) { 35 | Text(feature.title) 36 | .font(.system(size: 20, weight: .bold)) 37 | 38 | Text(feature.description) 39 | .font(.system(size: 16, weight: .light)) 40 | } 41 | } 42 | } 43 | } 44 | 45 | #Preview { 46 | OnboardingPageView( 47 | title: "Features of Marker Data", 48 | features: [ 49 | .init( 50 | icon: "puzzlepiece.extension", 51 | title: "Integration with Final Cut Pro", 52 | description: "Integrates with Final Cut Pro, boasting a native Share Destination & Workflow Extension." 53 | ), 54 | .init( 55 | icon: "briefcase", 56 | title: "Configurations", 57 | description: "Allows the creation of multiple configurations tailored to diverse project requirements." 58 | ), 59 | .init( 60 | icon: "server.rack", 61 | title: "Databases", 62 | description: "Native integration with renowned databases such as Airtable and Notion." 63 | ) 64 | ] 65 | ) 66 | .preferredColorScheme(.dark) 67 | .padding() 68 | } 69 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Onboarding/OnboardingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 08/06/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OnboardingView: View { 11 | @State var currentPage: Int = 0 12 | let totalPages = onboardingPages.count 13 | let lastPageIndex = onboardingPages.count - 1 14 | 15 | @AppStorage("showOnboarding") var showOnboarding = true 16 | 17 | var body: some View { 18 | VStack { 19 | if let onboardingPage = onboardingPages[safe: currentPage] { 20 | onboardingPage 21 | .frame(maxWidth: 600) 22 | .padding(.top) 23 | } 24 | 25 | Spacer() 26 | 27 | pageIndicators 28 | .padding(.bottom) 29 | 30 | buttonsView 31 | .padding(.bottom) 32 | } 33 | .frame(width: 700, height: 440) 34 | .overlay(alignment: .topTrailing) { 35 | closeButton 36 | } 37 | } 38 | 39 | var buttonsView: some View { 40 | HStack { 41 | // Back button 42 | Button("Back") { 43 | withAnimation { 44 | currentPage = max(currentPage - 1, 0) 45 | } 46 | } 47 | .buttonStyle(BigButtonStyle(color: .secondary, minWidth: 80)) 48 | .disabled(currentPage == 0) 49 | 50 | // Next and done button 51 | Group { 52 | if currentPage != lastPageIndex { 53 | Button("Next") { 54 | withAnimation { 55 | currentPage = min(currentPage + 1, lastPageIndex) 56 | } 57 | } 58 | } else { 59 | Button("Done") { 60 | showOnboarding = false 61 | } 62 | } 63 | } 64 | .buttonStyle(BigButtonStyle(color: .accent, minWidth: 80)) 65 | } 66 | } 67 | 68 | var pageIndicators: some View { 69 | HStack(spacing: 8) { 70 | ForEach(0..: View { 23 | @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel 24 | private let updater: SPUUpdater 25 | let label: ()->T 26 | 27 | init(updater: SPUUpdater, @ViewBuilder label: @escaping ()->T) { 28 | self.updater = updater 29 | 30 | // Create our view model for our CheckForUpdatesView 31 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) 32 | 33 | self.label = label 34 | } 35 | 36 | var body: some View { 37 | Button(action: updater.checkForUpdates, label: label) 38 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Other/ExportProfilePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportProfilePicker.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 02/12/2023. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import MarkersExtractor 11 | 12 | struct ExportProfilePicker: View { 13 | @EnvironmentObject var settings: SettingsContainer 14 | @EnvironmentObject var databaseManager: DatabaseManager 15 | 16 | var body: some View { 17 | Picker("Export Profile", selection: $settings.store.unifiedExportProfile) { 18 | Section("Extract Only (No Upload)") { 19 | ForEach(UnifiedExportProfile.noUploadProfiles) { profile in 20 | Label { 21 | Text(profile.displayName) 22 | } icon: { 23 | ResizedImage(profile.iconImageName, width: 20, height: 20) 24 | } 25 | .tag(Optional(profile)) 26 | } 27 | } 28 | 29 | Divider() 30 | 31 | Section("Database Profiles (Upload)") { 32 | ForEach(databaseManager.getUnifiedExportProfiles()) { profile in 33 | Label { 34 | Text(profile.displayName) 35 | } icon: { 36 | ResizedImage(profile.iconImageName, width: 20, height: 20) 37 | } 38 | .tag(Optional(profile)) 39 | } 40 | } 41 | } 42 | .labelStyle(.titleAndIcon) 43 | // To avoid getting errors like Picker: the selection ... is invalid 44 | // Uncomment the code below to set the selection with a delay 45 | // We need to wait until the picker is fully initialized to not get the error 46 | // But the picker works anyways just the error is kinda annoying 47 | // .onAppear { 48 | // DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 49 | // self.selection = UnifiedExportProfile.load() 50 | // } 51 | // } 52 | } 53 | } 54 | 55 | #Preview { 56 | ExportProfilePicker() 57 | } 58 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Other/FailedExtractionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FailedExtractionsView.swift 3 | // Marker Data 4 | // 5 | // Created by Milán Várady on 02/01/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FailedExtractionsView: View { 11 | let failedExtractions: [ExtractionFailure] 12 | 13 | var body: some View { 14 | Table(failedExtractions) { 15 | TableColumn("File Path") { failedTask in 16 | Text(failedTask.url.path(percentEncoded: false)) 17 | } 18 | 19 | TableColumn("Error Type", value: \.exitStatus.rawValue) 20 | 21 | TableColumn("Error Message", value: \.errorMessage) 22 | } 23 | } 24 | } 25 | 26 | #Preview { 27 | let failedExtractions: [ExtractionFailure] = [ 28 | ExtractionFailure(url: URL(string: "/folder/file1")!, exitStatus: .failedToExtract, errorMessage: "No export destination selected"), 29 | ExtractionFailure(url: URL(string: "/folder/file2")!, exitStatus: .failedToExtract, errorMessage: "Unexpected number of bananas"), 30 | ExtractionFailure(url: URL(string: "/folde2/file1")!, exitStatus: .failedToUpload, errorMessage: "Unkown error"), 31 | ExtractionFailure(url: URL(string: "/folde2/file2")!, exitStatus: .failedToUpload, errorMessage: "Couldn't locate ur mom"), 32 | ] 33 | 34 | return FailedExtractionsView(failedExtractions: failedExtractions) 35 | .preferredColorScheme(.dark) 36 | } 37 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Other/HelpButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | 10 | @MainActor 11 | struct HelpButton: NSViewRepresentable { 12 | 13 | let action: () -> Void 14 | 15 | init(action: @escaping () -> Void) { 16 | self.action = action 17 | } 18 | 19 | func makeCoordinator() -> Coordinator { 20 | return Coordinator(parent: self) 21 | } 22 | 23 | func makeNSView(context: Context) -> NSButton { 24 | let button = NSButton() 25 | button.bezelStyle = .helpButton 26 | button.title = "" 27 | context.coordinator.button = button 28 | return button 29 | } 30 | 31 | func updateNSView(_ nsView: NSButton, context: Context) { 32 | 33 | } 34 | 35 | @MainActor 36 | class Coordinator { 37 | 38 | let parent: HelpButton 39 | var button: NSButton? { 40 | didSet { 41 | if self.button != nil { 42 | self.bindButtonAction() 43 | } 44 | } 45 | } 46 | 47 | init(parent: HelpButton) { 48 | self.parent = parent 49 | self.button = nil 50 | 51 | } 52 | 53 | func bindButtonAction() { 54 | self.button?.target = self 55 | self.button?.action = #selector(self.didPressButton) 56 | } 57 | 58 | @objc func didPressButton() { 59 | self.parent.action() 60 | } 61 | 62 | } 63 | 64 | } 65 | 66 | #Preview { 67 | HelpButton(action: {}) 68 | } 69 | -------------------------------------------------------------------------------- /Source/Marker Data/Marker Data/Views/Other/PickerViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | //  Marker Data • https://github.com/TheAcharya/MarkerData 3 | //  Licensed under MIT License 4 | // 5 | // Maintained by Milán Várady 6 | // 7 | 8 | import SwiftUI 9 | import MarkersExtractor 10 | 11 | struct ImageModePicker: View { 12 | @EnvironmentObject var settings: SettingsContainer 13 | 14 | var body: some View { 15 | Picker("Image Format", selection: $settings.store.imageMode) { 16 | ForEach(ImageMode.allCases) { imageMode in 17 | Text(imageMode.displayName).tag(imageMode) 18 | } 19 | } 20 | } 21 | } 22 | 23 | struct FontNamePicker: View { 24 | @EnvironmentObject var settings: SettingsContainer 25 | 26 | var body: some View { 27 | Picker("", selection: $settings.store.fontNameType) { 28 | ForEach(FontNameType.allCases) { fontNameType in 29 | Text(fontNameType.displayName).tag(fontNameType) 30 | } 31 | } 32 | } 33 | } 34 | 35 | struct FontStylePicker: View { 36 | @EnvironmentObject var settings: SettingsContainer 37 | 38 | var body: some View { 39 | Picker("", selection: $settings.store.fontStyleType) { 40 | ForEach(FontStyleType.allCases) { fontStyleType in 41 | Text(fontStyleType.displayName).tag(fontStyleType) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-16@1x.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon-16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-32@1x.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon-32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-128@1x.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon-128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-256@1x.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Icon-256@2x 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-256@2x.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Icon-1024@1x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-1024@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-128@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-128@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-16@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-16@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-256@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-256@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-256@2x 1.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-256@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-32@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIcon.appiconset/Icon-32@2x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIconSingle.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-1024@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/AppIconSingle.imageset/Icon-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/Source/Marker Data/Workflow Extension/Assets.xcassets/AppIconSingle.imageset/Icon-1024@1x.png -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIconFile 6 | ExtensionIcon 7 | NSExtension 8 | 9 | NSExtensionPointIdentifier 10 | com.apple.FinalCut.WorkflowExtension 11 | ProExtensionPrincipalViewControllerClass 12 | $(PRODUCT_MODULE_NAME).WorkflowExtensionViewController 13 | NSAppleEventsUsageDescription 14 | Extensions can interact with Final Cut Pro. 15 | ProExtensionAttributes 16 | 17 | ContentViewMinimumWidth 18 | 700 19 | ContentViewMinimumHeight 20 | 550 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/WorkflowExtension-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // WorkflowExtension-Bridging-Header.h 3 | // WorkflowExtension 4 | // 5 | // Created by Milán Várady on 23/01/2024. 6 | // 7 | 8 | #import 9 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/WorkflowExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.automation.apple-events 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | com.apple.security.temporary-exception.files.home-relative-path.read-write 14 | 15 | /Library/Application Support/Marker Data/preferences.json 16 | 17 | com.apple.security.scripting-targets 18 | 19 | com.apple.FinalCut 20 | 21 | com.apple.FinalCut.library.inspection 22 | 23 | com.apple.FinalCutTrial 24 | 25 | com.apple.FinalCut.library.inspection 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Source/Marker Data/Workflow Extension/WorkflowExtensionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WorkflowExtensionViewController.swift 3 | // WorkflowExtension 4 | // 5 | // Created by Milán Várady on 23/01/2024. 6 | // 7 | 8 | import Cocoa 9 | import ProExtensionHost 10 | import SwiftUI 11 | 12 | @objc class WorkflowExtensionViewController: NSViewController { 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | let swiftuiView = NSHostingView(rootView: WorkflowExtensionView()) 17 | swiftuiView.translatesAutoresizingMaskIntoConstraints = false 18 | 19 | self.view.addSubview(swiftuiView) 20 | swiftuiView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true 21 | swiftuiView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true 22 | } 23 | 24 | @objc var hostInfoString: String { 25 | guard let host = ProExtensionHostSingleton() as? FCPXHost else { 26 | print("Could not get host information") 27 | return "" 28 | } 29 | 30 | return String(format:"%@ %@", host.name, host.versionString) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/macos_badge_noborder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/assets/macos_badge_noborder.png -------------------------------------------------------------------------------- /assets/marker_data_app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheAcharya/MarkerData/2774b1945d3d388380feacbeff796f1ec0219329/assets/marker_data_app_icon.png --------------------------------------------------------------------------------