├── .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
--------------------------------------------------------------------------------