mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
991 lines
30 KiB
Swift
991 lines
30 KiB
Swift
//
|
|
// Styles.swift
|
|
// Pearcleaner
|
|
//
|
|
// Created by Alin Lupascu on 10/31/23.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
struct SimpleButtonStyle: ButtonStyle {
|
|
@State private var hovered = false
|
|
let icon: String
|
|
let iconFlip: String
|
|
let label: String
|
|
let help: String
|
|
let color: Color
|
|
let size: CGFloat
|
|
let padding: CGFloat
|
|
let rotate: Bool
|
|
|
|
init(icon: String, iconFlip: String = "", label: String = "", help: String, color: Color = Color("mode"), size: CGFloat = 20, padding: CGFloat = 5, rotate: Bool = false) {
|
|
self.icon = icon
|
|
self.iconFlip = iconFlip
|
|
self.label = label
|
|
self.help = help
|
|
self.color = color
|
|
self.size = size
|
|
self.padding = padding
|
|
self.rotate = rotate
|
|
}
|
|
|
|
func makeBody(configuration: Self.Configuration) -> some View {
|
|
HStack(alignment: .center) {
|
|
Image(systemName: (hovered && !iconFlip.isEmpty) ? iconFlip : icon)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: size, height: size)
|
|
.scaleEffect(hovered ? 1.1 : 1.0)
|
|
.rotationEffect(.degrees(rotate ? (hovered ? 90 : 0) : 0))
|
|
.animation(.easeInOut(duration: 0.2), value: hovered)
|
|
if !label.isEmpty {
|
|
Text(label)
|
|
}
|
|
}
|
|
.foregroundColor(hovered ? color : color.opacity(0.5))
|
|
.padding(padding)
|
|
.onHover { hovering in
|
|
withAnimation() {
|
|
hovered = hovering
|
|
}
|
|
}
|
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
|
.help(help)
|
|
}
|
|
}
|
|
|
|
|
|
struct RegularButtonStyle: ButtonStyle {
|
|
let icon: String
|
|
let label: String
|
|
let help: String
|
|
@State private var hovered = false
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
HStack(alignment: .center) {
|
|
Image(systemName: icon).foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
|
|
Text(label).textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
|
|
|
|
}
|
|
.padding(8)
|
|
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
|
|
.cornerRadius(6)
|
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
|
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
|
|
.onHover { hovering in
|
|
withAnimation(Animation.easeIn(duration: 0.15)) {
|
|
hovered = hovering
|
|
}
|
|
}
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
|
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
|
)
|
|
.help(help)
|
|
|
|
}
|
|
}
|
|
|
|
|
|
struct ResetSettingsButtonStyle: ButtonStyle {
|
|
@Binding var isResetting: Bool
|
|
let label: String
|
|
let help: String
|
|
@State private var hovered = false
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
HStack(alignment: .center) {
|
|
if isResetting {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle())
|
|
.controlSize(.small)
|
|
.frame(width: 15)
|
|
} else {
|
|
Image(systemName: "gear")
|
|
.foregroundColor(Color("mode")).padding(.leading, 2).padding(.trailing, 0)
|
|
}
|
|
Text(label)
|
|
.textCase(.uppercase).foregroundColor(Color("mode")).fontWeight(.bold).padding(.trailing, 2)
|
|
}
|
|
.padding(8)
|
|
.background(!configuration.isPressed ? hovered ? Color("mode").opacity(0.4) : Color("mode").opacity(0) : Color("mode").opacity(0.5))
|
|
.cornerRadius(6)
|
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
|
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
|
|
.onHover { hovering in
|
|
withAnimation(Animation.easeIn(duration: 0.15)) {
|
|
hovered = hovering
|
|
}
|
|
}
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.strokeBorder(Color("mode").opacity(0.5), lineWidth: 1)
|
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
|
)
|
|
.help(help)
|
|
}
|
|
}
|
|
|
|
|
|
struct SimpleCheckboxToggleStyle: ToggleStyle {
|
|
@EnvironmentObject private var themeSettings: ThemeSettings
|
|
@State private var isHovered: Bool = false
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
HStack {
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(themeSettings.themeColor.darker(by: 5))
|
|
.frame(width: 18, height: 18)
|
|
.overlay {
|
|
if configuration.isOn {
|
|
Image(systemName: "checkmark")
|
|
.foregroundStyle(Color("mode"))
|
|
.scaleEffect(isHovered ? 0.8 : 1.0)
|
|
}
|
|
}
|
|
.onTapGesture {
|
|
withAnimation(.smooth()) {
|
|
configuration.isOn.toggle()
|
|
}
|
|
}
|
|
configuration.label
|
|
}
|
|
.onHover(perform: { hovering in
|
|
self.isHovered = hovering
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
struct InfoButton: View {
|
|
@State private var isPopoverPresented: Bool = false
|
|
let text: String
|
|
let color: Color
|
|
let label: String
|
|
let warning: Bool
|
|
let edge: Edge
|
|
|
|
init(text: String, color: Color = Color("mode"), label: String = "", warning: Bool = false, edge: Edge = .bottom) {
|
|
self.text = text
|
|
self.color = color
|
|
self.label = label
|
|
self.warning = warning
|
|
self.edge = edge
|
|
|
|
}
|
|
|
|
var body: some View {
|
|
Button(action: {
|
|
self.isPopoverPresented.toggle()
|
|
}) {
|
|
HStack(alignment: .center, spacing: 5) {
|
|
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 14, height: 14)
|
|
.foregroundColor(!warning ? color.opacity(0.7) : color)
|
|
.frame(height: 16)
|
|
if !label.isEmpty {
|
|
Text(label)
|
|
.font(.callout)
|
|
.foregroundColor(color.opacity(0.7))
|
|
|
|
}
|
|
}
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovered in
|
|
if isHovered {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
.popover(isPresented: $isPopoverPresented, arrowEdge: edge) {
|
|
VStack {
|
|
Spacer()
|
|
Text(text)
|
|
.font(.callout)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
Spacer()
|
|
}
|
|
.frame(width: 300)
|
|
}
|
|
.padding(.horizontal, 5)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct InfoButtonPerms: View {
|
|
@State private var isPopoverPresented: Bool = false
|
|
let color: Color
|
|
let label: String
|
|
let warning: Bool
|
|
|
|
init(color: Color = Color("mode"), label: String = "", warning: Bool = false) {
|
|
self.color = color
|
|
self.label = label
|
|
self.warning = warning
|
|
|
|
}
|
|
|
|
var body: some View {
|
|
Button(action: {
|
|
self.isPopoverPresented.toggle()
|
|
}) {
|
|
HStack(alignment: .center, spacing: 5) {
|
|
Image(systemName: !warning ? "info.circle.fill" : "exclamationmark.triangle.fill")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 14, height: 14)
|
|
.foregroundColor(!warning ? color.opacity(0.7) : color)
|
|
.frame(height: 16)
|
|
if !label.isEmpty {
|
|
Text(label)
|
|
.font(.callout)
|
|
.foregroundColor(color.opacity(0.7))
|
|
|
|
}
|
|
}
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
.onHover { isHovered in
|
|
if isHovered {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
.popover(isPresented: $isPopoverPresented, arrowEdge: .bottom) {
|
|
VStack(alignment: .leading, spacing: 15) {
|
|
|
|
HStack(alignment: .top, spacing: 20) {
|
|
Image(systemName: "externaldrive")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20, height: 20)
|
|
.foregroundStyle(Color("mode").opacity(0.5))
|
|
Text("Full Disk permission to access files/folders in system paths")
|
|
.font(.callout)
|
|
.foregroundStyle(Color("mode").opacity(0.5))
|
|
}
|
|
|
|
HStack(alignment: .top, spacing: 20) {
|
|
Image(systemName: "accessibility")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20, height: 20)
|
|
.foregroundStyle(Color("mode").opacity(0.5))
|
|
Text("Accessibility permission to allow execution of AppleScript")
|
|
.font(.callout)
|
|
.foregroundStyle(Color("mode").opacity(0.5))
|
|
}
|
|
|
|
HStack(alignment: .top, spacing: 20) {
|
|
Image(systemName: "gearshape.2")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20, height: 20)
|
|
.foregroundStyle(Color("mode").opacity(0.5))
|
|
Text("Automation permission to perform delete actions via Finder")
|
|
.font(.callout)
|
|
.foregroundStyle(Color("mode").opacity(0.5))
|
|
}
|
|
|
|
}
|
|
.padding()
|
|
}
|
|
.padding(.horizontal, 5)
|
|
}
|
|
}
|
|
|
|
|
|
struct UninstallButton: ButtonStyle {
|
|
@State private var hovered: Bool = false
|
|
var isEnabled: Bool
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
HStack(alignment: .center, spacing: 10) {
|
|
Image(systemName: !hovered ? "trash" : "trash.fill")
|
|
.foregroundColor(isEnabled ? .white.opacity(1) : .white.opacity(0.3))
|
|
.scaleEffect(hovered ? 1.2 : 1.0)
|
|
.animation(.easeInOut(duration: 0.2), value: hovered)
|
|
|
|
Divider()
|
|
.frame(height: 24)
|
|
.opacity(0.5)
|
|
|
|
configuration.label
|
|
.foregroundColor(isEnabled ? .white.opacity(1) : .white.opacity(0.3))
|
|
|
|
}
|
|
.frame(height: 24)
|
|
.frame(minWidth: 75)
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 4)
|
|
.background(configuration.isPressed ? Color("uninstall").opacity(0.8) : Color("uninstall"))
|
|
.cornerRadius(8)
|
|
.onHover { over in
|
|
hovered = over
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct RescanButton: ButtonStyle {
|
|
@State private var hovered: Bool = false
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
HStack(alignment: .center, spacing: 10) {
|
|
Image(systemName: !hovered ? "arrow.counterclockwise.circle" : "arrow.counterclockwise.circle.fill")
|
|
.foregroundColor(.white)
|
|
.scaleEffect(hovered ? 1.2 : 1.0)
|
|
.animation(.easeInOut(duration: 0.2), value: hovered)
|
|
|
|
Divider()
|
|
.frame(height: 24)
|
|
.foregroundColor(.white)
|
|
.opacity(0.5)
|
|
|
|
configuration.label
|
|
.foregroundColor(.white)
|
|
|
|
}
|
|
.frame(height: 24)
|
|
.frame(minWidth: 75)
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 4)
|
|
.background(configuration.isPressed ? Color("button").opacity(0.8) : Color("button"))
|
|
.cornerRadius(8)
|
|
.onHover { over in
|
|
hovered = over
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct WarningPopoverView: View {
|
|
var label: String
|
|
var bodyText: String
|
|
|
|
@Binding var isPresented: Bool
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundStyle(.red)
|
|
.popover(isPresented: $isPresented, arrowEdge: .top) {
|
|
VStack {
|
|
Spacer()
|
|
Text(bodyText)
|
|
.font(.callout)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
Spacer()
|
|
}
|
|
.frame(width: 300)
|
|
}
|
|
|
|
Text(label)
|
|
.foregroundStyle(Color.red)
|
|
|
|
Spacer()
|
|
}
|
|
.onTapGesture {
|
|
isPresented.toggle()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public struct NewFeatureView: View {
|
|
var text: String
|
|
var mini: Bool
|
|
@Binding var showFeature: Bool
|
|
|
|
public var body: some View {
|
|
VStack {
|
|
HStack {
|
|
Image(systemName: "star")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 20, height: 20)
|
|
Text("New features for v\((Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)!)!")
|
|
.font(.headline).bold()
|
|
Spacer()
|
|
Button("") {
|
|
withAnimation(Animation.easeInOut(duration: 0.5)) {
|
|
showFeature = false
|
|
}
|
|
}
|
|
.buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", help: "Close", color: (Color("mode").opacity(0.5))))
|
|
.onHover { isHovered in
|
|
if isHovered {
|
|
NSCursor.pointingHand.push()
|
|
} else {
|
|
NSCursor.pop()
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
|
|
ScrollView() {
|
|
Text(text)
|
|
.font(.callout)
|
|
.multilineTextAlignment(.leading)
|
|
.padding(.trailing)
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.bottom)
|
|
|
|
}
|
|
.frame(maxWidth: mini ? 200 : 400, maxHeight: mini ? 300 : 250)
|
|
.background(.ultraThinMaterial)
|
|
.cornerRadius(10)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 0.6)
|
|
)
|
|
.padding()
|
|
}
|
|
}
|
|
|
|
|
|
public struct SlideableDivider: View {
|
|
@EnvironmentObject private var themeSettings: ThemeSettings
|
|
@Binding var dimension: Double
|
|
@State private var dimensionStart: Double?
|
|
@State private var handleWidth: Double = 4
|
|
@State private var handleHeight: Double = 30
|
|
@State private var isHovered: Bool = false
|
|
public init(dimension: Binding<Double>) {
|
|
self._dimension = dimension
|
|
}
|
|
|
|
public var body: some View {
|
|
Rectangle()
|
|
.foregroundStyle(Color("mode").opacity(0.0))
|
|
.frame(width: 10)
|
|
.onHover { inside in
|
|
if inside {
|
|
NSCursor.resizeLeftRight.push()
|
|
|
|
} else {
|
|
NSCursor.pop()
|
|
|
|
}
|
|
}
|
|
.contextMenu {
|
|
Button("Reset Size") {
|
|
dimension = 280
|
|
}
|
|
}
|
|
.gesture(drag)
|
|
.help("Right click to reset size")
|
|
}
|
|
|
|
var drag: some Gesture {
|
|
DragGesture(minimumDistance: 5, coordinateSpace: CoordinateSpace.global)
|
|
.onChanged { val in
|
|
if dimensionStart == nil {
|
|
dimensionStart = dimension
|
|
}
|
|
let delta = val.location.x - val.startLocation.x
|
|
let newDimension = dimensionStart! + Double(delta)
|
|
|
|
// Set minimum and maximum width
|
|
let minWidth: Double = 250
|
|
let maxWidth: Double = 350
|
|
dimension = max(minWidth, min(maxWidth, newDimension))
|
|
NSCursor.closedHand.set()
|
|
handleWidth = 6
|
|
handleHeight = 40
|
|
}
|
|
.onEnded { val in
|
|
dimensionStart = nil
|
|
NSCursor.arrow.set()
|
|
handleWidth = 4
|
|
handleHeight = 30
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
struct LabeledDivider: View {
|
|
let label: String
|
|
@EnvironmentObject var appState: AppState
|
|
|
|
var body: some View {
|
|
HStack(spacing: 0) {
|
|
Rectangle()
|
|
.frame(height: 1)
|
|
.foregroundColor(Color("mode").opacity(0.2))
|
|
|
|
Text(label)
|
|
.textCase(.uppercase)
|
|
.font(.title2)
|
|
.foregroundColor(Color("mode").opacity(0.6))
|
|
.padding(.horizontal, 10)
|
|
.frame(minWidth: 80)
|
|
|
|
Rectangle()
|
|
.frame(height: 1)
|
|
.foregroundColor(Color("mode").opacity(0.2))
|
|
}
|
|
.frame(minHeight: 35)
|
|
}
|
|
}
|
|
|
|
|
|
struct AnimatedSearchStyle: TextFieldStyle {
|
|
@State private var isHovered = false
|
|
@FocusState private var isFocused: Bool
|
|
@Binding var text: String
|
|
@EnvironmentObject var appState: AppState
|
|
@EnvironmentObject var fsm: FolderSettingsManager
|
|
|
|
func _body(configuration: TextField<Self._Label>) -> some View {
|
|
|
|
HStack(spacing: 5) {
|
|
|
|
if isHovered || !text.isEmpty {
|
|
|
|
configuration
|
|
.frame(height: 15)
|
|
.font(.title3)
|
|
.textFieldStyle(PlainTextFieldStyle())
|
|
.padding(.leading, 50)
|
|
|
|
|
|
Image(systemName: "arrow.triangle.2.circlepath")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 15, height: 15)
|
|
.padding(.trailing, 5)
|
|
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
|
|
.onTapGesture {
|
|
withAnimation {
|
|
reloadAppsList(appState: appState, fsm: fsm)
|
|
}
|
|
|
|
}
|
|
.help("Refresh app list")
|
|
|
|
|
|
if text != "" {
|
|
Image(systemName: "xmark")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 10, height: 10)
|
|
.padding(.trailing, 5)
|
|
.foregroundStyle(isHovered ? Color("mode").opacity(0.8) : Color("mode").opacity(0.5))
|
|
.onTapGesture {
|
|
withAnimation {
|
|
text = ""
|
|
}
|
|
}
|
|
.help("Clear search")
|
|
}
|
|
|
|
|
|
} else {
|
|
HStack {
|
|
Spacer()
|
|
Image(systemName: "magnifyingglass")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 18, height: 18)
|
|
.foregroundColor(isHovered ? Color("mode") : Color("mode").opacity(0.5))
|
|
}
|
|
.frame(width: 150)
|
|
|
|
}
|
|
}
|
|
.padding(8)
|
|
.overlay(
|
|
Group {
|
|
if isHovered || !text.isEmpty {
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.strokeBorder(Color("mode").opacity(0.4), lineWidth: 1)
|
|
.allowsHitTesting(false)
|
|
.padding(.leading, 50)
|
|
}
|
|
}
|
|
)
|
|
.onHover { hovering in
|
|
withAnimation(Animation.easeInOut(duration: 0.4)) {
|
|
self.isHovered = hovering
|
|
self.isFocused = hovering
|
|
}
|
|
}
|
|
.focused($isFocused)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct GlassEffect: NSViewRepresentable {
|
|
var material: NSVisualEffectView.Material // Choose the material you want
|
|
var blendingMode: NSVisualEffectView.BlendingMode // Choose the blending mode
|
|
|
|
func makeNSView(context: Self.Context) -> NSView {
|
|
let visualEffectView = NSVisualEffectView()
|
|
visualEffectView.material = material
|
|
visualEffectView.blendingMode = blendingMode
|
|
return visualEffectView
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSView, context: Context) { }
|
|
}
|
|
|
|
|
|
|
|
struct VerticalDivider: View {
|
|
|
|
var color: Color = Color("AccentColor")
|
|
var width: CGFloat = 1
|
|
var height: CGFloat = 20
|
|
var opacity: Double = 1
|
|
|
|
var body: some View {
|
|
Divider()
|
|
.foregroundColor(color)
|
|
.frame(width: width, height: height)
|
|
.opacity(opacity)
|
|
.ignoresSafeArea(.all)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
struct WindowActionButton: ButtonStyle {
|
|
enum UserAction {
|
|
case accept
|
|
case cancel
|
|
}
|
|
@State private var hovered = false
|
|
let action: UserAction
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.frame(width: 100)
|
|
.padding(10)
|
|
.background(
|
|
!configuration.isPressed ?
|
|
!hovered ?
|
|
action == .accept ? Color("button") : Color.red :
|
|
action == .accept ? Color("button").opacity(0.8) : Color.red.opacity(0.8) :
|
|
action == .accept ? Color("button").opacity(0.5) : Color.red.opacity(0.5)
|
|
)
|
|
.foregroundColor(.white)
|
|
.cornerRadius(8)
|
|
.onHover { hovering in
|
|
withAnimation(Animation.easeIn(duration: 0.15)) {
|
|
hovered = hovering
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
struct SimpleButtonBrightStyle: ButtonStyle {
|
|
@State private var hovered = false
|
|
let icon: String
|
|
let label: String
|
|
let help: String
|
|
let color: Color
|
|
let shield: Bool?
|
|
|
|
init(icon: String, label: String = "", help: String, color: Color, shield: Bool? = nil) {
|
|
self.icon = icon
|
|
self.label = label
|
|
self.help = help
|
|
self.color = color
|
|
self.shield = shield
|
|
}
|
|
|
|
func makeBody(configuration: Self.Configuration) -> some View {
|
|
HStack {
|
|
Image(systemName: icon)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20)
|
|
.foregroundColor(hovered ? color.opacity(0.5) : color)
|
|
Text(label)
|
|
}
|
|
.padding(5)
|
|
.onHover { hovering in
|
|
withAnimation() {
|
|
hovered = hovering
|
|
}
|
|
}
|
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
|
.help(help)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
struct FilesViewActionButton: ButtonStyle {
|
|
enum UserAction {
|
|
case uninstall
|
|
case close
|
|
}
|
|
@State private var hovered = false
|
|
let action: UserAction
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.frame(width: 70)
|
|
.padding(6)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.strokeBorder(
|
|
configuration.isPressed ?
|
|
Color("AccentColor").opacity(0.7) :
|
|
hovered ? Color("AccentColor").opacity(0.8) : Color("AccentColor").opacity(0.9), lineWidth: 2
|
|
)
|
|
|
|
|
|
)
|
|
.foregroundColor(
|
|
action == .uninstall ? Color("AccentColor") : Color("mode")
|
|
)
|
|
.cornerRadius(6)
|
|
.onHover { hovering in
|
|
withAnimation(Animation.easeIn(duration: 0.15)) {
|
|
hovered = hovering
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
struct SentinelToggleStyle: ToggleStyle {
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
|
|
HStack {
|
|
|
|
ZStack{
|
|
RoundedRectangle(cornerRadius: 50)
|
|
.fill(configuration.isOn ? greenBG : redBG)
|
|
.frame(width: 70, height: 40)
|
|
HStack{
|
|
Image(systemName: "lock.shield")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 26, height: 26)
|
|
.padding(.leading, 10)
|
|
.foregroundColor(.white)
|
|
Spacer()
|
|
Image(systemName: "shield")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 26, height: 26)
|
|
.padding(.trailing, 10)
|
|
.foregroundColor(.white)
|
|
}
|
|
|
|
}
|
|
.frame(width: 70, height: 40, alignment: .center)
|
|
.overlay(
|
|
Circle()
|
|
.foregroundColor(.white)
|
|
.shadow(color: .black.opacity(0.5), radius: 3, x: 0, y: 0)
|
|
.padding(.all, 4)
|
|
.offset(x: configuration.isOn ? 15 : -15, y: 0)
|
|
)
|
|
.overlay(
|
|
Image(systemName: "power")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 20, height: 20)
|
|
.foregroundColor(.black.opacity(0.3))
|
|
.offset(x: configuration.isOn ? 15 : -15, y: 0)
|
|
)
|
|
.onTapGesture {
|
|
withAnimation(.easeInOut(duration: 0.5)) {
|
|
configuration.isOn.toggle()
|
|
}
|
|
}
|
|
.animation(.default, value: configuration.isOn)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private let greenBG: AnyShapeStyle = AnyShapeStyle(
|
|
.green.shadow(.inner(radius: 2, x: 0, y: 1))
|
|
)
|
|
|
|
private let redBG: AnyShapeStyle = AnyShapeStyle(
|
|
.red.shadow(.inner(radius: 2, x: 0, y: 1))
|
|
)
|
|
|
|
|
|
|
|
struct PillPicker: View {
|
|
@Binding var index: Int
|
|
var onTapAction: () -> Void
|
|
@Environment(\.colorScheme) var colorScheme
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 10) {
|
|
ForEach(0..<3) { i in
|
|
ZStack {
|
|
Capsule()
|
|
.fill(Color.white)
|
|
.offset(x: (CGFloat(index - i) * 100))
|
|
.animation(.default, value: index)
|
|
.opacity(index == i ? 1 : 0)
|
|
.scaleEffect(index == i ? 1 : 0.70)
|
|
.shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2)
|
|
|
|
HStack(spacing: 0) {
|
|
|
|
Text(i == 0 ? "Apps" : (i == 1 ? "Widgets" : "Plugins"))
|
|
.font(.system(size: 12))
|
|
.foregroundColor(index == i ? (colorScheme == .dark ? .black : Color("AccentColor")) : Color("mode"))
|
|
}
|
|
.padding(5)
|
|
.frame(height: 25)
|
|
.background(.white.opacity(0.0001))
|
|
}
|
|
.onTapGesture {
|
|
withAnimation {
|
|
index = i
|
|
onTapAction()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 25)
|
|
.padding(5)
|
|
.background(
|
|
Capsule()
|
|
.fill(colorScheme == .dark ? .white.opacity(0.1) : .black.opacity(0.06))
|
|
.overlay(
|
|
Capsule()
|
|
.stroke(Color.black.opacity(colorScheme == .dark ? 0.8 : 0.2), lineWidth: 1)
|
|
.blur(radius: 1)
|
|
.offset(y: 1)
|
|
)
|
|
)
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
struct PearDropView: View {
|
|
|
|
var body: some View {
|
|
VStack(alignment: .center, spacing: 0) {
|
|
HStack(spacing: 0) {
|
|
LinearGradient(gradient: Gradient(colors: [.green, .orange]), startPoint: .leading, endPoint: .trailing)
|
|
.frame(width: 220)
|
|
LinearGradient(gradient: Gradient(colors: [.orange, Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
|
|
.frame(width: 10)
|
|
LinearGradient(gradient: Gradient(colors: [Color("mode").opacity(0.5), Color("mode").opacity(0.5)]), startPoint: .leading, endPoint: .trailing)
|
|
.frame(width: 300)
|
|
}
|
|
.mask(
|
|
Image("logo_text_small")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 500)
|
|
.padding()
|
|
)
|
|
|
|
}
|
|
.frame(height: 120)
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Used for testing UI bounds
|
|
extension View {
|
|
func bounds() -> some View {
|
|
self.border(Color.red, width: 1)
|
|
}
|
|
}
|
|
|
|
// Background color/glass setter
|
|
@ViewBuilder
|
|
func backgroundView(themeSettings: ThemeSettings, darker: Bool = false, glass: Bool = false) -> some View {
|
|
if glass {
|
|
GlassEffect(material: .sidebar, blendingMode: .behindWindow)
|
|
.edgesIgnoringSafeArea(.all)
|
|
} else {
|
|
darker ? themeSettings.themeColor.darker(by: 5).edgesIgnoringSafeArea(.all) : themeSettings.themeColor.edgesIgnoringSafeArea(.all)
|
|
}
|
|
}
|
|
|
|
|
|
struct PickerModifier: ViewModifier {
|
|
@State private var isHovered: Bool = false
|
|
let themeSettings: ThemeSettings
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.buttonStyle(.borderless)
|
|
.padding(4)
|
|
.background {
|
|
backgroundView(themeSettings: themeSettings, darker: isHovered)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
.onHover { hovering in
|
|
isHovered = hovering
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func pickerStyle(themeSettings: ThemeSettings) -> some View {
|
|
self.modifier(PickerModifier(themeSettings: themeSettings))
|
|
}
|
|
}
|
|
|
|
|
|
// Preset view elements
|
|
struct PresetColor: ButtonStyle {
|
|
var fillColor: Color
|
|
var label: String
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
HStack(alignment: .center, spacing: 10) {
|
|
Circle()
|
|
.frame(width: 20, height: 20)
|
|
.fixedSize(horizontal: true, vertical: true)
|
|
.foregroundColor(fillColor)
|
|
.background(fillColor)
|
|
.clipShape(Circle())
|
|
.help(label)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 25)
|
|
.strokeBorder(Color("mode").opacity(0.8), lineWidth: 1.5))
|
|
// Text(label)
|
|
}
|
|
.onHover { inside in inside ? NSCursor.pointingHand.push() : NSCursor.pop() }
|
|
|
|
}
|
|
}
|