[App] Repositories in the Menu Bar view (#4)
This PR contains the work done to implement the rendering of the (lack of) repositories in the `MenuBarView` view UI interface. For this purpose, some components have been created: `ListHeader`, `ListItemEmpty`, and `ListItem`. Furthermore, some preview-specific code have been implemented as well, specially to add dummy repositories to the previews. Reviewed-on: #4 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
parent
f090784973
commit
3d78c599d1
@ -36,7 +36,6 @@
|
||||
UITests/UITests.swift,
|
||||
UITests/UITestsLaunchTests.swift,
|
||||
UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift,
|
||||
UnitTests/UnitTests.swift,
|
||||
);
|
||||
target = 46D4BE762CB06ED300FCFB84 /* Piper */;
|
||||
};
|
||||
@ -44,7 +43,6 @@
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift,
|
||||
UnitTests/UnitTests.swift,
|
||||
);
|
||||
target = 46D4BED32CB07C7A00FCFB84 /* UnitTests */;
|
||||
};
|
||||
@ -363,6 +361,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
@ -419,6 +418,7 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -431,7 +431,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = Piper/Resources/Catalogs/Previews.xcassets;
|
||||
DEVELOPMENT_ASSET_PATHS = "Piper/Sources/Previews/Extensions/Repository+Samples.swift Piper/Sources/Previews/Extensions/ModelContainer+Constants.swift Piper/Resources/Catalogs/Previews.xcassets";
|
||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -463,7 +463,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = Piper/Resources/Catalogs/Previews.xcassets;
|
||||
DEVELOPMENT_ASSET_PATHS = "Piper/Sources/Previews/Extensions/Repository+Samples.swift Piper/Sources/Previews/Extensions/ModelContainer+Constants.swift Piper/Resources/Catalogs/Previews.xcassets";
|
||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
56
Piper/Resources/Catalogs/Localizable.xcstrings
Normal file
56
Piper/Resources/Catalogs/Localizable.xcstrings
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"Add Item" : {
|
||||
|
||||
},
|
||||
"Item at %@" : {
|
||||
|
||||
},
|
||||
"menu-bar.item.empty.button.text" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Add a repository to the list"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu-bar.item.empty.title.text" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No repositories have been defined yet."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu-bar.section.repositories.button.text" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Add"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu-bar.section.repositories.title.text" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Repositories"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Select an item" : {
|
||||
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
@ -14,16 +14,14 @@ struct PiperApp: App {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
Item.self,
|
||||
])
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||
|
||||
private var container = {
|
||||
do {
|
||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
return try ModelContainer(
|
||||
for: .entities,
|
||||
configurations: [ModelConfiguration()]
|
||||
)
|
||||
} catch {
|
||||
fatalError("Could not create ModelContainer: \(error)")
|
||||
fatalError("ERROR: ModelContainer has not been created => \(error)")
|
||||
}
|
||||
}()
|
||||
|
||||
@ -31,20 +29,8 @@ struct PiperApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
MenuBarExtra {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Some text goes here...")
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Divider()
|
||||
|
||||
Button {
|
||||
// ...
|
||||
} label: {
|
||||
Text("Some text goes here...")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
MenuBarView()
|
||||
.modelContainer(container)
|
||||
} label: {
|
||||
Image(systemName: "circle.fill")
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
//
|
||||
// ModelConfiguration+Inits.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 05/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
extension ModelConfiguration {
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(
|
||||
_ name: String? = nil,
|
||||
schema: Schema = .entities,
|
||||
url: URL = .applicationSupportDirectory.appending(component: "piper.db")
|
||||
) {
|
||||
self.init(
|
||||
name,
|
||||
schema: schema,
|
||||
url: url,
|
||||
allowsSave: true,
|
||||
cloudKitDatabase: .none
|
||||
)
|
||||
}
|
||||
|
||||
}
|
20
Piper/Sources/Logic/Extensions/Schema+Constants.swift
Normal file
20
Piper/Sources/Logic/Extensions/Schema+Constants.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// Schema+Constants.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 05/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftData
|
||||
|
||||
extension Schema {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
static let entities = Schema([
|
||||
Repository.self
|
||||
])
|
||||
|
||||
|
||||
}
|
38
Piper/Sources/Logic/Models/Repository.swift
Normal file
38
Piper/Sources/Logic/Models/Repository.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// Repository.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 05/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Repository {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@Attribute(.unique) var path: URL
|
||||
|
||||
var addedAt: Date
|
||||
var sortOrder: Int
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(
|
||||
_ path: URL,
|
||||
sortOrder: Int,
|
||||
addedAt: Date = .now
|
||||
) {
|
||||
self.path = path
|
||||
self.addedAt = addedAt
|
||||
self.sortOrder = sortOrder
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
@Transient var name: String { path.lastPathComponent }
|
||||
|
||||
}
|
20
Piper/Sources/Logic/ViewModels/MenuBarViewModel.swift
Normal file
20
Piper/Sources/Logic/ViewModels/MenuBarViewModel.swift
Normal file
@ -0,0 +1,20 @@
|
||||
//
|
||||
// MenuBarViewModel.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
final class MenuBarViewModel {
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init () {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
//
|
||||
// ModelContainer+Constants.swift
|
||||
// Piper
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftData
|
||||
|
||||
extension ModelContainer {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
static let preview: ModelContainer = {
|
||||
do {
|
||||
return try ModelContainer(
|
||||
for: .entities,
|
||||
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
)
|
||||
} catch {
|
||||
fatalError("Model Container should have been created.")
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
36
Piper/Sources/Previews/Extensions/Repository+Samples.swift
Normal file
36
Piper/Sources/Previews/Extensions/Repository+Samples.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Repository+Samples.swift
|
||||
// Piper
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
extension Repository {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
@MainActor
|
||||
static func samples(in container: ModelContainer) {
|
||||
let context = container.mainContext
|
||||
|
||||
context.insert(Repository(
|
||||
URL(filePath: "/full/path/to/repository/name-0.git"),
|
||||
sortOrder: 0
|
||||
))
|
||||
|
||||
context.insert(Repository(
|
||||
URL(filePath: "/full/path/to/repository/name-1.git"),
|
||||
sortOrder: 1
|
||||
))
|
||||
|
||||
context.insert(Repository(
|
||||
URL(filePath: "/full/path/to/repository/name-2.git"),
|
||||
sortOrder: 2
|
||||
))
|
||||
}
|
||||
|
||||
}
|
77
Piper/Sources/UI/Components/ListHeader.swift
Normal file
77
Piper/Sources/UI/Components/ListHeader.swift
Normal file
@ -0,0 +1,77 @@
|
||||
//
|
||||
// ListHeader.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListHeader: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let title: LocalizedStringKey
|
||||
let button: LocalizedStringKey
|
||||
let showButton: Bool
|
||||
let onAction: () -> Void
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if showButton {
|
||||
Spacer()
|
||||
|
||||
Button(
|
||||
button,
|
||||
action: onAction
|
||||
)
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("List Section component with title") {
|
||||
List {
|
||||
Section {
|
||||
EmptyView()
|
||||
} header: {
|
||||
ListHeader(
|
||||
title: "Some title goes here...",
|
||||
button: "Some button goes here...",
|
||||
showButton: false
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 400, height: 100)
|
||||
}
|
||||
|
||||
#Preview("List Section component with title and button") {
|
||||
List {
|
||||
Section {
|
||||
EmptyView()
|
||||
} header: {
|
||||
ListHeader(
|
||||
title: "Some title goes here...",
|
||||
button: "Some button goes here...",
|
||||
showButton: true
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 400, height: 100)
|
||||
}
|
85
Piper/Sources/UI/Components/ListItem.swift
Normal file
85
Piper/Sources/UI/Components/ListItem.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// ListItem.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListItem: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let repository: Repository
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
Label {
|
||||
VStack(
|
||||
alignment: .leading,
|
||||
spacing: 4
|
||||
) {
|
||||
Text(repository.name)
|
||||
.font(.title)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Text(repository.path.relativePath)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "folder")
|
||||
.resizable()
|
||||
}
|
||||
.labelStyle(ItemLabelStyle())
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Label Styles
|
||||
|
||||
struct ItemLabelStyle: LabelStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(
|
||||
alignment: .center,
|
||||
spacing: 16
|
||||
) {
|
||||
configuration.icon
|
||||
.frame(width: 44)
|
||||
|
||||
configuration.title
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("List Item component") {
|
||||
List {
|
||||
ListItem(
|
||||
repository: .init(
|
||||
.init(filePath: "/full/path/to/repository/name.git")!,
|
||||
sortOrder: 0
|
||||
)
|
||||
)
|
||||
ListItem(
|
||||
repository: .init(
|
||||
.init(filePath: "/full/path/to/repository/name.git")!,
|
||||
sortOrder: 0
|
||||
)
|
||||
)
|
||||
ListItem(
|
||||
repository: .init(
|
||||
.init(filePath: "/full/path/to/repository/name.git")!,
|
||||
sortOrder: 0
|
||||
)
|
||||
)
|
||||
}
|
||||
.frame(width: 400, height: 220)
|
||||
}
|
61
Piper/Sources/UI/Components/ListItemEmpty.swift
Normal file
61
Piper/Sources/UI/Components/ListItemEmpty.swift
Normal file
@ -0,0 +1,61 @@
|
||||
//
|
||||
// ListItemEmpty.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListItemEmpty: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let title: LocalizedStringKey
|
||||
let button: LocalizedStringKey
|
||||
let onAction: () -> Void
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.overlay {
|
||||
VStack(spacing: 16) {
|
||||
Text(title)
|
||||
.font(.title)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Button(action: onAction) {
|
||||
Text(button)
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, proxy.size.width * 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("List Item Empty component with title and button") {
|
||||
List {
|
||||
ListItemEmpty(
|
||||
title: "Some title text goes here...",
|
||||
button: "Some button text goes here..."
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
.frame(height: 300)
|
||||
}
|
||||
.frame(width: 400, height: 300)
|
||||
}
|
81
Piper/Sources/UI/Views/MenuBarView.swift
Normal file
81
Piper/Sources/UI/Views/MenuBarView.swift
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// MenuBarView.swift
|
||||
// Piper ~ App
|
||||
//
|
||||
// Created by Javier Cicchelli on 06/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct MenuBarView: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@Query(sort: \Repository.sortOrder)
|
||||
private var repositories: [Repository]
|
||||
|
||||
@State private var viewMode = MenuBarViewModel()
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
if repositories.isEmpty {
|
||||
ListItemEmpty(
|
||||
title: "menu-bar.item.empty.title.text",
|
||||
button: "menu-bar.item.empty.button.text"
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
.frame(height: Layout.heightEmpty)
|
||||
} else {
|
||||
ForEach(repositories) {
|
||||
ListItem(repository: $0)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
ListHeader(
|
||||
title: "menu-bar.section.repositories.title.text",
|
||||
button: "menu-bar.section.repositories.button.text",
|
||||
showButton: !repositories.isEmpty
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
width: Layout.widthView,
|
||||
height: Layout.heightView
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private extension MenuBarView {
|
||||
enum Layout {
|
||||
static let heightEmpty = CGFloat(200)
|
||||
static let heightView = CGFloat(260)
|
||||
static let widthView = CGFloat(400)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Menu Bar view when no repositories found") {
|
||||
MenuBarView()
|
||||
.modelContainer(.preview)
|
||||
}
|
||||
|
||||
#Preview("Menu Bar view when some repositories found") {
|
||||
let container = ModelContainer.preview
|
||||
|
||||
Repository.samples(in: container)
|
||||
|
||||
return MenuBarView()
|
||||
.modelContainer(container)
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
//
|
||||
// UnitTests.swift
|
||||
// UnitTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 04/10/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Testing
|
||||
|
||||
struct UnitTests {
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user