Skip to content

Commit

Permalink
Merge pull request #132 from makinosp/develop
Browse files Browse the repository at this point in the history
  • Loading branch information
makinosp authored Sep 21, 2024
2 parents 1a92e51 + b7594ed commit f75e2f0
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 94 deletions.
6 changes: 4 additions & 2 deletions Harmonie.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.7.1;
MARKETING_VERSION = 0.7.2;
PRODUCT_BUNDLE_IDENTIFIER = jp.mknn.harmonie;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = minimal;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
Expand Down Expand Up @@ -505,10 +506,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.7.1;
MARKETING_VERSION = 0.7.2;
PRODUCT_BUNDLE_IDENTIFIER = jp.mknn.harmonie;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = minimal;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
Expand Down
25 changes: 19 additions & 6 deletions harmonie/Components/CircleURLImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,36 @@ struct CircleURLImage: View {
var body: some View {
lazyImage(url: imageUrl) {
lazyImage(url: thumbnailImageUrl) {
ProgressView()
.controlSize(.small)
.frame(size: size)
defaultPlaceholder
}
}
}

func lazyImage(url: URL?, placeholder: @escaping () -> some View) -> some View {
private var defaultPlaceholder: some View {
shape
.fill(color)
.frame(size: size)
}

private var shape: some Shape {
Circle()
}

private var color: some ShapeStyle {
Color(.systemFill)
}

private func lazyImage(url: URL?, placeholder: @escaping () -> some View) -> some View {
LazyImage(url: imageUrl) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(size: size)
.clipShape(Circle())
.clipShape(shape)
} else if state.error != nil {
Constants.Icon.exclamation
defaultPlaceholder
.overlay(Constants.Icon.exclamation)
.frame(size: size)
} else {
placeholder()
Expand Down
2 changes: 1 addition & 1 deletion harmonie/Extensions/Binding+NilCoalescing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SwiftUI

extension Binding {
static func ?? <T>(optional: Self, defaultValue: T) -> Binding<T> where Value == T? {
static func ?? <T>(optional: Self, defaultValue: T) -> Binding<T> where T: Sendable, Value == T? {
.init(
get: { optional.wrappedValue ?? defaultValue },
set: { optional.wrappedValue = $0 }
Expand Down
18 changes: 18 additions & 0 deletions harmonie/Utils/BundleUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BundleUtil.swift
// Harmonie
//
// Created by makinosp on 2024/09/21.
//

import Foundation

enum BundleUtil {
static var appName: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? ""
}

static var appVersion: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
}
}
5 changes: 2 additions & 3 deletions harmonie/Utils/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,8 @@ enum Constants {
}

enum IconSize {
static let thumbnail = CGSize(width: 28, height: 28)
static let thumbnailOutside = CGSize(width: 32, height: 32)
static let ll = CGSize(width: 40, height: 40)
static let thumbnail = CGSize(width: 32, height: 32)
static let ll = CGSize(width: 44, height: 44)
}

enum Messages {
Expand Down
2 changes: 1 addition & 1 deletion harmonie/Utils/DateUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

final class DateUtil {
actor DateUtil: Sendable {
static let shared = DateUtil()
private let relativeDateTimeFormatter: RelativeDateTimeFormatter
private let comparedComponents: Set<Calendar.Component>
Expand Down
2 changes: 1 addition & 1 deletion harmonie/Utils/KeychainUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

final class KeychainUtil {
actor KeychainUtil: Sendable {
static let shared = KeychainUtil()
private init() {}

Expand Down
6 changes: 3 additions & 3 deletions harmonie/ViewModels/AppViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,19 @@ final class AppViewModel {
username == Constants.Values.previewUser && password == Constants.Values.previewUser
}

private func setCredential(username: String, password: String, isSavedOnKeyChain: Bool) {
private func setCredential(username: String, password: String, isSavedOnKeyChain: Bool) async {
isPreviewMode = isPreviewUser(username: username, password: password)
client.setCledentials(username: username, password: password)
if isSavedOnKeyChain {
_ = KeychainUtil.shared.savePassword(password, for: username)
_ = await KeychainUtil.shared.savePassword(password, for: username)
}
}

func login(username: String, password: String, isSavedOnKeyChain: Bool) async -> VerifyType? {
isPreviewMode = isPreviewUser(username: username, password: password)
client.setCledentials(username: username, password: password)
if isSavedOnKeyChain {
_ = KeychainUtil.shared.savePassword(password, for: username)
_ = await KeychainUtil.shared.savePassword(password, for: username)
}
return await login()
}
Expand Down
102 changes: 83 additions & 19 deletions harmonie/Views/Authentication/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,60 +15,124 @@ struct LoginView: View, AuthenticationServicePresentable {
@State private var verifyType: VerifyType?
@State private var password: String = ""
@State private var isRequesting = false
@State private var isPresentedPopover = false
@State private var isPresentedSecurityPopover = false
@State private var isPresentedSavingPasswordPopover = false

var body: some View {
NavigationStack {
VStack(spacing: 16) {
title
loginFields
enterButton
}
.padding(32)
.ignoresSafeArea(.keyboard)
.padding(.horizontal, 24)
.navigationDestination(item: $verifyType) { verifyType in
OtpView(verifyType: verifyType)
.navigationBarBackButtonHidden()
}
}
.onAppear {
if isSavedOnKeyChain,
let password = KeychainUtil.shared.getPassword(for: username) {
.ignoresSafeArea(.keyboard)
.task {
guard isSavedOnKeyChain else { return }
if let password = await KeychainUtil.shared.getPassword(for: username) {
self.password = password
}
}
}

private var title: some View {
Text(BundleUtil.appName.uppercased())
.font(.custom("Avenir Next", size: titleFontSize))
.kerning(titleKerning)
}

private var subtitle: some View {
Group {
Text("Login")
.font(.headline)
VStack {
Text("Connect your VRChat account")
.foregroundStyle(Color(.systemGray))
.font(.body)
Button {
isPresentedSecurityPopover.toggle()
} label: {
Text("Is this secure?")
}
.popover(isPresented: $isPresentedSecurityPopover) {
securityPopover
}
}
}
}

private var titleFontSize: CGFloat {
UIDevice.current.userInterfaceIdiom == .pad ? 56 : 28
}

private var titleKerning: CGFloat {
UIDevice.current.userInterfaceIdiom == .pad ? 28 : 14
}

private var loginFields: some View {
VStack(spacing: 8) {
VStack(alignment: .leading, spacing: 8) {
TextField("UserName", text: $username)
.textInputAutocapitalization(.never)
.textFieldStyle(.roundedBorder)
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
Toggle(isOn: $isSavedOnKeyChain) {
HStack {
Label {
Text("Store in Keychain")
} icon: {
Image(systemName: "key.icloud")
}
LabeledContent {
Button {
isPresentedPopover.toggle()
isPresentedSavingPasswordPopover.toggle()
} label: {
Image(systemName: "questionmark.circle")
}
.popover(isPresented: $isPresentedPopover) {
Text(Constants.Messages.helpWithStoringKeychain)
.padding()
.presentationDetents([.fraction(1/4)])
.popover(isPresented: $isPresentedSavingPasswordPopover) {
savingPasswordPopover
}
} label: {
Label {
Text("Save Password")
} icon: {
Image(systemName: "key.icloud")
}
.foregroundStyle(Color(.systemGray))
}
.font(.callout)
.foregroundStyle(Color(.systemGray))
}
}
.frame(maxWidth: 560)
.padding(.horizontal, 8)
}

private var securityPopover: some View {
VStack(alignment: .leading) {
Text("Is this secure?")
.font(.headline)
.foregroundStyle(Color(.label))
Text(Constants.Messages.helpWithStoringKeychain)
.fixedSize(horizontal: false, vertical: true)
}
.foregroundStyle(Color(.systemGray))
.frame(width: WindowUtil.width * 2 / 3)
.padding()
.presentationDetents([.fraction(0.25)])
}

private var savingPasswordPopover: some View {
VStack(alignment: .leading) {
Text("In What Way?")
.font(.headline)
.foregroundStyle(Color(.label))
Text(Constants.Messages.helpWithStoringKeychain)
.fixedSize(horizontal: false, vertical: true)
}
.frame(width: WindowUtil.width * 2 / 3)
.padding()
.presentationDetents([.fraction(0.25)])
}

private var enterButton: some View {
AsyncButton {
await loginAction()
Expand Down
21 changes: 20 additions & 1 deletion harmonie/Views/Authentication/OtpView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ struct OtpView: View, AuthenticationServicePresentable {
@Environment(AppViewModel.self) var appVM: AppViewModel
@State private var code: String = ""
@State private var isRequesting = false
let verifyType: VerifyType?
private let verifyType: VerifyType

init(verifyType: VerifyType) {
self.verifyType = verifyType
}

var body: some View {
VStack(spacing: 16) {
Text("Two-step verification")
.font(.headline)
Text("Enter the 6-digit two-factor verification code recieved in your \(verifyType.method).")
.foregroundStyle(Color(.systemGray))
.font(.body)
TextField("Code", text: $code)
.keyboardType(.decimalPad)
.textFieldStyle(.roundedBorder)
.padding(.horizontal, 8)
.frame(maxWidth: 240)
enterButton
}
.padding(32)
Expand Down Expand Up @@ -56,6 +66,15 @@ struct OtpView: View, AuthenticationServicePresentable {
}
}

extension VerifyType {
var method: String {
switch self {
case .emailOtp: "email"
default: "authenticator app"
}
}
}

#Preview {
OtpView(verifyType: .totp)
.environment(AppViewModel())
Expand Down
51 changes: 28 additions & 23 deletions harmonie/Views/Location/LocationCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,38 @@ struct LocationCardView: View, InstanceServicePresentable {
imageUrl: instance.world.imageUrl(.x512),
thumbnailImageUrl: instance.world.imageUrl(.x256)
)
HStack {
VStack(alignment: .leading) {
Text(instance.world.name)
.font(.body)
.lineLimit(1)
HStack {
Text(instance.typeDescription)
.font(.footnote)
.foregroundStyle(Color.gray)
Text(personAmount(instance))
.font(.footnote)
.foregroundStyle(Color.gray)
VStack(spacing: 4) {
HStack {
VStack(alignment: .leading) {
Text(instance.world.name)
.font(.body)
.lineLimit(1)
HStack {
Text(instance.typeDescription)
.font(.footnote)
.foregroundStyle(Color.gray)
Text(personAmount(instance))
.font(.footnote)
.foregroundStyle(Color.gray)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if UIDevice.current.userInterfaceIdiom == .phone {
Constants.Icon.forward
}
ScrollView(.horizontal) {
HStack(spacing: -8) {
ForEach(location.friends) { friend in
CircleURLImage(
imageUrl: friend.imageUrl(.x256),
size: Constants.IconSize.thumbnail
)
}
}
ScrollView(.horizontal) {
HStack(spacing: -8) {
ForEach(location.friends) { friend in
CircleURLImage(
imageUrl: friend.imageUrl(.x256),
size: Constants.IconSize.thumbnail
)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if UIDevice.current.userInterfaceIdiom == .phone {
Constants.Icon.forward
.onTapGesture {
selected = InstanceLocation(location: location, instance: instance)
}
}
}
Expand Down
Loading

0 comments on commit f75e2f0

Please sign in to comment.