[Widgets]: (complete) Fixed previous pagination issues for ActiveAppsWidget

This commit is contained in:
Magesh K
2025-01-09 18:50:44 +05:30
parent 46871f63ed
commit 4e10527f03
7 changed files with 154 additions and 174 deletions

View File

@@ -56,7 +56,7 @@
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; }; A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; }; A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; };
A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; }; A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
A80D790F2D2F217000A40F40 /* PaginationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationViewModel.swift */; }; A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; }; A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; }; A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
@@ -627,7 +627,7 @@
A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = "<group>"; }; A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = "<group>"; };
A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = "<group>"; }; A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = "<group>"; };
A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = "<group>"; }; A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = "<group>"; };
A80D790E2D2F217000A40F40 /* PaginationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationViewModel.swift; sourceTree = "<group>"; }; A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = "<group>"; };
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; }; A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = "<group>"; };
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; }; A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; }; A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = "<group>"; };
@@ -2080,7 +2080,7 @@
D50C29F22A8ECD71009AB488 /* Widgets */ = { D50C29F22A8ECD71009AB488 /* Widgets */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A80D790E2D2F217000A40F40 /* PaginationViewModel.swift */, A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */,
D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */, D577AB7E2A96878A007FE952 /* AppDetailWidget.swift */,
D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */, D55467C42A8D72C300F4CE90 /* ActiveAppsWidget.swift */,
BF98917D250AAC4F002ACF50 /* LockScreenWidget.swift */, BF98917D250AAC4F002ACF50 /* LockScreenWidget.swift */,
@@ -2919,7 +2919,7 @@
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */, A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */, BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */, D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */,
A80D790F2D2F217000A40F40 /* PaginationViewModel.swift in Sources */, A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -24,29 +24,12 @@ struct AppsTimelineProvider
{ {
typealias Entry = AppsEntry typealias Entry = AppsEntry
private var viewModel: PaginationViewModel? private var dataHolder: PaginationDataHolder?
private let widgetID: String?
init(_ viewModel: PaginationViewModel? = nil){ init(_ dataHolder: PaginationDataHolder? = nil){
self.viewModel = viewModel self.dataHolder = dataHolder
self.widgetID = viewModel?.widgetID
} }
private func reloadEntries(apps: [AppSnapshot]){
guard let viewModel = self.viewModel else { return }
let entries = Set(viewModel.backup_entries.map{ (app: AppSnapshot) in app.bundleIdentifier })
let app_entries = Set(apps.map{ (app: AppSnapshot) in app.bundleIdentifier })
// this updates the in-memory entries
if entries.isEmpty || entries != app_entries{
self.viewModel?.setEntries(apps)
// initialize the view
self.viewModel?.handlePagination(.up)
}
}
func placeholder(in context: TimelineProviderContext) -> AppsEntry func placeholder(in context: TimelineProviderContext) -> AppsEntry
{ {
return AppsEntry(date: Date(), apps: [], isPlaceholder: true) return AppsEntry(date: Date(), apps: [], isPlaceholder: true)
@@ -58,10 +41,9 @@ struct AppsTimelineProvider
{ {
try await self.prepare() try await self.prepare()
let apps = try await self.fetchApps(withBundleIDs: appBundleIDs) var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
// send this for pagination apps = getUpdatedData(apps)
reloadEntries(apps: apps)
let entry = AppsEntry(date: Date(), apps: apps) let entry = AppsEntry(date: Date(), apps: apps)
return entry return entry
@@ -81,10 +63,9 @@ struct AppsTimelineProvider
{ {
try await self.prepare() try await self.prepare()
let apps = try await self.fetchApps(withBundleIDs: appBundleIDs) var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
// send this for pagination apps = getUpdatedData(apps)
reloadEntries(apps: apps)
let entries = self.makeEntries(for: apps) let entries = self.makeEntries(for: apps)
let timeline = Timeline(entries: entries, policy: .atEnd) let timeline = Timeline(entries: entries, policy: .atEnd)
@@ -103,6 +84,22 @@ struct AppsTimelineProvider
private extension AppsTimelineProvider private extension AppsTimelineProvider
{ {
private func getUpdatedData(_ apps: [AppSnapshot]) -> [AppSnapshot]{
var apps = apps
// #if DEBUG
// // this dummy data is for simulator (uncomment when testing ActiveAppsWidget pagination)
// apps = apps + apps + apps + apps + apps + apps + apps + apps
// #endif
if let dataHolder{
// get paged data if present if available
apps = dataHolder.getUpdatedData(entries: apps)
}
return apps
}
func prepare() async throws func prepare() async throws
{ {
try await DatabaseManager.shared.start() try await DatabaseManager.shared.start()

View File

@@ -55,7 +55,7 @@ extension View
} }
@ViewBuilder @ViewBuilder
func pageUpButton(widgetID: String) -> some View { func pageUpButton(_ widgetID: String) -> some View {
if #available(iOSApplicationExtension 17, *) { if #available(iOSApplicationExtension 17, *) {
Button(intent: PaginationIntent(.up, widgetID)){ Button(intent: PaginationIntent(.up, widgetID)){
self self
@@ -67,7 +67,7 @@ extension View
} }
@ViewBuilder @ViewBuilder
func pageDownButton(widgetID: String) -> some View { func pageDownButton(_ widgetID: String) -> some View {
if #available(iOSApplicationExtension 17, *) { if #available(iOSApplicationExtension 17, *) {
Button(intent: PaginationIntent(.down, widgetID)){ Button(intent: PaginationIntent(.down, widgetID)){
self self

View File

@@ -9,13 +9,13 @@
import AppIntents import AppIntents
import WidgetKit import WidgetKit
public enum Direction: String{ public enum Direction: String, Sendable{
case up = "up" case up = "up"
case down = "down" case down = "down"
} }
@available(iOS 17, *) @available(iOS 17, *)
struct PaginationIntent: AppIntent, @unchecked Sendable { class PaginationIntent: AppIntent, @unchecked Sendable {
static var title: LocalizedStringResource { "Scroll up or down in Active Apps Widget" } static var title: LocalizedStringResource { "Scroll up or down in Active Apps Widget" }
static var isDiscoverable: Bool { false } static var isDiscoverable: Bool { false }
@@ -25,7 +25,11 @@ struct PaginationIntent: AppIntent, @unchecked Sendable {
@Parameter(title: "Widget Identifier") @Parameter(title: "Widget Identifier")
var widgetID: String var widgetID: String
init(){} private lazy var widgetHolderQ = {
DispatchQueue(label: widgetID)
}()
required init(){}
init(_ direction: Direction, _ widgetID: String){ init(_ direction: Direction, _ widgetID: String){
self.direction = direction.rawValue self.direction = direction.rawValue
@@ -33,13 +37,20 @@ struct PaginationIntent: AppIntent, @unchecked Sendable {
} }
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
let direction = Direction(rawValue: direction)! guard let direction = Direction(rawValue: self.direction) else {
guard let viewModel = PaginationViewModel.instance(widgetID) else{
print("viewModel for widgetID: \(widgetID) not found, ignoring request")
return .result() return .result()
} }
viewModel.handlePagination(direction)
WidgetCenter.shared.reloadTimelines(ofKind: viewModel.widgetID) widgetHolderQ.sync {
// update direction for this widgetID
let dataholder = PaginationDataHolder.holder(for: self.widgetID)
dataholder?.updateDirection(direction)
// ask widget views to be re-drawn by triggering timeline update
// for the widget uniquely identified by the 'kind: widgetID'
WidgetCenter.shared.reloadTimelines(ofKind: self.widgetID)
}
return .result() return .result()
} }
} }

View File

@@ -1,5 +1,5 @@
// //
// HomeScreenWidget.swift // ActiveAppsWidget.swift
// AltWidgetExtension // AltWidgetExtension
// //
// Created by Riley Testut on 8/16/23. // Created by Riley Testut on 8/16/23.
@@ -8,10 +8,6 @@
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
import CoreData
import AltStoreCore
import AltSign
private extension Color private extension Color
{ {
@@ -24,24 +20,40 @@ private extension Color
//@available(iOS 17, *) //@available(iOS 17, *)
struct ActiveAppsWidget: Widget struct ActiveAppsWidget: Widget
{ {
private var viewModel = PaginationViewModel.getNewInstance( // only constants/singleton what needs to be for the life of all widgets of ActiveAppsWidget type
"ActiveApps" + UUID().uuidString // should be declared as instance or class fields for ActiveAppWidgets
)
// NOTE: The computed property (widget)body is recomputed/re-run for every
// 'type' refers to struct/class types and 'kind' refers to the tag which the widget is marked with
// multiple instances of same type or multiple types can be tagged with same 'kind'
// or each instance of same kind too, can be tagged as different(unique) 'kind'
// 1. widget-resizing(of same type)
// 2. new widget addition(of same type)
public var body: some WidgetConfiguration { public var body: some WidgetConfiguration {
if #available(iOS 17, *) if #available(iOS 17, *)
{ {
let kind = "ActiveApps"
let widgetID = kind + "-" + UUID().uuidString
let holder = PaginationDataHolder.instance(widgetID)
let timelineProvider = AppsTimelineProvider(holder) // pass the holder
// each instance of this widget type is identified by unique 'kind' tag
// so that a reloadTimelineFor(kind:) will trigger reload only for that instance
let staticConfig = StaticConfiguration( let staticConfig = StaticConfiguration(
kind: viewModel.widgetID, kind: widgetID,
provider: AppsTimelineProvider(viewModel) provider: timelineProvider
) { entry in ) { entry in
ActiveAppsWidgetView(entry: entry, viewModel: viewModel) // actual view of the widget
// this gets recreated for each trigger from the scheduled timeline entries provided by the timeline provider
// NOTE: widget views do not adhere to statefulness
// so, Combine constructs such as @State, @StateObject, @ObservedObject etc are simply ignored
ActiveAppsWidgetView(entry: entry, widgetID: widgetID)
} }
.supportedFamilies([.systemMedium]) .supportedFamilies([.systemMedium])
.configurationDisplayName("Active Apps") .configurationDisplayName("Active Apps")
.description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.") .description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.")
// this widgetConfiguration is requested/drawn once per widget per process lifecycle
return staticConfig return staticConfig
} }
else else
@@ -57,17 +69,11 @@ struct ActiveAppsWidget: Widget
private struct ActiveAppsWidgetView: View private struct ActiveAppsWidgetView: View
{ {
var entry: AppsEntry var entry: AppsEntry
var widgetID: String
@ObservedObject private var viewModel: PaginationViewModel
@Environment(\.colorScheme) @Environment(\.colorScheme)
private var colorScheme private var colorScheme
init(entry: AppsEntry, viewModel: PaginationViewModel){
self.entry = entry
self.viewModel = viewModel
}
var body: some View { var body: some View {
Group { Group {
if entry.apps.isEmpty if entry.apps.isEmpty
@@ -95,17 +101,14 @@ private struct ActiveAppsWidgetView: View
private var content: some View { private var content: some View {
GeometryReader { (geometry) in GeometryReader { (geometry) in
let MAX_ROWS_PER_PAGE = PaginationViewModel.MAX_ROWS_PER_PAGE let MAX_ROWS_PER_PAGE = PaginationDataHolder.MAX_ROWS_PER_PAGE
let preferredRowHeight = (geometry.size.height / Double(MAX_ROWS_PER_PAGE)) - 8 let preferredRowHeight = (geometry.size.height / Double(MAX_ROWS_PER_PAGE)) - 8
let rowHeight = min(preferredRowHeight, geometry.size.height / 2) let rowHeight = min(preferredRowHeight, geometry.size.height / 2)
HStack(alignment: .center) { HStack(alignment: .center) {
// VStack(spacing: 12) {
LazyVStack(spacing: 12) { LazyVStack(spacing: 12) {
ForEach($viewModel.sliding_window, id: \.bundleIdentifier) { app in ForEach(Array(entry.apps.enumerated()), id: \.offset) { index, app in
let app = app.wrappedValue // remove the binding
let icon: UIImage = app.icon ?? UIImage(named: "SideStore")! let icon: UIImage = app.icon ?? UIImage(named: "SideStore")!
@@ -160,6 +163,8 @@ private struct ActiveAppsWidgetView: View
.padding(.all, -5) .padding(.all, -5)
} }
.font(.system(size: 16, weight: .semibold, design: .rounded)) .font(.system(size: 16, weight: .semibold, design: .rounded))
// this modifier invalidates the view (disables userinteraction and shows a blinking effect)
// until new timeline events occur, unless a observable boolean state is presented as parameter
.invalidatableContent() .invalidatableContent()
.activatesRefreshAllAppsIntent() .activatesRefreshAllAppsIntent()
} }
@@ -174,9 +179,9 @@ private struct ActiveAppsWidgetView: View
Image(systemName: "arrow.up") Image(systemName: "arrow.up")
.resizable() .resizable()
.frame(width: buttonWidth, height: buttonWidth) .frame(width: buttonWidth, height: buttonWidth)
.mask(Capsule())
.opacity(0.3) .opacity(0.3)
.pageUpButton(widgetID: viewModel.widgetID) // .mask(Capsule())
.pageUpButton(widgetID)
Spacer() Spacer()
@@ -184,8 +189,8 @@ private struct ActiveAppsWidgetView: View
.resizable() .resizable()
.frame(width: buttonWidth, height: buttonWidth) .frame(width: buttonWidth, height: buttonWidth)
.opacity(0.3) .opacity(0.3)
.mask(Capsule()) // .mask(Capsule())
.pageDownButton(widgetID: viewModel.widgetID) .pageDownButton(widgetID)
} }
.padding(.vertical) .padding(.vertical)
} }

View File

@@ -0,0 +1,72 @@
//
// PaginationViewModel.swift
// AltStore
//
// Created by Magesh K on 09/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import Foundation
class PaginationDataHolder: ObservableObject {
public static let MAX_ROWS_PER_PAGE: UInt = 3
private static var instances: [String:PaginationDataHolder] = [:]
public static func instance(_ widgetID: String) -> PaginationDataHolder {
let instance = PaginationDataHolder(widgetID)
Self.instances[widgetID] = instance
return instance
}
public static func holder(for widgetID: String) -> PaginationDataHolder? {
return Self.instances[widgetID]
}
private lazy var serializationQ = {
DispatchQueue(label: widgetID)
}()
public let widgetID: String
private var currentPageindex: UInt = 0
private init(_ widgetID: String){
self.widgetID = widgetID
}
deinit {
Self.instances.removeValue(forKey: widgetID)
}
public func updateDirection(_ direction: Direction) {
switch(direction){
case .up:
let pageIndex = Int(currentPageindex)
currentPageindex = UInt(max(0, pageIndex-1))
case .down:
// upper-bounds is checked when computing targetPageIndex in getUpdatedData
currentPageindex+=1
}
}
public func getUpdatedData(entries: [AppSnapshot]) -> [AppSnapshot] {
let count = UInt(entries.count)
if(count == 0) { return entries }
let availablePages = UInt(ceil(Double(entries.count) / Double(Self.MAX_ROWS_PER_PAGE)))
let targetPageIndex: UInt = currentPageindex < availablePages ? currentPageindex : availablePages-1
// do blocking update
serializationQ.sync {
self.currentPageindex = targetPageIndex // preserve it
}
let startIndex = targetPageIndex * Self.MAX_ROWS_PER_PAGE
let estimatedEndIndex = startIndex + (Self.MAX_ROWS_PER_PAGE-1)
let endIndex: UInt = min(count-1, estimatedEndIndex)
let currentPageEntries = entries[Int(startIndex) ... Int(endIndex)]
return Array(currentPageEntries)
}
}

View File

@@ -1,105 +0,0 @@
//
// PaginationViewModel.swift
// AltStore
//
// Created by Magesh K on 09/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import Foundation
import Combine
class PaginationViewModel: ObservableObject {
private static var instances: [String:PaginationViewModel] = [:]
public static let MAX_ROWS_PER_PAGE = 3
@Published public var sliding_window: [AppSnapshot] = []
@Published public var refreshed: Bool = false
private init(){}
private var _widgetID: String?
public lazy var widgetID: String = {
return _widgetID!
}()
public static func getNewInstance(_ widgetID: String) -> PaginationViewModel {
let instance = PaginationViewModel()
PaginationViewModel.instances[widgetID] = instance
instance._widgetID = widgetID
return instance
}
public static func instance(_ widgetID: String) -> PaginationViewModel? {
return PaginationViewModel.instances[widgetID]
}
public private(set) var backup_entries: [AppSnapshot] = []
private var r_queue: [AppSnapshot] = []
private var l_queue: [AppSnapshot] = []
private var lastIndex: Int { r_queue.count - 1 }
public func setEntries(_ entries: [AppSnapshot]) {
r_queue = entries
backup_entries = entries
}
public func handlePagination(_ direction: Direction) {
var sliding_window = Array(sliding_window)
var l_queue = Array(l_queue)
var r_queue = Array(r_queue)
// If entries is empty, do nothing
guard !backup_entries.isEmpty else {
sliding_window.removeAll()
return
}
switch direction {
case .up:
// move window contents to left-q since we are moving right side
if !sliding_window.isEmpty {
// take the window as-is and put it to right of l_queue
l_queue.append(contentsOf: sliding_window)
}
// clear the window
sliding_window.removeAll()
let size = min(r_queue.count, Self.MAX_ROWS_PER_PAGE)
for _ in 0..<size {
if !r_queue.isEmpty {
sliding_window.append(r_queue.removeFirst())
}
}
case .down:
// move window contents to right-q since we are moving left side
if !sliding_window.isEmpty {
// take the window as-is and put it to left of r_queue
r_queue.insert(contentsOf: sliding_window, at: 0)
}
// clear the window
sliding_window.removeAll()
let size = min(l_queue.count, Self.MAX_ROWS_PER_PAGE)
for _ in 0..<size {
sliding_window.insert(l_queue.removeLast(), at: 0)
}
}
if !sliding_window.isEmpty {
// commit
self.sliding_window = sliding_window
self.l_queue = l_queue
self.r_queue = r_queue
self.refreshed = true
}
}
}