Last active
October 14, 2025 08:28
-
-
Save luthviar/482a0d28a921c2b704f222eb6ec6d410 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ContentView.swift | |
| // Try14Oct2025 | |
| // | |
| // Created by Luthfi Abdurrahim on 14/10/25. | |
| // | |
| import SwiftUI | |
| struct ContentView: View { | |
| @State private var selectedClipIndex: Int? = nil | |
| @State private var pressedButton: String? = nil | |
| @State private var timelineLabelPositions: [Int: CGFloat] = [:] | |
| let clips = [ | |
| VideoClip(id: 1, time: "05:12", name: "5 Klip", duration: 42), | |
| VideoClip(id: 2, time: "05:54", name: "3 Klip", duration: 34), | |
| VideoClip(id: 3, time: "06:28", name: "6 Klip", duration: 33), | |
| VideoClip(id: 4, time: "07:01", name: "6 Klip", duration: 33) | |
| ] | |
| var body: some View { | |
| ZStack { | |
| Color(hex: "F5F5F5") | |
| .ignoresSafeArea() | |
| VStack(spacing: 0) { | |
| cameraControlsBar() | |
| mainContentArea() | |
| } | |
| } | |
| } | |
| func cameraControlsBar() -> some View { | |
| // Top toolbar | |
| HStack(spacing: 20) { | |
| Button(action: {}) { | |
| Image(systemName: "video.fill") | |
| .font(.system(size: 24)) | |
| .foregroundColor(.white) | |
| } | |
| .buttonStyle(ScaleButtonStyle()) | |
| Spacer() | |
| Button(action: {}) { | |
| Image(systemName: "camera.fill") | |
| .font(.system(size: 24)) | |
| .foregroundColor(.white) | |
| } | |
| .buttonStyle(ScaleButtonStyle()) | |
| Spacer() | |
| Button(action: {}) { | |
| Image(systemName: "speaker.slash.fill") | |
| .font(.system(size: 24)) | |
| .foregroundColor(.white) | |
| } | |
| .buttonStyle(ScaleButtonStyle()) | |
| Spacer() | |
| Button(action: {}) { | |
| Image(systemName: "arrow.up.left.and.arrow.down.right") | |
| .font(.system(size: 22)) | |
| .foregroundColor(.white) | |
| } | |
| .buttonStyle(ScaleButtonStyle()) | |
| } | |
| .padding(.horizontal, 20) | |
| .padding(.vertical, 16) | |
| .background(Color(hex: "2C2C2E")) | |
| } | |
| func mainContentArea() -> some View { | |
| ScrollView { | |
| VStack(spacing: 0) { | |
| // Top controls | |
| HStack(spacing: 12) { | |
| Button(action: {}) { | |
| Image(systemName: "trash") | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: 18) | |
| .foregroundColor(Color(hex: "3C3C3E")) | |
| } | |
| .buttonStyle(ScaleButtonStyle()) | |
| .padding(.leading, 20) | |
| Spacer() | |
| CustomSegmentedControl() | |
| .frame(width: 100) | |
| Spacer() | |
| HStack(spacing: 8) { | |
| Image(systemName: "calendar") | |
| .font(.system(size: 16)) | |
| .foregroundColor(Color(hex: "3C3C3E")) | |
| Text("10 October 2025") | |
| .font(.system(size: 14, weight: .medium)) | |
| .foregroundColor(Color(hex: "1C1C1E")) | |
| } | |
| .padding(.horizontal, 16) | |
| .padding(.vertical, 12) | |
| .background( | |
| Capsule() | |
| .stroke(Color(hex: "D1D1D6"), lineWidth: 1.5) | |
| .background(Color.white) | |
| .clipShape(Capsule()) | |
| ) | |
| .padding(.trailing, 20) | |
| } | |
| .padding(.top, 28) | |
| .padding(.bottom, 36) | |
| // Timeline with clips | |
| ZStack(alignment: .topLeading) { | |
| let timelineLeftPadding: CGFloat = 80 | |
| let cardWidth: CGFloat = 200 | |
| let cardHeight: CGFloat = 130 | |
| let indicatorOffset = selectedClipIndex != nil ? (timelineLabelPositions[selectedClipIndex!] ?? 10) : 10 | |
| // Timeline vertical line | |
| Rectangle() | |
| .fill(Color(hex: "3C3C3E")) | |
| .frame(width: 4) | |
| .padding(.leading, timelineLeftPadding) | |
| VStack(spacing: 0) { | |
| ForEach(Array(clips.enumerated()), id: \.element.id) { index, clip in | |
| HStack(alignment: .top, spacing: 0) { | |
| // Time label - aligned to top | |
| Text(clip.time) | |
| .font(.system(size: 18, weight: .regular)) | |
| .foregroundColor(Color(hex: "8E8E93")) | |
| .frame(width: 60, alignment: .trailing) | |
| .padding(.leading, 10) | |
| .padding(.top, 4) | |
| .padding(.trailing, 30) | |
| .background( | |
| GeometryReader { geo in | |
| Color.clear | |
| .preference(key: TimelineLabelPositionKey.self, | |
| value: [index: geo.frame(in: .named("timelineContainer")).minY]) | |
| } | |
| ) | |
| // Video clip card | |
| Button(action: { | |
| withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { | |
| selectedClipIndex = selectedClipIndex == index ? nil : index | |
| } | |
| }) { | |
| ZStack(alignment: .center) { | |
| Image(.icClipMulti) | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: cardWidth) | |
| Text(clip.name) | |
| .font(.system(size: 16, weight: .medium)) | |
| .foregroundColor(.white.opacity(0.95)) | |
| } | |
| .frame(width: cardWidth, height: cardHeight) | |
| .scaleEffect(selectedClipIndex == index ? 0.96 : 1.0) | |
| .opacity(selectedClipIndex == index ? 0.9 : 1.0) | |
| .padding(.bottom, -20) | |
| .padding(.top, -20) | |
| .padding(.leading, -30) | |
| } | |
| .buttonStyle(PlainButtonStyle()) | |
| Spacer() | |
| } | |
| .padding(.bottom, index < clips.count - 1 ? 28 : 0) | |
| } | |
| } | |
| // Red timeline indicator (horizontal) - positioned at selected clip time label | |
| HStack(spacing: 0) { | |
| Spacer() | |
| .frame(width: timelineLeftPadding) | |
| Rectangle() | |
| .fill(Color(hex: "FF3B30")) | |
| .frame(height: 4) | |
| .padding(.trailing, 20) | |
| } | |
| .padding(.top, indicatorOffset) | |
| .animation(.spring(response: 0.3, dampingFraction: 0.7), value: selectedClipIndex) | |
| } | |
| .coordinateSpace(name: "timelineContainer") | |
| .onPreferenceChange(TimelineLabelPositionKey.self) { positions in | |
| timelineLabelPositions = positions | |
| } | |
| .padding(.bottom, 40) | |
| } | |
| } | |
| } | |
| } | |
| struct VideoClip: Identifiable { | |
| let id: Int | |
| let time: String | |
| let name: String | |
| let duration: Int | |
| } | |
| // Button style for scale effect on press | |
| struct ScaleButtonStyle: ButtonStyle { | |
| func makeBody(configuration: Configuration) -> some View { | |
| configuration.label | |
| .scaleEffect(configuration.isPressed ? 0.92 : 1.0) | |
| .opacity(configuration.isPressed ? 0.7 : 1.0) | |
| .animation(.easeInOut(duration: 0.15), value: configuration.isPressed) | |
| } | |
| } | |
| // Color extension for hex colors | |
| extension Color { | |
| init(hex: String) { | |
| let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) | |
| var int: UInt64 = 0 | |
| Scanner(string: hex).scanHexInt64(&int) | |
| let a, r, g, b: UInt64 | |
| switch hex.count { | |
| case 3: // RGB (12-bit) | |
| (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) | |
| case 6: // RGB (24-bit) | |
| (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) | |
| case 8: // ARGB (32-bit) | |
| (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) | |
| default: | |
| (a, r, g, b) = (1, 1, 1, 0) | |
| } | |
| self.init( | |
| .sRGB, | |
| red: Double(r) / 255, | |
| green: Double(g) / 255, | |
| blue: Double(b) / 255, | |
| opacity: Double(a) / 255 | |
| ) | |
| } | |
| } | |
| #Preview { | |
| ContentView() | |
| } | |
| struct CustomSegmentedControl: View { | |
| @State private var selectedOption: PlaybackSegmentOption = .cloud | |
| enum PlaybackSegmentOption { | |
| case cloud | |
| case storage | |
| } | |
| var body: some View { | |
| HStack(spacing: 0) { | |
| // Cloud option | |
| Button(action: { | |
| withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { | |
| selectedOption = .cloud | |
| } | |
| }) { | |
| ZStack { | |
| // Background | |
| RoundedRectangle(cornerRadius: 25) | |
| .fill(selectedOption == .cloud ? Color.white : Color.clear) | |
| .shadow(color: selectedOption == .cloud ? Color.black.opacity(0.12) : Color.clear, | |
| radius: 4, x: 0, y: 1) | |
| .scaleEffect(0.8) | |
| // Cloud with play icon | |
| ZStack { | |
| Image(systemName: "cloud.fill") | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: 20) | |
| .foregroundColor(Color(red: 0.3, green: 0.35, blue: 0.4)) | |
| Image(systemName: "play.fill") | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: 5) | |
| .foregroundColor(.white) | |
| .offset(x: 0.5, y: 0) | |
| } | |
| .padding(.vertical, 2) | |
| .padding(.horizontal, 3) | |
| } | |
| } | |
| .buttonStyle(ScaleButtonStyleV2()) | |
| // Storage option | |
| Button(action: { | |
| withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { | |
| selectedOption = .storage | |
| } | |
| }) { | |
| ZStack { | |
| // Background | |
| RoundedRectangle(cornerRadius: 25) | |
| .fill(selectedOption == .storage ? Color.white : Color.clear) | |
| .shadow(color: selectedOption == .storage ? Color.black.opacity(0.12) : Color.clear, | |
| radius: 4, x: 0, y: 1) | |
| .scaleEffect(0.8) | |
| // SD Card icon | |
| ZStack { | |
| Image(systemName: "sdcard.fill") | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: 13) | |
| .foregroundColor(Color(red: 0.3, green: 0.35, blue: 0.4)) | |
| } | |
| .padding(.vertical, 2) | |
| .padding(.horizontal, 3) | |
| } | |
| } | |
| .buttonStyle(ScaleButtonStyleV2()) | |
| } | |
| .frame(height: 40) | |
| .background( | |
| RoundedRectangle(cornerRadius: 25) | |
| .fill(Color(UIColor.systemGray4).opacity(0.6)) | |
| .shadow(color: Color.black.opacity(0.08), radius: 6, x: 0, y: 2) | |
| ) | |
| } | |
| } | |
| // Custom button style for subtle press effect | |
| struct ScaleButtonStyleV2: ButtonStyle { | |
| func makeBody(configuration: Configuration) -> some View { | |
| configuration.label | |
| .scaleEffect(configuration.isPressed ? 0.96 : 1.0) | |
| .opacity(configuration.isPressed ? 0.9 : 1.0) | |
| .animation(.easeInOut(duration: 0.15), value: configuration.isPressed) | |
| } | |
| } | |
| // PreferenceKey for tracking timeline label positions | |
| struct TimelineLabelPositionKey: PreferenceKey { | |
| static var defaultValue: [Int: CGFloat] = [:] | |
| static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) { | |
| value.merge(nextValue()) { $1 } | |
| } | |
| } |
Author
luthviar
commented
Oct 14, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment