Compare commits
	
		
			10 Commits 
		
	
	
		
			ae49a5cbfe
			...
			bd1923cfb4
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | bd1923cfb4 | |
|  | 19ca7be678 | |
|  | 168a057a2a | |
|  | 14e1bec9e5 | |
|  | 08b6116a28 | |
|  | efd05ce33b | |
|  | 4dfc93ea90 | |
|  | 32696d7b58 | |
|  | 9eb488a87a | |
|  | c069860c43 | 
|  | @ -0,0 +1,55 @@ | |||
| // | ||||
| //  BackButton.swift | ||||
| //  gloss | ||||
| // | ||||
| //  Created by Saint on 5/27/24. | ||||
| // | ||||
| 
 | ||||
| import Foundation | ||||
| import SwiftUI | ||||
| struct BackArrow: Shape { | ||||
|     func path(in rect: CGRect) -> Path { | ||||
|         var path = Path() | ||||
|         let width = rect.size.width | ||||
|         let height = rect.size.height | ||||
|         path.move(to: CGPoint(x: 0.83333*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.83333*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.25*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.25*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.16667*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.16667*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.25*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.25*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.83333*width, y: 0.45833*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.41667*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.29167*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.41667*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.20833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.20833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.29167*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.41667*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.33333*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.70833*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.41667*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.79167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.79167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.41667*width, y: 0.70833*height)) | ||||
|         path.closeSubpath() | ||||
|         return path | ||||
|     } | ||||
| } | ||||
|  | @ -5,6 +5,8 @@ | |||
| //  Created by Saint on 5/20/24. | ||||
| // | ||||
| 
 | ||||
