mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
180 lines
7 KiB
Swift
180 lines
7 KiB
Swift
/*
|
|
* Atoll (DynamicIsland)
|
|
* Copyright (C) 2024-2026 Atoll Contributors
|
|
*
|
|
* Originally from boring.notch project
|
|
* Modified and adapted for Atoll (DynamicIsland)
|
|
* See NOTICE for details.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import AtollExtensionKit
|
|
import SwiftUI
|
|
import Defaults
|
|
|
|
struct TabModel: Identifiable {
|
|
let id: String
|
|
let label: String
|
|
let icon: String
|
|
let view: NotchViews
|
|
let experienceID: String?
|
|
let accentColor: Color?
|
|
|
|
init(label: String, icon: String, view: NotchViews, experienceID: String? = nil, accentColor: Color? = nil) {
|
|
self.id = experienceID.map { "extension-\($0)" } ?? "system-\(view)-\(label)"
|
|
self.label = label
|
|
self.icon = icon
|
|
self.view = view
|
|
self.experienceID = experienceID
|
|
self.accentColor = accentColor
|
|
}
|
|
}
|
|
|
|
struct TabSelectionView: View {
|
|
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
|
|
@ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared
|
|
@Default(.enableTimerFeature) var enableTimerFeature
|
|
@Default(.enableStatsFeature) var enableStatsFeature
|
|
@Default(.enableColorPickerFeature) var enableColorPickerFeature
|
|
@Default(.timerDisplayMode) var timerDisplayMode
|
|
@Default(.enableThirdPartyExtensions) private var enableThirdPartyExtensions
|
|
@Default(.enableExtensionNotchExperiences) private var enableExtensionNotchExperiences
|
|
@Default(.enableExtensionNotchTabs) private var enableExtensionNotchTabs
|
|
@Default(.showCalendar) private var showCalendar
|
|
@Default(.showMirror) private var showMirror
|
|
@Default(.showStandardMediaControls) private var showStandardMediaControls
|
|
@Default(.enableMinimalisticUI) private var enableMinimalisticUI
|
|
@Namespace var animation
|
|
|
|
private var tabs: [TabModel] {
|
|
var tabsArray: [TabModel] = []
|
|
|
|
if homeTabVisible {
|
|
tabsArray.append(TabModel(label: "Home", icon: "house.fill", view: .home))
|
|
}
|
|
|
|
if Defaults[.dynamicShelf] {
|
|
tabsArray.append(TabModel(label: "Shelf", icon: "tray.fill", view: .shelf))
|
|
}
|
|
|
|
if enableTimerFeature && timerDisplayMode == .tab {
|
|
tabsArray.append(TabModel(label: "Timer", icon: "timer", view: .timer))
|
|
}
|
|
|
|
// Stats tab only shown when stats feature is enabled
|
|
if Defaults[.enableStatsFeature] {
|
|
tabsArray.append(TabModel(label: "Stats", icon: "chart.xyaxis.line", view: .stats))
|
|
}
|
|
|
|
if Defaults[.enableNotes] || (Defaults[.enableClipboardManager] && Defaults[.clipboardDisplayMode] == .separateTab) {
|
|
let label = Defaults[.enableNotes] ? "Notes" : "Clipboard"
|
|
let icon = Defaults[.enableNotes] ? "note.text" : "doc.on.clipboard"
|
|
tabsArray.append(TabModel(label: label, icon: icon, view: .notes))
|
|
}
|
|
if extensionTabsEnabled {
|
|
for payload in extensionTabPayloads {
|
|
guard let tab = payload.descriptor.tab else { continue }
|
|
let accent = payload.descriptor.accentColor.swiftUIColor
|
|
let iconName = tab.iconSymbolName ?? "puzzlepiece.extension"
|
|
tabsArray.append(
|
|
TabModel(
|
|
label: tab.title,
|
|
icon: iconName,
|
|
view: .extensionExperience,
|
|
experienceID: payload.descriptor.id,
|
|
accentColor: accent
|
|
)
|
|
)
|
|
}
|
|
}
|
|
DispatchQueue.main.async {
|
|
ensureValidSelection(with: tabsArray)
|
|
}
|
|
return tabsArray
|
|
}
|
|
var body: some View {
|
|
HStack(spacing: 0) {
|
|
ForEach(tabs) { tab in
|
|
let isSelected = isSelected(tab)
|
|
let activeAccent = tab.accentColor ?? .white
|
|
TabButton(label: tab.label, icon: tab.icon, selected: isSelected) {
|
|
withAnimation(.smooth) {
|
|
if tab.view == .extensionExperience {
|
|
coordinator.selectedExtensionExperienceID = tab.experienceID
|
|
}
|
|
coordinator.currentView = tab.view
|
|
}
|
|
}
|
|
.frame(height: 26)
|
|
.foregroundStyle(isSelected ? activeAccent : .gray)
|
|
.background {
|
|
if isSelected {
|
|
Capsule()
|
|
.fill((tab.accentColor ?? Color(nsColor: .secondarySystemFill)).opacity(0.25))
|
|
.shadow(color: (tab.accentColor ?? .clear).opacity(0.4), radius: 8)
|
|
.matchedGeometryEffect(id: "capsule", in: animation)
|
|
} else {
|
|
Capsule()
|
|
.fill(Color.clear)
|
|
.matchedGeometryEffect(id: "capsule", in: animation)
|
|
.hidden()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
private var extensionTabsEnabled: Bool {
|
|
enableThirdPartyExtensions && enableExtensionNotchExperiences && enableExtensionNotchTabs
|
|
}
|
|
|
|
private var extensionTabPayloads: [ExtensionNotchExperiencePayload] {
|
|
extensionNotchExperienceManager.activeExperiences.filter { $0.descriptor.tab != nil }
|
|
}
|
|
|
|
private var homeTabVisible: Bool {
|
|
if enableMinimalisticUI {
|
|
return true
|
|
}
|
|
return showStandardMediaControls || showCalendar || showMirror
|
|
}
|
|
|
|
private func isSelected(_ tab: TabModel) -> Bool {
|
|
if tab.view == .extensionExperience {
|
|
return coordinator.currentView == .extensionExperience
|
|
&& coordinator.selectedExtensionExperienceID == tab.experienceID
|
|
}
|
|
return coordinator.currentView == tab.view
|
|
}
|
|
|
|
private func ensureValidSelection(with tabs: [TabModel]) {
|
|
guard !tabs.isEmpty else { return }
|
|
if tabs.contains(where: { isSelected($0) }) {
|
|
return
|
|
}
|
|
guard let first = tabs.first else { return }
|
|
if first.view == .extensionExperience {
|
|
coordinator.selectedExtensionExperienceID = first.experienceID
|
|
} else {
|
|
coordinator.selectedExtensionExperienceID = nil
|
|
}
|
|
coordinator.currentView = first.view
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
DynamicIslandHeader().environmentObject(DynamicIslandViewModel())
|
|
}
|