diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 965b3b26..d2470c8a 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; }; 9FBFE641292A715600C250E9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FBFE640292A715600C250E9 /* Assets.xcassets */; }; 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; }; + 9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -40,6 +41,7 @@ 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = ""; }; 9FBFE640292A715600C250E9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9FBFE642292A715600C250E9 /* IceCubesApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IceCubesApp.entitlements; sourceTree = ""; }; + 9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,8 +84,8 @@ 9FAE4AC9293783A200772766 /* Tabs */ = { isa = PBXGroup; children = ( + 9FE151A4293C90EA00E9683D /* Settings */, 9F398AB229360A4C00A889F2 /* TimelineTab.swift */, - 9FAE4ACA293783B000772766 /* SettingsTab.swift */, ); path = Tabs; sourceTree = ""; @@ -137,6 +139,15 @@ name = Frameworks; sourceTree = ""; }; + 9FE151A4293C90EA00E9683D /* Settings */ = { + isa = PBXGroup; + children = ( + 9FAE4ACA293783B000772766 /* SettingsTab.swift */, + 9FE151A5293C90F900E9683D /* IconSelectorView.swift */, + ); + path = Settings; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -218,6 +229,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */, 9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */, 9FAE4AD32937A0C600772766 /* AppAccountsManager.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, @@ -342,6 +354,7 @@ 9FBFE649292A715600C250E9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate1 AppIconAlternate2 AppIconAlternate3 AppIconAlternate4"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; @@ -381,6 +394,7 @@ 9FBFE64A292A715600C250E9 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate1 AppIconAlternate2 AppIconAlternate3 AppIconAlternate4"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; diff --git a/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift new file mode 100644 index 00000000..2cfc2b76 --- /dev/null +++ b/IceCubesApp/App/Tabs/Settings/IconSelectorView.swift @@ -0,0 +1,54 @@ +import SwiftUI + +struct IconSelectorView: View { + enum Icon: String, CaseIterable, Identifiable { + var id: String { + self.rawValue + } + + case primary = "AppIcon" + case alternate1 = "AppIconAlternate1" + case alternate2 = "AppIconAlternate2" + case alternate3 = "AppIconAlternate3" + case alternate4 = "AppIconAlternate4" + } + + @State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.rawValue + + private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))] + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + LazyVGrid(columns: columns, spacing: 6) { + ForEach(Icon.allCases) { icon in + Button { + currentIcon = icon.rawValue + if icon.rawValue == Icon.primary.rawValue { + UIApplication.shared.setAlternateIconName(nil) + } else { + UIApplication.shared.setAlternateIconName(icon.rawValue) + } + } label: { + ZStack(alignment: .bottomTrailing) { + Image(uiImage: .init(named: icon.rawValue)!) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(minHeight: 125, maxHeight: 1024) + .cornerRadius(6) + .shadow(radius: 3) + if icon.rawValue == currentIcon { + Image(systemName: "checkmark.seal.fill") + .padding(4) + .tint(.green) + } + } + } + } + } + } + .padding(6) + .navigationTitle("Icons") + } + } +} diff --git a/IceCubesApp/App/Tabs/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift similarity index 53% rename from IceCubesApp/App/Tabs/SettingsTab.swift rename to IceCubesApp/App/Tabs/Settings/SettingsTab.swift index c5fa9267..1539060a 100644 --- a/IceCubesApp/App/Tabs/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -12,27 +12,15 @@ struct SettingsTabs: View { @State private var signInInProgress = false @State private var accountData: Account? + @State private var instanceData: Instance? @State private var signInServer = IceCubesApp.defaultServer var body: some View { NavigationStack { Form { - Section("Account") { - if let accountData { - VStack(alignment: .leading) { - Text(appAccountsManager.currentAccount.server) - .font(.headline) - Text(accountData.displayName) - Text(accountData.username) - .font(.footnote) - .foregroundColor(.gray) - } - signOutButton - } else { - TextField("Mastodon server", text: $signInServer) - signInButton - } - } + appSection + accountSection + instanceSection } .onOpenURL(perform: { url in Task { @@ -46,11 +34,61 @@ struct SettingsTabs: View { if appAccountsManager.currentAccount.oauthToken != nil { signInInProgress = true await refreshAccountInfo() + await refreshInstanceInfo() signInInProgress = false } } } + private var accountSection: some View { + Section("Account") { + if let accountData { + VStack(alignment: .leading) { + Text(appAccountsManager.currentAccount.server) + .font(.headline) + Text(accountData.displayName) + Text(accountData.username) + .font(.footnote) + .foregroundColor(.gray) + } + signOutButton + } else { + TextField("Mastodon server", text: $signInServer) + signInButton + } + } + } + + @ViewBuilder + private var instanceSection: some View { + if let instanceData { + Section("Instance info") { + LabeledContent("Name", value: instanceData.title) + Text(instanceData.shortDescription) + LabeledContent("Email", value: instanceData.email) + LabeledContent("Version", value: instanceData.version) + LabeledContent("Users", value: "\(instanceData.stats.userCount)") + LabeledContent("Status", value: "\(instanceData.stats.statusCount)") + LabeledContent("Domains", value: "\(instanceData.stats.domainCount)") + } + } + } + + private var appSection: some View { + Section("App") { + NavigationLink(destination: IconSelectorView()) { + Label { + Text("Icon selector") + } icon: { + Image(uiImage: .init(named: UIApplication.shared.alternateIconName ?? "AppIcon")!) + .resizable() + .frame(width: 25, height: 25) + .cornerRadius(4) + } + } + } + } + private var signInButton: some View { Button { signInInProgress = true @@ -68,6 +106,7 @@ struct SettingsTabs: View { private var signOutButton: some View { Button { + instanceData = nil accountData = nil appAccountsManager.delete(account: appAccountsManager.currentAccount) } label: { @@ -91,6 +130,7 @@ struct SettingsTabs: View { let oauthToken = try await client.continueOauthFlow(url: url) appAccountsManager.add(account: AppAccount(server: client.server, oauthToken: oauthToken)) await refreshAccountInfo() + await refreshInstanceInfo() signInInProgress = false } catch { signInInProgress = false @@ -100,4 +140,8 @@ struct SettingsTabs: View { private func refreshAccountInfo() async { accountData = try? await client.get(endpoint: Accounts.verifyCredentials) } + + private func refreshInstanceInfo() async { + instanceData = try? await client.get(endpoint: Instances.instance) + } } diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 532cd729..36cc4acb 100644 --- a/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/icon.png b/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 00000000..0fecb831 Binary files /dev/null and b/IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate1.appiconset/Contents.json b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate1.appiconset/Contents.json new file mode 100644 index 00000000..a657e336 --- /dev/null +++ b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate1.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate1.appiconset/icon.png b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate1.appiconset/icon.png new file mode 100644 index 00000000..a2a93988 Binary files /dev/null and b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate1.appiconset/icon.png differ diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate2.appiconset/Contents.json b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate2.appiconset/Contents.json new file mode 100644 index 00000000..a657e336 --- /dev/null +++ b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate2.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate2.appiconset/icon.png b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate2.appiconset/icon.png new file mode 100644 index 00000000..ba115a3b Binary files /dev/null and b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate2.appiconset/icon.png differ diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate3.appiconset/Contents.json b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate3.appiconset/Contents.json new file mode 100644 index 00000000..a657e336 --- /dev/null +++ b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate3.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate3.appiconset/icon.png b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate3.appiconset/icon.png new file mode 100644 index 00000000..7b34ce23 Binary files /dev/null and b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate3.appiconset/icon.png differ diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate4.appiconset/Contents.json b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate4.appiconset/Contents.json new file mode 100644 index 00000000..a657e336 --- /dev/null +++ b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate4.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "icon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate4.appiconset/icon.png b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate4.appiconset/icon.png new file mode 100644 index 00000000..e5034f3e Binary files /dev/null and b/IceCubesApp/Resources/Assets.xcassets/AppIconAlternate4.appiconset/icon.png differ diff --git a/Packages/Models/Sources/Models/Instance.swift b/Packages/Models/Sources/Models/Instance.swift new file mode 100644 index 00000000..631e05ea --- /dev/null +++ b/Packages/Models/Sources/Models/Instance.swift @@ -0,0 +1,18 @@ +import Foundation + +public struct Instance: Codable { + public struct Stats: Codable { + public let userCount: Int + public let statusCount: Int + public let domainCount: Int + } + + public let title: String + public let shortDescription: String + public let email: String + public let version: String + public let stats: Stats + public let languages: [String] + public let registrations: Bool + public let thumbnail: URL? +} diff --git a/Packages/Network/Sources/Network/Endpoint/Instances.swift b/Packages/Network/Sources/Network/Endpoint/Instances.swift new file mode 100644 index 00000000..9485fdc2 --- /dev/null +++ b/Packages/Network/Sources/Network/Endpoint/Instances.swift @@ -0,0 +1,16 @@ +import Foundation + +public enum Instances: Endpoint { + case instance + + public func path() -> String { + switch self { + case .instance: + return "instance" + } + } + + public func queryItems() -> [URLQueryItem]? { + nil + } +} diff --git a/README.md b/README.md index 580bd7e4..8997371c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # IceCubesApp A SwiftUI Mastodon client + +![Icon](IceCubesApp/Resources/Assets.xcassets/AppIcon.appiconset/icon.png?)