diff --git a/Elsewhen.xcodeproj/project.pbxproj b/Elsewhen.xcodeproj/project.pbxproj index ff3aee6..8522692 100644 --- a/Elsewhen.xcodeproj/project.pbxproj +++ b/Elsewhen.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ 5EB23BA126E406B900203153 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5EB23BA026E406B900203153 /* Assets.xcassets */; }; 5EB23BA426E406B900203153 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5EB23BA326E406B900203153 /* Preview Assets.xcassets */; }; 5EB31FB726E6390D00826907 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EB31FB626E6390D00826907 /* SearchBar.swift */; }; + 5EE70E2226EC9AA600F3F164 /* TimeZoneFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE70E2126EC9AA500F3F164 /* TimeZoneFlags.swift */; }; + 5EE70E2426ECBEED00F3F164 /* MykeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE70E2326ECBEED00F3F164 /* MykeMode.swift */; }; + 5EE70E2726ECCC0700F3F164 /* TimeCodeGeneratorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE70E2626ECCC0700F3F164 /* TimeCodeGeneratorView.swift */; }; 635C8FA726E66E8300361B08 /* DiscordFormattedDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635C8FA626E66E8300361B08 /* DiscordFormattedDate.swift */; }; 6378397E26EA5FD60055A10D /* UIApplication+clearLaunchScreenCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6378397D26EA5FD60055A10D /* UIApplication+clearLaunchScreenCache.swift */; }; 6378398026EA61AB0055A10D /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6378397F26EA61AB0055A10D /* Launch Screen.storyboard */; }; @@ -30,6 +33,8 @@ 637839B226EBF5FD0055A10D /* ConvertTimeIntentHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637839B126EBF5FD0055A10D /* ConvertTimeIntentHandling.swift */; }; 637839B526EBF8130055A10D /* FormatCodeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637839B426EBF8130055A10D /* FormatCodeExtension.swift */; }; 637839B626EBF9690055A10D /* DateHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6378398426EB80F50055A10D /* DateHelpers.swift */; }; + 637839CB26ED11360055A10D /* TimeZone+ItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637839CA26ED11360055A10D /* TimeZone+ItemProvider.swift */; }; + 63DDFB9A26ED178E0021ACFD /* TimeZone+DropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DDFB9926ED178E0021ACFD /* TimeZone+DropDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,6 +77,9 @@ 5EB23BA026E406B900203153 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5EB23BA326E406B900203153 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 5EB31FB626E6390D00826907 /* SearchBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + 5EE70E2126EC9AA500F3F164 /* TimeZoneFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneFlags.swift; sourceTree = ""; }; + 5EE70E2326ECBEED00F3F164 /* MykeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MykeMode.swift; sourceTree = ""; }; + 5EE70E2626ECCC0700F3F164 /* TimeCodeGeneratorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeCodeGeneratorView.swift; sourceTree = ""; }; 635C8FA626E66E8300361B08 /* DiscordFormattedDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscordFormattedDate.swift; sourceTree = ""; }; 6378397D26EA5FD60055A10D /* UIApplication+clearLaunchScreenCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+clearLaunchScreenCache.swift"; sourceTree = ""; }; 6378397F26EA61AB0055A10D /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; @@ -90,6 +98,8 @@ 637839AF26EBF4910055A10D /* Intents.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; }; 637839B126EBF5FD0055A10D /* ConvertTimeIntentHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertTimeIntentHandling.swift; sourceTree = ""; }; 637839B426EBF8130055A10D /* FormatCodeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatCodeExtension.swift; sourceTree = ""; }; + 637839CA26ED11360055A10D /* TimeZone+ItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeZone+ItemProvider.swift"; sourceTree = ""; }; + 63DDFB9926ED178E0021ACFD /* TimeZone+DropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeZone+DropDelegate.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -143,12 +153,12 @@ 5EB23B9B26E406B600203153 /* Elsewhen */ = { isa = PBXGroup; children = ( + 5EE70E2526ECCBF400F3F164 /* Views */, 6378398126EB7F510055A10D /* Elements */, 6378397C26EA5FA50055A10D /* Extensions */, + 5EE70E2826ED0EE700F3F164 /* Helpers */, 5EB23B9C26E406B600203153 /* ElsewhenApp.swift */, 5EB23B9E26E406B600203153 /* ContentView.swift */, - 6378398426EB80F50055A10D /* DateHelpers.swift */, - 5EB31FB626E6390D00826907 /* SearchBar.swift */, 5EB23BA026E406B900203153 /* Assets.xcassets */, 5EB23BA226E406B900203153 /* Preview Content */, 6378397F26EA61AB0055A10D /* Launch Screen.storyboard */, @@ -164,10 +174,31 @@ path = "Preview Content"; sourceTree = ""; }; + 5EE70E2526ECCBF400F3F164 /* Views */ = { + isa = PBXGroup; + children = ( + 5EE70E2626ECCC0700F3F164 /* TimeCodeGeneratorView.swift */, + 5EE70E2326ECBEED00F3F164 /* MykeMode.swift */, + ); + path = Views; + sourceTree = ""; + }; + 5EE70E2826ED0EE700F3F164 /* Helpers */ = { + isa = PBXGroup; + children = ( + 6378398426EB80F50055A10D /* DateHelpers.swift */, + 5EB31FB626E6390D00826907 /* SearchBar.swift */, + 5EE70E2126EC9AA500F3F164 /* TimeZoneFlags.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 6378397C26EA5FA50055A10D /* Extensions */ = { isa = PBXGroup; children = ( 6378397D26EA5FD60055A10D /* UIApplication+clearLaunchScreenCache.swift */, + 637839CA26ED11360055A10D /* TimeZone+ItemProvider.swift */, + 63DDFB9926ED178E0021ACFD /* TimeZone+DropDelegate.swift */, ); path = Extensions; sourceTree = ""; @@ -353,11 +384,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5EE70E2226EC9AA600F3F164 /* TimeZoneFlags.swift in Sources */, 5EB23B9F26E406B600203153 /* ContentView.swift in Sources */, + 5EE70E2426ECBEED00F3F164 /* MykeMode.swift in Sources */, 5EB31FB726E6390D00826907 /* SearchBar.swift in Sources */, 6378397E26EA5FD60055A10D /* UIApplication+clearLaunchScreenCache.swift in Sources */, 5EAB48DD26E624CD00B04A60 /* TimezoneChoiceView.swift in Sources */, + 5EE70E2726ECCC0700F3F164 /* TimeCodeGeneratorView.swift in Sources */, 6378398726EBA7C60055A10D /* DateTimeSelection.swift in Sources */, + 63DDFB9A26ED178E0021ACFD /* TimeZone+DropDelegate.swift in Sources */, + 637839CB26ED11360055A10D /* TimeZone+ItemProvider.swift in Sources */, 6378398326EB80C30055A10D /* ResultSheet.swift in Sources */, 6378398526EB80F50055A10D /* DateHelpers.swift in Sources */, 635C8FA726E66E8300361B08 /* DiscordFormattedDate.swift in Sources */, diff --git a/Elsewhen/ContentView.swift b/Elsewhen/ContentView.swift index 5a2daac..8e2afa3 100644 --- a/Elsewhen/ContentView.swift +++ b/Elsewhen/ContentView.swift @@ -14,47 +14,19 @@ let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "times") struct ContentView: View { - @State private var selectedDate = Date() - @State private var selectedFormatStyle: DateFormat = dateFormats[0] - @State private var selectedTimeZone: String = TimeZone.current.identifier - @State private var showLocalTimeInstead: Bool = false - - private var discordFormat: String { - var timeIntervalSince1970 = Int(selectedDate.timeIntervalSince1970) - - if let tz = TimeZone(identifier: selectedTimeZone) { - timeIntervalSince1970 = Int(convertSelectedDate(from: tz, to: TimeZone.current).timeIntervalSince1970) - } else { - logger.warning("\(selectedTimeZone, privacy: .public) is not a valid timezone identifier!") - } - - return "" - } + @State private var selectedTab: Int = 1 var body: some View { - NavigationView { - VStack(spacing: 0) { - - ScrollView(showsIndicators: true) { - DateTimeSelection(selectedFormatStyle: $selectedFormatStyle, selectedDate: $selectedDate, selectedTimeZone: $selectedTimeZone) - } - - ResultSheet(selectedDate: selectedDate, selectedTimeZone: selectedTimeZone, discordFormat: discordFormat, showLocalTimeInstead: $showLocalTimeInstead, selectedFormatStyle: $selectedFormatStyle) - - } - .edgesIgnoringSafeArea([.bottom, .horizontal]) - .navigationTitle("Discord Time Code Generator") - .navigationBarTitleDisplayMode(.inline) - } - .navigationViewStyle(StackNavigationViewStyle()) - .onChange(of: selectedTimeZone) { _ in - showLocalTimeInstead = false + TabView(selection: $selectedTab) { + TimeCodeGeneratorView() + .tabItem { Label("Time Codes", systemImage: "clock") } + .tag(0) + MykeMode() + .tabItem { Label("Myke Mode", systemImage: "keyboard") } + .tag(1) } } - func convertSelectedDate(from initialTimezone: TimeZone, to targetTimezone: TimeZone) -> Date { - return convert(date: selectedDate, from: initialTimezone, to: targetTimezone) - } } struct ContentView_Previews: PreviewProvider { diff --git a/Elsewhen/Elements/ResultSheet.swift b/Elsewhen/Elements/ResultSheet.swift index 4d25c95..2605b44 100644 --- a/Elsewhen/Elements/ResultSheet.swift +++ b/Elsewhen/Elements/ResultSheet.swift @@ -133,7 +133,7 @@ struct ResultSheet: View { .frame(minWidth: 0, maxWidth: .infinity) .background( Color(UIColor.secondarySystemBackground) - .shadow(radius: 10) + .shadow(radius: 5, x: 0, y: -5) ) } diff --git a/Elsewhen/Extensions/TimeZone+DropDelegate.swift b/Elsewhen/Extensions/TimeZone+DropDelegate.swift new file mode 100644 index 0000000..40c9394 --- /dev/null +++ b/Elsewhen/Extensions/TimeZone+DropDelegate.swift @@ -0,0 +1,39 @@ +// +// TimeZone+DropDelegate.swift +// TimeZone+DropDelegate +// +// Created by David on 11/09/2021. +// + +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +extension TimeZone { + struct TZDropDelegate: DropDelegate { + let onDrop: ((TimeZone?) -> ()) + func performDrop(info: DropInfo) -> Bool { + let itemProviders = info.itemProviders(for: [UTType.json, UTType.text]) + guard !itemProviders.isEmpty else { + return false + } + for itemProvider in itemProviders { + if itemProvider.canLoadObject(ofClass: TimeZone.ItemProvider.self) { + itemProvider.loadObject(ofClass: TimeZone.ItemProvider.self) { tzItemProvider, error in + guard let tzItemProvider = tzItemProvider as? TimeZone.ItemProvider, + error == nil else { + if let error = error { + logger.error("Unabled to load dropped item: \(error.localizedDescription)") + } + onDrop(nil) + return + } + onDrop(TimeZone(identifier: tzItemProvider.identifier)) + } + return true + } + } + return false + } + } +} diff --git a/Elsewhen/Extensions/TimeZone+ItemProvider.swift b/Elsewhen/Extensions/TimeZone+ItemProvider.swift new file mode 100644 index 0000000..816134f --- /dev/null +++ b/Elsewhen/Extensions/TimeZone+ItemProvider.swift @@ -0,0 +1,131 @@ +// +// TimeZone+ItemProvider.swift +// TimeZone+ItemProvider +// +// Created by David on 11/09/2021. +// + +import Foundation +import UniformTypeIdentifiers + +let jsonEncoder = JSONEncoder() +let jsonDecoder = JSONDecoder() + +let typeIdentifiers = [ + UTType.json.identifier, + UTType.text.identifier, UTType.utf8PlainText.identifier, UTType.plainText.identifier +] + +extension TimeZone { + class ItemProvider: NSObject, NSItemProviderWriting, Codable, NSItemProviderReading { + + let identifier: String + let name: String? + let abbreviation: String? + let flag: String + + var resolvedName: String { + "\(self.flag) \(self.name ?? self.identifier)" + } + + required init(from timezone: TimeZone) { + self.identifier = timezone.identifier + self.name = timezone.localizedName(for: .standard, locale: .current) + self.abbreviation = timezone.abbreviation() + self.flag = flagForTimeZone(timezone) + } + + static var writableTypeIdentifiersForItemProvider: [String] = typeIdentifiers + + func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { + switch typeIdentifier { + case UTType.json.identifier: + return self.loadJson(forItemProviderCompletionHandler: completionHandler) + case UTType.text.identifier, + UTType.utf8PlainText.identifier, + UTType.plainText.identifier: + return self.loadUtf8String(forItemProviderCompletionHandler: completionHandler) + default: + completionHandler(nil, ItemProviderError.unrecognisedType(identifier: typeIdentifier)) + return nil + } + } + + func loadJson(forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { + do { + completionHandler(try jsonEncoder.encode(self), nil) + } catch { + completionHandler(nil, error) + } + return nil + } + + func loadUtf8String(forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { + completionHandler(Data(self.resolvedName.utf8), nil) + return nil + } + + static var readableTypeIdentifiersForItemProvider: [String] = typeIdentifiers + + static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { + switch typeIdentifier { + case UTType.json.identifier: + return try jsonDecoder.decode(Self.self, from: data) + case UTType.text.identifier, + UTType.utf8PlainText.identifier, + UTType.plainText.identifier: + guard let string = String(data: data, encoding: .utf8) else { + throw ItemProviderError.invalidData(representing: "TimeZone") + } + if let provider = Self.from(string: string) { + return provider + } + if let provider = Self.from(string: string.replacingOccurrences(of: "\\", with: "").trimmingCharacters(in: .punctuationCharacters)) { + return provider + } + if let provider = Self.from(string: string.dropFirst(1).trimmingCharacters(in: .whitespaces).trimmingCharacters(in: .punctuationCharacters)) { + return provider + } + throw ItemProviderError.invalidData(representing: "TimeZone") + default: + throw ItemProviderError.unrecognisedType(identifier: typeIdentifier) + } + } + + static func from(string: String) -> Self? { + if let tz = TimeZone(identifier: string) { + return Self.init(from: tz) + } + if let tz = TimeZone(abbreviation: string) { + return Self.init(from: tz) + } + return nil + } + } + + var itemProvider: ItemProvider { + TimeZone.ItemProvider(from: self) + } +} + +enum ItemProviderError: LocalizedError { + case unrecognisedType(identifier: String) + case invalidData(representing: String) + + var errorDescription: String? { + return String.localizedStringWithFormat(NSLocalizedString("ItemProvidingFailed: %@", tableName: "Errors", comment: ""), failureReason!) + } + + var failureReason: String? { + switch self { + case .unrecognisedType(let identifier): + return String.localizedStringWithFormat(NSLocalizedString("UnrecognisedUTI: %@", tableName: "Errors", comment: ""), identifier) + case .invalidData(let representing): + return "Data did not represent a \(representing)" + } + } + + var recoverySuggestion: String? { + "Please contact support" + } +} diff --git a/Elsewhen/DateHelpers.swift b/Elsewhen/Helpers/DateHelpers.swift similarity index 100% rename from Elsewhen/DateHelpers.swift rename to Elsewhen/Helpers/DateHelpers.swift diff --git a/Elsewhen/SearchBar.swift b/Elsewhen/Helpers/SearchBar.swift similarity index 100% rename from Elsewhen/SearchBar.swift rename to Elsewhen/Helpers/SearchBar.swift diff --git a/Elsewhen/Helpers/TimeZoneFlags.swift b/Elsewhen/Helpers/TimeZoneFlags.swift new file mode 100644 index 0000000..8065e8f --- /dev/null +++ b/Elsewhen/Helpers/TimeZoneFlags.swift @@ -0,0 +1,457 @@ +// +// TimeZoneFlags.swift +// TimeZoneFlags +// +// Created by Ben Cardy on 11/09/2021. +// + +import Foundation + +func flagForTimeZone(_ zone: TimeZone) -> String { + return flagForTimeZone(zone.identifier) +} + +func flagForTimeZone(_ zone: String) -> String { + return timeZoneFlags[zone] ?? "🏳️" +} + +let timeZoneFlags: [String: String] = [ + "Africa/Abidjan": "🇨🇮", + "Africa/Accra": "🇬🇭", + "Africa/Addis_Ababa": "🇪🇹", + "Africa/Algiers": "🇩🇿", + "Africa/Asmara": "🇪🇷", + "Africa/Bamako": "🇲🇱", + "Africa/Bangui": "🇨🇫", + "Africa/Banjul": "🇬🇲", + "Africa/Bissau": "🇬🇳", + "Africa/Blantyre": "🇲🇼", + "Africa/Brazzaville": "🇨🇬", + "Africa/Bujumbura": "🇧🇮", + "Africa/Cairo": "🇪🇬", + "Africa/Casablanca": "🇲🇦", + "Africa/Ceuta": "🇪🇸", + "Africa/Conakry": "🇬🇳", + "Africa/Dakar": "🇸🇳", + "Africa/Dar_es_Salaam": "🇹🇿", + "Africa/Djibouti": "🇩🇯", + "Africa/Douala": "🇨🇲", + "Africa/El_Aaiun": "🇪🇭", + "Africa/Freetown": "🇸🇱", + "Africa/Gaborone": "🇧🇼", + "Africa/Harare": "🇿🇼", + "Africa/Johannesburg": "🇿🇦", + "Africa/Juba": "🇸🇸", + "Africa/Kampala": "🇺🇬", + "Africa/Khartoum": "🇸🇩", + "Africa/Kigali": "🇷🇼", + "Africa/Kinshasa": "🇨🇩", + "Africa/Lagos": "🇳🇬", + "Africa/Libreville": "🇬🇦", + "Africa/Lome": "🇹🇬", + "Africa/Luanda": "🇦🇴", + "Africa/Lubumbashi": "🇨🇩", + "Africa/Lusaka": "🇿🇲", + "Africa/Malabo": "🇬🇶", + "Africa/Maputo": "🇲🇿", + "Africa/Maseru": "🇱🇸", + "Africa/Mbabane": "🇸🇿", + "Africa/Mogadishu": "🇸🇴", + "Africa/Monrovia": "🇱🇷", + "Africa/Nairobi": "🇰🇪", + "Africa/Ndjamena": "🇹🇩", + "Africa/Niamey": "🇳🇪", + "Africa/Nouakchott": "🇲🇷", + "Africa/Ouagadougou": "🇧🇫", + "Africa/Porto-Novo": "🇧🇯", + "Africa/Sao_Tome": "🇸🇹", + "Africa/Tripoli": "🇱🇾", + "Africa/Tunis": "🇹🇳", + "Africa/Windhoek": "🇳🇦", + "America/Adak": "🇺🇸", + "America/Anchorage": "🇺🇸", + "America/Anguilla": "🇦🇮", + "America/Antigua": "🇦🇬", + "America/Araguaina": "🇧🇷", + "America/Argentina/Buenos_Aires": "🇦🇷", + "America/Argentina/Catamarca": "🇦🇷", + "America/Argentina/Cordoba": "🇦🇷", + "America/Argentina/Jujuy": "🇦🇷", + "America/Argentina/La_Rioja": "🇦🇷", + "America/Argentina/Mendoza": "🇦🇷", + "America/Argentina/Rio_Gallegos": "🇦🇷", + "America/Argentina/Salta": "🇦🇷", + "America/Argentina/San_Juan": "🇦🇷", + "America/Argentina/San_Luis": "🇦🇷", + "America/Argentina/Tucuman": "🇦🇷", + "America/Argentina/Ushuaia": "🇦🇷", + "America/Aruba": "🇦🇼", + "America/Asuncion": "🇵🇾", + "America/Atikokan": "🇨🇦", + "America/Bahia": "🇧🇷", + "America/Bahia_Banderas": "🇲🇽", + "America/Barbados": "🇧🇧", + "America/Belem": "🇧🇷", + "America/Belize": "🇧🇿", + "America/Blanc-Sablon": "🇨🇦", + "America/Boa_Vista": "🇨🇻", + "America/Bogota": "🇨🇴", + "America/Boise": "🇺🇸", + "America/Cambridge_Bay": "🇨🇦", + "America/Campo_Grande": "🇧🇷", + "America/Cancun": "🇲🇽", + "America/Caracas": "🇻🇪", + "America/Cayenne": "🇬🇫", + "America/Cayman": "🇰🇾", + "America/Chicago": "🇺🇸", + "America/Chihuahua": "🇲🇽", + "America/Costa_Rica": "🇨🇷", + "America/Creston": "🇨🇦", + "America/Cuiaba": "🇧🇷", + "America/Curacao": "🇨🇼", + "America/Danmarkshavn": "🇬🇱", + "America/Dawson": "🇨🇦", + "America/Dawson_Creek": "🇨🇦", + "America/Denver": "🇺🇸", + "America/Detroit": "🇺🇸", + "America/Dominica": "🇩🇲", + "America/Edmonton": "🇨🇦", + "America/Eirunepe": "🇧🇷", + "America/El_Salvador": "🇸🇻", + "America/Fort_Nelson": "🇨🇦", + "America/Fortaleza": "🇧🇷", + "America/Glace_Bay": "🇨🇦", + "America/Godthab": "🇬🇱", + "America/Goose_Bay": "🇨🇦", + "America/Grand_Turk": "🇹🇨", + "America/Grenada": "🇬🇩", + "America/Guadeloupe": "🇬🇵", + "America/Guatemala": "🇬🇹", + "America/Guayaquil": "🇪🇨", + "America/Guyana": "🇬🇾", + "America/Halifax": "🇨🇦", + "America/Havana": "🇨🇺", + "America/Hermosillo": "🇲🇽", + "America/Indiana/Indianapolis": "🇺🇸", + "America/Indiana/Knox": "🇺🇸", + "America/Indiana/Marengo": "🇺🇸", + "America/Indiana/Petersburg": "🇺🇸", + "America/Indiana/Tell_City": "🇺🇸", + "America/Indiana/Vevay": "🇺🇸", + "America/Indiana/Vincennes": "🇺🇸", + "America/Indiana/Winamac": "🇺🇸", + "America/Inuvik": "🇨🇦", + "America/Iqaluit": "🇨🇦", + "America/Jamaica": "🇯🇲", + "America/Juneau": "🇺🇸", + "America/Kentucky/Louisville": "🇺🇸", + "America/Kentucky/Monticello": "🇺🇸", + "America/Kralendijk": "🇧🇶", + "America/La_Paz": "🇧🇴", + "America/Lima": "🇵🇪", + "America/Los_Angeles": "🇺🇸", + "America/Lower_Princes": "🇸🇽", + "America/Maceio": "🇧🇷", + "America/Managua": "🇳🇮", + "America/Manaus": "🇧🇷", + "America/Marigot": "🇲🇫", + "America/Martinique": "🇲🇶", + "America/Matamoros": "🇲🇽", + "America/Mazatlan": "🇲🇽", + "America/Menominee": "🇺🇸", + "America/Merida": "🇲🇽", + "America/Metlakatla": "🇺🇸", + "America/Mexico_City": "🇲🇽", + "America/Miquelon": "🇵🇲", + "America/Moncton": "🇨🇦", + "America/Monterrey": "🇲🇽", + "America/Montevideo": "🇺🇾", + "America/Montreal": "🇨🇦", + "America/Montserrat": "🇲🇸", + "America/Nassau": "🇧🇸", + "America/New_York": "🇺🇸", + "America/Nipigon": "🇨🇦", + "America/Nome": "🇺🇸", + "America/Noronha": "🇧🇷", + "America/North_Dakota/Beulah": "🇺🇸", + "America/North_Dakota/Center": "🇺🇸", + "America/North_Dakota/New_Salem": "🇺🇸", + "America/Nuuk": "🇬🇱", + "America/Ojinaga": "🇲🇽", + "America/Panama": "🇵🇦", + "America/Pangnirtung": "🇨🇦", + "America/Paramaribo": "🇸🇷", + "America/Phoenix": "🇺🇸", + "America/Port-au-Prince": "🇭🇹", + "America/Port_of_Spain": "🇹🇹", + "America/Porto_Velho": "🇧🇷", + "America/Puerto_Rico": "🇵🇷", + "America/Punta_Arenas": "🇨🇱", + "America/Rainy_River": "🇨🇦", + "America/Rankin_Inlet": "🇨🇦", + "America/Recife": "🇧🇷", + "America/Regina": "🇨🇦", + "America/Resolute": "🇨🇦", + "America/Rio_Branco": "🇧🇷", + "America/Santa_Isabel": "", + "America/Santarem": "🇧🇷", + "America/Santiago": "🇨🇱", + "America/Santo_Domingo": "🇩🇴", + "America/Sao_Paulo": "🇧🇷", + "America/Scoresbysund": "🇬🇱", + "America/Shiprock": "🇺🇸", + "America/Sitka": "🇺🇸", + "America/St_Barthelemy": "🇧🇱", + "America/St_Johns": "🇨🇦", + "America/St_Kitts": "🇰🇳", + "America/St_Lucia": "🇱🇨", + "America/St_Thomas": "🇻🇮", + "America/St_Vincent": "🇻🇨", + "America/Swift_Current": "🇨🇦", + "America/Tegucigalpa": "🇭🇳", + "America/Thule": "🇬🇱", + "America/Thunder_Bay": "🇨🇦", + "America/Tijuana": "🇲🇽", + "America/Toronto": "🇨🇦", + "America/Tortola": "🇻🇬", + "America/Vancouver": "🇨🇦", + "America/Whitehorse": "🇨🇦", + "America/Winnipeg": "🇨🇦", + "America/Yakutat": "🇺🇸", + "America/Yellowknife": "🇨🇦", + "Antarctica/Casey": "🇦🇶", + "Antarctica/Davis": "🇦🇶", + "Antarctica/DumontDUrville": "🇦🇶", + "Antarctica/Macquarie": "🇦🇶", + "Antarctica/Mawson": "🇦🇶", + "Antarctica/McMurdo": "🇦🇶", + "Antarctica/Palmer": "🇦🇶", + "Antarctica/Rothera": "🇦🇶", + "Antarctica/South_Pole": "🇦🇶", + "Antarctica/Syowa": "🇦🇶", + "Antarctica/Troll": "🇦🇶", + "Antarctica/Vostok": "🇦🇶", + "Arctic/Longyearbyen": "🇸🇯", + "Asia/Aden": "🇾🇪", + "Asia/Almaty": "🇰🇿", + "Asia/Amman": "🇯🇴", + "Asia/Anadyr": "🇷🇺", + "Asia/Aqtau": "🇰🇿", + "Asia/Aqtobe": "🇰🇿", + "Asia/Ashgabat": "🇹🇲", + "Asia/Atyrau": "🇰🇿", + "Asia/Baghdad": "🇮🇶", + "Asia/Bahrain": "🇧🇭", + "Asia/Baku": "🇦🇿", + "Asia/Bangkok": "🇹🇭", + "Asia/Barnaul": "🇷🇺", + "Asia/Beirut": "🇱🇧", + "Asia/Bishkek": "🇰🇬", + "Asia/Brunei": "🇧🇳", + "Asia/Calcutta": "🇮🇳", + "Asia/Chita": "🇷🇺", + "Asia/Choibalsan": "🇲🇳", + "Asia/Chongqing": "🇨🇳", + "Asia/Colombo": "🇱🇰", + "Asia/Damascus": "🇸🇾", + "Asia/Dhaka": "🇧🇩", + "Asia/Dili": "🇹🇱", + "Asia/Dubai": "🇦🇪", + "Asia/Dushanbe": "🇹🇯", + "Asia/Famagusta": "🇨🇾", + "Asia/Gaza": "🇵🇸", + "Asia/Harbin": "🇨🇳", + "Asia/Hebron": "🇵🇸", + "Asia/Ho_Chi_Minh": "🇻🇳", + "Asia/Hong_Kong": "🇭🇰", + "Asia/Hovd": "🇲🇳", + "Asia/Irkutsk": "🇷🇺", + "Asia/Jakarta": "🇮🇩", + "Asia/Jayapura": "🇮🇩", + "Asia/Jerusalem": "🇮🇱", + "Asia/Kabul": "🇦🇫", + "Asia/Kamchatka": "🇷🇺", + "Asia/Karachi": "🇵🇰", + "Asia/Kashgar": "🇨🇳", + "Asia/Kathmandu": "🇳🇵", + "Asia/Katmandu": "🇳🇵", + "Asia/Khandyga": "🇷🇺", + "Asia/Krasnoyarsk": "🇷🇺", + "Asia/Kuala_Lumpur": "🇲🇾", + "Asia/Kuching": "🇲🇾", + "Asia/Kuwait": "🇰🇼", + "Asia/Macau": "🇲🇴", + "Asia/Magadan": "🇷🇺", + "Asia/Makassar": "🇮🇩", + "Asia/Manila": "🇵🇭", + "Asia/Muscat": "🇴🇲", + "Asia/Nicosia": "🇨🇾", + "Asia/Novokuznetsk": "🇷🇺", + "Asia/Novosibirsk": "🇷🇺", + "Asia/Omsk": "🇷🇺", + "Asia/Oral": "🇰🇿", + "Asia/Phnom_Penh": "🇰🇭", + "Asia/Pontianak": "🇮🇩", + "Asia/Pyongyang": "🇰🇵", + "Asia/Qatar": "🇶🇦", + "Asia/Qostanay": "🇰🇿", + "Asia/Qyzylorda": "🇰🇿", + "Asia/Rangoon": "🇲🇲", + "Asia/Riyadh": "🇸🇦", + "Asia/Sakhalin": "🇷🇺", + "Asia/Samarkand": "🇺🇿", + "Asia/Seoul": "🇰🇷", + "Asia/Shanghai": "🇨🇳", + "Asia/Singapore": "🇸🇬", + "Asia/Srednekolymsk": "🇷🇺", + "Asia/Taipei": "🇹🇼", + "Asia/Tashkent": "🇺🇿", + "Asia/Tbilisi": "🇬🇪", + "Asia/Tehran": "🇮🇷", + "Asia/Thimphu": "🇧🇹", + "Asia/Tokyo": "🇯🇵", + "Asia/Tomsk": "🇷🇺", + "Asia/Ulaanbaatar": "🇲🇳", + "Asia/Urumqi": "🇨🇳", + "Asia/Ust-Nera": "🇷🇺", + "Asia/Vientiane": "🇱🇦", + "Asia/Vladivostok": "🇷🇺", + "Asia/Yakutsk": "🇷🇺", + "Asia/Yangon": "🇲🇲", + "Asia/Yekaterinburg": "🇷🇺", + "Asia/Yerevan": "🇦🇲", + "Atlantic/Azores": "🇵🇹", + "Atlantic/Bermuda": "🇧🇲", + "Atlantic/Canary": "🇪🇸", + "Atlantic/Cape_Verde": "🇨🇻", + "Atlantic/Faroe": "🇫🇴", + "Atlantic/Madeira": "🇵🇹", + "Atlantic/Reykjavik": "🇮🇸", + "Atlantic/South_Georgia": "🇬🇸", + "Atlantic/St_Helena": "🇸🇭", + "Atlantic/Stanley": "🇫🇰", + "Australia/Adelaide": "🇦🇺", + "Australia/Brisbane": "🇦🇺", + "Australia/Broken_Hill": "🇦🇺", + "Australia/Currie": "🇦🇺", + "Australia/Darwin": "🇦🇺", + "Australia/Eucla": "🇦🇺", + "Australia/Hobart": "🇦🇺", + "Australia/Lindeman": "🇦🇺", + "Australia/Lord_Howe": "🇦🇺", + "Australia/Melbourne": "🇦🇺", + "Australia/Perth": "🇦🇺", + "Australia/Sydney": "🇦🇺", + "Europe/Amsterdam": "🇳🇱", + "Europe/Andorra": "🇦🇩", + "Europe/Astrakhan": "", + "Europe/Athens": "🇬🇷", + "Europe/Belgrade": "🇷🇺", + "Europe/Berlin": "🇩🇪", + "Europe/Bratislava": "🇸🇰", + "Europe/Brussels": "🇧🇪", + "Europe/Bucharest": "🇷🇴", + "Europe/Budapest": "🇭🇺", + "Europe/Busingen": "🇩🇪", + "Europe/Chisinau": "🇲🇩", + "Europe/Copenhagen": "🇩🇰", + "Europe/Dublin": "🇩🇪", + "Europe/Gibraltar": "🇬🇮", + "Europe/Guernsey": "🇬🇬", + "Europe/Helsinki": "🇳🇴", + "Europe/Isle_of_Man": "🇬🇧", + "Europe/Istanbul": "🇹🇷", + "Europe/Jersey": "🇯🇪", + "Europe/Kaliningrad": "🇷🇺", + "Europe/Kiev": "🇺🇦", + "Europe/Kirov": "🇷🇺", + "Europe/Lisbon": "🇵🇹", + "Europe/Ljubljana": "🇸🇮", + "Europe/London": "🇬🇧", + "Europe/Luxembourg": "🇱🇺", + "Europe/Madrid": "🇪🇸", + "Europe/Malta": "🇲🇹", + "Europe/Mariehamn": "🇫🇮", + "Europe/Minsk": "🇧🇾", + "Europe/Monaco": "🇲🇨", + "Europe/Moscow": "🇷🇺", + "Europe/Oslo": "🇳🇴", + "Europe/Paris": "🇫🇷", + "Europe/Podgorica": "🇲🇪", + "Europe/Prague": "🇨🇿", + "Europe/Riga": "🇱🇻", + "Europe/Rome": "🇮🇹", + "Europe/Samara": "🇷🇺", + "Europe/San_Marino": "🇸🇲", + "Europe/Sarajevo": "🇧🇦", + "Europe/Saratov": "🇷🇺", + "Europe/Simferopol": "🇷🇺", + "Europe/Skopje": "🇲🇰", + "Europe/Sofia": "🇧🇬", + "Europe/Stockholm": "🇸🇪", + "Europe/Tallinn": "🇪🇪", + "Europe/Tirane": "🇦🇱", + "Europe/Ulyanovsk": "🇷🇺", + "Europe/Uzhgorod": "🇺🇦", + "Europe/Vaduz": "🇱🇮", + "Europe/Vatican": "🇻🇦", + "Europe/Vienna": "🇦🇹", + "Europe/Vilnius": "🇱🇹", + "Europe/Volgograd": "🇷🇺", + "Europe/Warsaw": "🇵🇱", + "Europe/Zagreb": "🇭🇷", + "Europe/Zaporozhye": "🇺🇦", + "Europe/Zurich": "🇨🇭", + "Indian/Antananarivo": "🇲🇬", + "Indian/Chagos": "🇮🇴", + "Indian/Christmas": "🇨🇽", + "Indian/Cocos": "🇨🇨", + "Indian/Comoro": "🇰🇲", + "Indian/Kerguelen": "🇹🇫", + "Indian/Mahe": "🇸🇨", + "Indian/Maldives": "🇲🇻", + "Indian/Mauritius": "🇲🇺", + "Indian/Mayotte": "🇾🇹", + "Indian/Reunion": "🇷🇪", + "Pacific/Apia": "🇼🇸", + "Pacific/Auckland": "🇳🇿", + "Pacific/Bougainville": "🇵🇬", + "Pacific/Chatham": "🇳🇿", + "Pacific/Chuuk": "🇫🇲", + "Pacific/Easter": "🇨🇱", + "Pacific/Efate": "🇻🇺", + "Pacific/Enderbury": "🇰🇮", + "Pacific/Fakaofo": "🇹🇰", + "Pacific/Fiji": "🇫🇯", + "Pacific/Funafuti": "🇹🇻", + "Pacific/Galapagos": "🇪🇨", + "Pacific/Gambier": "🇵🇫", + "Pacific/Guadalcanal": "🇸🇧", + "Pacific/Guam": "🇬🇺", + "Pacific/Honolulu": "🇺🇸", + "Pacific/Johnston": "🇺🇸", + "Pacific/Kiritimati": "🇰🇮", + "Pacific/Kosrae": "🇫🇲", + "Pacific/Kwajalein": "🇲🇭", + "Pacific/Majuro": "🇲🇭", + "Pacific/Marquesas": "🇵🇫", + "Pacific/Midway": "🇺🇸", + "Pacific/Nauru": "🇳🇷", + "Pacific/Niue": "🇳🇺", + "Pacific/Norfolk": "🇳🇫", + "Pacific/Noumea": "🇳🇨", + "Pacific/Pago_Pago": "🇦🇸", + "Pacific/Palau": "🇵🇼", + "Pacific/Pitcairn": "🇵🇳", + "Pacific/Pohnpei": "🇫🇲", + "Pacific/Ponape": "🇫🇲", + "Pacific/Port_Moresby": "🇵🇬", + "Pacific/Rarotonga": "🇨🇰", + "Pacific/Saipan": "🇲🇵", + "Pacific/Tahiti": "🇵🇫", + "Pacific/Tarawa": "🇰🇮", + "Pacific/Tongatapu": "🇹🇴", + "Pacific/Truk": "🇫🇲", + "Pacific/Wake": "🇺🇸", + "Pacific/Wallis": "🇼🇫", +] diff --git a/Elsewhen/Views/MykeMode.swift b/Elsewhen/Views/MykeMode.swift new file mode 100644 index 0000000..0531ca0 --- /dev/null +++ b/Elsewhen/Views/MykeMode.swift @@ -0,0 +1,91 @@ +// +// MykeMode.swift +// MykeMode +// +// Created by Ben Cardy on 11/09/2021. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct MykeMode: View { + + @State private var selectedDate: Date = Date(timeIntervalSince1970: TimeInterval(1639239835)) +// @State private var selectedDate: Date = Date(timeIntervalSince1970: TimeInterval(1631892600)) + @State private var selectedTimeZones: [TimeZone] = [ + TimeZone(identifier: "America/Los_Angeles")!, + TimeZone(identifier: "America/New_York")!, + TimeZone(identifier: "Europe/London")!, + TimeZone(identifier: "Europe/Budapest")!, + ] + + func selectedTimeInZone(_ zone: TimeZone) -> String { + let df = DateFormatter() + df.dateStyle = .none + df.timeStyle = .short + df.timeZone = zone + df.locale = Locale(identifier: "en/us") + return df.string(from: selectedDate) + } + + var body: some View { + + VStack(alignment: .leading, spacing: 20) { + + Text("\(selectedDate)") + + List { + ForEach(selectedTimeZones, id: \.self) { tz in + HStack { + Text("\(flagForTimeZone(tz)) \(selectedTimeInZone(tz))") + Spacer() + if let abbreviation = fudgedAbbreviation(for: tz) { + Text(abbreviation) + .foregroundColor(.secondary) + } + } + .onDrag { + let tzItemProvider = tz.itemProvider + let itemProvider = NSItemProvider(object: tzItemProvider) + itemProvider.suggestedName = tzItemProvider.resolvedName + return itemProvider + } + } + .onMove(perform: move) + .onDrop(of: [UTType.json], delegate: TimeZone.TZDropDelegate(onDrop: { timezone in + guard let timezone = timezone else { + return + } + selectedTimeZones.append(timezone) + })) + } + + } + + } + + func move(from source: IndexSet, to destination: Int) { + selectedTimeZones.move(fromOffsets: source, toOffset: destination) + } + + func fudgedAbbreviation(for tz: TimeZone) -> String? { + guard let abbreviation = tz.abbreviation(for: selectedDate) else { return nil } + let isDaylightSavingTime = tz.isDaylightSavingTime(for: selectedDate) + if tz.identifier == "Europe/London" && isDaylightSavingTime { + return "BST" + } + if tz.identifier.starts(with: "Europe") { + if isDaylightSavingTime && abbreviation == "GMT+2" || !isDaylightSavingTime && abbreviation == "GMT+1" { + return "CET" + } + } + return abbreviation + } + +} + +struct MykeMode_Previews: PreviewProvider { + static var previews: some View { + MykeMode() + } +} diff --git a/Elsewhen/Views/TimeCodeGeneratorView.swift b/Elsewhen/Views/TimeCodeGeneratorView.swift new file mode 100644 index 0000000..95c4b6d --- /dev/null +++ b/Elsewhen/Views/TimeCodeGeneratorView.swift @@ -0,0 +1,61 @@ +// +// TimeCodeGeneratorView.swift +// TimeCodeGeneratorView +// +// Created by Ben Cardy on 11/09/2021. +// + +import SwiftUI +import MobileCoreServices +import UniformTypeIdentifiers + +struct TimeCodeGeneratorView: View { + + @State private var selectedDate = Date() + @State private var selectedFormatStyle: DateFormat = dateFormats[0] + @State private var selectedTimeZone: String = TimeZone.current.identifier + @State private var showLocalTimeInstead: Bool = false + + private var discordFormat: String { + var timeIntervalSince1970 = Int(selectedDate.timeIntervalSince1970) + + if let tz = TimeZone(identifier: selectedTimeZone) { + timeIntervalSince1970 = Int(convertSelectedDate(from: tz, to: TimeZone.current).timeIntervalSince1970) + } else { + logger.warning("\(selectedTimeZone, privacy: .public) is not a valid timezone identifier!") + } + + return "" + } + + var body: some View { + NavigationView { + VStack(spacing: 0) { + + ScrollView(showsIndicators: true) { + DateTimeSelection(selectedFormatStyle: $selectedFormatStyle, selectedDate: $selectedDate, selectedTimeZone: $selectedTimeZone) + } + + ResultSheet(selectedDate: selectedDate, selectedTimeZone: selectedTimeZone, discordFormat: discordFormat, showLocalTimeInstead: $showLocalTimeInstead, selectedFormatStyle: $selectedFormatStyle) + + } + .edgesIgnoringSafeArea(.horizontal) + .navigationTitle("Discord Time Code Generator") + .navigationBarTitleDisplayMode(.inline) + } + .navigationViewStyle(StackNavigationViewStyle()) + .onChange(of: selectedTimeZone) { _ in + showLocalTimeInstead = false + } + } + + func convertSelectedDate(from initialTimezone: TimeZone, to targetTimezone: TimeZone) -> Date { + return convert(date: selectedDate, from: initialTimezone, to: targetTimezone) + } +} + +struct TimeCodeGeneratorView_Previews: PreviewProvider { + static var previews: some View { + TimeCodeGeneratorView() + } +}