// // ContentView.swift // gloss // // Created by Saint on 10/23/22. // import GRDB import GRDBQuery import os import SwiftUI import SwiftUIIntrospect let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "network") var currentId: String? var currentOffset: CGFloat? var disableDrop = false //TODO: move to globals file let mainBackgroundColor = Color(red: 0.1, green: 0.1, blue: 0.1) let mainTextColor = Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00)) let secondBackgroundColor = Color(red: 0.18, green: 0.18, blue: 0.18) public extension UserDefaults { func optionalInt(forKey defaultName: String) -> Int? { let defaults = self if let value = defaults.value(forKey: defaultName) { return value as? Int } return nil } func optionalBool(forKey defaultName: String) -> Bool? { let defaults = self if let value = defaults.value(forKey: defaultName) { return value as? Bool } return nil } } func createUndoState(selectedRibbon: Ribbon, appDatabase : AppDatabase, paneConnector : PaneConnector) async throws -> [Ribbon] { let updateThreshold = 30 var updatedRibbon = selectedRibbon var scrollOffsetToSave = Int(floor(paneConnector.currentOffset)) var scrollIdToSave = paneConnector.currentId var offsetDiff = abs(scrollOffsetToSave - updatedRibbon.scrollOffset) > 30 var idDiff = Int(updatedRibbon.scrollId) != Int(scrollIdToSave) if idDiff || offsetDiff { updatedRibbon.scrollId = scrollIdToSave updatedRibbon.scrollOffset = scrollOffsetToSave print("meow bumping") let ret = try await appDatabase.bumpRibbon(&updatedRibbon) return ret } print("meow no bump") return [] } func goToRibbon(selectedRibbon: Ribbon, destRibbon: Ribbon, appDatabase: AppDatabase, paneConnector: PaneConnector, loading: Bool) { print("meow goto ribbon - selected ribbon: \(selectedRibbon), dest ribbon: \(destRibbon) ") DispatchQueue.main.asyncAfter(deadline: .now()) { Task { var scrollOffsetToSave = paneConnector.currentOffset var scrollIdToSave = paneConnector.currentId print("go to ribbon") print("\(selectedRibbon.id) \(destRibbon.id!)") // if selectedRibbon.id != destRibbon.id! || loading { if true { print("switching ribbons") paneConnector.showOverlay = true if loading { paneConnector.currentId = destRibbon.scrollId } paneConnector.scrollId = destRibbon.scrollId paneConnector.scrollOffset = CGFloat(destRibbon.scrollOffset) paneConnector.refresh.toggle() print("toggling") print("paneconnector: \(paneConnector.refresh)") var updateSelectRibbon = SelectedRibbon(id: Int64(1), ribbonGroupId: Int64(destRibbon.groupId)) // print("Saving selected ribbon") // print(updateSelectRibbon) do { _ = try await appDatabase.saveSelectedRibbon(&updateSelectRibbon) } catch { // Print("something wrong") } } } } } extension View { @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Transform) -> some View { if condition { transform(self) } else { self } } } struct RibbonCrown: View { var ribbon: Ribbon @ObservedObject var paneConnector: PaneConnector var draggedRibbon: Ribbon? var isDragging: Bool var height = CGFloat(41) var width = CGFloat(70) var scale = 0.70 @Environment(\.appDatabase) private var appDatabase @Query(SelectedRibbonRequest()) private var selectedRibbon: [Ribbon] @State var saveOffset = CGFloat() @Binding var refresh: Bool var body: some View { ZStack { MyIcon().frame( width: CGFloat(100 * 1.66 * scale), height: CGFloat(100 * scale), alignment: .center ).foregroundColor(Color(UIColor(red: 0.30, green: 0.30, blue: 0.30, alpha: 0.4))) .contentShape(.dragPreview, RoundedRectangle(cornerRadius: 32)) .if(draggedRibbon != nil && draggedRibbon!.id == ribbon.id && isDragging) { $0.overlay(Color(red: 0.1, green: 0.1, blue: 0.1)) } Text(ribbon.title) .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) .frame(minWidth: width, maxWidth: width, minHeight: height, maxHeight: height, alignment: .center) .if(draggedRibbon != nil && draggedRibbon!.id == ribbon.id && isDragging) { $0.overlay(Color(red: 0.1, green: 0.1, blue: 0.1)) } .background(Color(red: 0.1, green: 0.1, blue: 0.1)) .multilineTextAlignment(.center) // .minimumScaleFactor(0.5) // .padding([.top, .bottom], 10) .font(Font.custom("AveriaSerifLibre-Regular", size: CGFloat(10))) } .onTapGesture { Task { let sr = selectedRibbon[0] let updatedRibbon = try await createUndoState(selectedRibbon: sr, appDatabase: appDatabase, paneConnector: paneConnector) if sr.id == ribbon.id { paneConnector.scrollId = paneConnector.currentId paneConnector.scrollOffset = paneConnector.currentOffset paneConnector.hasMoved = false } else { goToRibbon(selectedRibbon: sr, destRibbon: ribbon, appDatabase: appDatabase, paneConnector: paneConnector, loading: false) } } } .frame(width: CGFloat(100 * 1.66 * scale + 10), height: CGFloat(100 * scale + 5)) } } // object used for JSON decoding of verses class Verse: NSObject, Codable { var body: String var verse: Int } func makeVerseView(seg: SegDenorm) -> some View { var retView = Text("") var segSplit = seg.body.components(separatedBy: ";;") let decoder = JSONDecoder() for (index, item) in segSplit.enumerated() { let verse = try! decoder.decode(Verse.self, from: item.data(using: .utf8)!) retView = retView + Text(String(verse.verse)) .font(Font.custom("AveriaSerifLibre-Regular", size: 6)) .baselineOffset(6.0) .foregroundColor(Color.white) retView = retView + Text(verse.body) .foregroundColor(Color.white) .font(Font.custom("AveriaSerifLibre-Regular", size: 15)) } return retView } class PaneConnector: NSObject, ObservableObject { var showOverlay: Bool = false @Published var refresh: Bool = false @Published var vertSep = CGFloat(20) var currentId = "" var currentOffset = CGFloat() var visibilityTracker: VisibilityTracker? @Published var scrollId = "" @Published var scrollOffset = CGFloat() @Published var hasMoved = false var setScrollOffset: CGFloat? @Published var disableScroll = false } class ScrollDelegate: NSObject, ObservableObject, UIScrollViewDelegate { @Published var isScrolling = false func scrollViewWillBeginDragging(_: UIScrollView) { print("pop started scroll") isScrolling.toggle() } func scrollViewDidEndDragging(_: UIScrollView, willDecelerate _: Bool) { print("pop ended scroll") isScrolling.toggle() } } struct ContentView: View { // this is for the whole view swiping @State var mainSwipe = CGSize.zero @State var pulledOut = CGSize.zero @State var selection = 0 @StateObject var paneConnector = PaneConnector() @StateObject var scrollDelegate = ScrollDelegate() @State var refresh: Bool = false @State var readOffset = CGPoint() @Query(SegDenormRequest(book: "bible.john")) private var segs: [SegDenorm] @State var draggedRibbon: Ribbon? @State var isDragging = false @State var dragOffset = CGFloat(0) @GestureState var dragGestureActive = CGSize.zero enum SwipeStartState { case center case right case left } enum SwipeStartDir { case left case right } // @State var curSwipeState: SwipeState = .start @State var startSwipeState: SwipeStartState? @State var startSwipeDir: SwipeStartDir? @Environment(\.appDatabase) private var appDatabase @Query(RibbonRequest()) private var ribbons: [Ribbon] @Query var selectedRibbon: [Ribbon] init() { UITableView.appearance().backgroundColor = UIColor(Color(red: 0.2, green: 0.2, blue: 0.2)) _selectedRibbon = Query(SelectedRibbonRequest()) } var body: some View { var fontSize = CGFloat(15) var scale = 0.65 var height = CGFloat(50) var width = CGFloat(100 * 1.66 * scale) GeometryReader { geometry in ZStack(alignment: .top) { VStack(alignment: .leading) { VStack { ForEach(ribbons) { ribbon in RibbonCrown(ribbon: ribbon, paneConnector: paneConnector, draggedRibbon: draggedRibbon, isDragging: isDragging, refresh: $refresh) .onDrag { self.draggedRibbon = ribbon return NSItemProvider() } .onDrop(of: [.item], delegate: DropViewDelegate(destinationItem: ribbon, draggedItem: $draggedRibbon, isDragging: $isDragging, appDatabase: appDatabase)) .offset(x: 6, y: 6) } } .frame(width: geometry.size.width, height: geometry.size.height - 100, alignment: .topLeading) .background(Color(red: 0.1, green: 0.1, blue: 0.1)) .zIndex(0) .animation(.default, value: ribbons) .onDrop(of: [.item], delegate: DropViewDelegate2(isDragging: $isDragging)) } .background(Color(red: 0.1, green: 0.1, blue: 0.1)) .frame(alignment: .topLeading) VStack { NaviBar(paneConnector: paneConnector) StatsPanel(paneConnector: paneConnector) .offset(y:20) } // .frame(maxWidth: 300) .offset(x: geometry.size.width - 300) VStack { // Top pane Pane(paneConnector: paneConnector, scrollDelegate: scrollDelegate, selectedRibbon: selectedRibbon, width: geometry.size.width - 15, height: geometry.size.height + 20, dragOffset: dragOffset) /////////////////////////////////// // Text("separator").foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) // .gesture( // DragGesture() // .onChanged { gesture in // paneConnector.vertSep = paneConnector.vertSep - gesture.translation.height // } // // // Bottom pane // ScrollViewReader { _ in // VisibilityTrackingScrollView(action: handleVisibilityChanged2) { // // ScrollView { // LazyVStack { // ForEach(segs) { seg in // SegRow(seg: seg, // ribbonId: selectedRibbon[0].id!) // .id("\(seg.id)") // .padding(EdgeInsets(top: 10, leading: 20, bottom: 40, trailing: 20)) // .trackVisibility(id: "\(seg.id)") // } // } // .background(Color(red: 0.18, green: 0.18, blue: 0.18)) // } // .onAppear { // Print("APPEAR") // } // .listStyle(PlainListStyle()) // } // .zIndex(1) // .background(Color(red: 0.2, green: 0.2, blue: 0.2)) /////////////////////////////////// } .offset(x: 20, y: 0) .offset(x: pulledOut.width) .offset(x: mainSwipe.width) .highPriorityGesture( DragGesture(minimumDistance: 20) .updating($dragGestureActive) { value, state, _ in print("pop here") state = value.translation } .onChanged { value in print("pop changed") // Calculate the offset let margin = CGFloat(30) var newOffset = value.translation.width if startSwipeState == nil { if pulledOut.width == 0 { startSwipeState = .center } else if pulledOut.width < 0 { startSwipeState = .left } else { startSwipeState = .right } print("start swipe meow: \(startSwipeState)") } if newOffset > 0 { startSwipeDir = .right } else { startSwipeDir = .left } // Apply resistance if out of bounds let maxRight: CGFloat = 140 let finalRight: CGFloat = 110 let rightDiff = maxRight - finalRight var maxOffsetLeft: CGFloat = -200 if newOffset > maxRight , pulledOut.width == 0 { // newOffset = maxRight + rubberBandEffect(newOffset) newOffset = maxRight + rubberBandEffect(newOffset - maxRight) } if newOffset > rightDiff, pulledOut.width > 0 { newOffset = rightDiff + rubberBandEffect(newOffset - rightDiff) } if newOffset < maxOffsetLeft, pulledOut.width == 0 { newOffset = maxOffsetLeft + rubberBandEffect(newOffset) } self.mainSwipe.width = newOffset // dragOffset is what is used to make the text be readable // with the right pane being visible if mainSwipe.width < -margin, pulledOut.width <= 0 { dragOffset = margin + mainSwipe.width + pulledOut.width } if mainSwipe.width > 0, pulledOut.width < 0 { dragOffset = margin + mainSwipe.width + pulledOut.width } } .onEnded { _ in onDragEnded() } ) .onChange(of: scrollDelegate.isScrolling) { value in print ("pop reset change") // if scrollDelegate.isScrolling == true { Task { mainSwipe.width = 0 } // } } } } .background(Color(red: 0.1, green: 0.1, blue: 0.1)) } func onDragEnded() { print("pop ended") paneConnector.disableScroll = false var finalSwipe = CGFloat(0) let swipeLeftFinal = CGFloat(-200) let swipeRightFinal = CGFloat(110) let margin = CGFloat(30) var setDragOffset = CGFloat(0) if startSwipeState == .center { if startSwipeDir == .right { finalSwipe = swipeRightFinal } else { finalSwipe = swipeLeftFinal setDragOffset = margin + swipeLeftFinal } } else if startSwipeState == .right { finalSwipe = .zero if startSwipeDir == .left { finalSwipe = .zero } else { finalSwipe = swipeRightFinal } } else if startSwipeState == .left { if startSwipeDir == .left { finalSwipe = swipeLeftFinal setDragOffset = margin + swipeLeftFinal } else { finalSwipe = .zero } } startSwipeState = nil startSwipeDir = nil print("foo") // if mainSwipe.width < 0 && pulledOut.width > 0 { // setPulledOutWith = CGFloat(0) // } else if mainSwipe.width > 0 && pulledOut.width < 0 { // setPulledOutWith = CGFloat(0) // } else if (mainSwipe.width < 0 && pulledOut.width < 0) || // (mainSwipe.width < 0 && pulledOut.width == 0) { // setPulledOutWith = pulledOutRight // setDragOffset = margin + setPulledOutWith // } else if abs(mainSwipe.width + pulledOut.width) > 30 { // setPulledOutWith = pulledOutLeft // } withAnimation(.spring(response: 0.2)) { pulledOut.width = finalSwipe dragOffset = setDragOffset mainSwipe = .zero } } // let hackyPinch = MagnificationGesture(minimumScaleDelta: 0.0) // .onChanged({ delta in // onDragEnded() // }) // .onEnded({ delta in // onDragEnded() // }) // let hackyRotation = RotationGesture(minimumAngleDelta: Angle(degrees: 0.0)) // .onChanged({ delta in // onDragEnded() // }) // .onEnded({ delta in // onDragEnded() // }) // let hackyPress = LongPressGesture(minimumDuration: 0.0, maximumDistance: 0.0) // .onChanged({ _ in // onDragEnded() // }) // .onEnded({ delta in // onDragEnded() // }) func handleVisibilityChanged2(_: String, change _: VisibilityChange, tracker _: VisibilityTracker) {} func rubberBandEffect(_ offset: CGFloat) -> CGFloat { let resistance: CGFloat = 0.05 return 6 * log(offset + 1) // return resistance * pow(abs(offset), 1) * (offset < 0 ? -1 : 1) } private func dragCancelled() { print("pop dragCancelled") mainSwipe = .zero } } private let itemFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .medium return formatter }() struct DropViewDelegate2: DropDelegate { @Binding var isDragging: Bool func dropUpdated(info _: DropInfo) -> DropProposal? { return DropProposal(operation: .move) } func performDrop(info _: DropInfo) -> Bool { isDragging = false return true } func dropEntered(info _: DropInfo) { isDragging = true print("SECOND DROPPPOOO") } } struct DropViewDelegate: DropDelegate { let destinationItem: Ribbon // @Binding var colors: [Color] @Binding var draggedItem: Ribbon? @Binding var isDragging: Bool let appDatabase: AppDatabase func dropUpdated(info _: DropInfo) -> DropProposal? { return DropProposal(operation: .move) } func dropExited(info _: DropInfo) { print("EXITED") isDragging = false } func dropEntered(info _: DropInfo) { Task { isDragging = true if draggedItem == nil { return } if disableDrop { return } var newRibbon = draggedItem! var newDest = destinationItem var oldPos = draggedItem!.pos var newPos = destinationItem.pos print("dragged item") print(draggedItem) print("dest item") print(destinationItem) if draggedItem!.id! == destinationItem.id! { return } newRibbon.pos = destinationItem.pos _ = try await appDatabase.updateRibbonPosition(&newRibbon, oldPos, newPos) disableDrop = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { disableDrop = false } draggedItem!.pos = newPos // draggedItem = nil } } func performDrop(info _: DropInfo) -> Bool { print("PERFORMED DROPPPP") draggedItem = nil isDragging = false return true } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() ContentView().environment(\.appDatabase, .random()) } } extension View { @discardableResult func Print(_ vars: Any...) -> some View { for v in vars { print(v) } return EmptyView() } }