diff --git a/.changes/file-association-content-type.md b/.changes/file-association-content-type.md new file mode 100644 index 000000000000..985bbfa2411b --- /dev/null +++ b/.changes/file-association-content-type.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": minor:feat +"@tauri-apps/cli": minor:feat +--- + +Added support to defining the content type of the declared file association on macOS (maps to LSItemContentTypes property). diff --git a/.changes/file-association-exported-type-cli.md b/.changes/file-association-exported-type-cli.md new file mode 100644 index 000000000000..b2685b37dd58 --- /dev/null +++ b/.changes/file-association-exported-type-cli.md @@ -0,0 +1,6 @@ +--- +"tauri-cli": minor:feat +"@tauri-apps/cli": minor:feat +--- + +Added support to defining the metadata for custom types declared in `tauri.conf.json > bundle > fileAssociations > exportedType` via the `UTExportedTypeDeclarations` Info.plist property. diff --git a/.changes/file-association-exported-type.md b/.changes/file-association-exported-type.md new file mode 100644 index 000000000000..75cf0545fdfd --- /dev/null +++ b/.changes/file-association-exported-type.md @@ -0,0 +1,5 @@ +--- +"tauri-utils": minor:feat +--- + +Added `FileAssociation::exported_type` and `FileAssociation::content_types` for better support to defining custom types on macOS. diff --git a/crates/tauri-bundler/src/bundle/macos/app.rs b/crates/tauri-bundler/src/bundle/macos/app.rs index 87e900ac9203..09925e19168a 100644 --- a/crates/tauri-bundler/src/bundle/macos/app.rs +++ b/crates/tauri-bundler/src/bundle/macos/app.rs @@ -267,6 +267,55 @@ fn create_info_plist( } if let Some(associations) = settings.file_associations() { + let exported_associations = associations + .iter() + .filter_map(|association| { + association.exported_type.as_ref().map(|exported_type| { + let mut dict = plist::Dictionary::new(); + + dict.insert( + "UTTypeIdentifier".into(), + exported_type.identifier.clone().into(), + ); + if let Some(description) = &association.description { + dict.insert("UTTypeDescription".into(), description.clone().into()); + } + if let Some(conforms_to) = &exported_type.conforms_to { + dict.insert( + "UTTypeConformsTo".into(), + plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()), + ); + } + + let mut specification = plist::Dictionary::new(); + specification.insert( + "public.filename-extension".into(), + plist::Value::Array( + association + .ext + .iter() + .map(|s| s.to_string().into()) + .collect(), + ), + ); + if let Some(mime_type) = &association.mime_type { + specification.insert("public.mime-type".into(), mime_type.clone().into()); + } + + dict.insert("UTTypeTagSpecification".into(), specification.into()); + + plist::Value::Dictionary(dict) + }) + }) + .collect::>(); + + if !exported_associations.is_empty() { + plist.insert( + "UTExportedTypeDeclarations".into(), + plist::Value::Array(exported_associations), + ); + } + plist.insert( "CFBundleDocumentTypes".into(), plist::Value::Array( @@ -274,16 +323,27 @@ fn create_info_plist( .iter() .map(|association| { let mut dict = plist::Dictionary::new(); - dict.insert( - "CFBundleTypeExtensions".into(), - plist::Value::Array( - association - .ext - .iter() - .map(|ext| ext.to_string().into()) - .collect(), - ), - ); + + if !association.ext.is_empty() { + dict.insert( + "CFBundleTypeExtensions".into(), + plist::Value::Array( + association + .ext + .iter() + .map(|ext| ext.to_string().into()) + .collect(), + ), + ); + } + + if let Some(content_types) = &association.content_types { + dict.insert( + "LSItemContentTypes".into(), + plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()), + ); + } + dict.insert( "CFBundleTypeName".into(), association diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 8b97ad39540c..805d1da0f45a 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -2433,6 +2433,16 @@ "$ref": "#/definitions/AssociationExt" } }, + "contentTypes": { + "description": "Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.\n\n This allows supporting any file format declared by another application that conforms to this type.\n Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "name": { "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`", "type": [ @@ -2471,6 +2481,17 @@ "$ref": "#/definitions/HandlerRank" } ] + }, + "exportedType": { + "description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.\n\n You should define this if the associated file is a custom file type defined by your application.", + "anyOf": [ + { + "$ref": "#/definitions/ExportedFileAssociation" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -2552,6 +2573,30 @@ } ] }, + "ExportedFileAssociation": { + "description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "identifier": { + "description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.", + "type": "string" + }, + "conformsTo": { + "description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.image`, `public.database`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "WindowsConfig": { "description": "Windows bundler configuration.\n\n See more: ", "type": "object", diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 8b97ad39540c..805d1da0f45a 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -2433,6 +2433,16 @@ "$ref": "#/definitions/AssociationExt" } }, + "contentTypes": { + "description": "Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.\n\n This allows supporting any file format declared by another application that conforms to this type.\n Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "name": { "description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`", "type": [ @@ -2471,6 +2481,17 @@ "$ref": "#/definitions/HandlerRank" } ] + }, + "exportedType": { + "description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.\n\n You should define this if the associated file is a custom file type defined by your application.", + "anyOf": [ + { + "$ref": "#/definitions/ExportedFileAssociation" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -2552,6 +2573,30 @@ } ] }, + "ExportedFileAssociation": { + "description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "identifier": { + "description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.", + "type": "string" + }, + "conformsTo": { + "description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.image`, `public.database`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "WindowsConfig": { "description": "Windows bundler configuration.\n\n See more: ", "type": "object", diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 3891220b7eee..884762068f34 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -1171,6 +1171,11 @@ impl<'d> serde::Deserialize<'d> for AssociationExt { pub struct FileAssociation { /// File extensions to associate with this app. e.g. 'png' pub ext: Vec, + /// Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS. + /// + /// This allows supporting any file format declared by another application that conforms to this type. + /// Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`]. + pub content_types: Option>, /// The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]` pub name: Option, /// The association description. Windows-only. It is displayed on the `Type` column on Windows Explorer. @@ -1184,6 +1189,24 @@ pub struct FileAssociation { /// The ranking of this app among apps that declare themselves as editors or viewers of the given file type. Maps to `LSHandlerRank` on macOS. #[serde(default)] pub rank: HandlerRank, + /// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS. + /// + /// You should define this if the associated file is a custom file type defined by your application. + pub exported_type: Option, +} + +/// The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS. +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExportedFileAssociation { + /// The unique identifier for the exported type. Maps to `UTTypeIdentifier`. + pub identifier: String, + /// The types that this type conforms to. Maps to `UTTypeConformsTo`. + /// + /// Examples are `public.data`, `public.image`, `public.image`, `public.database`. + #[serde(alias = "conforms-to")] + pub conforms_to: Option>, } /// Deep link protocol configuration.