Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -0,0 +1,173 @@
import Network
import OSMEditor
/// The OpenStreetMap profile
@objc class Profile: NSObject {
// MARK: Properties
/// Key for storing the current authorization token in the user defaults
static private let userDefaultsKeyAuthorizationToken = "OSMAuthToken"
/// Key for storing the name of the OpenStreetMap profile in the user defaults
static private let userDefaultsKeyName = "UDOsmUserName"
/// Key for storing the number of edits of the OpenStreetMap profile in the user defaults
static private let userDefaultsKeyNumberOfEdits = "OSMUserChangesetsCount"
/// Key for storing iff the OpenStreetMap profile needs to be reauthorized in the user defaults
static private let userDefaultsNeedsReauthorization = "AuthNeedCheck"
/// The URL for registering an OpenStreetMap profile
static var registrationUrl: URL {
return URL(string: String(describing: osm.OsmOAuth.ServerAuth().GetRegistrationURL()))!
}
/// The URL for letting an OpenStreetMap profile authorize this app
static var authorizationUrl: URL {
return URL(string: String(describing: osm.OsmOAuth.ServerAuth().BuildOAuth2Url()))!
}
/// The optional current authorization token (can be empty)
@objc static var authorizationToken: String? {
if let authorizationToken = UserDefaults.standard.string(forKey: userDefaultsKeyAuthorizationToken), !authorizationToken.isEmpty {
return authorizationToken
}
return nil
}
/// If the OpenStreetMap profile needs to be reauthorized
@objc static var needsReauthorization: Bool {
return UserDefaults.standard.bool(forKey: userDefaultsNeedsReauthorization)
}
/// If there is an OpenStreetMap profile existing in the app
@objc static var isExisting: Bool {
return authorizationToken != nil
}
/// The optional name of the OpenStreetMap profile
@objc static var name: String? {
if isExisting {
return UserDefaults.standard.string(forKey: userDefaultsKeyName)
}
return nil
}
/// The optional number of edits of the OpenStreetMap profile
static var numberOfEdits: Int? {
if isExisting {
return UserDefaults.standard.integer(forKey: userDefaultsKeyNumberOfEdits)
}
return nil
}
/// The optional URL for the edit history of the OpenStreetMap profile
static var editHistoryUrl: URL? {
if let name, let url = URL(string: String(describing: osm.OsmOAuth.ServerAuth().GetHistoryURL(std.string(name)))) {
return url
}
return nil
}
/// The optional URL for the map notes of the OpenStreetMap profile
static var notesUrl: URL? {
if let name, let url = URL(string: String(describing: osm.OsmOAuth.ServerAuth().GetNotesURL(std.string(name)))) {
return url
}
return nil
}
/// The URL for deleting an OpenStreetMap profile
static var deleteUrl: URL {
return URL(string: String(describing: osm.OsmOAuth.ServerAuth().GetDeleteURL()))!
}
// MARK: Methods
/// Save the authorization token based on a code during the Oauth process
/// - Parameter authorizationCode: The code
static func saveAuthorizationToken(from authorizationCode: String) async {
var serverAuth = osm.OsmOAuth.ServerAuth()
let authorizationToken = String(describing: serverAuth.FinishAuthorization(std.string(authorizationCode)))
serverAuth.SetAuthToken(std.string(authorizationToken))
let userDefaults = UserDefaults.standard
userDefaults.set(authorizationToken, forKey: userDefaultsKeyAuthorizationToken)
if let userPreferences = await reloadUserPreferences() {
userDefaults.set(String(describing: userPreferences.m_displayName), forKey: userDefaultsKeyName)
userDefaults.set(Int(userPreferences.m_changesets), forKey: userDefaultsKeyNumberOfEdits)
userDefaults.set(false, forKey: userDefaultsNeedsReauthorization)
}
}
/// Reload the OpenStreetMap profile data
/// - Returns: Optional profile data
static private func reloadUserPreferences() async -> osm.UserPreferences? {
var userPreferences: osm.UserPreferences? = nil
userPreferences = osm.ServerApi06(osm.OsmOAuth.ServerAuth(std.string(authorizationToken ?? ""))).GetUserPreferences()
if let userPreferences, userPreferences.m_id == 0 {
return nil
}
return userPreferences
}
/// Reload the OpenStreetMap profile
static func reload() async {
if isExisting {
// Could be done in nicer way, but that would require iOS 17+
await withCheckedContinuation { continuation in
let networkPathMonitor: NWPathMonitor = NWPathMonitor()
networkPathMonitor.pathUpdateHandler = { path in
Task {
if path.status != .unsatisfied {
let userDefaults = UserDefaults.standard
if let userPreferences = await reloadUserPreferences() {
userDefaults.set(String(describing: userPreferences.m_displayName), forKey: userDefaultsKeyName)
userDefaults.set(Int(userPreferences.m_changesets), forKey: userDefaultsKeyNumberOfEdits)
} else if path.status == .satisfied {
userDefaults.set(true, forKey: userDefaultsNeedsReauthorization)
}
}
networkPathMonitor.cancel()
continuation.resume()
}
}
networkPathMonitor.start(queue: .main)
}
}
}
/// Logout of the OpenStreetMap profile
static func logout() {
let userDefaults = UserDefaults.standard
userDefaults.removeObject(forKey: userDefaultsKeyAuthorizationToken)
userDefaults.removeObject(forKey: userDefaultsKeyName)
userDefaults.removeObject(forKey: userDefaultsKeyNumberOfEdits)
userDefaults.removeObject(forKey: userDefaultsNeedsReauthorization)
}
}

