[App] Repositories in the Menu Bar view #4

Merged
javier merged 12 commits from app/repositories into main 2024-10-06 11:50:37 +00:00
14 changed files with 542 additions and 43 deletions

View File

@ -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;

View 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"
}

View File

@ -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")
}

View File

@ -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
)
}
}

View 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
])
}

View 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 }
}

View 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 () {
}
}

View File

@ -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.")
}
}()
}

View 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
))
}
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View File

@ -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.
}
}