From 9618bf15d317a47caba30332ffec247c49f44971 Mon Sep 17 00:00:00 2001
From: ErrorErrorError <16653389+ErrorErrorError@users.noreply.github.com>
Date: Tue, 12 Dec 2023 23:25:13 -0800
Subject: [PATCH] wip: bump composable-architecture to 1.5.5, use native
navigation and toolbar
also fixes showing build number and lowercasing repo urls
---
App/Mochi.xcodeproj/project.pbxproj | 4 +-
.../xcshareddata/xcschemes/Mochi.xcscheme | 9 +
App/Shared/MochiApp.swift | 2 +-
Package.swift | 3 +-
.../Dependencies/ComposableArchitecture.swift | 2 +-
Package/Sources/Shared/Architecture.swift | 1 +
Sources/Clients/BuildClient/Client.swift | 3 +-
.../Clients/LocalizableClient/Client.swift | 4 +
.../JS+Bindings/JSContext+JSRuntime.swift | 2 +-
Sources/Clients/RepoClient/Live.swift | 6 +-
Sources/Clients/RepoClient/Models.swift | 1 +
.../Clients/UserSettingsClient/Theme.swift | 6 +-
Sources/Features/App/AppFeature.swift | 2 +-
.../Features/App/iOS/AppFeatureView+iOS.swift | 132 +++----
.../App/macOS/AppFeatureView+macOS.swift | 6 +-
.../Discover/DiscoverFeature+Reducer.swift | 5 +-
.../Discover/DiscoverFeature+View.swift | 102 ++---
.../iOS/PlaylistDetailsFeature+View+iOS.swift | 40 +-
.../RepoPackagesFeature+View.swift | 28 +-
.../Features/Repos/ReposFeature+Reducer.swift | 29 +-
.../Features/Repos/ReposFeature+View.swift | 181 ++++-----
.../Search/SearchFeature+Reducer.swift | 4 -
.../Features/Search/SearchFeature+View.swift | 356 +++++++++---------
Sources/Features/Search/SearchFeature.swift | 14 +-
.../Features/Settings/Components/Logs.swift | 61 +--
.../Platforms/SettingsFeature+iOS.swift | 22 +-
.../Settings/SettingsFeature+View.swift | 40 +-
.../VideoPlayer/VideoPlayerFeature+View.swift | 4 +-
Sources/Shared/Architecture/Exported.swift | 9 +-
Sources/Shared/Architecture/Feature.swift | 8 +
.../Shared/SharedModels/RepoModuleID.swift | 9 +-
Sources/Shared/Styling/NavStack.swift | 66 +++-
.../Styling/Settings/SettingsGroup.swift | 3 +-
.../Shared/Styling/Settings/SettingsRow.swift | 2 +-
Sources/Shared/Styling/TopBar.swift | 337 ++---------------
.../ViewComponents/InsetValue+Values.swift | 20 +-
.../ViewComponents/ScrollViewTracker.swift | 60 +++
37 files changed, 667 insertions(+), 916 deletions(-)
create mode 100644 Sources/Shared/ViewComponents/ScrollViewTracker.swift
diff --git a/App/Mochi.xcodeproj/project.pbxproj b/App/Mochi.xcodeproj/project.pbxproj
index 4e06698..a7a9bc6 100644
--- a/App/Mochi.xcodeproj/project.pbxproj
+++ b/App/Mochi.xcodeproj/project.pbxproj
@@ -371,7 +371,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = Shared/mochi.entitlements;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 3;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = ..;
DEVELOPMENT_TEAM = A6HC4Y86NJ;
@@ -411,7 +411,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = Shared/mochi.entitlements;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 3;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = ..;
DEVELOPMENT_TEAM = A6HC4Y86NJ;
diff --git a/App/Mochi.xcodeproj/xcshareddata/xcschemes/Mochi.xcscheme b/App/Mochi.xcodeproj/xcshareddata/xcschemes/Mochi.xcscheme
index 8ff64c4..80a8315 100644
--- a/App/Mochi.xcodeproj/xcshareddata/xcschemes/Mochi.xcscheme
+++ b/App/Mochi.xcodeproj/xcshareddata/xcschemes/Mochi.xcscheme
@@ -73,5 +73,14 @@
+
+
+
+
+
+
diff --git a/App/Shared/MochiApp.swift b/App/Shared/MochiApp.swift
index bd7b61b..7e2d01d 100644
--- a/App/Shared/MochiApp.swift
+++ b/App/Shared/MochiApp.swift
@@ -63,7 +63,7 @@ struct MochiApp: App {
SettingsFeature.View(
store: appDelegate.store.scope(
state: \.settings,
- action: { .internal(.settings($0)) }
+ action: \.internal.settings
)
)
.themeable()
diff --git a/Package.swift b/Package.swift
index 24c55d1..857dec9 100644
--- a/Package.swift
+++ b/Package.swift
@@ -920,7 +920,7 @@ extension _Client {
struct ComposableArchitecture: PackageDependency {
var dependency: Package.Dependency {
- .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.4.2")
+ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.5.5")
}
}
//
@@ -1361,6 +1361,7 @@ struct Architecture: _Shared {
FoundationHelpers()
ComposableArchitecture()
LocalizableClient()
+ LoggerClient()
}
}
//
diff --git a/Package/Sources/Dependencies/ComposableArchitecture.swift b/Package/Sources/Dependencies/ComposableArchitecture.swift
index 7c56e2b..472b659 100644
--- a/Package/Sources/Dependencies/ComposableArchitecture.swift
+++ b/Package/Sources/Dependencies/ComposableArchitecture.swift
@@ -8,6 +8,6 @@
struct ComposableArchitecture: PackageDependency {
var dependency: Package.Dependency {
- .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.4.2")
+ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.5.5")
}
}
diff --git a/Package/Sources/Shared/Architecture.swift b/Package/Sources/Shared/Architecture.swift
index e104112..433c0fa 100644
--- a/Package/Sources/Shared/Architecture.swift
+++ b/Package/Sources/Shared/Architecture.swift
@@ -11,5 +11,6 @@ struct Architecture: _Shared {
FoundationHelpers()
ComposableArchitecture()
LocalizableClient()
+ LoggerClient()
}
}
diff --git a/Sources/Clients/BuildClient/Client.swift b/Sources/Clients/BuildClient/Client.swift
index 9376712..0ae53cd 100644
--- a/Sources/Clients/BuildClient/Client.swift
+++ b/Sources/Clients/BuildClient/Client.swift
@@ -23,7 +23,8 @@ public struct BuildKey: DependencyKey {
.flatMap { $0 as? String }
.flatMap { try? Semver($0) } ?? .init(0, 0, 0),
number: Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion")
- .flatMap { $0 as? Int }
+ .flatMap { $0 as? String }
+ .flatMap { Int($0) }
.flatMap { .init(rawValue: $0) } ?? .init(0)
)
}
diff --git a/Sources/Clients/LocalizableClient/Client.swift b/Sources/Clients/LocalizableClient/Client.swift
index ab5fcd9..40e98bf 100644
--- a/Sources/Clients/LocalizableClient/Client.swift
+++ b/Sources/Clients/LocalizableClient/Client.swift
@@ -17,6 +17,10 @@ extension LocalizableClient: DependencyKey {
public static let liveValue: LocalizableClient = .init(
localize: { String(localized: .init($0), bundle: .module) }
)
+
+ public static let previewValue: LocalizableClient = .init(localize: { $0 })
+
+ public static let testValue: LocalizableClient = .init(localize: unimplemented(".localize"))
}
extension DependencyValues {
diff --git a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift
index ad6045f..1e4fde8 100644
--- a/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift
+++ b/Sources/Clients/ModuleClient/JS+Bindings/JSContext+JSRuntime.swift
@@ -25,7 +25,7 @@ extension JSContext {
let jsURL = try fileClient.retrieveModuleDirectory(module.mainJSFile)
try evaluateScript(String(contentsOf: jsURL))
- evaluateScript("const Instance = new source.default()")
+ evaluateScript("const Instance = new source.default();")
}
}
diff --git a/Sources/Clients/RepoClient/Live.swift b/Sources/Clients/RepoClient/Live.swift
index efdf08b..d23a9b1 100644
--- a/Sources/Clients/RepoClient/Live.swift
+++ b/Sources/Clients/RepoClient/Live.swift
@@ -12,6 +12,7 @@ import DatabaseClient
import Dependencies
import FileClient
import Foundation
+import LoggerClient
import Semaphore
import SharedModels
@@ -145,8 +146,7 @@ private class ModulesDownloadManager {
}
guard response.mimeType == "text/javascript" || response.mimeType == "application/javascript" else {
- print("Unknown mime type of file \(response.mimeType ?? "Unknown")")
- throw RepoClient.Error.failedToDownloadModule
+ throw RepoClient.Error.invalidMimeTypeForModule(received: response.mimeType ?? "Unknown")
}
guard let directory = URL(
@@ -169,7 +169,7 @@ private class ModulesDownloadManager {
}
}
} catch {
- print(error)
+ logger.error("\(error.localizedDescription)")
states.value[repoModuleId] = .failed((error as? RepoClient.Error) ?? .failedToDownloadModule)
}
return nil
diff --git a/Sources/Clients/RepoClient/Models.swift b/Sources/Clients/RepoClient/Models.swift
index 24ccc52..2978432 100644
--- a/Sources/Clients/RepoClient/Models.swift
+++ b/Sources/Clients/RepoClient/Models.swift
@@ -15,6 +15,7 @@ public extension RepoClient {
enum Error: Swift.Error, Equatable, Sendable {
case failedToFindRepo
case failedToDownloadModule
+ case invalidMimeTypeForModule(received: String)
case failedToDownloadRepo
case failedToAddRepo
case failedToInstallModule
diff --git a/Sources/Clients/UserSettingsClient/Theme.swift b/Sources/Clients/UserSettingsClient/Theme.swift
index e56c63c..4767c43 100644
--- a/Sources/Clients/UserSettingsClient/Theme.swift
+++ b/Sources/Clients/UserSettingsClient/Theme.swift
@@ -66,9 +66,9 @@ public enum Theme: Codable, Sendable, Hashable, Identifiable, CaseIterable {
)
#else
.init(
- red: 0x0A / 0xFF,
- green: 0x0A / 0xFF,
- blue: 0x0A / 0xFF
+ red: 0x10 / 0xFF,
+ green: 0x10 / 0xFF,
+ blue: 0x10 / 0xFF
)
#endif
}
diff --git a/Sources/Features/App/AppFeature.swift b/Sources/Features/App/AppFeature.swift
index 5cfe123..5d1a172 100644
--- a/Sources/Features/App/AppFeature.swift
+++ b/Sources/Features/App/AppFeature.swift
@@ -43,7 +43,7 @@ public struct AppFeature: Feature {
self.selected = selected
}
- public enum Tab: String, CaseIterable, Sendable, Localizable {
+ public enum Tab: String, CaseIterable, Sendable, Localizable, Hashable {
case discover = "Discover"
case repos = "Repos"
case settings = "Settings"
diff --git a/Sources/Features/App/iOS/AppFeatureView+iOS.swift b/Sources/Features/App/iOS/AppFeatureView+iOS.swift
index 7ffc4bf..e8f378e 100644
--- a/Sources/Features/App/iOS/AppFeatureView+iOS.swift
+++ b/Sources/Features/App/iOS/AppFeatureView+iOS.swift
@@ -24,45 +24,57 @@ extension AppFeature.View: View {
@MainActor
public var body: some View {
WithViewStore(store, observe: \.selected) { viewStore in
- ZStack {
- switch viewStore.state {
- case .discover:
- DiscoverFeature.View(
- store: store.scope(
- state: \.discover,
- action: Action.InternalAction.discover
- )
- )
- case .repos:
- ReposFeature.View(
- store: store.scope(
- state: \.repos,
- action: Action.InternalAction.repos
- )
- )
- case .settings:
- SettingsFeature.View(
- store: store.scope(
- state: \.settings,
- action: Action.InternalAction.settings
- )
- )
+ TabView(
+ selection: viewStore.binding(
+ get: \.`self`,
+ send: { .didSelectTab($0) }
+ )
+ ) {
+ ForEach(Self.State.Tab.allCases, id: \.self) { (tab: Self.State.Tab) in
+ Group {
+ switch tab {
+ case .discover:
+ DiscoverFeature.View(
+ store: store.scope(
+ state: \.discover,
+ action: \.internal.discover
+ )
+ )
+ .tint(nil)
+ case .repos:
+ ReposFeature.View(
+ store: store.scope(
+ state: \.repos,
+ action: \.internal.repos
+ )
+ )
+ .tint(nil)
+ case .settings:
+ SettingsFeature.View(
+ store: store.scope(
+ state: \.settings,
+ action: \.internal.settings
+ )
+ )
+ .tint(nil)
+ }
+ }
+ .tabItem {
+ Label(tab.localized, systemImage: viewStore.state == tab ? tab.selected : tab.image)
+ }
+ .tag(tab)
}
}
- .inset(for: \.bottomNavigation, alignment: .bottom) {
- navbar(viewStore.state)
- }
- .ignoresSafeArea(.keyboard, edges: .all)
- }
- .onAppear {
- store.send(.view(.didAppear))
+ // Set tint of tab item
+ .tint(viewStore.state.colorAccent)
}
+ .onAppear { store.send(.view(.didAppear)) }
.overlay {
WithViewStore(store, observe: \.videoPlayer != nil) { isVisible in
IfLetStore(
store.scope(
state: \.$videoPlayer,
- action: { .internal(.videoPlayer($0)) }
+ action: \.internal.videoPlayer
),
then: { VideoPlayerFeature.View(store: $0) }
)
@@ -73,68 +85,12 @@ extension AppFeature.View: View {
}
.themeable()
}
-
- @MainActor
- func navbar(_ selected: Self.State.Tab) -> some View {
- HStack(alignment: .top, spacing: 0) {
- ForEach(State.Tab.allCases, id: \.rawValue) { tab in
- Button {
- store.send(.view(.didSelectTab(tab)))
- } label: {
- VStack(spacing: 2) {
- RoundedRectangle(cornerRadius: 12)
- .frame(width: tab == selected ? 18 : 0, height: 4)
- .transition(.scale.combined(with: .opacity))
- .opacity(tab == selected ? 1.0 : 0.0)
-
- Image(systemName: tab == selected ? tab.selected : tab.image)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .font(.system(size: 20, weight: .semibold))
- .frame(height: 18)
- .padding(.top, 8)
-
- Text(tab.localized)
- .font(.system(size: 10, weight: .medium))
- }
- .foregroundColor(tab == selected ? tab.colorAccent : .gray)
- .frame(maxWidth: .infinity)
- .background(
- Rectangle()
- .foregroundColor(tab.colorAccent.opacity(tab == selected ? 0.08 : 0.0))
- .ignoresSafeArea(.all)
- .edgesIgnoringSafeArea(.all)
- .blur(radius: 24)
- )
- .contentShape(Rectangle())
- }
- .buttonStyle(.scaled)
- .contentShape(Rectangle())
- .animation(.easeInOut(duration: 0.2), value: tab == selected)
- }
- .frame(maxWidth: .infinity)
- }
- .frame(maxWidth: .infinity, alignment: .bottom)
- .background {
- Rectangle()
- .fill(.regularMaterial)
- .ignoresSafeArea()
- .edgesIgnoringSafeArea(.all)
- }
- .overlay(alignment: .top) {
- Color.gray.opacity(0.2)
- .frame(height: 0.5)
- .frame(maxWidth: .infinity)
- .ignoresSafeArea()
- .edgesIgnoringSafeArea(.all)
- }
- }
}
#Preview {
AppFeature.View(
store: .init(
- initialState: .init(),
+ initialState: .init(selected: .settings),
reducer: { AppFeature() }
)
)
diff --git a/Sources/Features/App/macOS/AppFeatureView+macOS.swift b/Sources/Features/App/macOS/AppFeatureView+macOS.swift
index 6a6c102..2f7237a 100644
--- a/Sources/Features/App/macOS/AppFeatureView+macOS.swift
+++ b/Sources/Features/App/macOS/AppFeatureView+macOS.swift
@@ -36,14 +36,14 @@ extension AppFeature.View: View {
DiscoverFeature.View(
store: store.scope(
state: \.discover,
- action: Action.InternalAction.discover
+ action: \.internal.discover
)
)
case .repos:
ReposFeature.View(
store: store.scope(
state: \.repos,
- action: Action.InternalAction.repos
+ action: \.internal.repos
)
)
case .settings:
@@ -65,7 +65,7 @@ extension AppFeature.View: View {
.window(
store: store.scope(
state: \.$videoPlayer,
- action: Action.InternalAction.videoPlayer
+ action: \.internal.videoPlayer
),
content: VideoPlayerFeature.View.init
)
diff --git a/Sources/Features/Discover/DiscoverFeature+Reducer.swift b/Sources/Features/Discover/DiscoverFeature+Reducer.swift
index 9967411..316516f 100644
--- a/Sources/Features/Discover/DiscoverFeature+Reducer.swift
+++ b/Sources/Features/Discover/DiscoverFeature+Reducer.swift
@@ -40,10 +40,7 @@ extension DiscoverFeature {
state.path.append(.playlistDetails(.init(content: .init(repoModuleId: id, playlist: playlist))))
case .view(.didTapSearchButton):
- state.search = SearchFeature.State(
- searchFieldFocused: true,
- repoModuleId: state.section.module?.module.id
- )
+ state.search = SearchFeature.State(repoModuleId: state.section.module?.module.id)
case let .internal(.selectedModule(selection)):
if let selection {
diff --git a/Sources/Features/Discover/DiscoverFeature+View.swift b/Sources/Features/Discover/DiscoverFeature+View.swift
index cd660f9..7ba00a8 100644
--- a/Sources/Features/Discover/DiscoverFeature+View.swift
+++ b/Sources/Features/Discover/DiscoverFeature+View.swift
@@ -7,7 +7,6 @@
//
import Architecture
-@_spi(Presentation)
import ComposableArchitecture
import ModuleLists
import Nuke
@@ -27,7 +26,7 @@ extension DiscoverFeature.View: View {
NavStack(
store.scope(
state: \.path,
- action: Action.InternalAction.screens
+ action: \.internal.screens
)
) {
WithViewStore(store, observe: \.section) { viewStore in
@@ -45,55 +44,10 @@ extension DiscoverFeature.View: View {
}
}
.animation(.easeInOut(duration: 0.25), value: viewStore.state)
- #if os(iOS)
- .topBar(
- backgroundStyle: .gradient(),
- leadingAccessory: {
- Button {
- viewStore.send(.didTapOpenModules)
- } label: {
- HStack(spacing: 8) {
- if let url = viewStore.icon {
- LazyImage(url: url) { state in
- if let image = state.image {
- image
- .resizable()
- .scaledToFit()
- .frame(width: 22, height: 22)
- } else {
- EmptyView()
- }
- }
- .transition(.opacity)
- }
-
- Text(viewStore.title)
- Image(systemName: "chevron.down")
- .font(.body.weight(.bold))
- Spacer()
- }
- .font(.title.bold())
- .contentShape(Rectangle())
- .scaleEffect(1.0)
- .transition(.opacity)
- .animation(.easeInOut, value: viewStore.icon)
- }
- .buttonStyle(.plain)
- },
- trailingAccessory: {
- Button {
- viewStore.send(.didTapSearchButton)
- } label: {
- Image(systemName: "magnifyingglass")
- }
- .buttonStyle(.materialToolbarImage)
- .transition(.slide)
- .matchedGeometryEffect(id: "Search", in: searchAnimation)
- },
- bottomAccessory: EmptyView.init
- )
- #elseif os(macOS)
.navigationTitle("")
+ #if os(iOS)
+ .navigationBarTitleDisplayMode(.inline)
+ #endif
.toolbar {
ToolbarItem(placement: .navigation) {
Button {
@@ -115,17 +69,26 @@ extension DiscoverFeature.View: View {
}
Text(viewStore.title)
- .font(.title3.bold())
Image(systemName: "chevron.down")
- .font(.system(size: 1).weight(.semibold))
+ .font(.caption.weight(.bold))
+ .foregroundColor(.gray)
}
+ #if os(iOS)
+ .font(.title.bold())
+ #else
+ .font(.title3.bold())
+ #endif
.contentShape(Rectangle())
.scaleEffect(1.0)
.transition(.opacity)
.animation(.easeInOut, value: viewStore.icon)
}
+ #if os(macOS)
.buttonStyle(.bordered)
+ #else
+ .buttonStyle(.plain)
+ #endif
}
ToolbarItem(placement: .automatic) {
@@ -134,9 +97,11 @@ extension DiscoverFeature.View: View {
} label: {
Image(systemName: "magnifyingglass")
}
+ #if os(iOS)
+ .buttonStyle(.materialToolbarItem)
+ #endif
}
}
- #endif
}
.ignoresSafeArea(.keyboard)
.frame(
@@ -144,27 +109,20 @@ extension DiscoverFeature.View: View {
maxHeight: .infinity
)
.onAppear { store.send(.view(.didAppear)) }
+ .stackDestination(
+ store: store.scope(
+ state: \.$search,
+ action: \.internal.search
+ )
+ ) { store in
+ SearchFeature.View(store: store)
+ }
.moduleListsSheet(
store.scope(
state: \.$moduleLists,
- action: { .internal(.moduleLists($0)) }
+ action: \.internal.moduleLists
)
)
- .presentation(
- store: store.scope(
- state: \.$search,
- action: Action.InternalAction.search
- )
- ) { (content, binding: Binding, destination) in
- ZStack {
- if binding.wrappedValue {
- destination({ SearchFeature.View(store: $0, namespace: searchAnimation) })
- } else {
- content
- }
- }
- .animation(.interactiveSpring(duration: 0.3), value: binding.wrappedValue)
- }
} destination: { store in
SwitchStore(store) { state in
switch state {
@@ -544,3 +502,9 @@ extension DiscoverFeature.View {
)
)
}
+
+#if os(macOS)
+extension ToolbarItemPlacement {
+ static var topBarTrailing = Self.automatic
+}
+#endif
diff --git a/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift b/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift
index 13e3d58..e5b641a 100644
--- a/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift
+++ b/Sources/Features/PlaylistDetails/iOS/PlaylistDetailsFeature+View+iOS.swift
@@ -81,28 +81,30 @@ extension PlaylistDetailsFeature.View: View {
.edgesIgnoringSafeArea(.top)
.ignoresSafeArea(.container, edges: .top)
#if os(iOS)
- .topBar(backgroundStyle: .clear) {
- store.send(.view(.didTappedBackButton))
- } trailingAccessory: {
- // TODO: Make this change depending if it's in library already or not
- Button {} label: {
- Image(systemName: "plus")
+ .navigationBarTitle("", displayMode: .inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {} label: {
+ Image(systemName: "plus")
+ }
+ .buttonStyle(.materialToolbarItem)
}
- .buttonStyle(.materialToolbarImage)
- Menu {
- WithViewStore(store, observe: \.playlist.url) { viewStore in
- Button {
- openURL(viewStore.state)
- } label: {
- Image(systemName: "arrow.up.right.square.fill")
- Text("Open Playlist URL")
+ ToolbarItem(placement: .topBarTrailing) {
+ Menu {
+ WithViewStore(store, observe: \.playlist.url) { viewStore in
+ Button {
+ openURL(viewStore.state)
+ } label: {
+ Image(systemName: "arrow.up.right.square.fill")
+ Text("Open Playlist URL")
+ }
}
+ } label: {
+ Image(systemName: "ellipsis")
}
- } label: {
- Image(systemName: "ellipsis")
+ .menuStyle(.materialToolbarItem)
}
- .menuStyle(.materialToolbarImage)
}
#elseif os(macOS)
.toolbar {
@@ -132,7 +134,7 @@ extension PlaylistDetailsFeature.View: View {
.sheet(
store: store.scope(
state: \.$destination,
- action: { .internal(.destination($0)) }
+ action: \.internal.destination
),
state: /PlaylistDetailsFeature.Destination.State.readMore,
action: PlaylistDetailsFeature.Destination.Action.readMore
@@ -354,7 +356,7 @@ extension PlaylistDetailsFeature.View {
ContentCore.View(
store: store.scope(
state: \.content,
- action: Action.InternalAction.content
+ action: \.internal.content
),
contentType: playlistInfo.type
)
diff --git a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift
index a813f66..12e244e 100644
--- a/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift
+++ b/Sources/Features/Repos/RepoPackages/RepoPackagesFeature+View.swift
@@ -93,15 +93,27 @@ extension RepoPackagesFeature.View: View {
maxHeight: .infinity
)
#if os(iOS)
- .topBar {
- store.send(.view(.didTapClose))
- } trailingAccessory: {
- Button {
- store.send(.view(.didTapToRefreshRepo))
- } label: {
- Image(systemName: "arrow.triangle.2.circlepath")
+ .navigationTitle("")
+ .navigationBarTitleDisplayMode(.inline)
+// .navigationBarBackButtonHidden()
+ .toolbar {
+// ToolbarItem(placement: .topBarLeading) {
+// Button {
+// store.send(.view(.didTapClose))
+// } label: {
+// Image(systemName: "chevron.left")
+// }
+// .buttonStyle(.materialToolbarItem)
+// }
+
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ store.send(.view(.didTapToRefreshRepo))
+ } label: {
+ Image(systemName: "arrow.triangle.2.circlepath")
+ }
+ .buttonStyle(.materialToolbarItem)
}
- .buttonStyle(.materialToolbarImage)
}
#elseif os(macOS)
.toolbar {
diff --git a/Sources/Features/Repos/ReposFeature+Reducer.swift b/Sources/Features/Repos/ReposFeature+Reducer.swift
index 152c57e..c7f47cf 100644
--- a/Sources/Features/Repos/ReposFeature+Reducer.swift
+++ b/Sources/Features/Repos/ReposFeature+Reducer.swift
@@ -9,6 +9,7 @@
import Architecture
import ComposableArchitecture
import Foundation
+import LoggerClient
import RepoClient
import SharedModels
import Styling
@@ -67,7 +68,7 @@ extension ReposFeature {
state.path.append(RepoPackagesFeature.State(repo: repo))
case .view(.binding(\.$url)):
- guard let url = URL(string: state.url.lowercased()) else {
+ guard let url = URL(sanitize: state.url) else {
state.searchedRepo = .pending
return .cancel(id: Cancellables.repoURLDebounce)
}
@@ -80,7 +81,7 @@ extension ReposFeature {
try await send(.internal(.validateRepoURL(.loaded(repoClient.validate(url)))))
}
} catch: { error, send in
- print(error)
+ logger.error("Failed to validate repo: \(error.localizedDescription)")
await send(.internal(.validateRepoURL(.failed(Error.notValidRepo))))
}
@@ -109,3 +110,27 @@ extension ReposFeature {
}
}
}
+
+extension URL {
+ init?(sanitize string: String) {
+ var components = URLComponents(string: string)
+ // Lowercase host and schema since they're not case sensitive
+ let host = components?.host?.lowercased()
+ let schema = components?.scheme?.lowercased()
+ components?.host = host
+ components?.scheme = schema
+
+ // Everything else is case sensitive, so check if there's a foward slash. If not, add it.
+
+ guard var string = components?.string else {
+ return nil
+ }
+
+ // Remove trailing slash
+ if string.hasSuffix("/") {
+ string = .init(string.dropLast())
+ }
+
+ self.init(string: string)
+ }
+}
diff --git a/Sources/Features/Repos/ReposFeature+View.swift b/Sources/Features/Repos/ReposFeature+View.swift
index 8f42718..25f0b60 100644
--- a/Sources/Features/Repos/ReposFeature+View.swift
+++ b/Sources/Features/Repos/ReposFeature+View.swift
@@ -22,74 +22,79 @@ extension ReposFeature.View: View {
NavStack(
store.scope(
state: \.path,
- action: { .internal(.path($0)) }
+ action: \.internal.path
)
) {
- WithViewStore(store, observe: \.repos) { viewStore in
- ScrollView(
- viewStore.isEmpty ? [] : .vertical,
- showsIndicators: false
- ) {
- LazyVStack(spacing: 0) {
- repoUrlTextInput
- .padding(.horizontal)
+ ScrollView(.vertical, showsIndicators: false) {
+ LazyVStack(spacing: 0) {
+ repoUrlTextInput
+ .padding(.horizontal)
+ WithViewStore(store, observe: \.repos) { viewStore in
Spacer()
.frame(height: 24)
- if !viewStore.isEmpty {
- Text("Installed Repos")
- .font(.subheadline.weight(.semibold))
- .foregroundColor(.gray)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal)
+ Text("\(viewStore.count) Installed Repos")
+ .font(.subheadline)
+ .foregroundColor(.gray)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
- ForEach(viewStore.state) { repo in
- repoRow(repo)
- .padding(.horizontal)
- .background(theme.backgroundColor)
- .contentShape(Rectangle())
- .onTapGesture {
- self.store.send(.view(.didTapRepo(repo.id)))
- }
- .contextMenu {
- Button {
- self.store.send(.view(.didTapDeleteRepo(repo.id)))
- } label: {
- Label("Delete Repo", systemImage: "trash.fill")
- .foregroundColor(.red)
+ Spacer()
+ .frame(height: 8)
+
+ ZStack {
+ if !viewStore.isEmpty {
+ ForEach(viewStore.state) { repo in
+ repoRow(repo)
+ .padding(.horizontal)
+ .background(theme.backgroundColor)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ self.store.send(.view(.didTapRepo(repo.id)))
+ }
+ .contextMenu {
+ Button {
+ self.store.send(.view(.didTapDeleteRepo(repo.id)))
+ } label: {
+ Label("Delete Repo", systemImage: "trash.fill")
+ .foregroundColor(.red)
+ }
+ .buttonStyle(.plain)
}
- .buttonStyle(.plain)
+
+ if viewStore.last?.id != repo.id {
+ Divider()
+ .padding(.horizontal)
}
+ }
+ } else {
+ VStack(alignment: .leading) {
+ Text("No Repos Added")
+ .font(.callout.weight(.medium))
- if viewStore.last?.id != repo.id {
- Divider()
- .padding(.horizontal)
+ Text("Add repos to view and install modules.")
+ .font(.callout)
}
+ .foregroundColor(.gray)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(16)
+ .background {
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .foregroundColor(.gray.opacity(0.12))
+ }
+ .padding(.horizontal)
}
}
+ .animation(.easeInOut, value: viewStore.state)
}
- .animation(.easeInOut, value: viewStore.state)
- }
- .overlay {
- if viewStore.isEmpty {
- noReposView
- }
- }
- }
- #if os(iOS)
- .topBar(title: "Repos") {
- Button {
- store.send(.view(.didTapRefreshRepos(nil)))
- } label: {
- Image(systemName: "arrow.triangle.2.circlepath")
}
- .buttonStyle(.materialToolbarImage)
- } bottomAccessory: {
- EmptyView()
}
- #elseif os(macOS)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Repos")
+ #if os(iOS)
+ .navigationBarTitleDisplayMode(.large)
+ #endif
.toolbar {
ToolbarItem(placement: .automatic) {
Button {
@@ -97,16 +102,12 @@ extension ReposFeature.View: View {
} label: {
Image(systemName: "arrow.triangle.2.circlepath")
}
+ #if os(iOS)
+ .buttonStyle(.materialToolbarItem)
+ #endif
}
}
- #endif
- .frame(
- maxWidth: .infinity,
- maxHeight: .infinity
- )
- .task {
- store.send(.view(.onTask))
- }
+ .task { store.send(.view(.onTask)) }
} destination: { store in
RepoPackagesFeature.View(store: store)
}
@@ -114,24 +115,6 @@ extension ReposFeature.View: View {
}
extension ReposFeature.View {
- @MainActor
- var noReposView: some View {
- VStack(spacing: 0) {
- Spacer()
- .frame(height: 4)
-
- repoUrlTextInput
- .hidden()
-
- Spacer()
-
- Text("No repos installed")
- .font(.callout)
-
- Spacer()
- }
- }
-
private struct RepoURLInputViewState: Equatable, @unchecked Sendable {
@BindingViewState
var url: String
@@ -162,6 +145,7 @@ extension ReposFeature.View {
case .loading:
ProgressView()
.fixedSize(horizontal: true, vertical: true)
+ .controlSize(.small)
default:
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
@@ -177,6 +161,7 @@ extension ReposFeature.View {
.autocorrectionDisabled(true)
#if os(iOS)
.textInputAutocapitalization(.never)
+ .keyboardType(.URL)
#endif
.font(.system(size: 16, weight: .regular))
.frame(maxWidth: .infinity)
@@ -243,18 +228,20 @@ extension ReposFeature.View {
func repoRow(_ repo: Repo) -> some View {
HStack(alignment: .top, spacing: 16) {
LazyImage(url: repo.iconURL) { state in
- if let image = state.image {
- image
- .resizable()
- .aspectRatio(contentMode: .fit)
- } else {
- Image(systemName: "questionmark.square.dashed")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .font(.body.weight(.light))
+ Group {
+ if let image = state.image {
+ image
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ } else {
+ Image(systemName: "questionmark.square.dashed")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .font(.body.weight(.light))
+ }
}
+ .frame(width: 38, height: 38)
}
- .frame(width: 38, height: 38)
.squircle()
VStack(alignment: .leading, spacing: 2) {
@@ -270,24 +257,6 @@ extension ReposFeature.View {
}
Spacer()
-
-// WithViewStore(store, observe: \.repoModules[repo.id] ?? .pending) { viewStore in
-// ZStack {
-// LoadableView(loadable: viewStore.state) { _ in
-// Image(systemName: "checkmark.circle.fill")
-// .foregroundColor(.green)
-// } failedView: { _ in
-// Image(systemName: "exclamationmark.triangle.fill")
-// .foregroundColor(.red)
-// } waitingView: {
-// ProgressView()
-// .controlSize(.small)
-// }
-// .frame(width: 34, height: 34)
-// .transition(.opacity.combined(with: .scale))
-// }
-// .animation(.easeInOut(duration: 0.25), value: viewStore.state)
-// }
}
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
@@ -300,7 +269,7 @@ extension ReposFeature.View {
ReposFeature.View(
store: .init(
initialState: .init(),
- reducer: { ReposFeature() }
+ reducer: { EmptyReducer() }
)
)
}
diff --git a/Sources/Features/Search/SearchFeature+Reducer.swift b/Sources/Features/Search/SearchFeature+Reducer.swift
index d7626ea..c82e897 100644
--- a/Sources/Features/Search/SearchFeature+Reducer.swift
+++ b/Sources/Features/Search/SearchFeature+Reducer.swift
@@ -114,16 +114,12 @@ extension SearchFeature: Reducer {
case let .view(.didTapPlaylist(playlist)):
if let repoModuleId = state.repoModuleId {
- state.searchFieldFocused = false
return .send(.delegate(.playlistTapped(repoModuleId, playlist)))
}
case .view(.binding(\.$query)):
return state.fetchQuery()
- case .view(.binding(\.$searchFieldFocused)):
- break
-
case .view(.binding):
break
diff --git a/Sources/Features/Search/SearchFeature+View.swift b/Sources/Features/Search/SearchFeature+View.swift
index c85682a..b224810 100644
--- a/Sources/Features/Search/SearchFeature+View.swift
+++ b/Sources/Features/Search/SearchFeature+View.swift
@@ -22,72 +22,57 @@ import ViewComponents
extension SearchFeature.View: View {
@MainActor
public var body: some View {
- WithViewStore(store, observe: \.`self`) { viewStore in
- VStack(alignment: .leading) {
- #if os(macOS)
- if viewStore.items.value?.isEmpty ?? true {
- filters
- }
- #endif
- LoadableView(loadable: viewStore.items) { pagings in
+ ScrollViewTracker(.vertical) { offset in
+// print(offset)
+ showStatusBarBackground = offset.y < 0
+ } content: {
+ WithViewStore(store, observe: \.items) { viewStore in
+ LoadableView(loadable: viewStore.state) { pagings in
if pagings.isEmpty {
Text("No results found.")
} else {
- ScrollView(.vertical) {
- #if os(macOS)
- filters
- .frame(maxWidth: .infinity, alignment: .leading)
-
- Text("Items")
- .font(.body.weight(.bold))
- .foregroundColor(.gray)
- .padding(.horizontal)
- .frame(maxWidth: .infinity, alignment: .leading)
- #endif
-
- LazyVGrid(
- columns: .init(
- repeating: .init(alignment: .top),
- count: 3
- ),
- alignment: .leading
- ) {
- let allItems = pagings.values.flatMap { $0.value?.items ?? [] }
- ForEach(allItems) { item in
- VStack(alignment: .leading) {
- FillAspectImage(url: item.posterImage)
- .aspectRatio(2 / 3, contentMode: .fit)
- .cornerRadius(12)
+ LazyVGrid(
+ columns: .init(
+ repeating: .init(alignment: .top),
+ count: 3
+ ),
+ alignment: .leading
+ ) {
+ let allItems = pagings.values.flatMap { $0.value?.items ?? [] }
+ ForEach(allItems) { item in
+ VStack(alignment: .leading) {
+ FillAspectImage(url: item.posterImage)
+ .aspectRatio(2 / 3, contentMode: .fit)
+ .cornerRadius(12)
- Text(item.title ?? "Title Unavailable")
- .font(.footnote)
- }
- .contentShape(Rectangle())
- .onTapGesture {
- viewStore.send(.didTapPlaylist(item))
- }
+ Text(item.title ?? "Title Unavailable")
+ .font(.footnote)
+ }
+ .contentShape(Rectangle())
+ .onTapGesture {
+ viewStore.send(.didTapPlaylist(item))
}
}
- .padding(.horizontal)
+ }
+ .padding(.horizontal)
- if let lastPage = pagings.values.last {
- LoadableView(loadable: lastPage) { page in
- LazyView {
- Spacer()
- .frame(height: 1)
- .onAppear {
- if let nextPageId = page.nextPage {
- store.send(.view(.didShowNextPageIndicator(nextPageId)))
- }
+ if let lastPage = pagings.values.last {
+ LoadableView(loadable: lastPage) { page in
+ LazyView {
+ Spacer()
+ .frame(height: 1)
+ .onAppear {
+ if let nextPageId = page.nextPage {
+ store.send(.view(.didShowNextPageIndicator(nextPageId)))
}
- }
- } failedView: { _ in
- Text("Failed to retrieve content")
- .foregroundColor(.red)
- } waitingView: {
- ProgressView()
- .padding(.vertical, 8)
+ }
}
+ } failedView: { _ in
+ Text("Failed to retrieve content")
+ .foregroundColor(.red)
+ } waitingView: {
+ ProgressView()
+ .padding(.vertical, 8)
}
}
}
@@ -100,63 +85,60 @@ extension SearchFeature.View: View {
.font(.body.weight(.semibold))
.foregroundColor(.gray)
}
- .frame(maxWidth: .infinity, maxHeight: .infinity)
}
- .bind(viewStore.$searchFieldFocused, to: self.$searchFieldFocused)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
+ .safeAreaInset(edge: .top) { filters }
#if os(iOS)
- .topBar(
- backgroundStyle: .system,
- backCallback: {
- store.send(.view(.didTapBackButton))
- },
- leadingAccessory: {
+ .navigationBarTitle("", displayMode: .inline)
+ .navigationBarBackButtonHidden()
+ .toolbar {
+ ToolbarItem(placement: .navigation) {
+ SwiftUI.Button {
+ store.send(.view(.didTapBackButton))
+ } label: {
+ Image(systemName: "chevron.left")
+ }
+ .buttonStyle(.materialToolbarItem)
+ }
+
+ ToolbarItem(placement: .principal) {
WithViewStore(store, observe: \.`self`) { viewStore in
- VStack(spacing: 10) {
- HStack(spacing: 8) {
- HStack(spacing: 8) {
- Image(systemName: "magnifyingglass")
- .foregroundColor(.gray)
+ HStack(spacing: 8) {
+ Image(systemName: "magnifyingglass")
+ .font(.caption)
+ .foregroundColor(.gray)
- TextField("Search...", text: viewStore.$query.removeDuplicates())
- .textFieldStyle(.plain)
- .focused($searchFieldFocused)
- .frame(maxWidth: .infinity)
- .transition(.slide)
- .matchedGeometryEffect(id: "Search", in: searchAnimation)
+ TextField("Search...", text: viewStore.$query.removeDuplicates())
+ .textFieldStyle(.plain)
+ .frame(maxWidth: .infinity)
- ZStack {
- if !viewStore.query.isEmpty {
- Image(systemName: "xmark.circle.fill")
- .foregroundColor(.gray)
- .onTapGesture {
- viewStore.send(.didTapClearQuery)
- }
- }
- }
- .animation(.easeInOut, value: viewStore.query.isEmpty)
- }
- .padding(.horizontal)
- .padding(.vertical, 12)
- .background {
- RoundedRectangle(cornerRadius: 12, style: .continuous)
- .style(
- withStroke: Color.gray.opacity(0.16),
- lineWidth: 1,
- fill: Color.gray.opacity(0.1)
- )
- }
- .frame(maxHeight: .infinity)
+ Button {
+ viewStore.send(.didTapClearQuery)
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .font(.caption)
+ .foregroundColor(.gray)
}
- .fixedSize(horizontal: false, vertical: true)
+ .opacity(viewStore.query.isEmpty ? 0 : 1.0)
+ .animation(.easeInOut, value: viewStore.query.isEmpty)
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 4)
+ .background {
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .style(
+ withStroke: Color.gray.opacity(0.16),
+ fill: .thickMaterial
+ )
}
+ .frame(maxHeight: .infinity)
+ .padding(.leading, 8)
+ .fixedSize(horizontal: false, vertical: true)
}
- .padding(.leading, 8)
- },
- trailingAccessory: EmptyView.init,
- bottomAccessory: { filters }
- )
+ }
+ }
#elseif os(macOS)
.navigationTitle("Search")
.toolbar {
@@ -264,71 +246,68 @@ extension SearchFeature.View {
var filters: some View {
WithViewStore(store, observe: FiltersState.init) { viewStore in
- VStack(alignment: .leading) {
- if viewStore.isThereFilters {
- #if os(macOS)
- Text("Filters")
- .font(.body.weight(.bold))
- .foregroundColor(.gray)
- .padding(.horizontal)
- #endif
-
- ScrollView(.horizontal, showsIndicators: false) {
- HStack(spacing: 8) {
- if !viewStore.selectedFilters.isEmpty {
- Menu {
- Section {
- Button(role: .destructive) {
- viewStore.send(.didTapClearFilters)
- } label: {
- Text("Clear all filters")
- .foregroundColor(.red)
- }
- } header: {
- Text("\(viewStore.selectedFilters.count) filters applied")
+ if viewStore.isThereFilters {
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 8) {
+ if !viewStore.selectedFilters.isEmpty {
+ Menu {
+ Section {
+ Button(role: .destructive) {
+ viewStore.send(.didTapClearFilters)
+ } label: {
+ Text("Clear all filters")
+ .foregroundColor(.red)
}
- } label: {
- HStack(spacing: 4) {
- Image(systemName: "line.3.horizontal.decrease")
- Text(viewStore.selectedFilters.count.description)
- }
- .font(.footnote)
- .padding(8)
- .foregroundColor(.white)
- .background(
- Capsule()
- .style(
- withStroke: .gray.opacity(0.2),
- fill: Theme.pastelGreen
- )
- )
+ } header: {
+ Text("\(viewStore.selectedFilters.count) filters applied")
+ }
+ } label: {
+ HStack(spacing: 4) {
+ Image(systemName: "line.3.horizontal.decrease")
+ Text(viewStore.selectedFilters.count.description)
}
- .buttonStyle(.plain)
- .frame(maxHeight: .infinity)
+ .font(.footnote)
+ .padding(8)
+ .foregroundColor(.white)
+ .background(
+ Capsule()
+ .style(
+ withStroke: .gray.opacity(0.2),
+ fill: Theme.pastelGreen
+ )
+ )
}
+ .buttonStyle(.plain)
+ .frame(maxHeight: .infinity)
+ }
- ForEach(viewStore.sortedAllFilters) { filter in
- FilterView(
- filter: filter,
- selectedOptions: viewStore.selectedFilters[id: filter.id]?.options ?? []
- ) { option in
- viewStore.send(.didTapFilter(filter, option))
- }
- .frame(maxHeight: .infinity)
+ ForEach(viewStore.sortedAllFilters) { filter in
+ FilterView(
+ filter: filter,
+ selectedOptions: viewStore.selectedFilters[id: filter.id]?.options ?? []
+ ) { option in
+ viewStore.send(.didTapFilter(filter, option))
}
+ .frame(maxHeight: .infinity)
}
- .frame(maxHeight: .infinity)
- #if os(macOS)
- .padding(.horizontal)
- #endif
}
- .fixedSize(horizontal: false, vertical: true)
+ .frame(maxHeight: .infinity)
+ .padding(.horizontal)
+ }
+ .fixedSize(horizontal: false, vertical: true)
+ .frame(maxWidth: .infinity)
+ .animation(.easeInOut(duration: 0.2), value: viewStore.selectedFilters.count)
+ .padding(.vertical, 12)
+ .background {
+ if showStatusBarBackground {
+ Rectangle()
+ .fill(.ultraThinMaterial)
+ } else {
+ Rectangle()
+ .fill(theme.backgroundColor)
+ }
}
}
- .animation(.easeInOut(duration: 0.2), value: viewStore.selectedFilters.count)
- #if os(macOS)
- .padding(.top)
- #endif
}
}
}
@@ -336,33 +315,34 @@ extension SearchFeature.View {
// MARK: - SearchFeatureView_Previews
#Preview {
- SearchFeature.View(
- store: .init(
- initialState: .init(
- query: "demo",
- selectedFilters: .init([
- SearchFilter(
- id: .init("1"),
- displayName: "Filter",
- multiselect: true,
- required: false,
- options: [.init(id: .init("1"), displayName: "Option 1")]
- )
- ]),
- allFilters: .init([
- SearchFilter(
- id: .init("1"),
- displayName: "Filter",
- multiselect: true,
- required: false,
- options: [.init(id: .init("1"), displayName: "Option 1")]
- )
- ]),
- items: .pending
- ),
- reducer: { EmptyReducer() }
- ),
- namespace: Namespace().wrappedValue
- )
+ NavigationView {
+ SearchFeature.View(
+ store: .init(
+ initialState: .init(
+ query: "demo",
+ selectedFilters: .init([
+ SearchFilter(
+ id: .init("1"),
+ displayName: "Filter",
+ multiselect: true,
+ required: false,
+ options: [.init(id: .init("1"), displayName: "Option 1")]
+ )
+ ]),
+ allFilters: .init([
+ SearchFilter(
+ id: .init("1"),
+ displayName: "Filter",
+ multiselect: true,
+ required: false,
+ options: [.init(id: .init("1"), displayName: "Option 1")]
+ )
+ ]),
+ items: .pending
+ ),
+ reducer: { EmptyReducer() }
+ )
+ )
+ }
.themeable()
}
diff --git a/Sources/Features/Search/SearchFeature.swift b/Sources/Features/Search/SearchFeature.swift
index 0ffbabc..dc1db27 100644
--- a/Sources/Features/Search/SearchFeature.swift
+++ b/Sources/Features/Search/SearchFeature.swift
@@ -21,8 +21,6 @@ import ViewComponents
public struct SearchFeature: Feature {
public struct State: FeatureState {
- @BindingState
- public var searchFieldFocused: Bool
@BindingState
public var query: String
@BindingState
@@ -33,7 +31,6 @@ public struct SearchFeature: Feature {
public var items: Loadable>>>
public init(
- searchFieldFocused: Bool = false,
repoModuleId: RepoModuleID? = nil,
query: String = "",
selectedFilters: [SearchFilter] = [],
@@ -41,7 +38,6 @@ public struct SearchFeature: Feature {
items: Loadable>>> = .pending
) {
self.repoModuleId = repoModuleId
- self.searchFieldFocused = searchFieldFocused
self.query = query
self.selectedFilters = selectedFilters
self.allFilters = allFilters
@@ -84,15 +80,15 @@ public struct SearchFeature: Feature {
public struct View: FeatureView {
public let store: StoreOf
- public var searchAnimation: Namespace.ID
+ @SwiftUI.State
+ var showStatusBarBackground = false
- @FocusState
- var searchFieldFocused: Bool
+ @Environment(\.theme)
+ var theme
@MainActor
- public init(store: StoreOf, namespace: Namespace.ID) {
+ public init(store: StoreOf) {
self.store = store
- self.searchAnimation = namespace
}
}
diff --git a/Sources/Features/Settings/Components/Logs.swift b/Sources/Features/Settings/Components/Logs.swift
index 963ec52..e943081 100644
--- a/Sources/Features/Settings/Components/Logs.swift
+++ b/Sources/Features/Settings/Components/Logs.swift
@@ -202,43 +202,50 @@ public extension Logs {
.padding()
}
#if os(iOS)
- .topBar(title: "Logs", backCallback: {
- store.send(.didTapBackButton)
- }, trailingAccessory: {
- Button {
- store.send(.didTapViewerList)
- } label: {
- HStack {
- WithViewStore(store, observe: \.selected) { viewStore in
- switch viewStore.state {
- case .system:
- Text("System")
- case let .module(_, module, _):
- Text(module.name)
+ .navigationBarTitle("Logs", displayMode: .inline)
+ .navigationBarBackButtonHidden()
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button {
+ store.send(.didTapBackButton)
+ } label: {
+ Image(systemName: "chevron.left")
+ }
+ .buttonStyle(.materialToolbarItem)
+ }
+
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ store.send(.didTapViewerList)
+ } label: {
+ HStack {
+ WithViewStore(store, observe: \.selected) { viewStore in
+ switch viewStore.state {
+ case .system:
+ Text("System")
+ case let .module(_, module, _):
+ Text(module.name)
+ }
}
- }
- Image(systemName: "chevron.up.chevron.down")
+ Image(systemName: "chevron.up.chevron.down")
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 6)
+ .background(.gray.opacity(0.12), in: Capsule())
}
- .padding(.horizontal, 8)
- .padding(.vertical, 6)
- .background(.gray.opacity(0.12), in: Capsule())
+ .buttonStyle(.plain)
+ .font(.footnote.weight(.medium))
}
- .buttonStyle(.plain)
- .font(.footnote.weight(.medium))
- .frame(maxHeight: .infinity)
- }, bottomAccessory: {
- // TODO: Add filters
- EmptyView()
- })
+ }
#else
- .topBar(title: "Logs")
+ .navigationTitle("Logs")
#endif
.task { store.send(.onTask) }
.moduleListsSheet(
store.scope(
state: \.$moduleLists,
- action: Logs.Action.moduleLists
+ action: \.moduleLists
)
)
}
diff --git a/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift b/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift
index e645f6f..e22ac3d 100644
--- a/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift
+++ b/Sources/Features/Settings/Platforms/SettingsFeature+iOS.swift
@@ -7,6 +7,7 @@
//
import BuildClient
+import ComposableArchitecture
import Dependencies
import SwiftUI
@@ -39,13 +40,30 @@ private struct VersionView: View {
var build
var body: some View {
- VStack {
- Text("Made with ❤️")
+ VStack(spacing: 12) {
+ Text("""
+ Design and developed by \
+ [@errorerrorerror](https://errorerrorerror.dev) \
+ & \
+ [contributors](https://github.com/Mochi-Team/mochi/contributors)
+ """
+ )
+ .multilineTextAlignment(.center)
Text("Version: \(build.version.description) (\(build.number.rawValue))")
}
+ .padding(.vertical, 12)
+ .padding(.horizontal, 12)
.font(.footnote.weight(.medium))
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .center)
}
}
+
+#Preview {
+ SettingsFeature.View(
+ store: .init(initialState: .init()) {
+ EmptyReducer()
+ }
+ )
+}
#endif
diff --git a/Sources/Features/Settings/SettingsFeature+View.swift b/Sources/Features/Settings/SettingsFeature+View.swift
index 628ac1e..2cc24ab 100644
--- a/Sources/Features/Settings/SettingsFeature+View.swift
+++ b/Sources/Features/Settings/SettingsFeature+View.swift
@@ -17,11 +17,11 @@ import ViewComponents
extension SettingsFeature.View: View {
@MainActor
public var body: some View {
- NavStack(store.scope(state: \.path, action: Action.InternalAction.path)) {
+ NavStack(store.scope(state: \.path, action: \.internal.path)) {
listSections
.animation(.easeInOut, value: viewStore.userSettings.developerModeEnabled)
.frame(maxWidth: .infinity, maxHeight: .infinity)
- .topBar(title: "Settings")
+ .navigationTitle("Settings")
.task { viewStore.send(.onTask) }
} destination: { store in
SwitchStore(store) { state in
@@ -49,15 +49,16 @@ struct GeneralView: View {
var viewStore: FeatureViewStore
var body: some View {
- SettingsGroup(title: showTitle ? SettingsFeature.Section.general.localized : "") {
- // TODO: Actually allow users to set which discover page to show on startup
- SettingRow(title: "Discover Page", accessory: {
- Toggle("", isOn: .constant(true))
- .labelsHidden()
- .toggleStyle(.switch)
- .controlSize(.small)
- })
- }
+ EmptyView()
+// SettingsGroup(title: showTitle ? SettingsFeature.Section.general.localized : "") {
+// // TODO: Actually allow users to set which discover page to show on startup
+// SettingRow(title: "Discover Page", accessory: {
+// Toggle("", isOn: .constant(true))
+// .labelsHidden()
+// .toggleStyle(.switch)
+// .controlSize(.small)
+// })
+// }
}
}
@@ -82,7 +83,7 @@ struct AppearanceView: View {
}
// TODO: Add option to change app icon
- SettingRow(title: "App Icon", accessory: EmptyView.init) {}
+// SettingRow(title: "App Icon", accessory: EmptyView.init) {}
}
}
}
@@ -124,6 +125,9 @@ struct DeveloperView: View {
@MainActor
struct ThemePicker: View {
+ @ScaledMetric(relativeTo: .body)
+ var heightSize = 54
+
@Binding
var theme: Theme
@@ -142,17 +146,15 @@ struct ThemePicker: View {
withStroke: self.theme == theme ? Color.accentColor : Color.gray.opacity(0.5),
lineWidth: 2,
fill: LinearGradient(
- colors: [
- Theme.light.backgroundColor,
- Theme.light.overBackgroundColor,
- Theme.dark.overBackgroundColor,
- Theme.dark.backgroundColor
+ stops: [
+ .init(color: Theme.light.backgroundColor, location: 0.5),
+ .init(color: Theme.dark.backgroundColor, location: 0.5)
],
startPoint: .leading,
endPoint: .trailing
)
)
- .frame(height: 54)
+ .frame(height: heightSize)
.padding(.top, 1)
} else {
Circle()
@@ -165,7 +167,7 @@ struct ThemePicker: View {
endPoint: .trailing
)
)
- .frame(height: 54)
+ .frame(height: heightSize)
.padding(.top, 1)
}
diff --git a/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift b/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift
index 5bd2df4..3ab1117 100644
--- a/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift
+++ b/Sources/Features/VideoPlayer/VideoPlayerFeature+View.swift
@@ -363,7 +363,7 @@ extension VideoPlayerFeature.View {
ProgressBar(
store: store.scope(
state: \.player.playback,
- action: { $0 }
+ action: \.self
)
)
.foregroundColor(.white)
@@ -790,7 +790,6 @@ extension VideoPlayerFeature.View {
}
}
-
#Preview {
VideoPlayerFeature.View(
store: .init(
@@ -809,4 +808,3 @@ extension VideoPlayerFeature.View {
)
.previewInterfaceOrientation(.landscapeRight)
}
-
diff --git a/Sources/Shared/Architecture/Exported.swift b/Sources/Shared/Architecture/Exported.swift
index 135e5d4..c3a78d3 100644
--- a/Sources/Shared/Architecture/Exported.swift
+++ b/Sources/Shared/Architecture/Exported.swift
@@ -6,9 +6,6 @@
//
//
-@_exported
-import ComposableArchitecture
-@_exported
-import FoundationHelpers
-@_exported
-import LocalizableClient
+@_exported import ComposableArchitecture
+@_exported import FoundationHelpers
+@_exported import LocalizableClient
diff --git a/Sources/Shared/Architecture/Feature.swift b/Sources/Shared/Architecture/Feature.swift
index 5296fe8..b12a2b9 100644
--- a/Sources/Shared/Architecture/Feature.swift
+++ b/Sources/Shared/Architecture/Feature.swift
@@ -50,8 +50,16 @@ public protocol FeatureAction: Equatable, Sendable {
// MARK: - FeatureView
+@MainActor
public protocol FeatureView: View {
associatedtype State: FeatureState
associatedtype Action: FeatureAction
var store: Store { get }
}
+
+public extension FeatureView {
+ @discardableResult
+ func send(_ action: Action.ViewAction) -> StoreTask {
+ store.send(.view(action))
+ }
+}
diff --git a/Sources/Shared/SharedModels/RepoModuleID.swift b/Sources/Shared/SharedModels/RepoModuleID.swift
index d5bf306..04a5035 100644
--- a/Sources/Shared/SharedModels/RepoModuleID.swift
+++ b/Sources/Shared/SharedModels/RepoModuleID.swift
@@ -19,13 +19,8 @@ public struct RepoModuleID: Hashable, Sendable {
public extension Repo.ID {
// Follow reverse domain name notation
var displayIdentifier: String {
- if rawValue.hasDirectoryPath {
- // Assuming this repository is locally stored
- "dev.errorerrorerror.mochi.repo.local"
- } else {
- // Assumes it's a remote url
- rawValue.host?.split(separator: ".").reversed().joined(separator: ".") ?? "unknown"
- }
+ // "dev.errorerrorerror.mochi.repo.local" for local storage
+ rawValue.host?.split(separator: ".").reversed().joined(separator: ".").lowercased() ?? rawValue.absoluteString
}
}
diff --git a/Sources/Shared/Styling/NavStack.swift b/Sources/Shared/Styling/NavStack.swift
index d274791..2d8c187 100644
--- a/Sources/Shared/Styling/NavStack.swift
+++ b/Sources/Shared/Styling/NavStack.swift
@@ -6,6 +6,7 @@
//
//
+@_spi(Presentation)
import ComposableArchitecture
import Foundation
import OrderedCollections
@@ -34,22 +35,17 @@ public struct NavStack:
}
public var body: some View {
- if #available(iOS 16, macOS 13, *) {
+ if #available(iOS 16.0, macOS 13.0, *) {
NavigationStackStore(store) {
root()
- #if os(iOS)
+ #if os(iOS)
.themeable()
- .safeInset(from: \.bottomNavigation, edge: .bottom)
- #endif
+ #endif
} destination: { store in
- #if os(iOS)
destination(store)
- .navigationBarHidden(true)
+ #if os(iOS)
.themeable()
- .safeInset(from: \.bottomNavigation, edge: .bottom)
- #else
- destination(store)
- #endif
+ #endif
}
} else {
#if os(iOS)
@@ -57,7 +53,6 @@ public struct NavStack:
ZStack {
root()
.themeable()
- .safeInset(from: \.bottomNavigation, edge: .bottom)
Group {
WithViewStore(store, observe: \.ids, removeDuplicates: areOrderedSetsDuplicates) { viewStore in
@@ -66,9 +61,7 @@ public struct NavStack:
isActive: .init(
get: { viewStore.state.contains(id) },
set: { isActive, transaction in
- if isActive {
- // Stub
- } else if !isActive, viewStore.state.contains(id) {
+ if !isActive, viewStore.state.contains(id) {
viewStore.send(.popFrom(id: id), transaction: transaction)
}
}
@@ -81,13 +74,12 @@ public struct NavStack:
)
) { store in
destination(store)
- .navigationBarHidden(true)
.themeable()
- .safeInset(from: \.bottomNavigation, edge: .bottom)
}
} label: {
EmptyView()
}
+ .hidden()
}
}
}
@@ -158,10 +150,50 @@ private func returningLastNonNilValue(_ f: @escaping (A) -> B?) -> (A) ->
}
}
+public extension View {
+ @MainActor
+ func stackDestination(
+ store: Store, PresentationAction>,
+ @ViewBuilder destination: @escaping (_ store: Store) -> Destination
+ ) -> some View {
+ self.presentation(store: store) { `self`, $item, destinationContent in
+ if #available(iOS 16.0, macOS 13.0, *) {
+ self.navigationDestination(isPresented: $item.isPresent()) {
+ destinationContent(destination)
+ .themeable()
+ }
+ } else if #unavailable(iOS 16.0) {
+ ZStack {
+ NavigationLink(isActive: $item.isPresent()) {
+ destinationContent(destination)
+ .themeable()
+ } label: {
+ EmptyView()
+ }
+ .hidden()
+
+ self
+ }
+ } else {
+ // macOS only
+ ZStack {
+ if $item.isPresent().wrappedValue {
+ destinationContent(destination)
+ .themeable()
+ } else {
+ self
+ }
+ }
+ .animation(.interactiveSpring(duration: 0.3), value: $item.isPresent().wrappedValue)
+ }
+ }
+ }
+}
+
// MARK: - UINavigationController + UIGestureRecognizerDelegate
#if os(iOS)
-// FIXME: This causes crashes on iOS 17
+// FIXME: This causes crashes on iOS 17?
/// Hacky way to allow swipe back navigation when status bar is hidden
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
diff --git a/Sources/Shared/Styling/Settings/SettingsGroup.swift b/Sources/Shared/Styling/Settings/SettingsGroup.swift
index 3a7d4cf..342ea49 100644
--- a/Sources/Shared/Styling/Settings/SettingsGroup.swift
+++ b/Sources/Shared/Styling/Settings/SettingsGroup.swift
@@ -36,13 +36,14 @@ public struct SettingsGroup: View {
content()
}
.background {
- RoundedRectangle(cornerRadius: 12)
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
.style(
withStroke: Color.gray.opacity(0.2),
lineWidth: 1,
fill: theme.overBackgroundColor
)
}
+ .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.clipped()
}
.frame(maxWidth: .infinity)
diff --git a/Sources/Shared/Styling/Settings/SettingsRow.swift b/Sources/Shared/Styling/Settings/SettingsRow.swift
index 195fe77..23356c5 100644
--- a/Sources/Shared/Styling/Settings/SettingsRow.swift
+++ b/Sources/Shared/Styling/Settings/SettingsRow.swift
@@ -61,7 +61,7 @@ public struct SettingRow: View {
HStack {
VStack {
Text(title)
- .font(.callout.weight(.medium))
+ .font(.callout)
.foregroundColor(theme.textColor)
if let footer {
Text(footer)
diff --git a/Sources/Shared/Styling/TopBar.swift b/Sources/Shared/Styling/TopBar.swift
index 3983e53..bb0f4ed 100644
--- a/Sources/Shared/Styling/TopBar.swift
+++ b/Sources/Shared/Styling/TopBar.swift
@@ -11,319 +11,26 @@ import SwiftUI
import UserSettingsClient
import ViewComponents
-// MARK: - TopBarBackgroundStyle
-
-public enum TopBarBackgroundStyle: Equatable {
- case system
- case gradient(Easing = .easeIn)
- case blurred(fade: Bool = false)
- case clear
-}
-
-// MARK: - TopBarView
-
-#if os(iOS)
-@MainActor
-public struct TopBarView: View {
- let backCallback: (() -> Void)?
- var backgroundStyle: TopBarBackgroundStyle = .system
- let leadingAccessory: () -> LeadingAccessory
- let trailingAccessory: () -> TrailingAccessory
- let bottomAccessory: () -> BottomAccessory
-
- @Environment(\.theme)
- var theme
-
- public init(
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder leadingAccessory: @escaping () -> LeadingAccessory,
- @ViewBuilder trailingAccessory: @escaping () -> TrailingAccessory,
- @ViewBuilder bottomAccessory: @escaping () -> BottomAccessory
- ) {
- self.backCallback = backCallback
- self.leadingAccessory = leadingAccessory
- self.trailingAccessory = trailingAccessory
- self.bottomAccessory = bottomAccessory
- self.backgroundStyle = backgroundStyle
- }
-
- public var body: some View {
- VStack(alignment: .leading, spacing: 16) {
- HStack {
- if let backCallback {
- SwiftUI.Button {
- backCallback()
- } label: {
- Image(systemName: "chevron.backward")
- }
- .buttonStyle(.materialToolbarImage)
- .frame(maxHeight: .infinity)
- }
-
- leadingAccessory()
- .frame(maxHeight: .infinity)
-
- Spacer()
-
- trailingAccessory()
- .frame(maxHeight: .infinity)
- }
- .fixedSize(horizontal: false, vertical: true)
-
- bottomAccessory()
- }
- .safeAreaInset(edge: .leading) {
- Spacer()
- .frame(width: 0, height: 0)
- .padding(.leading)
- }
- .safeAreaInset(edge: .trailing) {
- Spacer()
- .frame(width: 0, height: 0)
- .padding(.trailing)
- }
- .padding(.top, 12)
- .padding(.bottom, 12)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(
- ZStack {
- switch backgroundStyle {
- case .system:
- theme.backgroundColor
- .transition(.opacity)
- case .gradient:
- LinearGradient(
- gradient: .init(
- colors: [
- theme.backgroundColor,
- theme.backgroundColor.opacity(0.0)
- ]
- ),
- startPoint: .top,
- endPoint: .bottom
- )
- .transition(.opacity)
- case let .blurred(fade):
- Rectangle()
- .fill(.ultraThinMaterial)
- .preferredColorScheme(theme.colorScheme)
- .mask(LinearGradient(
- gradient: .init(colors: fade ? [.black, .black.opacity(0)] : [], easing: .easeIn),
- startPoint: .top,
- endPoint: .bottom
- ))
- .transition(.opacity)
- case .clear:
- EmptyView()
- .transition(.opacity)
- }
- }
- .edgesIgnoringSafeArea(.top)
- .animation(.easeInOut, value: backgroundStyle)
- )
- }
-
- public func backgroundStyle(_ style: TopBarBackgroundStyle) -> Self {
- var copy = self
- copy.backgroundStyle = style
- return copy
- }
+public extension ButtonStyle where Self == MaterialToolbarItemButtonStyle {
+ static var materialToolbarItem: MaterialToolbarItemButtonStyle { .init() }
}
-public extension TopBarView {
- init(
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder leadingAccessory: @escaping () -> LeadingAccessory
- ) where TrailingAccessory == EmptyView, BottomAccessory == EmptyView {
- self.init(
- backgroundStyle: backgroundStyle,
- backCallback: backCallback,
- leadingAccessory: leadingAccessory,
- trailingAccessory: EmptyView.init,
- bottomAccessory: EmptyView.init
- )
- }
-
- init(
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder leadingAccessory: @escaping () -> LeadingAccessory,
- @ViewBuilder trailingAccessory: @escaping () -> TrailingAccessory
- ) where BottomAccessory == EmptyView {
- self.init(
- backgroundStyle: backgroundStyle,
- backCallback: backCallback,
- leadingAccessory: leadingAccessory,
- trailingAccessory: trailingAccessory,
- bottomAccessory: EmptyView.init
- )
- }
-
- init(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder trailingAccessory: @escaping () -> TrailingAccessory,
- @ViewBuilder bottomAccessory: @escaping () -> BottomAccessory
- ) where LeadingAccessory == Text? {
- self.init(
- backgroundStyle: backgroundStyle,
- backCallback: backCallback,
- leadingAccessory: {
- if let title {
- Text(title)
- .font(.title.bold())
- }
- },
- trailingAccessory: trailingAccessory,
- bottomAccessory: bottomAccessory
- )
- }
-
- init(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil
- ) where LeadingAccessory == Text?, TrailingAccessory == EmptyView, BottomAccessory == EmptyView {
- self.init(title: title, backgroundStyle: backgroundStyle, backCallback: backCallback) {
- EmptyView()
- } bottomAccessory: {
- EmptyView()
- }
- }
+// MARK: - MaterialToolbarItemButtonStyle
- init(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder trailingAccessory: @escaping () -> TrailingAccessory
- ) where LeadingAccessory == Text?, BottomAccessory == EmptyView {
- self.init(title: title, backgroundStyle: backgroundStyle, backCallback: backCallback) {
- trailingAccessory()
- } bottomAccessory: {
- EmptyView()
- }
- }
-
- init(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder bottomAccessory: @escaping () -> BottomAccessory
- ) where LeadingAccessory == Text?, TrailingAccessory == EmptyView {
- self.init(title: title, backgroundStyle: backgroundStyle, backCallback: backCallback) {
- EmptyView()
- } bottomAccessory: {
- bottomAccessory()
- }
- }
-}
-
-@MainActor
-public extension View {
- func topBar(
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder leadingAccessory: @escaping () -> some View,
- @ViewBuilder trailingAccessory: @escaping () -> some View,
- @ViewBuilder bottomAccessory: @escaping () -> some View
- ) -> some View {
- safeAreaInset(edge: .top) {
- TopBarView(
- backgroundStyle: backgroundStyle,
- backCallback: backCallback,
- leadingAccessory: leadingAccessory,
- trailingAccessory: trailingAccessory,
- bottomAccessory: bottomAccessory
- )
- .frame(maxWidth: .infinity)
- }
- }
-
- func topBar(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder trailingAccessory: @escaping () -> some View,
- @ViewBuilder bottomAccessory: @escaping () -> some View
- ) -> some View {
- safeAreaInset(edge: .top) {
- TopBarView(
- title: title,
- backgroundStyle: backgroundStyle,
- backCallback: backCallback,
- trailingAccessory: trailingAccessory,
- bottomAccessory: bottomAccessory
- )
- .frame(maxWidth: .infinity)
- }
- }
-
- func topBar(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil
- ) -> some View {
- topBar(title: title, backgroundStyle: backgroundStyle, backCallback: backCallback) {
- EmptyView()
- } bottomAccessory: {
- EmptyView()
- }
- }
-
- func topBar(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder trailingAccessory: @escaping () -> some View
- ) -> some View {
- topBar(title: title, backgroundStyle: backgroundStyle, backCallback: backCallback, trailingAccessory: trailingAccessory) {
- EmptyView()
- }
- }
-
- func topBar(
- title: String? = nil,
- backgroundStyle: TopBarBackgroundStyle = .system,
- backCallback: (() -> Void)? = nil,
- @ViewBuilder bottomAccessory: @escaping () -> some View
- ) -> some View {
- topBar(title: title, backgroundStyle: backgroundStyle, backCallback: backCallback) {
- EmptyView()
- } bottomAccessory: {
- bottomAccessory()
- }
- }
-}
-
-public extension ButtonStyle where Self == MaterialToolbarImageButtonStyle {
- static var materialToolbarImage: MaterialToolbarImageButtonStyle { .init() }
-}
-
-// MARK: - MaterialToolbarImageButtonStyle
-
-public struct MaterialToolbarImageButtonStyle: ButtonStyle {
+public struct MaterialToolbarItemButtonStyle: ButtonStyle {
public init() {}
- @ScaledMetric
- var size = 28.0
-
public func makeBody(configuration: Configuration) -> some View {
configuration
.label
- .font(.callout.bold())
- .frame(width: size, height: size)
- .background(.ultraThinMaterial, in: Circle())
- .contentShape(Rectangle())
+ .materialToolbarItemStyle()
.scaleEffect(configuration.isPressed ? 0.85 : 1.0)
.animation(.spring(), value: configuration.isPressed)
}
}
public extension MenuStyle where Self == MaterialToolbarButtonMenuStyle {
- static var materialToolbarImage: MaterialToolbarButtonMenuStyle { .init() }
+ static var materialToolbarItem: MaterialToolbarButtonMenuStyle { .init() }
}
// MARK: - MaterialToolbarButtonMenuStyle
@@ -333,20 +40,32 @@ public struct MaterialToolbarButtonMenuStyle: MenuStyle {
public func makeBody(configuration: Configuration) -> some View {
Menu(configuration)
- .foregroundColor(.label)
- .font(.callout.bold())
- .frame(width: 28, height: 28)
- .background(.ultraThinMaterial, in: Circle())
- .contentShape(Rectangle())
+ .materialToolbarItemStyle()
}
}
-#endif
+private struct MaterialToolbarItemStyle: ViewModifier {
+ @ScaledMetric
+ var fontSize = 12
+
+ @ScaledMetric
+ var viewSize = 28
+
+ func body(content: Content) -> some View {
+ Circle()
+ .style(withStroke: .gray.opacity(0.16), fill: .regularMaterial)
+ .overlay {
+ content
+ .foregroundColor(.label)
+ .font(.system(size: fontSize, weight: .bold, design: .default))
+ }
+ .frame(width: viewSize, height: viewSize, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
+ .contentShape(Rectangle())
+ }
+}
-#if canImport(AppKit)
public extension View {
- func topBar(title: String) -> some View {
- self.navigationTitle(title)
+ func materialToolbarItemStyle() -> some View {
+ self.modifier(MaterialToolbarItemStyle())
}
}
-#endif
diff --git a/Sources/Shared/ViewComponents/InsetValue+Values.swift b/Sources/Shared/ViewComponents/InsetValue+Values.swift
index af22cc1..2ccbc72 100644
--- a/Sources/Shared/ViewComponents/InsetValue+Values.swift
+++ b/Sources/Shared/ViewComponents/InsetValue+Values.swift
@@ -10,13 +10,13 @@ import Foundation
// MARK: - InsetTabNavigationKey
-public struct InsetTabNavigationKey: InsetableKey {
- public static var defaultValue: CGSize = .zero
-}
-
-public extension InsetableValues {
- var bottomNavigation: CGSize {
- get { self[InsetTabNavigationKey.self] }
- set { self[InsetTabNavigationKey.self] = newValue }
- }
-}
+// public struct InsetTabNavigationKey: InsetableKey {
+// public static var defaultValue: CGSize = .zero
+// }
+//
+// public extension InsetableValues {
+// var bottomNavigation: CGSize {
+// get { self[InsetTabNavigationKey.self] }
+// set { self[InsetTabNavigationKey.self] = newValue }
+// }
+// }
diff --git a/Sources/Shared/ViewComponents/ScrollViewTracker.swift b/Sources/Shared/ViewComponents/ScrollViewTracker.swift
new file mode 100644
index 0000000..56365e0
--- /dev/null
+++ b/Sources/Shared/ViewComponents/ScrollViewTracker.swift
@@ -0,0 +1,60 @@
+//
+// ScrollViewTracker.swift
+//
+//
+// Created by ErrorErrorError on 12/12/23.
+//
+//
+
+import Foundation
+import SwiftUI
+
+private struct ScrollOffsetPreferenceKey: SwiftUI.PreferenceKey {
+ static var defaultValue: CGPoint = .zero
+
+ static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
+ value = nextValue()
+ }
+}
+
+private let scrollOffsetNamespace = "scrollView"
+
+public struct ScrollViewTracker: View {
+ public init(
+ _ axis: Axis.Set = [.horizontal, .vertical],
+ showsIndicators: Bool = true,
+ onScroll: ScrollAction? = nil,
+ content: @escaping () -> Content
+ ) {
+ self.axis = axis
+ self.showsIndicators = showsIndicators
+ self.action = onScroll
+ self.content = content
+ }
+
+ private let axis: Axis.Set
+ private let showsIndicators: Bool
+ private let content: () -> Content
+ private let action: ScrollAction?
+
+ public typealias ScrollAction = (_ offset: CGPoint) -> Void
+
+ public var body: some View {
+ ScrollView(axis, showsIndicators: showsIndicators) {
+ ZStack(alignment: .top) {
+ GeometryReader { geo in
+ Color.clear
+ .preference(
+ key: ScrollOffsetPreferenceKey.self,
+ value: geo.frame(in: .named(scrollOffsetNamespace)).origin
+ )
+ }
+ .frame(height: 0)
+
+ content()
+ }
+ }
+ .coordinateSpace(name: scrollOffsetNamespace)
+ .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: { action?($0) })
+ }
+}