| import GRDB | ||||
| import GRDBQuery | ||||
| import Foundation | ||||
| import SwiftUI | ||||
| import WrappingHStack | ||||
|  | @ -14,6 +16,7 @@ struct SegRow: View { | |||
|     var ribbonId: Int64 | ||||
|     @State var highlights = Set<Int>() | ||||
| 
 | ||||
|     let intraWordSpacing = CGFloat(1.6) | ||||
|     var body: some View { | ||||
|         var segSplit = seg.body.components(separatedBy: ";;") | ||||
|         let decoder = JSONDecoder() | ||||
|  | @ -53,7 +56,8 @@ struct SegRow: View { | |||
|                         Text(arrayOfText[index]) | ||||
|                             .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                             .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|                             .padding(.horizontal, 1.5) // intra word spacing | ||||
| 
 | ||||
|                             .padding(.horizontal, intraWordSpacing) // intra word spacing | ||||
|                             .if(self.highlights.contains(verse.verse)) { $0.background(Color(hex: highlightColor)) } | ||||
|                             .foregroundColor(Color.white) | ||||
|                             .onTapGesture { | ||||
|  | @ -79,7 +83,6 @@ struct SegRow: View { | |||
| struct Pane: View { | ||||
|     @ObservedObject var paneConnector: PaneConnector | ||||
| 
 | ||||
|     @State var segs: [SegDenorm] | ||||
|     @State var selectedRibbon: [Ribbon] | ||||
| 
 | ||||
|     @State var width: CGFloat | ||||
|  | @ -89,13 +92,15 @@ struct Pane: View { | |||
| 
 | ||||
|     @State var refresh: Bool = false | ||||
| 
 | ||||
|     @State var vertSep = CGFloat(20) | ||||
|     @Query(SegDenormRequest(book: "bible.mark")) private var segs: [SegDenorm] | ||||
| 
 | ||||
|     @Environment(\.appDatabase) private var appDatabase | ||||
| 
 | ||||
|     // var handleVisibilityChanged: (String, VisibilityChange,  VisibilityTracker<String>) -> Void | ||||
| 
 | ||||
|     var body: some View{ | ||||
|     var body: some View { | ||||
|         var adjustedHeight = height - paneConnector.vertSep | ||||
|         ZStack { | ||||
|             ScrollViewReader { proxy in | ||||
|                 VisibilityTrackingScrollView(action: handleVisibilityChanged) { | ||||
|                     LazyVStack { | ||||
|  | @ -116,8 +121,8 @@ struct Pane: View { | |||
|                                destRibbon: selectedRibbon[0], | ||||
|                                appDatabase: appDatabase, | ||||
|                                paneConnector: paneConnector, | ||||
|                                refresh: $refresh, | ||||
|                                loading: true) | ||||
|                                loading: true, | ||||
|                                bump: false) | ||||
|                 } | ||||
|                 .onChange(of: paneConnector.refresh) { _ in | ||||
|                     print("inside change") | ||||
|  | @ -125,6 +130,9 @@ struct Pane: View { | |||
| 
 | ||||
|                     Task { | ||||
|                         DispatchQueue.main.async { | ||||
|                             if paneConnector.visibilityTracker == nil { | ||||
|                                 return | ||||
|                             } | ||||
|                             let gTracker = paneConnector.visibilityTracker! | ||||
| 
 | ||||
|                             Print("scroll Id target: \(paneConnector.scrollId)") | ||||
|  | @ -190,7 +198,27 @@ struct Pane: View { | |||
|             } | ||||
|             .zIndex(1) | ||||
|             .background(Color(red: 0.2, green: 0.2, blue: 0.2)) | ||||
|             .frame(width: width, height: height) | ||||
|             .frame(width: width, height: adjustedHeight) | ||||
| 
 | ||||
|             if self.paneConnector.showOverlay { | ||||
|                 Rectangle() | ||||
|                     // .frame(width: width, height: height + 200) | ||||
|                     .background(.ultraThinMaterial) | ||||
|                     .opacity(0.98) | ||||
|                     .offset(y: -50) | ||||
|                     .frame(width: width, height: adjustedHeight + 100) | ||||
| 
 | ||||
|                     // .blur(radius: 0.8) | ||||
|                     // .opacity(1) | ||||
|                     .transition(.opacity) | ||||
| 
 | ||||
|                     // .frame(width: geometry.size.width - 50) | ||||
|                     // .offset(x: pulledOut.width) | ||||
|                     // .offset(x: viewState.width, y: viewState.height) | ||||
|                     .zIndex(2) | ||||
|             } | ||||
|         } | ||||
|         .frame(width: width, height: adjustedHeight) | ||||
|     } | ||||
| 
 | ||||
|     func handleVisibilityChanged(_: String, change _: VisibilityChange, tracker: VisibilityTracker<String>) { | ||||
|  |  | |||
|  | @ -0,0 +1,56 @@ | |||
| // | ||||
| //  ForwardArrow.swift | ||||
| //  gloss | ||||
| // | ||||
| //  Created by Saint on 5/27/24. | ||||
| // | ||||
| 
 | ||||
| import Foundation | ||||
| import SwiftUI | ||||
| 
 | ||||
| struct ForwardArrow: Shape { | ||||
|     func path(in rect: CGRect) -> Path { | ||||
|         var path = Path() | ||||
|         let width = rect.size.width | ||||
|         let height = rect.size.height | ||||
|         path.move(to: CGPoint(x: 0.16667*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.16667*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.75*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.75*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.83333*width, y: 0.54167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.83333*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.75*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.75*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.45833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.16667*width, y: 0.45833*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.58333*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.375*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.29167*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.58333*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.29167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.20833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.20833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.29167*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.58333*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.66667*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.625*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.70833*height)) | ||||
|         path.closeSubpath() | ||||
|         path.move(to: CGPoint(x: 0.58333*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.70833*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.5*width, y: 0.79167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.79167*height)) | ||||
|         path.addLine(to: CGPoint(x: 0.58333*width, y: 0.70833*height)) | ||||
|         path.closeSubpath() | ||||
|         return path | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,74 @@ | |||
| 
 | ||||
| import GRDB | ||||
| import GRDBQuery | ||||
| import Foundation | ||||
| import SwiftUI | ||||
| 
 | ||||
| struct NaviBar: View { | ||||
|     @ObservedObject var paneConnector: PaneConnector | ||||
| 
 | ||||
|     @Query(RibbonRequest(dir: .prev, groupId: 1)) private var backRibbon: [Ribbon] | ||||
|     @Query(RibbonRequest(dir: .next, groupId: 1)) private var nextRibbon: [Ribbon] | ||||
|     @Query(SelectedRibbonRequest()) private var selectedRibbon: [Ribbon] | ||||
|     @Environment(\.appDatabase) private var appDatabase | ||||
| 
 | ||||
| 
 | ||||
|     var body: some View { | ||||
|         VStack { | ||||
|             HStack { | ||||
|                 Print("meow navi reloaded back ribbon: \(backRibbon[0])") | ||||
| 
 | ||||
|                 BackArrow() | ||||
|                     .frame(width: CGFloat(30), height: CGFloat(30)) | ||||
|                     .background(Color(red: 0.1, green: 0.1, blue: 0.1)) | ||||
|                     .foregroundColor(Color(UIColor(red: 0.30, green: 0.30, blue: 0.30, alpha: 0.4))) | ||||
|                     .onTapGesture { | ||||
|                         print("back backRibbon: \(backRibbon)") | ||||
|                         Task { | ||||
|                             print("back backRibbon: \(backRibbon)") | ||||
|                             if backRibbon.count == 0 { | ||||
|                                 return | ||||
|                             } | ||||
| 
 | ||||
| 
 | ||||
|                             var br = backRibbon[0] | ||||
|                             //var sr = selectedRibbon[0] | ||||
| 
 | ||||
|                             do { | ||||
| 
 | ||||
|                                 var sr = try await appDatabase.getSelectedRibbon() | ||||
|                                 print("meow first sr:\(sr)") | ||||
|                                 print("meow first selectedRibbon:\(selectedRibbon[0])") | ||||
|                                 try await updateRibbon(selectedRibbon: selectedRibbon[0], | ||||
|                                              appDatabase: appDatabase, | ||||
|                                              paneConnector: paneConnector) | ||||
| 
 | ||||
|                                 print("meow back from update in main") | ||||
|                                 sr = try await appDatabase.getSelectedRibbon() | ||||
|                                 print("meow second sr:\(sr)") | ||||
|                                 print("meow backribbon br:\(br)") | ||||
| 
 | ||||
|                                 _ = try await appDatabase.undoRibbon(&sr[0]) | ||||
|                                 goToRibbon(selectedRibbon: sr[0], | ||||
|                                            destRibbon: br, | ||||
|                                            appDatabase: appDatabase, | ||||
|                                            paneConnector: paneConnector, | ||||
|                                            loading: false, | ||||
|                                            bump:false) | ||||
| 
 | ||||
|                             } catch { | ||||
|                                 print("back 2 error") | ||||
|                                 print("Error info: \(error)") | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                 ForwardArrow() | ||||
|                     .frame(width: CGFloat(30), height: CGFloat(30)) | ||||
|                     .background(Color(red: 0.1, green: 0.1, blue: 0.1)) | ||||
|                     .foregroundColor(Color(UIColor(red: 0.30, green: 0.30, blue: 0.30, alpha: 0.4))) | ||||
|             } | ||||
|             .cornerRadius(5) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -2,30 +2,18 @@ import Combine | |||
| import GRDB | ||||
| import GRDBQuery | ||||
| 
 | ||||
| /// A player request can be used with the `@Query` property wrapper in order to | ||||
| /// feed a view with a list of players. | ||||
| /// | ||||
| /// For example: | ||||
| /// | ||||
| ///     struct MyView: View { | ||||
| ///         @Query(RibbonRequest(ordering: .byName)) private var players: [Ribbon] | ||||
| /// | ||||
| ///         var body: some View { | ||||
| ///             List(players) { player in ... ) | ||||
| ///         } | ||||
| ///     } | ||||
| 
 | ||||
| 
 | ||||
| var idColumn = Column("id") | ||||
| struct RibbonRequest: Queryable { | ||||
|     // enum Ordering { | ||||
|     //     case byScore | ||||
|     //     case byName | ||||
|     // } | ||||
|     enum UndoDir { | ||||
|         case prev | ||||
|         case next | ||||
|     } | ||||
| 
 | ||||
|     /// The ordering used by the player request. | ||||
|     // var ordering: Ordering | ||||
|     var id: Int64! | ||||
|     var dir: UndoDir? | ||||
|     var groupId: Int? | ||||
| 
 | ||||
| 
 | ||||
|     // MARK: - Queryable Implementation | ||||
|  | @ -49,12 +37,103 @@ struct RibbonRequest: Queryable { | |||
| 
 | ||||
|     // This method is not required by Queryable, but it makes it easier | ||||
|     func fetchValue(_ db: Database) throws -> [Ribbon] { | ||||
|         if (id == nil) { | ||||
|             return try Ribbon.order(Column("pos")).fetchAll(db) | ||||
|         } else { | ||||
|             return try Ribbon.filter(idColumn == id).fetchAll(db) | ||||
| 
 | ||||
|         var ret: [Ribbon] | ||||
|         var sql: String | ||||
| 
 | ||||
|         // this has to be a global variable | ||||
|         let totalLevels = 3 | ||||
|         do { | ||||
|             print("back reload") | ||||
|             if dir != nil && groupId != nil { | ||||
|                 sql = """ | ||||
|                 SELECT * FROM Ribbon \ | ||||
|                 WHERE groupId = ? | ||||
|                 """ | ||||
|                 ret = try Ribbon.fetchAll(db, sql: sql, arguments: [groupId]) | ||||
|                 if ret.count == 0 { | ||||
|                     print("error no ribbons found") | ||||
|                     return [] | ||||
|                 } | ||||
| 
 | ||||
|                 print("back all ribbon all: \(ret)") | ||||
| 
 | ||||
|                 let currentLevel = ret[0].currentLevel | ||||
|                 let minLevel = ret[0].minLevel | ||||
|                 let maxLevel = ret[0].maxLevel | ||||
| 
 | ||||
|                 var newCurrentLevel = (currentLevel - 1) %% totalLevels | ||||
| 
 | ||||
|                 // probably need more error checking to check | ||||
|                 // if current level gets into an error state | ||||
|                 // between minLevel and maxLevel somehow but there | ||||
|                 // are probably a bunch of edge cases casue of the | ||||
|                 // mod stuff | ||||
|                 if dir == .prev { | ||||
|                     // no back undo steps left | ||||
|                     if currentLevel == minLevel { | ||||
|                         newCurrentLevel = currentLevel | ||||
|                     } | ||||
|                 } else if dir == .prev { | ||||
|                     // no forward redo steps left | ||||
|                     if currentLevel == maxLevel { | ||||
|                         newCurrentLevel = maxLevel | ||||
|                     } else { | ||||
|                         newCurrentLevel = (currentLevel + 1) %% totalLevels | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 print("back newcurrentlevel \(newCurrentLevel)") | ||||
|                 sql = """ | ||||
|                 SELECT * FROM Ribbon \ | ||||
|                 WHERE groupId = ? AND | ||||
|                 undoLevel = ? | ||||
|                 LIMIT 1 | ||||
|                 """ | ||||
|                 ret = try Ribbon.fetchAll(db, sql: sql, arguments: [groupId, newCurrentLevel]) | ||||
| 
 | ||||
|                 print("back ribbon return: \(ret)") | ||||
|                 return ret | ||||
| 
 | ||||
|             } else { | ||||
|                 if groupId != nil { | ||||
|                     let sql = """ | ||||
|                     SELECT * from Ribbon r1 \ | ||||
|                     WHERE r1.groupId = ? | ||||
|                     ORDER BY undoLevel ASC | ||||
|                     """ | ||||
| 
 | ||||
|                     var ret = try Ribbon.fetchAll(db, sql: sql, arguments: [groupId]) | ||||
|                     print("xxxxx fetching ribbons") | ||||
|                     print(ret) | ||||
|                     return ret | ||||
|                 } else { | ||||
| 
 | ||||
|                     let sql = """ | ||||
|                     select distinct r1.* from Ribbon r1 join Ribbon r2 ON \ | ||||
|                     r1.undoLevel = r2.currentLevel AND r1.id = r2.id ORDER BY pos ASC | ||||
|                     """ | ||||
|                     var ret = try Ribbon.fetchAll(db, sql: sql) | ||||
|                     print("xxxxx fetching ribbons") | ||||
|                     print(ret) | ||||
|                     return ret | ||||
|                 } | ||||
|             } | ||||
|         } catch { | ||||
|             print(error.localizedDescription) | ||||
|             print(error) | ||||
|             print("Error") | ||||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|         // if id == nil { | ||||
|         //     return try Ribbon.order(Column("pos")).fetchAll(db) | ||||
|         // } else { | ||||
|         //     return try Ribbon.filter(idColumn == id).fetchAll(db) | ||||
|         // } | ||||
|         // { | ||||
| 
 | ||||
|         // if book == "" { | ||||
|         //     return try Ribbon.filter(bookColumn ==  Ribbon.randomBook()).fetchAll(db) | ||||
|         // } else { | ||||
|  | @ -69,3 +148,14 @@ struct RibbonRequest: Queryable { | |||
|         // } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| infix operator %% | ||||
| 
 | ||||
| extension Int { | ||||
| 
 | ||||
|     static  func %% (_ left: Int, _ right: Int) -> Int { | ||||
|        let mod = left % right | ||||
|        return mod >= 0 ? mod : mod + right | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -24,15 +24,37 @@ struct SegDenormRequest: Queryable { | |||
|     } | ||||
| 
 | ||||
|     func fetchValue(_ db: Database) throws -> [SegDenorm] { | ||||
|         print("WOOOOOOF") | ||||
|         var sql = "select seg_id as id, seg.book as book, group_concat(line.body, ';;') as body from seg join line on seg.line_id = line.rowid WHERE seg.book = 'bible.john' group by seg.seg_id" | ||||
|         print("segs denorm fetching for \(book)") | ||||
|         print(book) | ||||
|         var sql = """ | ||||
|         select seg_id as id, seg.book as book, group_concat(line.body, ';;') as body from \ | ||||
|         (select * from seg  where seg.book = '\(book)') as seg \ | ||||
|         join (select * from line  where line.book = '\(book)') as line \ | ||||
|         on seg.line_id = line.line_id  group by seg.seg_id | ||||
|         """ | ||||
| 
 | ||||
|         do { | ||||
|             var ret =  try SegDenorm.fetchAll(db, sql: sql) // [Player] | ||||
|         do | ||||
|         { | ||||
|             var ret =  try SegDenorm.fetchAll(db, sql: sql) | ||||
| 
 | ||||
|         // print("SEGS DENORM") | ||||
|         // print(ret) | ||||
|         // print(ret[0]) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         // var sql2 = """ | ||||
|         // select count(1) from seg where seg.book = '\(book)' | ||||
|         // """ | ||||
| 
 | ||||
|         //     var ret2 =  try SegDenorm.fetchAll(db, sql: sql2) | ||||
| 
 | ||||
|         // print("test sql result") | ||||
|         // print(ret2[0]) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         return ret | ||||
| 
 | ||||
|     } catch let error { | ||||
|         print(error.localizedDescription) | ||||
|         print(error) | ||||
|  |  | |||
|  | @ -0,0 +1,26 @@ | |||
| @State var selection = 0 | ||||
| var body: some View { | ||||
|     HStack { | ||||
|         BackArrow() | ||||
|             .frame(width: CGFloat(30), height: CGFloat(30)) | ||||
|             .foregroundColor(Color(UIColor(red: 0.30, green: 0.30, blue: 0.30, alpha: 0.4))) | ||||
|             .if(selection == 0) { $0.background(Color.white) } | ||||
|             .if(selection != 0) { $0.background(Color.black) } | ||||
|             .onTapGesture { | ||||
|                 withAnimation(.spring(response: 0.5)) { | ||||
|                     self.selection = 0 | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|         ForwardArrow() | ||||
|             .frame(width: CGFloat(30), height: CGFloat(30)) | ||||
|             .foregroundColor(Color(UIColor(red: 0.30, green: 0.30, blue: 0.30, alpha: 0.4))) | ||||
|             .if(selection == 1) { $0.background(Color.white) } | ||||
|             .if(selection != 1) { $0.background(Color.black) } | ||||
|             .onTapGesture { | ||||
|                 withAnimation(.spring(response: 0.5)) { | ||||
|                     self.selection = 1 | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| } | ||||
|  | @ -17,7 +17,7 @@ struct SelectedRibbon: Identifiable, Equatable { | |||
|     /// Int64 is the recommended type for auto-incremented database ids. | ||||
|     /// Use nil for players that are not inserted yet in the database. | ||||
|     var id: Int64? | ||||
|     var ribbonId: Int64 | ||||
|     var ribbonGroupId: Int64 | ||||
| } | ||||
| 
 | ||||
| extension SelectedRibbon { | ||||
|  | @ -32,7 +32,7 @@ extension SelectedRibbon: Codable, FetchableRecord, MutablePersistableRecord { | |||
|     // Define database columns from CodingKeys | ||||
|     fileprivate enum Columns { | ||||
|         static let id = Column(CodingKeys.id) | ||||
|         static let ribbonId = Column(CodingKeys.ribbonId) | ||||
|         static let ribbonGroupId = Column(CodingKeys.ribbonGroupId) | ||||
|     } | ||||
| 
 | ||||
|     /// Updates a player id after it has been inserted in the database. | ||||
|  |  | |||
|  | @ -51,11 +51,16 @@ struct SelectedRibbonRequest: Queryable { | |||
|         // var ret3 =  try Ribbon.fetchAll(db, sql: "SELECT * FROM Ribbon") // [Player] | ||||
|         // print(ret3) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         // print("FETCH JOIN RIBBON") | ||||
|         var ret =  try Ribbon.fetchAll(db, sql: "SELECT Ribbon.* FROM SelectedRibbon join Ribbon on SelectedRibbon.ribbonId = ribbon.rowId WHERE SelectedRibbon.rowId = 1") // [Player] | ||||
|         // print(ret) | ||||
|         var ret =  try Ribbon.fetchAll(db, sql: """ | ||||
|     SELECT Ribbon.* FROM SelectedRibbon \ | ||||
|     JOIN (select distinct r1.* from Ribbon r1 join Ribbon r2 ON \ | ||||
|         r1.undoLevel = r2.currentLevel AND r1.id = r2.id ORDER BY pos ASC) as Ribbon \ | ||||
|     ON SelectedRibbon.ribbonGroupId = Ribbon.groupId \ | ||||
|     WHERE SelectedRibbon.rowId = 1 | ||||
|     """) | ||||
|     // [Player] | ||||
|         print("Selected Ribbon query result: \(ret)") | ||||
|         return ret | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,113 @@ | |||
| import GRDB | ||||
| import GRDBQuery | ||||
| import Foundation | ||||
| import SwiftUI | ||||
| 
 | ||||
| var fontSize = CGFloat(12) | ||||
| 
 | ||||
| struct StatsPanel: View { | ||||
|     @ObservedObject var paneConnector: PaneConnector | ||||
| 
 | ||||
|     @Query(RibbonRequest(dir: .prev, groupId: 1)) private var backRibbon: [Ribbon] | ||||
|     @Query(RibbonRequest(dir: .next, groupId: 1)) private var nextRibbon: [Ribbon] | ||||
|     @Query(RibbonRequest(groupId: 1)) private var allRibbons: [Ribbon] | ||||
|     @Query(SelectedRibbonRequest()) private var selectedRibbon: [Ribbon] | ||||
|     @Environment(\.appDatabase) private var appDatabase | ||||
| 
 | ||||
|     var body: some View { | ||||
|         VStack (spacing: 10) { | ||||
|             VStack { | ||||
|                 Text("back Ribbon") | ||||
|                     .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                     .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|                 RibbonDebug(ribbonDebug: RibbonDebugPrint(ribbon: backRibbon[0])) | ||||
|             } | ||||
| 
 | ||||
|             VStack { | ||||
|                 Text("next Ribbon") | ||||
|                     .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                     .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|                 if (nextRibbon.count > 0) { | ||||
|                 RibbonDebug(ribbonDebug: RibbonDebugPrint(ribbon: nextRibbon[0])) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             VStack { | ||||
|                 Text("selected Ribbon") | ||||
|                     .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                     .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|                 RibbonDebug(ribbonDebug: RibbonDebugPrint(ribbon: selectedRibbon[0])) | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|                ForEach(RibbonMap(ribbons: allRibbons), id: \.self) { ribbon in | ||||
|                    Print("quack3: \(ribbon)") | ||||
|                    RibbonDebug(ribbonDebug: ribbon) | ||||
|                } | ||||
| 
 | ||||
|             //VStack(spacing: 10) { | ||||
|             //    Text("All Ribbons") | ||||
|             //        .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|             //        .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
| 
 | ||||
|             //    ForEach(RibbonMap(ribbons: allRibbons), id: \.self) { ribbon in | ||||
|             //        //Print("quack2: \(ribbon)") | ||||
|             //        RibbonDebug(ribbonDebug: RibbonDebugPrint(ribbon: ribbon)) | ||||
|             //    } | ||||
|             //} | ||||
| 
 | ||||
|             VStack { | ||||
|                 Text("pc offset: \(paneConnector.currentOffset)") | ||||
|                     .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                     .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|             } | ||||
| 
 | ||||
|             VStack { | ||||
|                 Text("pc id: \(paneConnector.currentId)") | ||||
|                     .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                     .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| func RibbonMap(ribbons: [Ribbon]) -> [[String]] { | ||||
|     var retStrings = [[String]]() | ||||
|     for r in ribbons { | ||||
|         var debugString = RibbonDebugPrint(ribbon:r) | ||||
|         retStrings.append(debugString) | ||||
|     } | ||||
|     return retStrings | ||||
| } | ||||
| 
 | ||||
| func RibbonDebugPrint(ribbon: Ribbon) -> [String] { | ||||
|     var ribbonStats = [String]() | ||||
|     ribbonStats.append("id: \(ribbon.id) ") | ||||
|     ribbonStats.append("scrollOffset: \(ribbon.scrollOffset)") | ||||
|     ribbonStats.append("scrollId: \(ribbon.scrollId)") | ||||
|     ribbonStats.append("undoLevel: \(ribbon.undoLevel)") | ||||
|     ribbonStats.append("currentLevel: \(ribbon.currentLevel)") | ||||
|     ribbonStats.append("minLevel: \(ribbon.minLevel)") | ||||
|     ribbonStats.append("maxLevel: \(ribbon.maxLevel)") | ||||
|     return ribbonStats | ||||
| } | ||||
| 
 | ||||
| struct RibbonDebug: View { | ||||
|     @State var ribbonDebug: [String] | ||||
| 
 | ||||
|     var body: some View { | ||||
| 
 | ||||
|         VStack { | ||||
|             VStack { | ||||
|                 // let ribbonStats = RibbonDebugPrint(ribbon: ribbon) | ||||
| 
 | ||||
|                 Print("quack2: \(ribbonDebug)") | ||||
|                 ForEach(ribbonDebug,  id: \.self) { | ||||
|                     Print("quack2 : \($0)") | ||||
|                     Text($0) | ||||
|                         .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                         .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -7,6 +7,10 @@ | |||
| 	objects = { | ||||
| 
 | ||||
| /* Begin PBXBuildFile section */ | ||||
| 		851259B02C05281300BE70F8 /* BackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851259AF2C05281300BE70F8 /* BackButton.swift */; }; | ||||
| 		851259B22C05299200BE70F8 /* ForwardArrow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851259B12C05299200BE70F8 /* ForwardArrow.swift */; }; | ||||
| 		851259B62C07560800BE70F8 /* NaviBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851259B52C07560800BE70F8 /* NaviBar.swift */; }; | ||||
| 		851259B82C0A145500BE70F8 /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851259B72C0A145500BE70F8 /* Stats.swift */; }; | ||||
| 		8514D5BC299EFB780054F185 /* store.db in Resources */ = {isa = PBXBuildFile; fileRef = 8514D5BB299EFB780054F185 /* store.db */; }; | ||||
| 		8514D5BF299F04710054F185 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 8514D5BE299F04710054F185 /* GRDB */; }; | ||||
| 		852774C129A150B100458CA7 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852774C029A150B100458CA7 /* Line.swift */; }; | ||||
|  | @ -45,6 +49,10 @@ | |||
| /* End PBXBuildFile section */ | ||||
| 
 | ||||
| /* Begin PBXFileReference section */ | ||||
| 		851259AF2C05281300BE70F8 /* BackButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackButton.swift; sourceTree = "<group>"; }; | ||||
| 		851259B12C05299200BE70F8 /* ForwardArrow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardArrow.swift; sourceTree = "<group>"; }; | ||||
| 		851259B52C07560800BE70F8 /* NaviBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NaviBar.swift; sourceTree = "<group>"; }; | ||||
| 		851259B72C0A145500BE70F8 /* Stats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; }; | ||||
| 		8514D5BB299EFB780054F185 /* store.db */ = {isa = PBXFileReference; lastKnownFileType = file; path = store.db; sourceTree = "<group>"; }; | ||||
| 		852774C029A150B100458CA7 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; }; | ||||
| 		8528897429B2B86B003F2E16 /* CrownOfThorns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrownOfThorns.swift; sourceTree = "<group>"; }; | ||||
|  | @ -108,6 +116,8 @@ | |||
| 		85431A7C2905F4F500EE0760 = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				851259B12C05299200BE70F8 /* ForwardArrow.swift */, | ||||
| 				851259AF2C05281300BE70F8 /* BackButton.swift */, | ||||
| 				857C34482BFB7DC800661A63 /* Fenestra.swift */, | ||||
| 				8594ED972BF6845F001213F2 /* HexColor.swift */, | ||||
| 				85E00E7B29F34D2D00FF9E78 /* ScrollState.swift */, | ||||
|  | @ -117,6 +127,8 @@ | |||
| 				85942EEE29AEA18300307621 /* SelectedRibbonRequest.swift */, | ||||
| 				85942EF429B108C600307621 /* Seg.swift */, | ||||
| 				85942EF629B108EA00307621 /* SegDenormRequest.swift */, | ||||
| 				851259B52C07560800BE70F8 /* NaviBar.swift */, | ||||
| 				851259B72C0A145500BE70F8 /* Stats.swift */, | ||||
| 				85942EF829B1150B00307621 /* SegDenorm.swift */, | ||||
| 				85942EEA29AD55A400307621 /* RibbonRequest.swift */, | ||||
| 				85942EE329ACF54A00307621 /* ScrollableView.swift */, | ||||
|  | @ -285,6 +297,7 @@ | |||
| 				857C34492BFB7DC800661A63 /* Fenestra.swift in Sources */, | ||||
| 				85942EEB29AD55A400307621 /* RibbonRequest.swift in Sources */, | ||||
| 				85431A8B2905F4F500EE0760 /* ContentView.swift in Sources */, | ||||
| 				851259B02C05281300BE70F8 /* BackButton.swift in Sources */, | ||||
| 				85942EF529B108C600307621 /* Seg.swift in Sources */, | ||||
| 				85E00E7C29F34D2D00FF9E78 /* ScrollState.swift in Sources */, | ||||
| 				8594ED982BF6845F001213F2 /* HexColor.swift in Sources */, | ||||
|  | @ -297,12 +310,15 @@ | |||
| 				85942EEF29AEA18300307621 /* SelectedRibbonRequest.swift in Sources */, | ||||
| 				8590D96C29A92146001EF84F /* JsonImport.swift in Sources */, | ||||
| 				85942EED29AEA04200307621 /* SelectedRibbon.swift in Sources */, | ||||
| 				851259B82C0A145500BE70F8 /* Stats.swift in Sources */, | ||||
| 				85431A892905F4F500EE0760 /* glossApp.swift in Sources */, | ||||
| 				8528897C29BD69B2003F2E16 /* VisibilityTrackingScrollView.swift in Sources */, | ||||
| 				85942EF929B1150B00307621 /* SegDenorm.swift in Sources */, | ||||
| 				85431A9C2905F5D800EE0760 /* SwiftUIView.swift in Sources */, | ||||
| 				851259B22C05299200BE70F8 /* ForwardArrow.swift in Sources */, | ||||
| 				8528897E29BD69B2003F2E16 /* VisibilityTracker.swift in Sources */, | ||||
| 				85942EF729B108EA00307621 /* SegDenormRequest.swift in Sources */, | ||||
| 				851259B62C07560800BE70F8 /* NaviBar.swift in Sources */, | ||||
| 				8528897D29BD69B2003F2E16 /* VisibilityTrackingModifier.swift in Sources */, | ||||
| 				85942EE429ACF54A00307621 /* ScrollableView.swift in Sources */, | ||||
| 			); | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| import Foundation | ||||
| import GRDB | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| let totalLevels = 3 | ||||
| 
 | ||||
| /// AppDatabase lets the application access the database. | ||||
| /// | ||||
| /// It applies the pratices recommended at | ||||
|  | @ -39,6 +43,7 @@ struct AppDatabase { | |||
|                 t.autoIncrementedPrimaryKey("id") | ||||
|                 t.column("body", .text).notNull() | ||||
|                 t.column("chap", .integer).notNull() | ||||
|                 t.column("line_id", .integer).notNull() | ||||
|                 t.column("book", .text).notNull() | ||||
|                 t.column("verse", .integer) | ||||
|             } | ||||
|  | @ -53,6 +58,13 @@ struct AppDatabase { | |||
|             try db.create(table: "Ribbon") { t in | ||||
|                 t.autoIncrementedPrimaryKey("id") | ||||
|                 t.column("pos", .integer).notNull() | ||||
|                 t.column("groupId", .integer).notNull() | ||||
|                 t.column("undoLevel", .integer).notNull() | ||||
| 
 | ||||
|                 t.column("currentLevel", .integer).notNull() | ||||
|                 t.column("minLevel", .integer).notNull() | ||||
|                 t.column("maxLevel", .integer).notNull() | ||||
| .defaults(to: 1) | ||||
|                 t.column("title", .text).notNull() | ||||
|                 t.column("book", .text).notNull() | ||||
|                 t.column("scrollOffset", .integer).notNull() | ||||
|  | @ -61,7 +73,7 @@ struct AppDatabase { | |||
| 
 | ||||
|             try db.create(table: "SelectedRibbon") { t in | ||||
|                 t.autoIncrementedPrimaryKey("id") | ||||
|                 t.column("ribbonId", .integer).notNull() | ||||
|                 t.column("ribbonGroupId", .integer).notNull() | ||||
|             } | ||||
| 
 | ||||
|             try db.create(table: "ScrollState") { t in | ||||
|  | @ -70,7 +82,8 @@ struct AppDatabase { | |||
|                 t.column("scrollOffset", .integer).notNull() | ||||
|             } | ||||
| 
 | ||||
|             try db.create(table: "foo2") { t in | ||||
|             // change this to nuke/remake the database | ||||
|             try db.create(table: "foo1") { t in | ||||
|                 t.autoIncrementedPrimaryKey("id") | ||||
|                 t.column("ribbonId", .integer).notNull() | ||||
|             } | ||||
|  | @ -113,6 +126,22 @@ func load<T: Decodable>(_ filename: String) -> T { | |||
| 
 | ||||
| extension AppDatabase { | ||||
| 
 | ||||
|     func getSelectedRibbon() async throws -> [Ribbon] { | ||||
|         try await dbWriter.write { db in | ||||
|             var sr = try Ribbon.fetchAll(db, sql: """ | ||||
|             SELECT Ribbon.* FROM SelectedRibbon \ | ||||
|             JOIN (select distinct r1.* from Ribbon r1 join Ribbon r2 ON \ | ||||
|                 r1.undoLevel = r2.currentLevel AND r1.id = r2.id ORDER BY pos ASC) as Ribbon \ | ||||
|             ON SelectedRibbon.ribbonGroupId = Ribbon.groupId \ | ||||
|             WHERE SelectedRibbon.rowId = 1 | ||||
|             """) | ||||
| 
 | ||||
|             print("meow get selected ribbon \(sr)") | ||||
|             return sr | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     func updateRibbonPosition(_ ribbon: inout Ribbon, _ oldPos: Int, _ newPos: Int) async throws { | ||||
|         try await dbWriter.write { [ribbon] db in | ||||
| 
 | ||||
|  | @ -193,6 +222,121 @@ extension AppDatabase { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     func undoRibbon(_ ribbon: inout Ribbon) async throws { | ||||
|         let currentLevel = ribbon.currentLevel | ||||
|         let minLevel = ribbon.minLevel | ||||
|         if currentLevel == minLevel { | ||||
|             print("no where to undo") | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         let newCurrent = (ribbon.currentLevel - 1) %% totalLevels | ||||
|         print("meow new current: \(newCurrent)") | ||||
| 
 | ||||
|         do { | ||||
|             try await dbWriter.write { [ribbon] db in | ||||
|                 print("back executing") | ||||
|                 try db.execute(sql: """ | ||||
|                                UPDATE Ribbon  \ | ||||
|                                SET currentLevel = ? WHERE groupId = ? | ||||
|                                """, arguments: [newCurrent, ribbon.groupId]) | ||||
| 
 | ||||
|                 var ret3 = try Ribbon.fetchAll(db, sql: """ | ||||
|                                SELECT * from Ribbon \ | ||||
|                                WHERE groupId = ? | ||||
|                                """, arguments: [ribbon.groupId]) | ||||
| 
 | ||||
|                 print("back ret3: \(ret3)") | ||||
|             } | ||||
| 
 | ||||
|         } catch { | ||||
|             print("back error") | ||||
|             print("Error info: \(error)") | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     func bumpRibbon(_ ribbon: inout Ribbon) async throws -> [Ribbon] { | ||||
|         var level = ribbon.currentLevel | ||||
|         let maxLevel = ribbon.maxLevel | ||||
|         var delLevels2 = [Int]() | ||||
|         while level != maxLevel { | ||||
|             level = (level + 1) % totalLevels | ||||
|             delLevels2.append(level) | ||||
|         } | ||||
|         let delLevels = delLevels2 | ||||
| 
 | ||||
|         let newMax = (ribbon.currentLevel + 1) %% totalLevels | ||||
|         let newCurrent = newMax | ||||
|         let newMin = newMax == ribbon.minLevel ? (ribbon.minLevel + 1) %% totalLevels | ||||
|             : ribbon.minLevel | ||||
| 
 | ||||
|         ribbon.minLevel = newMin | ||||
|         ribbon.maxLevel = newMax | ||||
|         ribbon.undoLevel = newCurrent | ||||
|         ribbon.currentLevel = newCurrent | ||||
|         ribbon.id = nil | ||||
| 
 | ||||
|         do { | ||||
|             try await dbWriter.write { [ribbon] db in | ||||
|                 print("meow enter bump 1") | ||||
|                 for l in delLevels { | ||||
|                     try db.execute(sql: """ | ||||
|                     DELETE FROM Ribbon \ | ||||
|                     WHERE groupId = ? \ | ||||
|                         AND undoLevel = ? | ||||
|                     """, arguments: [ribbon.groupId, l]) | ||||
|                 } | ||||
| 
 | ||||
|                 try db.execute(sql: """ | ||||
|                 UPDATE Ribbon  \ | ||||
|                 SET minLevel = ?, maxLevel = ?, currentLevel = ? WHERE groupId = ? | ||||
|                 """, arguments: [newMin, newMax, newCurrent, ribbon.groupId]) | ||||
| 
 | ||||
| 
 | ||||
|                 // upsert | ||||
|                 var ret = try Ribbon.fetchAll(db, sql: """ | ||||
|                     SELECT * from Ribbon WHERE groupId = ? AND undoLevel = ? | ||||
|                 """, arguments: [ribbon.groupId, ribbon.undoLevel]) | ||||
| 
 | ||||
|                 print("meow enter bump 3") | ||||
|                 if ret.count == 0 { | ||||
|                     // insert | ||||
|                  _ = try ribbon.inserted(db) | ||||
|                 } else { | ||||
|                     var updatedRibbon = ret[0] | ||||
|                     updatedRibbon.minLevel = newMin | ||||
|                     updatedRibbon.maxLevel = newMax | ||||
|                     updatedRibbon.undoLevel = newCurrent | ||||
|                     updatedRibbon.currentLevel = newCurrent | ||||
|                     updatedRibbon.scrollId = ribbon.scrollId | ||||
|                     updatedRibbon.scrollOffset = ribbon.scrollOffset | ||||
|                     try updatedRibbon.update(db) | ||||
|                 } | ||||
| 
 | ||||
|                 ret = try Ribbon.fetchAll(db, sql: """ | ||||
|                     SELECT * from Ribbon WHERE groupId = ? AND undoLevel = ? | ||||
|                 """, arguments: [ribbon.groupId, ribbon.undoLevel]) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|                 let ret2 = try Ribbon.fetchAll(db, sql: """ | ||||
|                     SELECT * from Ribbon WHERE groupId = 1 ORDER BY undoLevel ASC | ||||
|                 """) | ||||
| 
 | ||||
|                 for r2 in ret2 { | ||||
|                     print("meow ribbon dump: \(r2)") | ||||
|                 } | ||||
| 
 | ||||
|                 print("meow bumped ribbon: \(ret)") | ||||
|                 return ret | ||||
|             } | ||||
|         } catch { | ||||
|             print("Error info: \(error)") | ||||
|         } | ||||
|         return [] | ||||
|     } | ||||
| 
 | ||||
|     func saveSelectedRibbon(_ selectedRibbon: inout SelectedRibbon) async throws { | ||||
|         // if ribbon.name.isEmpty { | ||||
|         //     throw ValidationError.missingName | ||||
|  | @ -212,35 +356,106 @@ extension AppDatabase { | |||
|     } | ||||
| 
 | ||||
|     func importJson(_ filename: String, _ db: Database) throws { | ||||
|         let importJson : JsonImport = load(filename) | ||||
|         let importJson: JsonImport = load(filename) | ||||
| 
 | ||||
|         if try Line.all().isEmpty(db) { | ||||
|         var x = 0 | ||||
|         // if try Line.all().isEmpty(db) { | ||||
|         for l in importJson.lines { | ||||
|                 print("importing Lines") | ||||
|             // print("importing Lines") | ||||
|             if x < 5 { | ||||
|                 print(l) | ||||
|                 x += 1 | ||||
|             } | ||||
|             _ = try l.inserted(db) | ||||
|         } | ||||
| 
 | ||||
|         x = 0 | ||||
|         for l in importJson.segs { | ||||
|                 print("importing SEGS") | ||||
|                 _ = try l.inserted(db) | ||||
|             // print("importing SEGS") | ||||
| 
 | ||||
|             if x < 5 { | ||||
|                 print(l) | ||||
|                 x += 1 | ||||
|             } | ||||
|             _ = try l.inserted(db) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Create random Lines if the database is empty. | ||||
|     func initDatabase() throws { | ||||
|         do { | ||||
|             try dbWriter.write { db in | ||||
| 
 | ||||
|             if try Line.all().isEmpty(db) { | ||||
| 
 | ||||
|                 if try Line.all().isEmpty(db) | ||||
|                 { | ||||
|                     try importJson("john_export.json", db) | ||||
|                     try importJson("mark_export.json", db) | ||||
|                 _ = try Ribbon(id: 1, pos: 1, title: "John", book: "bible.john", scrollId: "1", scrollOffset: 0).inserted(db) | ||||
|                 _ = try Ribbon(id: 2, pos: 2, title: "Gospel of Mark", book: "bible.mark", scrollId: "1", scrollOffset: 300).inserted(db) | ||||
|                 _ = try Ribbon(id: 3, pos: 3, title: "John 2", book: "bible.john", scrollId: "1", scrollOffset: 0).inserted(db) | ||||
|                 _ = try SelectedRibbon(id: 1, ribbonId: 1).inserted(db) | ||||
|                     _ = try Ribbon(id: 1, | ||||
|                                    groupId: 1, | ||||
|                                    pos: 1, | ||||
|                                    undoLevel: 0, | ||||
|                                    currentLevel: 0, | ||||
|                                    minLevel: 0, | ||||
|                                    maxLevel: 0, | ||||
|                                    title: "John", | ||||
|                                    book: "bible.john", | ||||
|                                    scrollId: "1", | ||||
|                                    scrollOffset: 0).inserted(db) | ||||
|                     _ = try Ribbon(id: 2, | ||||
|                                    groupId: 2, | ||||
|                                    pos: 2, | ||||
|                                    undoLevel: 0, | ||||
|                                    currentLevel: 0, | ||||
|                                    minLevel: 0, | ||||
|                                    maxLevel: 0, | ||||
|                                    title: "Gospel of Mark", | ||||
|                                    book: "bible.mark", | ||||
|                                    scrollId: "1", | ||||
|                                    scrollOffset: 300).inserted(db) | ||||
| 
 | ||||
|                     ///// | ||||
| 
 | ||||
|                     _ = try Ribbon(id: 3, | ||||
|                                    groupId: 3, | ||||
|                                    pos: 3, | ||||
|                                    undoLevel: 0, | ||||
|                                    currentLevel: 2, | ||||
|                                    minLevel: 0, | ||||
|                                    maxLevel: 2, | ||||
|                                    title: "bottom", | ||||
|                                    book: "bible.john", | ||||
|                                    scrollId: "1", | ||||
|                                    scrollOffset: 0).inserted(db) | ||||
| 
 | ||||
|                     _ = try Ribbon(id: 4, | ||||
|                                    groupId: 3, | ||||
|                                    pos: 3, | ||||
|                                    undoLevel: 1, | ||||
|                                    currentLevel: 2, | ||||
|                                    minLevel: 0, | ||||
|                                    maxLevel: 2, | ||||
|                                    title: "topp", | ||||
|                                    book: "bible.john", | ||||
|                                    scrollId: "1", | ||||
|                                    scrollOffset: 0).inserted(db) | ||||
| 
 | ||||
|                     _ = try Ribbon(id: 5, | ||||
|                                    groupId: 3, | ||||
|                                    pos: 3, | ||||
|                                    undoLevel: 2, | ||||
|                                    currentLevel: 2, | ||||
|                                    minLevel: 0, | ||||
|                                    maxLevel: 2, | ||||
|                                    title: "topp", | ||||
|                                    book: "bible.john", | ||||
|                                    scrollId: "1", | ||||
|                                    scrollOffset: 0).inserted(db) | ||||
| 
 | ||||
|                 _ = try SelectedRibbon(id: 1, ribbonGroupId: 1).inserted(db) | ||||
|             } | ||||
|         } | ||||
|         } catch { | ||||
|             print("Error info: \(error)") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -39,20 +39,45 @@ public extension UserDefaults { | |||
|     } | ||||
| } | ||||
| 
 | ||||
|  func updateRibbon(selectedRibbon: Ribbon, | ||||
|                   appDatabase   : AppDatabase, | ||||
|                   paneConnector: PaneConnector) async throws -> [Ribbon] | ||||
| { | ||||
|     print("meow updating ribbon") | ||||
|     var updatedRibbon = selectedRibbon | ||||
|     var scrollOffsetToSave = Int(floor(paneConnector.currentOffset)) | ||||
|     var scrollIdToSave = paneConnector.currentId | ||||
|     print("meow scrolloffsets \(scrollIdToSave) \(scrollOffsetToSave)") | ||||
|     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) | ||||
|         print("meow finished bumping") | ||||
|         return ret | ||||
|     } | ||||
|     print("meow no bump") | ||||
|     return [] | ||||
| } | ||||
| 
 | ||||
| func goToRibbon(selectedRibbon: Ribbon, | ||||
|                 destRibbon: Ribbon, | ||||
|                 appDatabase: AppDatabase, | ||||
|                 paneConnector: PaneConnector, | ||||
|                 refresh: Binding<Bool>, | ||||
|                 loading: Bool) | ||||
|                 loading: Bool, | ||||
|                 bump: 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 | ||||
| 
 | ||||
|             var updatedRibbon = selectedRibbon | ||||
|             print("go to ribbon") | ||||
|             print("\(selectedRibbon.id) \(destRibbon.id!)") | ||||
| 
 | ||||
|  | @ -72,9 +97,7 @@ func goToRibbon(selectedRibbon: Ribbon, | |||
|                 print("toggling") | ||||
|                 print("paneconnector: \(paneConnector.refresh)") | ||||
| 
 | ||||
|                 refresh.wrappedValue.toggle() | ||||
| 
 | ||||
|                 var updateSelectRibbon = SelectedRibbon(id: Int64(1), ribbonId: destRibbon.id!) | ||||
|                 var updateSelectRibbon = SelectedRibbon(id: Int64(1), ribbonGroupId: Int64(destRibbon.groupId)) | ||||
|                 // print("Saving selected ribbon") | ||||
|                 // print(updateSelectRibbon) | ||||
|                 do { | ||||
|  | @ -84,15 +107,26 @@ func goToRibbon(selectedRibbon: Ribbon, | |||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if !loading { | ||||
|                 print("not loading") | ||||
|                 updatedRibbon.scrollOffset = Int(floor(scrollOffsetToSave)) | ||||
|                 updatedRibbon.scrollId = scrollIdToSave | ||||
|                 _ = try await appDatabase.saveRibbon(&updatedRibbon) | ||||
|             // // this gets run regardless of if we switch ribbons or not | ||||
|             // // so if you click the same ribbon or a different ribbon | ||||
|             // if !loading || bump { | ||||
|             //     print("not loading") | ||||
| 
 | ||||
|             } else { | ||||
|                 print("loading") | ||||
|             } | ||||
| 
 | ||||
|             //     print("saving ribbon") | ||||
|             //     // updating the ribbon location | ||||
|             //     updatedRibbon.scrollOffset = Int(floor(scrollOffsetToSave)) | ||||
|             //     updatedRibbon.scrollId = scrollIdToSave | ||||
|             //     // updatedRibbon.undoLevel = updatedRibbon.undoLevel + 1 | ||||
|             //     // updatedRibbon.currentLevel = updatedRibbon.currentLevel + 1 | ||||
| 
 | ||||
|             //     if bump { | ||||
|             //         _ = try await appDatabase.bumpRibbon(&updatedRibbon) | ||||
|             //     } | ||||
| 
 | ||||
|             // } else { | ||||
|             //     print("loading") | ||||
|             // } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -117,7 +151,7 @@ struct RibbonCrown: View { | |||
| 
 | ||||
|     @Environment(\.appDatabase) private var appDatabase | ||||
| 
 | ||||
|     @Query(SelectedRibbonRequest()) private var sr: [Ribbon] | ||||
|     @Query(SelectedRibbonRequest()) private var selectedRibbon: [Ribbon] | ||||
| 
 | ||||
|     @State var saveOffset = CGFloat() | ||||
|     @Binding var refresh: Bool | ||||
|  | @ -149,12 +183,18 @@ struct RibbonCrown: View { | |||
|         } | ||||
|         .onTapGesture { | ||||
|             Task { | ||||
|                 goToRibbon(selectedRibbon: sr[0], | ||||
| 
 | ||||
|                 let sr = selectedRibbon[0] | ||||
|                 let updatedRibbon = try await updateRibbon(selectedRibbon: sr, | ||||
|                                                           appDatabase: appDatabase, | ||||
|                                                           paneConnector: paneConnector) | ||||
| 
 | ||||
|                 goToRibbon(selectedRibbon: sr, | ||||
|                            destRibbon: ribbon, | ||||
|                            appDatabase: appDatabase, | ||||
|                            paneConnector: paneConnector, | ||||
|                            refresh: $refresh, | ||||
|                            loading: false) | ||||
|                            loading: false, | ||||
|                            bump: true) | ||||
|                    } | ||||
|         } | ||||
|         .frame(width: CGFloat(100 * 1.66 * scale + 10), height: CGFloat(100 * scale + 5)) | ||||
|  | @ -191,6 +231,7 @@ class PaneConnector: NSObject, ObservableObject { | |||
|     // var setScrollOffset: CGFloat | ||||
|     var showOverlay: Bool = false | ||||
|     @Published var refresh: Bool = false | ||||
|     @Published var vertSep = CGFloat(20) | ||||
|     var currentId = "" | ||||
|     var currentOffset = CGFloat() | ||||
|     var visibilityTracker: VisibilityTracker<String>? | ||||
|  | @ -204,25 +245,21 @@ struct ContentView: View { | |||
|     @State var viewState = CGSize.zero | ||||
|     @State var pulledOut = CGSize.zero | ||||
| 
 | ||||
|     @State var selection = 0 | ||||
| 
 | ||||
|     @StateObject var paneConnector = PaneConnector() | ||||
| 
 | ||||
|     @State var refresh: Bool = false | ||||
| 
 | ||||
|     @State var vertSep = CGFloat(20) | ||||
| 
 | ||||
|     @State var endedDrag = true | ||||
| 
 | ||||
|     @State var readOffset = CGPoint() | ||||
|     @State var dragOffset = CGFloat() | ||||
| 
 | ||||
|     @Query(SegDenormRequest(book: "bible.john")) private var segs: [SegDenorm] | ||||
| 
 | ||||
|     @State var draggedRibbon: Ribbon? | ||||
|     @State var isDragging = false | ||||
| 
 | ||||
|     @Environment(\.appDatabase) private var appDatabase | ||||
| 
 | ||||
|     @Query(SegDenormRequest(book: "bible.mark")) private var segs: [SegDenorm] | ||||
| 
 | ||||
|     @Query(RibbonRequest()) private var ribbons: [Ribbon] | ||||
|     @Query<SelectedRibbonRequest> var selectedRibbon: [Ribbon] | ||||
| 
 | ||||
|  | @ -271,13 +308,25 @@ struct ContentView: View { | |||
|                 .background(Color(red: 0.1, green: 0.1, blue: 0.1)) | ||||
|                 .frame(alignment: .topLeading) | ||||
| 
 | ||||
|                 VStack { | ||||
|                     Text("pc offset: \(paneConnector.currentOffset)") | ||||
|                         .foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|                         .font(Font.custom("AveriaSerifLibre-Regular", size: fontSize)) | ||||
|             } | ||||
| 
 | ||||
|                 VStack { | ||||
|                     NaviBar(paneConnector: paneConnector) | ||||
|                     StatsPanel(paneConnector: paneConnector) | ||||
|                 } | ||||
|                     .frame(maxWidth: 250) | ||||
|                 .offset(x: geometry.size.width - 250) | ||||
| 
 | ||||
|                 VStack { | ||||
|                     // Top pane | ||||
|                     Pane(paneConnector: paneConnector, | ||||
|                          segs: segs, | ||||
|                          selectedRibbon: selectedRibbon, | ||||
|                          width: geometry.size.width - 50, | ||||
|                          height: geometry.size.height / 2 - vertSep) | ||||
|                          height: geometry.size.height / 2) | ||||
|                     ////// | ||||
| 
 | ||||
|                     Text("separator").foregroundColor(Color(UIColor(red: 0.76, green: 0.76, blue: 0.76, alpha: 1.00))) | ||||
|  | @ -285,7 +334,7 @@ struct ContentView: View { | |||
|                             DragGesture() | ||||
|                                 .onChanged { gesture in | ||||
| 
 | ||||
|                                     vertSep = vertSep - gesture.translation.height | ||||
|                                     paneConnector.vertSep = paneConnector.vertSep - gesture.translation.height | ||||
|                                     Print(gesture.translation.width) | ||||
|                                     Print(gesture.translation.height) | ||||
| 
 | ||||
|  | @ -335,33 +384,30 @@ struct ContentView: View { | |||
|                     DragGesture() | ||||
|                         .onChanged { gesture in | ||||
| 
 | ||||
|                             if endedDrag { | ||||
|                                 endedDrag = false | ||||
| 
 | ||||
|                                 //TODO: should this still be adjusted | ||||
|                                 //scrollOffset = readOffset.y - 20 | ||||
|                             } | ||||
|                             Print(viewState.width) | ||||
|                             if abs(gesture.translation.width) > 20 { | ||||
|                             print(viewState.width) | ||||
|                             print(pulledOut.width) | ||||
|                             // threshold of how much to swipe before the view drags | ||||
|                             if abs(gesture.translation.width) > 10 { | ||||
|                                 viewState.width = gesture.translation.width | ||||
|                                 if gesture.translation.width < -50, pulledOut.width == CGFloat(0) { | ||||
|                                     dragOffset = gesture.translation.width + 50 | ||||
|                                 } | ||||
|                                 // if gesture.translation.width < -50, pulledOut.width == CGFloat(0) { | ||||
|                                 // } | ||||
|                             } | ||||
|                         } | ||||
|                         .onEnded { _ in | ||||
|                             endedDrag = true | ||||
|                             var pulledOutWidth = CGFloat(0) | ||||
|                             if viewState.width < 0 { | ||||
|                                 pulledOutWidth = CGFloat(0) | ||||
|                             var setPulledOutWith = CGFloat(0) | ||||
|                             if viewState.width < 0 && pulledOut.width > 0 { | ||||
|                                 setPulledOutWith = CGFloat(0) | ||||
|                             } else if viewState.width > 0 && pulledOut.width < 0 { | ||||
|                                 setPulledOutWith = CGFloat(0) | ||||
|                             } else if viewState.width < 0 && pulledOut.width == 0 { | ||||
|                                 setPulledOutWith = CGFloat(-200) | ||||
|                             } else if abs(viewState.width + pulledOut.width) > 30 { | ||||
|                                 pulledOutWidth = CGFloat(200) | ||||
|                                 setPulledOutWith = CGFloat(200) | ||||
|                             } | ||||
| 
 | ||||
|                             withAnimation(.spring(response: 0.2)) { | ||||
|                                 pulledOut.width = pulledOutWidth | ||||
|                                 pulledOut.width = setPulledOutWith | ||||
|                                 viewState = .zero | ||||
|                                 dragOffset = .zero | ||||
|                             } | ||||
|                         } | ||||
|                     ) | ||||
|  | @ -389,27 +435,7 @@ struct ContentView: View { | |||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     func handleVisibilityChanged2(_: String, change _: VisibilityChange, tracker _: VisibilityTracker<String>) {} | ||||
| 
 | ||||
|     func handleVisibilityChanged(_: String, change _: VisibilityChange, tracker: VisibilityTracker<String>) { | ||||
|         // var printRate: Int64 = 10 | ||||
|         // gTracker = tracker | ||||
|         self.paneConnector.visibilityTracker = tracker | ||||
| 
 | ||||
| 
 | ||||
|         let visibleViews2 = Array(tracker.visibleViews.keys) | ||||
|         if visibleViews2.count == 0 { | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         // currentId = tracker.sortedViewIDs[tracker.sortedViewIDs.count - 1] | ||||
|         currentId = tracker.sortedViewIDs[0] | ||||
|         currentOffset = tracker.visibleViews[currentId!]! | ||||
| 
 | ||||
|         self.paneConnector.currentId = tracker.sortedViewIDs[0] | ||||
|         self.paneConnector.currentOffset = tracker.visibleViews[currentId!]! | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private let itemFormatter: DateFormatter = { | ||||
|  |  | |||
|  | @ -1,39 +1,17 @@ | |||
| import GRDB | ||||
| /// The Line struct. | ||||
| /// | ||||
| /// Identifiable conformance supports SwiftUI list animations, and type-safe | ||||
| /// GRDB primary key methods. | ||||
| /// Equatable conformance supports tests. | ||||
| struct Line: Identifiable, Equatable { | ||||
|     /// The player id. | ||||
|     /// | ||||
|     /// Int64 is the recommended type for auto-incremented database ids. | ||||
|     /// Use nil for players that are not inserted yet in the database. | ||||
|     var id: Int64? | ||||
|     var chap: Int | ||||
|     var line_id: Int // this is a line_id per book | ||||
|     var verse: Int | ||||
|     var body: String | ||||
|     var book: String | ||||
| } | ||||
| 
 | ||||
| extension Line { | ||||
| 
 | ||||
|     private static let books = [ | ||||
|         "John", "Matthew", "Imitation of Christ"] | ||||
|      | ||||
|     /// Creates a new player with empty name and zero score | ||||
|     // static func new() -> Line { | ||||
|     //     Line(id: nil, chap: 1, body: "") | ||||
|     // } | ||||
|      | ||||
|     /// Returns a random score | ||||
|     static func randomScore() -> Int { | ||||
|         10 * Int.random(in: 0...100) | ||||
|     } | ||||
| 
 | ||||
|     static func randomBook() -> String { | ||||
|         books.randomElement()! | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // MARK: - Persistence | ||||
|  |  | |||
|  | @ -49,11 +49,12 @@ struct LineRequest: Queryable { | |||
|     // This method is not required by Queryable, but it makes it easier | ||||
|     // to test LineRequest. | ||||
|     func fetchValue(_ db: Database) throws -> [Line] { | ||||
|         if book == "" { | ||||
|             return try Line.filter(bookColumn ==  Line.randomBook()).fetchAll(db) | ||||
|         } else { | ||||
|         return try Line.filter(bookColumn ==  book).fetchAll(db) | ||||
|         } | ||||
|         // if book == "" { | ||||
|         //     return try Line.filter(bookColumn ==  Line.randomBook()).fetchAll(db) | ||||
|         // } else { | ||||
|         //     return try Line.filter(bookColumn ==  book).fetchAll(db) | ||||
|         // } | ||||
|         // switch ordering { | ||||
|         // case .byScore: | ||||
|         //     return try Line.all().fetchAll(db) | ||||
|  |  | |||
|  | @ -40,6 +40,7 @@ extension AppDatabase { | |||
|             //     // demo purpose. | ||||
|             //     try appDatabase.createRandomPlayersIfEmpty() | ||||
|             // } | ||||
|             print("initing database") | ||||
|             try appDatabase.initDatabase() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,7 +17,12 @@ struct Ribbon: Identifiable, Equatable { | |||
|     /// Int64 is the recommended type for auto-incremented database ids. | ||||
|     /// Use nil for players that are not inserted yet in the database. | ||||
|     var id: Int64? | ||||
|     var groupId: Int | ||||
|     var pos: Int | ||||
|     var undoLevel: Int | ||||
|     var currentLevel: Int | ||||
|     var minLevel: Int | ||||
|     var maxLevel: Int | ||||
|     var title: String | ||||
|     var book: String | ||||
|     var scrollId: String | ||||
|  | @ -27,7 +32,6 @@ struct Ribbon: Identifiable, Equatable { | |||
| extension Ribbon { | ||||
| } | ||||
| 
 | ||||
| // MARK: - Persistence | ||||
| 
 | ||||
| /// Make Line a Codable Record. | ||||
| /// | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
		Reference in New Issue