View file

@ -0,0 +1,30 @@
extension Settings {
/// The announcing of speed traps for voice guidance during routing
@objc enum AnnouncingSpeedTrapsWhileVoiceRouting: Int, Codable, CaseIterable, Identifiable {
case always = 1
case onlyWhenTooFast = 2
case never = 0
// MARK: Properties
/// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .always:
return String(localized: "pref_tts_speedcams_always")
case .onlyWhenTooFast:
return String(localized: "pref_tts_speedcams_auto")
case .never:
return String(localized: "pref_tts_speedcams_never")
default:
return String()
}
}
}
}

View file

@ -0,0 +1,30 @@
extension Settings {
/// The visual appeareance
@objc enum Appearance: Int, Codable, CaseIterable, Identifiable {
case auto = 1
case light = 2
case dark = 3
// MARK: Properties
/// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .auto:
return String(localized: "auto")
case .light:
return String(localized: "pref_appearance_light")
case .dark:
return String(localized: "pref_appearance_dark")
default:
return String()
}
}
}
}

View file

@ -0,0 +1,27 @@
extension Settings {
/// The unit system used for distances
@objc enum DistanceUnit: Int, Codable, CaseIterable, Identifiable {
case metric = 0
case imperial = 1
// MARK: Properties
/// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .metric:
return String(localized: "kilometres")
case .imperial:
return String(localized: "miles")
default:
return String()
}
}
}
}

View file

@ -0,0 +1,31 @@
extension Settings {
/// A language
protocol Language: Codable, Identifiable, Equatable, Comparable {
// MARK: Properties
/// The id
var id: String { get }
/// The localized name
var localizedName: String { get }
}
}
// MARK: - Comparable
extension Settings.Language {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}
// MARK: - Comparable
extension Settings.Language {
static func < (lhs: Self, rhs: Self) -> Bool {
return lhs.localizedName.localizedCaseInsensitiveCompare(rhs.localizedName) == .orderedAscending
}
}

View file

@ -0,0 +1,52 @@
extension Settings {
/// The type of the left bottom bar button
enum LeftButtonType: String, Codable, CaseIterable, Identifiable {
case hidden = "Hidden"
case addPlace = "AddPlace"
case recordTrack = "RecordTrack"
case settings = "Settings"
case help = "Help"
// MARK: Properties
/// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .hidden:
return String(localized: "disabled")
case .addPlace:
return String(localized: "placepage_add_place_button")
case .recordTrack:
return String(localized: "start_track_recording")
case .settings:
return String(localized: "settings")
case .help:
return String(localized: "help")
}
}
/// The image
var image: UIImage {
let configuration = UIImage.SymbolConfiguration(pointSize: 24, weight: .semibold)
switch self {
case .addPlace:
return UIImage(systemName: "plus", withConfiguration: configuration)!
case .recordTrack:
return UIImage(named: "track", in: nil, with: configuration)!
case .settings:
return UIImage(systemName: "gearshape.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .semibold))!
case .help:
return UIImage(systemName: "info.circle", withConfiguration: configuration)!
default:
return UIImage()
}
}
}
}

View file

@ -0,0 +1,13 @@
extension Settings {
/// A language used for the map
struct MapLanguage: Language {
// MARK: Properties
/// The id
let id: String
/// The localized name
let localizedName: String
}
}

View file

@ -0,0 +1,30 @@
extension Settings {
/// The mobile data policy
@objc enum MobileDataPolicy: Int, Codable, CaseIterable, Identifiable {
case always = 1
case ask = 2
case never = 0
// MARK: Properties
/// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .always:
return String(localized: "mobile_data_option_always")
case .ask:
return String(localized: "mobile_data_option_ask")
case .never:
return String(localized: "mobile_data_option_never")
default:
return String()
}
}
}
}

View file

@ -0,0 +1,30 @@
extension Settings {
/// The power saving mode
@objc enum PowerSavingMode: Int, Codable, CaseIterable, Identifiable {
case auto = 1
case maximum = 2
case never = 0
// MARK: Properties
/// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .auto:
return String(localized: "power_managment_setting_auto")
case .maximum:
return String(localized: "power_managment_setting_manual_max")
case .never:
return String(localized: "power_managment_setting_never")
default:
return String()
}
}
}
}

View file

@ -0,0 +1,13 @@
extension Settings {
/// A language used for voice guidance during routing
struct VoiceRoutingLanguage: Language {
// MARK: Properties
/// The id
let id: String
/// The localized name
let localizedName: String
}
}

View file

@ -0,0 +1,485 @@
import Combine
import AVFoundation
/// The settings
@objc class Settings: NSObject {
// MARK: Properties
// The notification name for changed routing options
static let routingOptionsChangedNotificationName: Notification.Name = Notification.Name(rawValue: "RoutingOptionsChanged")
/// Key for storing if the sync beta alert has been shown in the user defaults
static private let userDefaultsKeyHasShownSyncBetaAlert = "kUDDidShowICloudSynchronizationEnablingAlert"
/// Key for storing the type of action used for the bottom left main interface button in the user defaults
static private let userDefaultsKeyLeftButtonType = "LeftButtonType"
/// Key for storing the map appearance in the user defaults
static private let userDefaultsKeyMapAppearance = "MapAppearance"
/// The current distance unit
static var distanceUnit: DistanceUnit {
get {
if SettingsBridge.measurementUnits() == .imperial {
return .imperial
} else {
return .metric
}
}
set {
if newValue == .imperial {
SettingsBridge.setMeasurementUnits(.imperial)
} else {
SettingsBridge.setMeasurementUnits(.metric)
}
}
}
/// If zoom buttons should be displayed
@objc static var hasZoomButtons: Bool {
get {
return SettingsBridge.zoomButtonsEnabled()
}
set {
SettingsBridge.setZoomButtonsEnabled(newValue)
}
}
/// The type of action used for the bottom left main interface button
static var leftButtonType: LeftButtonType {
get {
if let leftButtonTypeRawValue = UserDefaults.standard.string(forKey: userDefaultsKeyLeftButtonType), let leftButtonType = LeftButtonType(rawValue: leftButtonTypeRawValue) {
return leftButtonType
}
return .help
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: userDefaultsKeyLeftButtonType)
}
}
/// If 3D buildings should be displayed
@objc static var has3dBuildings: Bool {
get {
return SettingsBridge.buildings3dViewEnabled()
}
set {
SettingsBridge.setBuildings3dViewEnabled(newValue)
}
}
/// If automatic map downloads should be enabled
@objc static var hasAutomaticDownload: Bool {
get {
return SettingsBridge.autoDownloadEnabled()
}
set {
SettingsBridge.setAutoDownloadEnabled(newValue)
}
}
/// The current mobile data policy
@objc static var mobileDataPolicy: MobileDataPolicy {
get {
let networkPolicyPermission = NetworkPolicy.shared().permission
if networkPolicyPermission == .always {
return .always
} else if networkPolicyPermission == .never {
return .never
} else {
return .ask
}
}
set {
if newValue == .always {
NetworkPolicy.shared().permission = .always
} else if newValue == .never {
NetworkPolicy.shared().permission = .never
} else {
NetworkPolicy.shared().permission = .ask
}
}
}
/// The current power saving mode
@objc static var powerSavingMode: PowerSavingMode {
get {
return PowerSavingMode(rawValue: SettingsBridge.powerManagement()) ?? .never
}
set {
SettingsBridge.setPowerManagement(newValue.rawValue)
}
}
/// If an increased font size should be used for map labels
@objc static var hasIncreasedFontsize: Bool {
get {
return SettingsBridge.largeFontSize()
}
set {
SettingsBridge.setLargeFontSize(newValue)
}
}
/// If names should be transliterated to Latin
@objc static var shouldTransliterateToLatin: Bool {
get {
return SettingsBridge.transliteration()
}
set {
SettingsBridge.setTransliteration(newValue)
}
}
/// The available languages for the map
static var availableLanguagesForMap: [MapLanguage] {
var languages = SettingsBridge.availableMapLanguages().map { language in
return MapLanguage(id: language.key, localizedName: language.value)
}.sorted()
languages.insert(MapLanguage(id: "default", localizedName: String(localized: "pref_maplanguage_local")), at: 0)
languages.insert(MapLanguage(id: "auto", localizedName: String(localized: "auto")), at: 0)
return languages
}
/// The current language for the map
static var languageForMap: MapLanguage.ID {
get {
return SettingsBridge.mapLanguageCode()
}
set {
SettingsBridge.setMapLanguageCode(newValue)
}
}
/// If the compass should be calibrated
@objc static var shouldCalibrateCompass: Bool {
get {
return SettingsBridge.compassCalibrationEnabled()
}
set {
SettingsBridge.setCompassCalibrationEnabled(newValue)
}
}
/// The current map appearance
@objc static var mapAppearance: Appearance {
get {
let mapAppearanceRawValue = UserDefaults.standard.integer(forKey: userDefaultsKeyMapAppearance)
if mapAppearanceRawValue != 0, let mapAppearance = Appearance(rawValue: mapAppearanceRawValue) {
return mapAppearance
}
return .auto
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: userDefaultsKeyMapAppearance)
ThemeManager.invalidate()
}
}
/// The current appearance
@objc static var appearance: Appearance {
get {
let theme = SettingsBridge.theme()
if theme == MWMTheme.day {
return .light
} else if theme == MWMTheme.night {
return .dark
} else {
return .auto
}
}
set {
if newValue == .light {
SettingsBridge.setTheme(MWMTheme.day)
} else if newValue == .dark {
SettingsBridge.setTheme(MWMTheme.night)
} else {
SettingsBridge.setTheme(MWMTheme.auto)
}
}
}
/// If the bookmarks should be synced via iCloud
@objc static var shouldSync: Bool {
get {
return SettingsBridge.iCLoudSynchronizationEnabled()
}
set {
SettingsBridge.setICLoudSynchronizationEnabled(newValue)
}
}
/// If the sync beta alert has been shown
@objc static var hasShownSyncBetaAlert: Bool {
get {
return UserDefaults.standard.bool(forKey: userDefaultsKeyHasShownSyncBetaAlert)
}
set {
UserDefaults.standard.set(newValue, forKey: userDefaultsKeyHasShownSyncBetaAlert)
}
}
/// The publisher for state changes of the iCloud sync
static var syncStatePublisher: AnyPublisher<SynchronizationManagerState, Never> {
iCloudSynchronizaionManager.shared.notifyObservers()
return iCloudSynchronizaionManager.shared.statePublisher
}
/// If our custom logging is enabled
@objc static var isLogging: Bool {
get {
return SettingsBridge.isFileLoggingEnabled()
}
set {
SettingsBridge.setFileLoggingEnabled(newValue)
}
}
/// The formatter for the size of the log file
@objc static var logSizeFormatter: MeasurementFormatter {
let measurementFormatter = MeasurementFormatter()
measurementFormatter.unitStyle = .medium
measurementFormatter.unitOptions = .naturalScale
return measurementFormatter
}
/// The size of the log file in bytes
@objc static var logSize: Measurement<UnitInformationStorage> {
return Measurement<UnitInformationStorage>(value: Double(SettingsBridge.logFileSize()), unit: .bytes)
}
/// If the perspective view should be used during routing
@objc static var hasPerspectiveViewWhileRouting: Bool {
get {
return SettingsBridge.perspectiveViewEnabled()
}
set {
SettingsBridge.setPerspectiveViewEnabled(newValue)
}
}
/// If auto zoom should be used during routing
@objc static var hasAutoZoomWhileRouting: Bool {
get {
return SettingsBridge.autoZoomEnabled()
}
set {
SettingsBridge.setAutoZoomEnabled(newValue)
}
}
/// If voice guidance should be provided during routing
@objc static var shouldProvideVoiceRouting: Bool {
get {
return MWMTextToSpeech.isTTSEnabled()
}
set {
MWMTextToSpeech.setTTSEnabled(newValue)
}
}
/// The available languages for voice guidance during routing
static var availableLanguagesForVoiceRouting: [VoiceRoutingLanguage] {
return MWMTextToSpeech.availableLanguages().map { language in
return VoiceRoutingLanguage(id: language.key, localizedName: language.value)
}.sorted()
}
/// The current language for voice guidance during routing
@objc static var languageForVoiceRouting: VoiceRoutingLanguage.ID {
get {
return MWMTextToSpeech.selectedLanguage()
}
set {
MWMTextToSpeech.tts().setNotificationsLocale(newValue)
}
}
/// The voice used for voice guidance during routing
@objc static var voiceForVoiceRouting: String? {
if let voice = MWMTextToSpeech.tts().voice() {
return voice.name
}
return nil
}
/// If street names should be announced in the voice guidance during routing
@objc static var shouldAnnounceStreetnamesWhileVoiceRouting: Bool {
get {
return MWMTextToSpeech.isStreetNamesTTSEnabled()
}
set {
MWMTextToSpeech.setStreetNamesTTSEnabled(newValue)
}
}
/// The current announcement of speed traps in the voice guidance during routing
@objc static var announcingSpeedTrapsWhileVoiceRouting: AnnouncingSpeedTrapsWhileVoiceRouting {
get {
return AnnouncingSpeedTrapsWhileVoiceRouting(rawValue: MWMTextToSpeech.speedCameraMode()) ?? .never
}
set {
MWMTextToSpeech.setSpeedCameraMode(newValue.rawValue)
}
}
/// If toll roads should be avoided during routing
@objc static var shouldAvoidTollRoadsWhileRouting: Bool {
get {
return RoutingOptions().avoidToll
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidToll = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If unpaved roads should be avoided during routing
@objc static var shouldAvoidUnpavedRoadsWhileRouting: Bool {
get {
return RoutingOptions().avoidDirty
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidDirty = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If paved roads should be avoided during routing
@objc static var shouldAvoidPavedRoadsWhileRouting: Bool {
get {
return RoutingOptions().avoidPaved
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidPaved = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If ferries should be avoided during routing
@objc static var shouldAvoidFerriesWhileRouting: Bool {
get {
return RoutingOptions().avoidFerry
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidFerry = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If motorways should be avoided during routing
@objc static var shouldAvoidMotorwaysWhileRouting: Bool {
get {
return RoutingOptions().avoidMotorway
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidMotorway = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
/// If steps should be avoided during routing
@objc static var shouldAvoidStepsWhileRouting: Bool {
get {
return RoutingOptions().avoidSteps
}
set {
let routingOptions = RoutingOptions()
routingOptions.avoidSteps = newValue
routingOptions.save()
NotificationCenter.default.post(name: routingOptionsChangedNotificationName, object: nil)
}
}
// MARK: Methods
/// Create a bookmarks backup before enabling the sync beta
/// - Parameter completionHandler: A compeltion handler, which returns if a backup has been created
static func createBookmarksBackupBecauseOfSyncBeta(completionHandler: ((_ hasCreatedBackup: Bool) -> Void)?) {
BookmarksManager.shared().shareAllCategories { status, url in
switch status {
case .success:
let window = (UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }.first(where: { $0 is UIWindowScene }) as? UIWindowScene)?.keyWindow
if let viewController = window?.rootViewController?.presentedViewController {
let shareController = ActivityViewController.share(for: url, message: String(localized: "share_bookmarks_email_body")) { _, _, _, _ in
completionHandler?(true)
}
shareController.present(inParentViewController: viewController, anchorView: nil)
}
case .emptyCategory:
Toast.show(withText: String(localized: "bookmarks_error_title_share_empty"))
completionHandler?(false)
case .archiveError:
Toast.show(withText: String(localized: "dialog_routing_system_error"))
completionHandler?(false)
case .fileError:
Toast.show(withText: String(localized: "dialog_routing_system_error"))
completionHandler?(false)
}
}
}
/// Play a test audio snippet for voice guidance during routing
@objc static func playVoiceRoutingTest() {
MWMTextToSpeech.playTest()
}
}

View file

@ -0,0 +1,85 @@
import SwiftUI
enum SocialMedia: CaseIterable, Identifiable {
case codeberg
case mastodon
case matrix
case lemmy
case bluesky
case pixelfed
case email
// MARK: Properties
/// The e-mail address
static let emailAddress: String = "ios@comaps.app"
//// The id
var id: Self { self }
/// The description text
var description: String {
switch self {
case .codeberg:
return String(localized: "social_codeberg")
case .mastodon:
return String(localized: "social_mastodon")
case .matrix:
return String(localized: "social_matrix")
case .lemmy:
return String(localized: "social_lemmy")
case .bluesky:
return String(localized: "social_bluesky")
case .pixelfed:
return String(localized: "social_pixelfed")
case .email:
return String(localized: "social_email")
}
}
/// The url
var url: URL {
switch self {
case .codeberg:
return URL(string: "https://codeberg.org/comaps/")!
case .mastodon:
return URL(string: "https://floss.social/@CoMaps")!
case .matrix:
return URL(string: "https://matrix.to/#/#comaps:matrix.org")!
case .lemmy:
return URL(string: "https://sopuli.xyz/c/CoMaps")!
case .bluesky:
return URL(string: "https://bsky.app/profile/comaps.app")!
case .pixelfed:
return URL(string: "https://pixelfed.social/CoMaps")!
case .email:
return URL(string: "mailto:\(SocialMedia.emailAddress)")!
}
}
/// The image text
var image: Image {
switch self {
case .codeberg:
return Image(.SocialMedia.codeberg)
case .mastodon:
return Image(.SocialMedia.mastodon)
case .matrix:
return Image(.SocialMedia.matrix)
case .lemmy:
return Image(.SocialMedia.lemmy)
case .bluesky:
return Image(.SocialMedia.bluesky)
case .pixelfed:
return Image(.SocialMedia.pixelfed)
case .email:
return Image(systemName: "envelope.fill")
}
}
}