Merge branch 'pr/183' into dev

This commit is contained in:
cranci1 2025-06-12 21:34:08 +02:00
commit 35c4f35f5e
6 changed files with 365 additions and 127 deletions

View file

@ -210,6 +210,9 @@
},
"Error" : {
},
"Error Fetching Results" : {
},
"Failed to load contributors" : {
"localizations" : {
@ -351,7 +354,10 @@
"Match with AniList" : {
},
"Matched with: %@" : {
"Match with TMDB" : {
},
"Matched ID: %lld" : {
},
"Max Concurrent Downloads" : {
@ -430,6 +436,9 @@
},
"Please select a module from settings" : {
},
"Provider: %@" : {
},
"Queued" : {
@ -552,6 +561,9 @@
},
"The module provided only a single episode, this is most likely a movie, so we decided to make separate screens for these cases." : {
},
"TMDB Match" : {
},
"Trackers" : {
@ -561,6 +573,9 @@
},
"Try different search terms" : {
},
"Unable to fetch matches. Please try again later." : {
},
"Use TMDB Poster Image" : {

View file

@ -23,7 +23,7 @@ class TMDBFetcher {
let results: [TMDBResult]
}
private let apiKey = "738b4edd0a156cc126dc4a4b8aea4aca"
let apiKey = "738b4edd0a156cc126dc4a4b8aea4aca"
private let session = URLSession.custom
func fetchBestMatchID(for title: String, completion: @escaping (Int?, MediaType?) -> Void) {

View file

@ -65,6 +65,8 @@ struct MediaInfoView: View {
@State private var showStreamLoadingView: Bool = false
@State private var currentStreamTitle: String = ""
@State private var activeFetchID: UUID? = nil
@State private var activeProvider: String?
@State private var isTMDBMatchingPresented = false
@State private var refreshTrigger: Bool = false
@State private var buttonRefreshTrigger: Bool = false
@ -85,6 +87,15 @@ struct MediaInfoView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.verticalSizeClass) private var verticalSizeClass
@AppStorage("metadataProvidersOrder") private var metadataProvidersOrderData: Data = {
try! JSONEncoder().encode(["AniList","TMDB"])
}()
private var metadataProvidersOrder: [String] {
get { (try? JSONDecoder().decode([String].self, from: metadataProvidersOrderData)) ?? ["AniList","TMDB"] }
set { metadataProvidersOrderData = try! JSONEncoder().encode(newValue) }
}
private var isGroupedBySeasons: Bool {
return groupedEpisodes().count > 1
}
@ -252,10 +263,7 @@ struct MediaInfoView: View {
ZStack(alignment: .top) {
gradientOverlay
VStack(alignment: .leading, spacing: 24) {
Spacer()
.frame(height: 100)
VStack(alignment: .leading, spacing: 16) {
headerSection
if !episodeLinks.isEmpty {
episodesSection
@ -263,34 +271,31 @@ struct MediaInfoView: View {
noEpisodesSection
}
}
.padding(.horizontal)
.padding()
}
}
}
@ViewBuilder
private var gradientOverlay: some View {
ZStack {
ProgressiveBlurView()
.opacity(0.8)
LinearGradient(
gradient: Gradient(stops: [
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.5),
.init(color: (colorScheme == .dark ? Color.black : Color.white), location: 1.0)
]),
startPoint: .top,
endPoint: .bottom
)
}
LinearGradient(
gradient: Gradient(stops: [
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.0), location: 0.0),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.5), location: 0.2),
.init(color: (colorScheme == .dark ? Color.black : Color.white).opacity(0.8), location: 0.5),
.init(color: (colorScheme == .dark ? Color.black : Color.white), location: 1.0)
]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 0))
.shadow(color: (colorScheme == .dark ? Color.black : Color.white).opacity(1), radius: 10, x: 0, y: 10)
}
@ViewBuilder
private var headerSection: some View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" {
HStack(spacing: 4) {
Image(systemName: "calendar")
@ -303,7 +308,7 @@ struct MediaInfoView: View {
}
Text(title)
.font(.system(size: 32, weight: .bold))
.font(.system(size: 28, weight: .bold))
.foregroundColor(.primary)
.lineLimit(3)
.onLongPressGesture {
@ -653,6 +658,13 @@ struct MediaInfoView: View {
.sheet(isPresented: $isMatchingPresented) {
AnilistMatchPopupView(seriesTitle: title) { selectedID in
handleAniListMatch(selectedID: selectedID)
fetchMetadataIDIfNeeded() // use your new async re-try loop
}
}
.sheet(isPresented: $isTMDBMatchingPresented) {
TMDBMatchPopupView(seriesTitle: title) { id, type in
tmdbID = id; tmdbType = type
fetchMetadataIDIfNeeded()
}
}
}
@ -660,34 +672,44 @@ struct MediaInfoView: View {
@ViewBuilder
private var menuContent: some View {
Group {
if let id = itemID ?? customAniListID {
let labelText = (matchedTitle?.isEmpty == false ? matchedTitle! : "\(id)")
Text("Matched with: \(labelText)")
// Show which provider won
if let active = activeProvider {
Text("Provider: \(active)")
.font(.caption)
.foregroundColor(.gray)
.padding(.vertical, 4)
Divider()
}
Divider()
Text("Matched ID: \(itemID ?? 0)")
.font(.caption2)
.foregroundColor(.secondary)
if let _ = customAniListID {
if activeProvider == "AniList" {
Button("Match with AniList") {
isMatchingPresented = true
}
Text("Matched ID: \(itemID ?? 0)")
.font(.caption2)
.foregroundColor(.secondary)
Button(action: { resetAniListID() }) {
Label("Reset AniList ID", systemImage: "arrow.clockwise")
}
}
if let id = itemID ?? customAniListID {
Button(action: { openAniListPage(id: id) }) {
Button(action: { openAniListPage(id: itemID ?? 0) }) {
Label("Open in AniList", systemImage: "link")
}
}
if UserDefaults.standard.string(forKey: "metadataProviders") ?? "TMDB" == "AniList" {
Button(action: { isMatchingPresented = true }) {
Label("Match with AniList", systemImage: "magnifyingglass")
// TMDB branch: only match
else if activeProvider == "TMDB" {
Button("Match with TMDB") {
isTMDBMatchingPresented = true
}
}
// Keep all of your existing poster & debug options
posterMenuOptions
Divider()
@ -697,6 +719,7 @@ struct MediaInfoView: View {
}
}
}
@ViewBuilder
private var posterMenuOptions: some View {
@ -1169,30 +1192,60 @@ struct MediaInfoView: View {
}
private func fetchMetadataIDIfNeeded() {
let provider = UserDefaults.standard.string(forKey: "metadataProviders") ?? "TMDB"
let cleaned = cleanTitle(title)
let order = metadataProvidersOrder
let cleanedTitle = cleanTitle(title)
itemID = nil
tmdbID = nil
tmdbFetcher.fetchBestMatchID(for: cleaned) { id, type in
DispatchQueue.main.async {
self.tmdbID = id
self.tmdbType = type
Logger.shared.log("Fetched TMDB ID: \(id ?? -1) (\(type?.rawValue ?? "unknown")) for title: \(cleaned)", type: "Debug")
}
}
fetchItemID(byTitle: cleaned) { result in
activeProvider = nil
isError = false
fetchItemID(byTitle: cleanedTitle) { result in
switch result {
case .success(let id):
DispatchQueue.main.async {
self.itemID = id
Logger.shared.log("Fetched AniList ID: \(id) for title: \(cleaned)", type: "Debug")
}
DispatchQueue.main.async { self.itemID = id }
case .failure(let error):
Logger.shared.log("Failed to fetch AniList ID: \(error)", type: "Error")
Logger.shared.log("Failed to fetch AniList ID for tracking: \(error)", type: "Error")
}
}
func tryNext(_ index: Int) {
guard index < order.count else {
isError = true
return
}
let provider = order[index]
if provider == "TMDB" {
tmdbFetcher.fetchBestMatchID(for: cleanedTitle) { id, type in
DispatchQueue.main.async {
if let id = id, let type = type {
self.tmdbID = id
self.tmdbType = type
self.activeProvider = "TMDB"
UserDefaults.standard.set("TMDB", forKey: "metadataProviders")
} else {
tryNext(index + 1)
}
}
}
} else if provider == "AniList" {
fetchItemID(byTitle: cleanedTitle) { result in
switch result {
case .success:
DispatchQueue.main.async {
self.activeProvider = "AniList"
UserDefaults.standard.set("AniList", forKey: "metadataProviders")
}
case .failure:
tryNext(index + 1)
}
}
} else {
tryNext(index + 1)
}
}
tryNext(0)
}
private func fetchItemID(byTitle title: String, completion: @escaping (Result<Int, Error>) -> Void) {

View file

@ -0,0 +1,176 @@
//
// TMDBMatchPopupView.swift
// Sulfur
//
// Created by seiike on 12/06/2025.
//
import SwiftUI
import NukeUI
struct TMDBMatchPopupView: View {
let seriesTitle: String
let onSelect: (Int, TMDBFetcher.MediaType) -> Void
@State private var results: [ResultItem] = []
@State private var isLoading = true
@State private var showingError = false
@Environment(\.dismiss) private var dismiss
struct ResultItem: Identifiable {
let id: Int
let title: String
let mediaType: TMDBFetcher.MediaType
let posterURL: String?
}
private struct TMDBSearchResult: Decodable {
let id: Int
let name: String?
let title: String?
let poster_path: String?
let popularity: Double
}
private struct TMDBSearchResponse: Decodable {
let results: [TMDBSearchResult]
}
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 0) {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
.padding()
} else if results.isEmpty {
Text("No matches found")
.font(.subheadline)
.foregroundStyle(.gray)
.frame(maxWidth: .infinity)
.padding()
} else {
LazyVStack(spacing: 15) {
ForEach(results) { item in
Button(action: {
onSelect(item.id, item.mediaType)
dismiss()
}) {
HStack(spacing: 12) {
if let poster = item.posterURL, let url = URL(string: poster) {
LazyImage(url: url) { state in
if let image = state.imageContainer?.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 50, height: 75)
.cornerRadius(6)
} else {
Rectangle()
.fill(.tertiary)
.frame(width: 50, height: 75)
.cornerRadius(6)
}
}
}
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.font(.body)
.foregroundStyle(.primary)
Text(item.mediaType.rawValue.capitalized)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(11)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(.ultraThinMaterial)
)
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(
Color.accentColor.opacity(0.2),
lineWidth: 0.5
)
)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
}
}
}
.navigationTitle("TMDB Match")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
}
.alert("Error Fetching Results", isPresented: $showingError) {
Button("OK", role: .cancel) { }
} message: {
Text("Unable to fetch matches. Please try again later.")
}
}
.onAppear(perform: fetchMatches)
}
private func fetchMatches() {
isLoading = true
results = []
let fetcher = TMDBFetcher()
let apiKey = fetcher.apiKey
let dispatchGroup = DispatchGroup()
var temp: [ResultItem] = []
var encounteredError = false
for type in TMDBFetcher.MediaType.allCases {
dispatchGroup.enter()
let query = seriesTitle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
let urlString = "https://api.themoviedb.org/3/search/\(type.rawValue)?api_key=\(apiKey)&query=\(query)"
guard let url = URL(string: urlString) else {
encounteredError = true
dispatchGroup.leave()
continue
}
URLSession.shared.dataTask(with: url) { data, _, error in
defer { dispatchGroup.leave() }
guard error == nil, let data = data,
let response = try? JSONDecoder().decode(TMDBSearchResponse.self, from: data) else {
encounteredError = true
return
}
let items = response.results.prefix(6).map { res -> ResultItem in
let title = (type == .tv ? res.name : res.title) ?? "Unknown"
let poster = res.poster_path.map { "https://image.tmdb.org/t/p/w500\($0)" }
return ResultItem(id: res.id, title: title, mediaType: type, posterURL: poster)
}
temp.append(contentsOf: items)
}.resume()
}
dispatchGroup.notify(queue: .main) {
if encounteredError {
showingError = true
}
// Keep API order (by popularity), limit to top 6 overall
results = Array(temp.prefix(6))
isLoading = false
}
}
}

View file

@ -154,12 +154,17 @@ struct SettingsViewGeneral: View {
@AppStorage("fetchEpisodeMetadata") private var fetchEpisodeMetadata: Bool = true
@AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false
@AppStorage("hideSplashScreen") private var hideSplashScreenEnable: Bool = false
@AppStorage("metadataProviders") private var metadataProviders: String = "TMDB"
@AppStorage("metadataProvidersOrder") private var metadataProvidersOrderData: Data = {
try! JSONEncoder().encode(["TMDB","AniList"])
}()
@AppStorage("tmdbImageWidth") private var TMDBimageWidht: String = "original"
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
private let metadataProvidersList = ["AniList", "TMDB"]
private var metadataProvidersOrder: [String] {
get { (try? JSONDecoder().decode([String].self, from: metadataProvidersOrderData)) ?? ["AniList","TMDB"] }
set { metadataProvidersOrderData = try! JSONEncoder().encode(newValue) }
}
private let TMDBimageWidhtList = ["300", "500", "780", "1280", "original"]
private let sortOrderOptions = ["Ascending", "Descending"]
@EnvironmentObject var settings: Settings
@ -208,85 +213,70 @@ struct SettingsViewGeneral: View {
isOn: $fetchEpisodeMetadata
)
if metadataProviders == "TMDB" {
List {
ForEach(metadataProvidersOrder, id: \.self) { prov in
Text(prov)
.padding(.vertical, 8)
}
.onMove { idx, dest in
var arr = metadataProvidersOrder
arr.move(fromOffsets: idx, toOffset: dest)
metadataProvidersOrderData = try! JSONEncoder().encode(arr)
}
}
.environment(\.editMode, .constant(.active))
.frame(height: 140)
SettingsSection(
title: "Media Grid Layout",
footer: "Adjust the number of media items per row in portrait and landscape modes."
) {
SettingsPickerRow(
icon: "server.rack",
title: "Metadata Provider",
options: metadataProvidersList,
optionToString: { $0 },
selection: $metadataProviders,
showDivider: true
icon: "rectangle.portrait",
title: "Portrait Columns",
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4),
optionToString: { "\($0)" },
selection: $mediaColumnsPortrait
)
SettingsPickerRow(
icon: "square.stack.3d.down.right",
title: "Thumbnails Width",
options: TMDBimageWidhtList,
optionToString: { $0 },
selection: $TMDBimageWidht,
icon: "rectangle",
title: "Landscape Columns",
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5),
optionToString: { "\($0)" },
selection: $mediaColumnsLandscape,
showDivider: false
)
} else {
SettingsPickerRow(
icon: "server.rack",
title: "Metadata Provider",
options: metadataProvidersList,
optionToString: { $0 },
selection: $metadataProviders,
}
SettingsSection(
title: "Modules",
footer: "Note that the modules will be replaced only if there is a different version string inside the JSON file."
) {
SettingsToggleRow(
icon: "arrow.clockwise",
title: "Refresh Modules on Launch",
isOn: $refreshModulesOnLaunch,
showDivider: false
)
}
SettingsSection(
title: "Advanced",
footer: "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time."
) {
SettingsToggleRow(
icon: "chart.bar",
title: "Enable Analytics",
isOn: $analyticsEnabled,
showDivider: false
)
}
}
SettingsSection(
title: "Media Grid Layout",
footer: "Adjust the number of media items per row in portrait and landscape modes."
) {
SettingsPickerRow(
icon: "rectangle.portrait",
title: "Portrait Columns",
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(1...5) : Array(1...4),
optionToString: { "\($0)" },
selection: $mediaColumnsPortrait
)
SettingsPickerRow(
icon: "rectangle",
title: "Landscape Columns",
options: UIDevice.current.userInterfaceIdiom == .pad ? Array(2...8) : Array(2...5),
optionToString: { "\($0)" },
selection: $mediaColumnsLandscape,
showDivider: false
)
}
SettingsSection(
title: "Modules",
footer: "Note that the modules will be replaced only if there is a different version string inside the JSON file."
) {
SettingsToggleRow(
icon: "arrow.clockwise",
title: "Refresh Modules on Launch",
isOn: $refreshModulesOnLaunch,
showDivider: false
)
}
SettingsSection(
title: "Advanced",
footer: "Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time."
) {
SettingsToggleRow(
icon: "chart.bar",
title: "Enable Analytics",
isOn: $analyticsEnabled,
showDivider: false
)
}
.padding(.vertical, 20)
}
.padding(.vertical, 20)
.navigationTitle("General")
.scrollViewBottomPadding()
}
.navigationTitle("General")
.scrollViewBottomPadding()
}
}

View file

@ -88,6 +88,7 @@
1E47859B2DEBC5960095BF2F /* AnilistMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */; };
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; };
1EDA48F42DFAC374002A4EC3 /* TMDBMatchPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */; };
1EF5C3A92DB988E40032BF07 /* CommunityLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */; };
7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */; };
722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */; };
@ -181,6 +182,7 @@
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnilistMatchPopupView.swift; sourceTree = "<group>"; };
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TMDBMatchPopupView.swift; sourceTree = "<group>"; };
1EF5C3A82DB988D70032BF07 /* CommunityLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityLib.swift; sourceTree = "<group>"; };
7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadModels.swift; sourceTree = "<group>"; };
7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = M3U8StreamExtractor.swift; sourceTree = "<group>"; };
@ -368,6 +370,7 @@
133D7C7F2D2BE2630075467E /* MediaInfoView */ = {
isa = PBXGroup;
children = (
1EDA48F32DFAC374002A4EC3 /* TMDBMatchPopupView.swift */,
138AA1B52D2D66EC0021F9DF /* EpisodeCell */,
133D7C802D2BE2630075467E /* MediaInfoView.swift */,
1E47859A2DEBC5960095BF2F /* AnilistMatchPopupView.swift */,
@ -705,6 +708,7 @@
files = (
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */,
131270172DC13A010093AA9C /* DownloadManager.swift in Sources */,
1EDA48F42DFAC374002A4EC3 /* TMDBMatchPopupView.swift in Sources */,
1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */,
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */,
1359ED142D76F49900C13034 /* finTopView.swift in Sources */,
@ -940,7 +944,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
DEVELOPMENT_TEAM = 399LMK6Q2Y;
DEVELOPMENT_TEAM = 385Y24WAN5;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@ -958,7 +962,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur1;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -982,7 +986,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
DEVELOPMENT_TEAM = 399LMK6Q2Y;
DEVELOPMENT_TEAM = 385Y24WAN5;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@ -1000,7 +1004,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur;
PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur1;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";