Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/badge.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'badge/base'
require 'badge/runner'
require 'badge/options.rb'
require 'badge/icon_catalog.rb'

require 'fastlane_core'

Expand Down
2 changes: 1 addition & 1 deletion lib/badge/base.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Badge

VERSION = "0.13.0"
VERSION = "0.13.1"
DESCRIPTION = "Add a badge overlay to your app icon"

def self.root
Expand Down
126 changes: 126 additions & 0 deletions lib/badge/icon_catalog.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
require 'json'
require 'pathname'

module Badge
class IconCatalog
attr_reader :path, :format_type, :icon_files

FORMAT_LEGACY = :legacy
FORMAT_SINGLE_SIZE = :single_size
FORMAT_LAYERED = :layered

def initialize(appiconset_path)
@path = Pathname.new(appiconset_path)
@contents_json_path = @path.join('Contents.json')
@icon_files = []
@format_type = nil

detect_format
end

# Returns list of icon file paths that should be badged
def badgeable_icons
case @format_type
when FORMAT_LAYERED
# For layered icons, badge all variants (all.png, dark.png, tint.png)
@icon_files
when FORMAT_SINGLE_SIZE
# Single size format - badge the single icon
@icon_files
when FORMAT_LEGACY
# Legacy format - badge all size variants
@icon_files
else
# Fallback to glob if format detection failed
glob_fallback
end
end

def self.find_catalogs(search_path, glob_pattern = nil)
if glob_pattern
# Use custom glob if provided (backward compatibility)
UI.verbose "Using custom glob pattern: #{glob_pattern}".blue
return glob_pattern
end

# Find all .appiconset directories
appiconset_dirs = Dir.glob("#{search_path}/**/*.appiconset")

catalogs = appiconset_dirs.map { |dir| new(dir) }
UI.verbose "Found #{catalogs.count} app icon catalog(s)".blue
catalogs.each do |catalog|
UI.verbose " - #{catalog.path.basename} (#{catalog.format_type})".blue
end

catalogs
end

private

def detect_format
unless File.exist?(@contents_json_path)
UI.verbose "No Contents.json found at #{@contents_json_path}, using fallback".yellow
@format_type = :unknown
return
end

begin
contents = JSON.parse(File.read(@contents_json_path))
images = contents['images'] || []

if images.empty?
UI.verbose "Contents.json has no images array".yellow
@format_type = :unknown
return
end

# Check for layered format (iOS 18+)
# Layered icons have images with "appearances" array containing luminosity variants
has_appearances = images.any? { |img| img['appearances'] }

if has_appearances
@format_type = FORMAT_LAYERED
@icon_files = extract_layered_icons(images)
UI.verbose "Detected layered icon format with #{@icon_files.count} variant(s)".blue
return
end

# Check for single-size format (Xcode 14+)
# Single size has one image with size "1024x1024" and "idiom" = "universal"
if images.count == 1 && images[0]['size'] == '1024x1024'
@format_type = FORMAT_SINGLE_SIZE
@icon_files = extract_icon_files(images)
UI.verbose "Detected single-size icon format".blue
return
end

# Legacy multi-size format
@format_type = FORMAT_LEGACY
@icon_files = extract_icon_files(images)
UI.verbose "Detected legacy multi-size icon format with #{@icon_files.count} size(s)".blue

rescue JSON::ParserError => e
UI.error "Failed to parse Contents.json: #{e.message}".red
@format_type = :unknown
end
end

def extract_layered_icons(images)
extract_icon_files(images)
end

def extract_icon_files(images)
images.map do |image|
filename = image['filename']
next unless filename

icon_path = @path.join(filename)
icon_path.to_s if File.exist?(icon_path) && icon_path.extname.downcase == '.png'
end.compact
end

def glob_fallback
Dir.glob("#{@path}/*.{png,PNG}")
end
end
end
18 changes: 14 additions & 4 deletions lib/badge/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ class Runner

def run(path, options)
check_tools!
glob = "/**/*.appiconset/*.{png,PNG}"
glob = options[:glob] if options[:glob]

app_icons = Dir.glob("#{path}#{glob}")
UI.verbose "Verbose active... VERSION: #{Badge::VERSION}".blue
UI.verbose "Parameters: #{options.values.inspect}".blue

# If custom glob is provided, use legacy behavior for backward compatibility
if options[:glob]
UI.verbose "Using custom glob pattern (legacy mode)".blue
glob = options[:glob]
app_icons = Dir.glob("#{path}#{glob}")
else
# Use new IconCatalog approach to intelligently detect icon formats
UI.verbose "Using smart icon detection with Contents.json parsing".blue
catalogs = IconCatalog.find_catalogs(path, options[:glob])
app_icons = catalogs.flat_map(&:badgeable_icons)
end

UI.verbose "Found #{app_icons.count} icon file(s) to badge".blue

if options[:custom] && !File.exist?(options[:custom])
UI.error("Could not find custom badge image")
UI.user_error!("Specify a valid custom badge image path!")
Expand Down