Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 149 additions & 93 deletions FreeYT/LiquidGlassView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,40 +45,51 @@ enum TintPalette: String, CaseIterable, Codable, Equatable {

private struct GlassCardModifier: ViewModifier {
let radius: CGFloat
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency

func body(content: Content) -> some View {
content
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.fill(
LinearGradient(
colors: [
Color.white.opacity(0.10),
Color.white.opacity(0.04)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.background(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.strokeBorder(
LinearGradient(
colors: [
LiquidGlassTheme.strokeStrong,
LiquidGlassTheme.strokeSoft
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1.2
if #available(iOS 26.0, macOS 26.0, *) {
content
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.glassEffect(
reduceTransparency ? .identity : .regular,
in: RoundedRectangle(cornerRadius: radius, style: .continuous)
)
} else {
content
.padding(18)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.fill(
LinearGradient(
colors: [
Color.white.opacity(0.10),
Color.white.opacity(0.04)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
)
.shadow(color: Color.black.opacity(0.28), radius: 22, x: 0, y: 12)
)
.background(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: radius, style: .continuous)
.strokeBorder(
LinearGradient(
colors: [
LiquidGlassTheme.strokeStrong,
LiquidGlassTheme.strokeSoft
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1.2
)
)
)
.shadow(color: Color.black.opacity(0.28), radius: 22, x: 0, y: 12)
}
}
}

Expand Down Expand Up @@ -390,55 +401,76 @@ private struct Pill: View {
let icon: String

var body: some View {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 12, weight: .bold))
Text(text)
.font(.system(size: 12, weight: .semibold))
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
LinearGradient(
colors: [
Color.white.opacity(0.08),
Color.white.opacity(0.03)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
pillContent
}

@ViewBuilder
private var pillContent: some View {
if #available(iOS 26.0, macOS 26.0, *) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 12, weight: .bold))
Text(text)
.font(.system(size: 12, weight: .semibold))
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.foregroundColor(.white)
.glassEffect(.regular, in: .capsule)
} else {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 12, weight: .bold))
Text(text)
.font(.system(size: 12, weight: .semibold))
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
LinearGradient(
colors: [
Color.white.opacity(0.08),
Color.white.opacity(0.03)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
)
.overlay(
RoundedRectangle(cornerRadius: 999, style: .continuous)
.strokeBorder(Color.white.opacity(0.16), lineWidth: 1)
)
.clipShape(Capsule())
.foregroundColor(.white)
.overlay(
RoundedRectangle(cornerRadius: 999, style: .continuous)
.strokeBorder(Color.white.opacity(0.16), lineWidth: 1)
)
.clipShape(Capsule())
.foregroundColor(.white)
}
}
}

/// iOS 26 Liquid Glass toggle wrapper with availability-safe fallbacks
private struct LiquidToggle: View {
let binding: Binding<Bool>
let glassSpace: Namespace.ID
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency

var body: some View {
toggleBody
}

@ViewBuilder
private var toggleBody: some View {
if #available(iOS 26.0, *) {
if #available(iOS 26.0, macOS 26.0, *) {
Toggle(isOn: binding) {
Text(binding.wrappedValue ? "On" : "Off")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.white.opacity(0.8))
}
.toggleStyle(.switch)
.tint(LiquidGlassTheme.success)
.glassEffect()
.glassEffect(
reduceTransparency ? .identity : .regular.interactive(),
in: .capsule
)
.glassEffectID("liquid-toggle", in: glassSpace)
.glassEffectUnion(id: "status-cluster", namespace: glassSpace)
.accessibilityLabel("FreeYT Shield toggle")
#if os(iOS)
.onChange(of: binding.wrappedValue) { _ in Haptics.light() }
Expand Down Expand Up @@ -748,45 +780,70 @@ private struct ActionButton: View {
let tint: TintPalette
let openSettings: () -> Void
@State private var isPressed = false
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency

var body: some View {
Button(action: openSettings) {
HStack(spacing: 10) {
Image(systemName: isEnabled ? "checkmark.circle.fill" : "safari")
.font(.system(size: 18, weight: .semibold))
Text(isEnabled ? "Extension active" : "Open Safari Settings")
.font(.system(size: 17, weight: .semibold))
buttonContent
}

@ViewBuilder
private var buttonContent: some View {
if #available(iOS 26.0, macOS 26.0, *) {
Button(action: openSettings) {
HStack(spacing: 10) {
Image(systemName: isEnabled ? "checkmark.circle.fill" : "safari")
.font(.system(size: 18, weight: .semibold))
Text(isEnabled ? "Extension active" : "Open Safari Settings")
.font(.system(size: 17, weight: .semibold))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 58)
.glassEffect(
reduceTransparency ? .identity : .regular.interactive().tint(tint.primary),
in: .capsule
)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 58)
.background(
Capsule(style: .continuous)
.fill(
LinearGradient(
colors: [
tint.primary.opacity(isEnabled ? 0.45 : 0.65),
tint.secondary.opacity(0.55)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
.disabled(isEnabled)
} else {
Button(action: openSettings) {
HStack(spacing: 10) {
Image(systemName: isEnabled ? "checkmark.circle.fill" : "safari")
.font(.system(size: 18, weight: .semibold))
Text(isEnabled ? "Extension active" : "Open Safari Settings")
.font(.system(size: 17, weight: .semibold))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 58)
.background(
Capsule(style: .continuous)
.fill(
LinearGradient(
colors: [
tint.primary.opacity(isEnabled ? 0.45 : 0.65),
tint.secondary.opacity(0.55)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
)
.overlay(
Capsule(style: .continuous)
.strokeBorder(Color.white.opacity(0.25), lineWidth: 1.2)
)
.shadow(color: tint.secondary.opacity(0.35), radius: 18, x: 0, y: 10)
.overlay(
Capsule(style: .continuous)
.strokeBorder(Color.white.opacity(0.25), lineWidth: 1.2)
)
.shadow(color: tint.secondary.opacity(0.35), radius: 18, x: 0, y: 10)
)
.scaleEffect(isPressed ? 0.97 : 1)
.animation(.spring(response: 0.28, dampingFraction: 0.7), value: isPressed)
}
.disabled(isEnabled)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in isPressed = true }
.onEnded { _ in isPressed = false; Haptics.light() }
)
.scaleEffect(isPressed ? 0.97 : 1)
.animation(.spring(response: 0.28, dampingFraction: 0.7), value: isPressed)
}
.disabled(isEnabled)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in isPressed = true }
.onEnded { _ in isPressed = false; Haptics.light() }
)
}
}

Expand All @@ -795,11 +852,10 @@ private struct ActionButton: View {
/// Wraps content in a GlassEffectContainer when available to enable Liquid Glass unions/morphs
@MainActor @ViewBuilder
private func GlassCluster<Content: View>(glassSpace: Namespace.ID, @ViewBuilder _ content: () -> Content) -> some View {
if #available(iOS 26.0, *) {
if #available(iOS 26.0, macOS 26.0, *) {
GlassEffectContainer(spacing: 12) {
content()
}
.glassEffectUnion(id: "status-cluster", namespace: glassSpace)
} else {
content()
}
Expand All @@ -809,7 +865,7 @@ private func GlassCluster<Content: View>(glassSpace: Namespace.ID, @ViewBuilder
private extension View {
@ViewBuilder
func glassID<H: Hashable & Sendable>(_ id: H, in namespace: Namespace.ID) -> some View {
if #available(iOS 26.0, *) {
if #available(iOS 26.0, macOS 26.0, *) {
self.glassEffectID(id, in: namespace)
} else {
self
Expand Down
Loading