mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-31 04:44:14 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  FLEXDBQueryRowCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/24. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @class FLEXDBQueryRowCell; | ||||
|  | ||||
| extern NSString * const kFLEXDBQueryRowCellReuse; | ||||
|  | ||||
| @protocol FLEXDBQueryRowCellLayoutSource <NSObject> | ||||
|  | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column; | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface FLEXDBQueryRowCell : UITableViewCell | ||||
|  | ||||
| /// An array of NSString, NSNumber, or NSData objects | ||||
| @property (nonatomic) NSArray *data; | ||||
| @property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,75 @@ | ||||
| // | ||||
| //  FLEXDBQueryRowCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/24. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXDBQueryRowCell.h" | ||||
| #import "FLEXMultiColumnTableView.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "UIFont+FLEX.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse"; | ||||
|  | ||||
| @interface FLEXDBQueryRowCell () | ||||
| @property (nonatomic) NSInteger columnCount; | ||||
| @property (nonatomic) NSArray<UILabel *> *labels; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXDBQueryRowCell | ||||
|  | ||||
| - (void)setData:(NSArray *)data { | ||||
|     _data = data; | ||||
|     self.columnCount = data.count; | ||||
|      | ||||
|     [self.labels flex_forEach:^(UILabel *label, NSUInteger idx) { | ||||
|         id content = self.data[idx]; | ||||
|          | ||||
|         if ([content isKindOfClass:[NSString class]]) { | ||||
|             label.text = content; | ||||
|         } else if (content == NSNull.null) { | ||||
|             label.text = @"<null>"; | ||||
|             label.textColor = FLEXColor.deemphasizedTextColor; | ||||
|         } else { | ||||
|             label.text = [content description]; | ||||
|         } | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (void)setColumnCount:(NSInteger)columnCount { | ||||
|     if (columnCount != _columnCount) { | ||||
|         _columnCount = columnCount; | ||||
|          | ||||
|         // Remove existing labels | ||||
|         for (UILabel *l in self.labels) { | ||||
|             [l removeFromSuperview]; | ||||
|         } | ||||
|          | ||||
|         // Create new labels | ||||
|         self.labels = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) { | ||||
|             UILabel *label = [UILabel new]; | ||||
|             label.font = UIFont.flex_defaultTableCellFont; | ||||
|             label.textAlignment = NSTextAlignmentLeft; | ||||
|             [self.contentView addSubview:label]; | ||||
|              | ||||
|             return label; | ||||
|         }]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat height = self.contentView.frame.size.height; | ||||
|      | ||||
|     [self.labels flex_forEach:^(UILabel *label, NSUInteger i) { | ||||
|         CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i]; | ||||
|         CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i]; | ||||
|         label.frame = CGRectMake(minX + 5, 0, (width - 10), height); | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,35 @@ | ||||
| // | ||||
| //  PTDatabaseManager.h | ||||
| //  Derived from: | ||||
| // | ||||
| //  FMDatabase.h | ||||
| //  FMDB( https://github.com/ccgus/fmdb ) | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| // | ||||
| //  Licensed to Flying Meat Inc. under one or more contributor license agreements. | ||||
| //  See the LICENSE file distributed with this work for the terms under | ||||
| //  which Flying Meat Inc. licenses this file to you. | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXSQLResult.h" | ||||
|  | ||||
| /// Conformers should automatically open and close the database | ||||
| @protocol FLEXDatabaseManager <NSObject> | ||||
|  | ||||
| @required | ||||
|  | ||||
| /// @return \c nil if the database couldn't be opened | ||||
| + (instancetype)managerForDatabase:(NSString *)path; | ||||
|  | ||||
| /// @return a list of all table names | ||||
| - (NSArray<NSString *> *)queryAllTables; | ||||
| - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName; | ||||
| - (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName; | ||||
|  | ||||
| @optional | ||||
|  | ||||
| - (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName; | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,47 @@ | ||||
| // | ||||
| //  PTMultiColumnTableView.h | ||||
| //  PTMultiColumnTableViewDemo | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/16. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "FLEXTableColumnHeader.h" | ||||
|  | ||||
| @class FLEXMultiColumnTableView; | ||||
|  | ||||
| @protocol FLEXMultiColumnTableViewDelegate <NSObject> | ||||
|  | ||||
| @required | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row; | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectHeaderForColumn:(NSInteger)column sortType:(FLEXTableColumnHeaderSortType)sortType; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @protocol FLEXMultiColumnTableViewDataSource <NSObject> | ||||
|  | ||||
| @required | ||||
|  | ||||
| - (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
| - (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
| - (NSString *)columnTitle:(NSInteger)column; | ||||
| - (NSString *)rowTitle:(NSInteger)row; | ||||
| - (NSArray<NSString *> *)contentForRow:(NSInteger)row; | ||||
|  | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column; | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row; | ||||
| - (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
| - (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @interface FLEXMultiColumnTableView : UIView | ||||
|  | ||||
| @property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource> dataSource; | ||||
| @property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate> delegate; | ||||
|  | ||||
| - (void)reloadData; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,339 @@ | ||||
| // | ||||
| //  PTMultiColumnTableView.m | ||||
| //  PTMultiColumnTableViewDemo | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/16. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMultiColumnTableView.h" | ||||
| #import "FLEXDBQueryRowCell.h" | ||||
| #import "FLEXTableLeftCell.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @interface FLEXMultiColumnTableView () < | ||||
|     UITableViewDataSource, UITableViewDelegate, | ||||
|     UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource | ||||
| > | ||||
|  | ||||
| @property (nonatomic) UIScrollView *contentScrollView; | ||||
| @property (nonatomic) UIScrollView *headerScrollView; | ||||
| @property (nonatomic) UITableView  *leftTableView; | ||||
| @property (nonatomic) UITableView  *contentTableView; | ||||
| @property (nonatomic) UIView       *leftHeader; | ||||
|  | ||||
| @property (nonatomic) NSArray<UIView *> *headerViews; | ||||
|  | ||||
| /// \c NSNotFound if no column selected | ||||
| @property (nonatomic) NSInteger sortColumn; | ||||
| @property (nonatomic) FLEXTableColumnHeaderSortType sortType; | ||||
|  | ||||
| @property (nonatomic, readonly) NSInteger numberOfColumns; | ||||
| @property (nonatomic, readonly) NSInteger numberOfRows; | ||||
| @property (nonatomic, readonly) CGFloat topHeaderHeight; | ||||
| @property (nonatomic, readonly) CGFloat leftHeaderWidth; | ||||
| @property (nonatomic, readonly) CGFloat columnMargin; | ||||
|  | ||||
| @end | ||||
|  | ||||
| static const CGFloat kColumnMargin = 1; | ||||
|  | ||||
| @implementation FLEXMultiColumnTableView | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (instancetype)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.autoresizingMask |= UIViewAutoresizingFlexibleWidth; | ||||
|         self.autoresizingMask |= UIViewAutoresizingFlexibleHeight; | ||||
|         self.autoresizingMask |= UIViewAutoresizingFlexibleTopMargin; | ||||
|         self.backgroundColor  = FLEXColor.groupedBackgroundColor; | ||||
|          | ||||
|         [self loadHeaderScrollView]; | ||||
|         [self loadContentScrollView]; | ||||
|         [self loadLeftView]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat width  = self.frame.size.width; | ||||
|     CGFloat height = self.frame.size.height; | ||||
|     CGFloat topheaderHeight = self.topHeaderHeight; | ||||
|     CGFloat leftHeaderWidth = self.leftHeaderWidth; | ||||
|     CGFloat topInsets = 0.f; | ||||
|  | ||||
|     if (@available (iOS 11.0, *)) { | ||||
|         topInsets = self.safeAreaInsets.top; | ||||
|     } | ||||
|      | ||||
|     CGFloat contentWidth = 0.0; | ||||
|     NSInteger columnsCount = self.numberOfColumns; | ||||
|     for (int i = 0; i < columnsCount; i++) { | ||||
|         contentWidth += CGRectGetWidth(self.headerViews[i].bounds); | ||||
|     } | ||||
|      | ||||
|     CGFloat contentHeight = height - topheaderHeight - topInsets; | ||||
|      | ||||
|     self.leftHeader.frame = CGRectMake(0, topInsets, self.leftHeaderWidth, self.topHeaderHeight); | ||||
|     self.leftTableView.frame = CGRectMake( | ||||
|         0, topheaderHeight + topInsets, leftHeaderWidth, contentHeight | ||||
|     ); | ||||
|     self.headerScrollView.frame = CGRectMake( | ||||
|         leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight | ||||
|     ); | ||||
|     self.headerScrollView.contentSize = CGSizeMake( | ||||
|         self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height | ||||
|     ); | ||||
|     self.contentTableView.frame = CGRectMake( | ||||
|         0, 0, contentWidth + self.numberOfColumns * self.columnMargin , contentHeight | ||||
|     ); | ||||
|     self.contentScrollView.frame = CGRectMake( | ||||
|         leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, contentHeight | ||||
|     ); | ||||
|     self.contentScrollView.contentSize = self.contentTableView.frame.size; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - UI | ||||
|  | ||||
| - (void)loadHeaderScrollView { | ||||
|     UIScrollView *headerScrollView   = [UIScrollView new]; | ||||
|     headerScrollView.delegate        = self; | ||||
|     headerScrollView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor; | ||||
|     self.headerScrollView            = headerScrollView; | ||||
|      | ||||
|     [self addSubview:headerScrollView]; | ||||
| } | ||||
|  | ||||
| - (void)loadContentScrollView { | ||||
|     UIScrollView *scrollView = [UIScrollView new]; | ||||
|     scrollView.bounces       = NO; | ||||
|     scrollView.delegate      = self; | ||||
|      | ||||
|     UITableView *tableView   = [UITableView new]; | ||||
|     tableView.delegate       = self; | ||||
|     tableView.dataSource     = self; | ||||
|     tableView.separatorStyle = UITableViewCellSeparatorStyleNone; | ||||
|     [tableView registerClass:[FLEXDBQueryRowCell class] | ||||
|         forCellReuseIdentifier:kFLEXDBQueryRowCellReuse | ||||
|     ]; | ||||
|      | ||||
|     [scrollView addSubview:tableView]; | ||||
|     [self addSubview:scrollView]; | ||||
|      | ||||
|     self.contentScrollView = scrollView; | ||||
|     self.contentTableView  = tableView; | ||||
| } | ||||
|  | ||||
| - (void)loadLeftView { | ||||
|     UITableView *leftTableView   = [UITableView new]; | ||||
|     leftTableView.delegate       = self; | ||||
|     leftTableView.dataSource     = self; | ||||
|     leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone; | ||||
|     self.leftTableView           = leftTableView; | ||||
|     [self addSubview:leftTableView]; | ||||
|      | ||||
|     UIView *leftHeader         = [UIView new]; | ||||
|     leftHeader.backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|     self.leftHeader            = leftHeader; | ||||
|     [self addSubview:leftHeader]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Data | ||||
|  | ||||
| - (void)reloadData { | ||||
|     [self loadHeaderData]; | ||||
|     [self loadLeftViewData]; | ||||
|     [self loadContentData]; | ||||
| } | ||||
|  | ||||
| - (void)loadHeaderData { | ||||
|     // Remove existing headers, if any | ||||
|     for (UIView *subview in self.headerViews) { | ||||
|         [subview removeFromSuperview]; | ||||
|     } | ||||
|      | ||||
|     __block CGFloat xOffset = 0; | ||||
|      | ||||
|     self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) { | ||||
|         FLEXTableColumnHeader *header = [FLEXTableColumnHeader new]; | ||||
|         header.titleLabel.text = [self columnTitle:column]; | ||||
|          | ||||
|         CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1); | ||||
|         CGFloat width = self.columnMargin + MAX( | ||||
|             [self minContentWidthForColumn:column], | ||||
|             [header sizeThatFits:fittingSize].width | ||||
|         ); | ||||
|         header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1); | ||||
|  | ||||
|         if (column == self.sortColumn) { | ||||
|             header.sortType = self.sortType; | ||||
|         } | ||||
|          | ||||
|         // Header tap gesture | ||||
|         UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] | ||||
|             initWithTarget:self action:@selector(contentHeaderTap:) | ||||
|         ]; | ||||
|         [header addGestureRecognizer:gesture]; | ||||
|         header.userInteractionEnabled = YES; | ||||
|          | ||||
|         xOffset += width; | ||||
|         [self.headerScrollView addSubview:header]; | ||||
|         return header; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (void)contentHeaderTap:(UIGestureRecognizer *)gesture { | ||||
|     NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view]; | ||||
|     FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType); | ||||
|      | ||||
|     // Reset old header | ||||
|     FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn]; | ||||
|     oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone; | ||||
|      | ||||
|     // Update new header | ||||
|     FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn]; | ||||
|     newHeader.sortType = newType; | ||||
|      | ||||
|     // Update self | ||||
|     self.sortColumn = newSortColumn; | ||||
|     self.sortType = newType; | ||||
|  | ||||
|     // Notify delegate | ||||
|     [self.delegate multiColumnTableView:self didSelectHeaderForColumn:newSortColumn sortType:newType]; | ||||
| } | ||||
|  | ||||
| - (void)loadContentData { | ||||
|     [self.contentTableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)loadLeftViewData { | ||||
|     [self.leftTableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     // Alternating background color | ||||
|     UIColor *backgroundColor = FLEXColor.primaryBackgroundColor; | ||||
|     if (indexPath.row % 2 != 0) { | ||||
|         backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|     } | ||||
|      | ||||
|     // Left side table view for row numbers | ||||
|     if (tableView == self.leftTableView) { | ||||
|         FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView]; | ||||
|         cell.contentView.backgroundColor = backgroundColor; | ||||
|         cell.titlelabel.text = [self rowTitle:indexPath.row]; | ||||
|         return cell; | ||||
|     } | ||||
|     // Right side table view for data | ||||
|     else { | ||||
|         FLEXDBQueryRowCell *cell = [tableView | ||||
|             dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath | ||||
|         ]; | ||||
|          | ||||
|         cell.contentView.backgroundColor = backgroundColor; | ||||
|         cell.data = [self.dataSource contentForRow:indexPath.row]; | ||||
|         cell.layoutSource = self; | ||||
|         NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect"); | ||||
|         return cell; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return [self.dataSource numberOfRowsInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row]; | ||||
| } | ||||
|  | ||||
| // Scroll all scroll views in sync | ||||
| - (void)scrollViewDidScroll:(UIScrollView *)scrollView { | ||||
|     if (scrollView == self.contentScrollView) { | ||||
|         self.headerScrollView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
|     else if (scrollView == self.headerScrollView) { | ||||
|         self.contentScrollView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
|     else if (scrollView == self.leftTableView) { | ||||
|         self.contentTableView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
|     else if (scrollView == self.contentTableView) { | ||||
|         self.leftTableView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark UITableView Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (tableView == self.leftTableView) { | ||||
|         [self.contentTableView | ||||
|             selectRowAtIndexPath:indexPath | ||||
|             animated:NO | ||||
|             scrollPosition:UITableViewScrollPositionNone | ||||
|         ]; | ||||
|     } | ||||
|     else if (tableView == self.contentTableView) { | ||||
|         [self.delegate multiColumnTableView:self didSelectRow:indexPath.row]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark FLEXDBQueryRowCellLayoutSource | ||||
|  | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column { | ||||
|     return CGRectGetMinX(self.headerViews[column].frame); | ||||
| } | ||||
|  | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column { | ||||
|     return CGRectGetWidth(self.headerViews[column].bounds); | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark DataSource Accessor | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     return [self.dataSource numberOfRowsInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfColumns { | ||||
|     return [self.dataSource numberOfColumnsInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (NSString *)columnTitle:(NSInteger)column { | ||||
|     return [self.dataSource columnTitle:column]; | ||||
| } | ||||
|  | ||||
| - (NSString *)rowTitle:(NSInteger)row { | ||||
|     return [self.dataSource rowTitle:row]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)minContentWidthForColumn:(NSInteger)column { | ||||
|     return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)contentHeightForRow:(NSInteger)row { | ||||
|     return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)topHeaderHeight { | ||||
|     return [self.dataSource heightForTopHeaderInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)leftHeaderWidth { | ||||
|     return [self.dataSource widthForLeftHeaderInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)columnMargin { | ||||
|     return kColumnMargin; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXRealmDatabaseManager.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tim Oliver on 28/01/2016. | ||||
| //  Copyright © 2016 Realm. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXDatabaseManager.h" | ||||
|  | ||||
| @interface FLEXRealmDatabaseManager : NSObject <FLEXDatabaseManager> | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,102 @@ | ||||
| // | ||||
| //  FLEXRealmDatabaseManager.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tim Oliver on 28/01/2016. | ||||
| //  Copyright © 2016 Realm. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRealmDatabaseManager.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXSQLResult.h" | ||||
|  | ||||
| #if __has_include(<Realm/Realm.h>) | ||||
| #import <Realm/Realm.h> | ||||
| #import <Realm/RLMRealm_Dynamic.h> | ||||
| #else | ||||
| #import "FLEXRealmDefines.h" | ||||
| #endif | ||||
|  | ||||
| @interface FLEXRealmDatabaseManager () | ||||
|  | ||||
| @property (nonatomic, copy) NSString *path; | ||||
| @property (nonatomic) RLMRealm *realm; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXRealmDatabaseManager | ||||
| static Class RLMRealmClass = nil; | ||||
|  | ||||
| + (void)load { | ||||
|     RLMRealmClass = NSClassFromString(@"RLMRealm"); | ||||
| } | ||||
|  | ||||
| + (instancetype)managerForDatabase:(NSString *)path { | ||||
|     return [[self alloc] initWithPath:path]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithPath:(NSString *)path { | ||||
|     if (!RLMRealmClass) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _path = path; | ||||
|          | ||||
|         if (![self open]) { | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (BOOL)open { | ||||
|     Class configurationClass = NSClassFromString(@"RLMRealmConfiguration"); | ||||
|     if (!RLMRealmClass || !configurationClass) { | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     NSError *error = nil; | ||||
|     id configuration = [configurationClass new]; | ||||
|     [(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]]; | ||||
|     self.realm = [RLMRealmClass realmWithConfiguration:configuration error:&error]; | ||||
|      | ||||
|     return (error == nil); | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllTables { | ||||
|     // Map each schema to its name | ||||
|     NSArray<NSString *> *tableNames = [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) { | ||||
|         return schema.className ?: nil; | ||||
|     }]; | ||||
|  | ||||
|     return [tableNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName { | ||||
|     RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName]; | ||||
|     // Map each column to its name | ||||
|     return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) { | ||||
|         return property.name; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName { | ||||
|     RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName]; | ||||
|     RLMResults *results = [self.realm allObjects:tableName]; | ||||
|     if (results.count == 0 || !objectSchema) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     // Map results to an array of rows | ||||
|     return [NSArray flex_mapped:results block:^id(RLMObject *result, NSUInteger idx) { | ||||
|         // Map each row to an array of the values of its properties  | ||||
|         return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) { | ||||
|             return [result valueForKey:property.name] ?: NSNull.null; | ||||
|         }]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,46 @@ | ||||
| // | ||||
| //  Realm.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tim Oliver on 16/02/2016. | ||||
| //  Copyright © 2016 Realm. All rights reserved. | ||||
| // | ||||
|  | ||||
| #if __has_include(<Realm/Realm.h>) | ||||
| #else | ||||
|  | ||||
| @class RLMObject, RLMResults, RLMRealm, RLMRealmConfiguration, RLMSchema, RLMObjectSchema, RLMProperty; | ||||
|  | ||||
| @interface RLMRealmConfiguration : NSObject | ||||
| @property (nonatomic, copy) NSURL *fileURL; | ||||
| @end | ||||
|  | ||||
| @interface RLMRealm : NSObject | ||||
| @property (nonatomic, readonly) RLMSchema *schema; | ||||
| + (RLMRealm *)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error; | ||||
| - (RLMResults *)allObjects:(NSString *)className; | ||||
| @end | ||||
|  | ||||
| @interface RLMSchema : NSObject | ||||
| @property (nonatomic, readonly) NSArray<RLMObjectSchema *> *objectSchema; | ||||
| - (RLMObjectSchema *)schemaForClassName:(NSString *)className; | ||||
| @end | ||||
|  | ||||
| @interface RLMObjectSchema : NSObject | ||||
| @property (nonatomic, readonly) NSString *className; | ||||
| @property (nonatomic, readonly) NSArray<RLMProperty *> *properties; | ||||
| @end | ||||
|  | ||||
| @interface RLMProperty : NSString | ||||
| @property (nonatomic, readonly) NSString *name; | ||||
| @end | ||||
|  | ||||
| @interface RLMResults : NSObject <NSFastEnumeration> | ||||
| @property (nonatomic, readonly) NSInteger count; | ||||
| @end | ||||
|  | ||||
| @interface RLMObject : NSObject | ||||
|  | ||||
| @end | ||||
|  | ||||
| #endif | ||||
| @@ -0,0 +1,48 @@ | ||||
| // | ||||
| //  FLEXSQLResult.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/3/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXSQLResult : NSObject | ||||
|  | ||||
| /// Describes the result of a non-select query, or an error of any kind of query | ||||
| + (instancetype)message:(NSString *)message; | ||||
| /// Describes the result of a known failed execution | ||||
| + (instancetype)error:(NSString *)message; | ||||
|  | ||||
| /// @param rowData A list of rows, where each element in the row | ||||
| /// corresponds to the column given in /c columnNames | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                 rows:(NSArray<NSArray<NSString *> *> *)rowData; | ||||
|  | ||||
| @property (nonatomic, readonly, nullable) NSString *message; | ||||
|  | ||||
| /// A value of YES means this is surely an error, | ||||
| /// but it still might be an error even with a value of NO | ||||
| @property (nonatomic, readonly) BOOL isError; | ||||
|  | ||||
| /// A list of column names | ||||
| @property (nonatomic, readonly, nullable) NSArray<NSString *> *columns; | ||||
| /// A list of rows, where each element in the row corresponds | ||||
| /// to the value of the column at the same index in \c columns. | ||||
| /// | ||||
| /// That is, given a row, looping over the contents of the row and | ||||
| /// the contents of \c columns will give you key-value pairs of | ||||
| /// column names to column values for that row. | ||||
| @property (nonatomic, readonly, nullable) NSArray<NSArray<NSString *> *> *rows; | ||||
| /// A list of rows where the fields are paired to column names. | ||||
| /// | ||||
| /// This property is lazily constructed by looping over | ||||
| /// the rows and columns present in the other two properties. | ||||
| @property (nonatomic, readonly, nullable) NSArray<NSDictionary<NSString *, id> *> *keyedRows; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  FLEXSQLResult.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/3/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSQLResult.h" | ||||
| #import "NSArray+FLEX.h" | ||||
|  | ||||
| @implementation FLEXSQLResult | ||||
| @synthesize keyedRows = _keyedRows; | ||||
|  | ||||
| + (instancetype)message:(NSString *)message { | ||||
|     return [[self alloc] initWithMessage:message columns:nil rows:nil]; | ||||
| } | ||||
|  | ||||
| + (instancetype)error:(NSString *)message { | ||||
|     FLEXSQLResult *result = [self message:message]; | ||||
|     result->_isError = YES; | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData { | ||||
|     return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows { | ||||
|     NSParameterAssert(message || (columns && rows)); | ||||
|     NSParameterAssert(rows.count == 0 || columns.count == rows.firstObject.count); | ||||
|      | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _message = message; | ||||
|         _columns = columns; | ||||
|         _rows = rows; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSDictionary<NSString *,id> *> *)keyedRows { | ||||
|     if (!_keyedRows) { | ||||
|         _keyedRows = [self.rows flex_mapped:^id(NSArray<NSString *> *row, NSUInteger idx) { | ||||
|             return [NSDictionary dictionaryWithObjects:row forKeys:self.columns]; | ||||
|         }]; | ||||
|     } | ||||
|      | ||||
|     return _keyedRows; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| //  PTDatabaseManager.h | ||||
| //  Derived from: | ||||
| // | ||||
| //  FMDatabase.h | ||||
| //  FMDB( https://github.com/ccgus/fmdb ) | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| // | ||||
| //  Licensed to Flying Meat Inc. under one or more contributor license agreements. | ||||
| //  See the LICENSE file distributed with this work for the terms under | ||||
| //  which Flying Meat Inc. licenses this file to you. | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXDatabaseManager.h" | ||||
| #import "FLEXSQLResult.h" | ||||
|  | ||||
| @interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager> | ||||
|  | ||||
| /// Contains the result of the last operation, which may be an error | ||||
| @property (nonatomic, readonly) FLEXSQLResult *lastResult; | ||||
| /// Calls into \c sqlite3_last_insert_rowid() | ||||
| @property (nonatomic, readonly) NSInteger lastRowID; | ||||
|  | ||||
| /// Given a statement like 'SELECT * from @table where @col = @val' and arguments | ||||
| /// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will | ||||
| /// invoke the statement and properly bind the given arguments to the statement. | ||||
| /// | ||||
| /// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values. | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,329 @@ | ||||
| // | ||||
| //  PTDatabaseManager.m | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSQLiteDatabaseManager.h" | ||||
| #import "FLEXManager.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXRuntimeConstants.h" | ||||
| #import <sqlite3.h> | ||||
|  | ||||
| #define kQuery(name, str) static NSString * const QUERY_##name = str | ||||
|  | ||||
| kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"); | ||||
| kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC"); | ||||
|  | ||||
| @interface FLEXSQLiteDatabaseManager () | ||||
| @property (nonatomic) sqlite3 *db; | ||||
| @property (nonatomic, copy) NSString *path; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXSQLiteDatabaseManager | ||||
|  | ||||
| #pragma mark - FLEXDatabaseManager | ||||
|  | ||||
| + (instancetype)managerForDatabase:(NSString *)path { | ||||
|     return [[self alloc] initWithPath:path]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithPath:(NSString *)path { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         self.path = path; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)dealloc { | ||||
|     [self close]; | ||||
| } | ||||
|  | ||||
| - (BOOL)open { | ||||
|     if (self.db) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     int err = sqlite3_open(self.path.UTF8String, &_db); | ||||
|  | ||||
| #if SQLITE_HAS_CODEC | ||||
|     NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword; | ||||
|     if (defaultSqliteDatabasePassword) { | ||||
|         const char *key = defaultSqliteDatabasePassword.UTF8String; | ||||
|         sqlite3_key(_db, key, (int)strlen(key)); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     if (err != SQLITE_OK) { | ||||
|         return [self storeErrorForLastTask:@"Open"]; | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|      | ||||
| - (BOOL)close { | ||||
|     if (!self.db) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     int  rc; | ||||
|     BOOL retry, triedFinalizingOpenStatements = NO; | ||||
|      | ||||
|     do { | ||||
|         retry = NO; | ||||
|         rc    = sqlite3_close(_db); | ||||
|         if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { | ||||
|             if (!triedFinalizingOpenStatements) { | ||||
|                 triedFinalizingOpenStatements = YES; | ||||
|                 sqlite3_stmt *pStmt; | ||||
|                 while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) { | ||||
|                     NSLog(@"Closing leaked statement"); | ||||
|                     sqlite3_finalize(pStmt); | ||||
|                     retry = YES; | ||||
|                 } | ||||
|             } | ||||
|         } else if (SQLITE_OK != rc) { | ||||
|             [self storeErrorForLastTask:@"Close"]; | ||||
|             self.db = nil; | ||||
|             return NO; | ||||
|         } | ||||
|     } while (retry); | ||||
|      | ||||
|     self.db = nil; | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (NSInteger)lastRowID { | ||||
|     return (NSInteger)sqlite3_last_insert_rowid(self.db); | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllTables { | ||||
|     return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) { | ||||
|         return table.firstObject; | ||||
|     }] ?: @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName { | ||||
|     NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName]; | ||||
|     FLEXSQLResult *results = [self executeStatement:sql]; | ||||
|      | ||||
|     // https://github.com/FLEXTool/FLEX/issues/554 | ||||
|     if (!results.keyedRows.count) { | ||||
|         sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName]; | ||||
|         results = [self executeStatement:sql]; | ||||
|          | ||||
|         // Fallback to empty query | ||||
|         if (!results.keyedRows.count) { | ||||
|             sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName]; | ||||
|             return [self executeStatement:sql].columns ?: @[]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) { | ||||
|         return column[@"name"]; | ||||
|     }] ?: @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName { | ||||
|     NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName]; | ||||
|     return [self executeStatement:command].rows ?: @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName { | ||||
|     NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName]; | ||||
|     NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[]; | ||||
|      | ||||
|     return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) { | ||||
|         return obj.firstObject; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)sql { | ||||
|     return [self executeStatement:sql arguments:nil]; | ||||
| } | ||||
|  | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args { | ||||
|     [self open]; | ||||
|      | ||||
|     FLEXSQLResult *result = nil; | ||||
|      | ||||
|     sqlite3_stmt *pstmt; | ||||
|     int status; | ||||
|     if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) { | ||||
|         NSMutableArray<NSArray *> *rows = [NSMutableArray new]; | ||||
|          | ||||
|         // Bind parameters, if any | ||||
|         if (![self bindParameters:args toStatement:pstmt]) { | ||||
|             return self.lastResult; | ||||
|         } | ||||
|          | ||||
|         // Grab columns (columnCount will be 0 for insert/update/delete)  | ||||
|         int columnCount = sqlite3_column_count(pstmt); | ||||
|         NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) { | ||||
|             return @(sqlite3_column_name(pstmt, (int)i)); | ||||
|         }]; | ||||
|          | ||||
|         // Execute statement | ||||
|         while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) { | ||||
|             // Grab rows if this is a selection query | ||||
|             int dataCount = sqlite3_data_count(pstmt); | ||||
|             if (dataCount > 0) { | ||||
|                 [rows addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) { | ||||
|                     return [self objectForColumnIndex:(int)i stmt:pstmt]; | ||||
|                 }]]; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (status == SQLITE_DONE) { | ||||
|             // columnCount will be 0 for insert/update/delete | ||||
|             if (rows.count || columnCount > 0) { | ||||
|                 // We executed a SELECT query | ||||
|                 result = _lastResult = [FLEXSQLResult columns:columns rows:rows]; | ||||
|             } else { | ||||
|                 // We executed a query like INSERT, UDPATE, or DELETE | ||||
|                 int rowsAffected = sqlite3_changes(_db); | ||||
|                 NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected]; | ||||
|                 result = _lastResult = [FLEXSQLResult message:message]; | ||||
|             } | ||||
|         } else { | ||||
|             // An error occured executing the query | ||||
|             result = _lastResult = [self errorResult:@"Execution"]; | ||||
|         } | ||||
|     } else { | ||||
|         // An error occurred creating the prepared statement | ||||
|         result = _lastResult = [self errorResult:@"Prepared statement"]; | ||||
|     } | ||||
|      | ||||
|     sqlite3_finalize(pstmt); | ||||
|     return result; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| /// @return YES on success, NO if an error was encountered and stored in \c lastResult | ||||
| - (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt { | ||||
|     for (NSString *param in args.allKeys) { | ||||
|         int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String); | ||||
|         id value = args[param]; | ||||
|          | ||||
|         if (idx == 0) { | ||||
|             // No parameter matching that arg | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|          | ||||
|         // Null | ||||
|         if ([value isKindOfClass:[NSNull class]]) { | ||||
|             status = sqlite3_bind_null(pstmt, idx); | ||||
|         } | ||||
|         // String params | ||||
|         else if ([value isKindOfClass:[NSString class]]) { | ||||
|             const char *str = [value UTF8String]; | ||||
|             status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT); | ||||
|         } | ||||
|         // Data params | ||||
|         else if ([value isKindOfClass:[NSData class]]) { | ||||
|             const void *blob = [value bytes]; | ||||
|             status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT); | ||||
|         } | ||||
|         // Primitive params | ||||
|         else if ([value isKindOfClass:[NSNumber class]]) { | ||||
|             FLEXTypeEncoding type = [value objCType][0]; | ||||
|             switch (type) { | ||||
|                 case FLEXTypeEncodingCBool: | ||||
|                 case FLEXTypeEncodingChar: | ||||
|                 case FLEXTypeEncodingUnsignedChar: | ||||
|                 case FLEXTypeEncodingShort: | ||||
|                 case FLEXTypeEncodingUnsignedShort: | ||||
|                 case FLEXTypeEncodingInt: | ||||
|                 case FLEXTypeEncodingUnsignedInt: | ||||
|                 case FLEXTypeEncodingLong: | ||||
|                 case FLEXTypeEncodingUnsignedLong: | ||||
|                 case FLEXTypeEncodingLongLong: | ||||
|                 case FLEXTypeEncodingUnsignedLongLong: | ||||
|                     status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]); | ||||
|                     break; | ||||
|                  | ||||
|                 case FLEXTypeEncodingFloat: | ||||
|                 case FLEXTypeEncodingDouble: | ||||
|                     status = sqlite3_bind_double(pstmt, idx, [value doubleValue]); | ||||
|                     break; | ||||
|                      | ||||
|                 default: | ||||
|                     @throw NSInternalInconsistencyException; | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         // Unsupported type | ||||
|         else { | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|          | ||||
|         if (status != SQLITE_OK) { | ||||
|             return [self storeErrorForLastTask: | ||||
|                 [NSString stringWithFormat:@"Binding param named '%@'", param] | ||||
|             ]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)storeErrorForLastTask:(NSString *)action { | ||||
|     _lastResult = [self errorResult:action]; | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (FLEXSQLResult *)errorResult:(NSString *)description { | ||||
|     const char *error = sqlite3_errmsg(_db); | ||||
|     NSString *message = error ? @(error) : [NSString | ||||
|         stringWithFormat:@"(%@: empty error)", description | ||||
|     ]; | ||||
|      | ||||
|     return [FLEXSQLResult error:message]; | ||||
| } | ||||
|  | ||||
| - (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt { | ||||
|     int columnType = sqlite3_column_type(stmt, columnIdx); | ||||
|      | ||||
|     switch (columnType) { | ||||
|         case SQLITE_INTEGER: | ||||
|             return @(sqlite3_column_int64(stmt, columnIdx)).stringValue; | ||||
|         case SQLITE_FLOAT: | ||||
|             return  @(sqlite3_column_double(stmt, columnIdx)).stringValue; | ||||
|         case SQLITE_BLOB: | ||||
|             return [NSString stringWithFormat:@"Data (%@ bytes)", | ||||
|                 @([self dataForColumnIndex:columnIdx stmt:stmt].length) | ||||
|             ]; | ||||
|              | ||||
|         default: | ||||
|             // Default to a string for everything else | ||||
|             return [self stringForColumnIndex:columnIdx stmt:stmt] ?: NSNull.null; | ||||
|     } | ||||
| } | ||||
|                  | ||||
| - (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt { | ||||
|     if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || columnIdx < 0) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     const char *text = (const char *)sqlite3_column_text(stmt, columnIdx); | ||||
|     return text ? @(text) : nil; | ||||
| } | ||||
|  | ||||
| - (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt { | ||||
|     if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     const void *blob = sqlite3_column_blob(stmt, columnIdx); | ||||
|     NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx); | ||||
|      | ||||
|     return blob ? [NSData dataWithBytes:blob length:size] : nil; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,38 @@ | ||||
| // | ||||
| //  FLEXTableContentHeaderCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/26. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) { | ||||
|     FLEXTableColumnHeaderSortTypeNone = 0, | ||||
|     FLEXTableColumnHeaderSortTypeAsc, | ||||
|     FLEXTableColumnHeaderSortTypeDesc, | ||||
| }; | ||||
|  | ||||
| NS_INLINE FLEXTableColumnHeaderSortType FLEXNextTableColumnHeaderSortType( | ||||
|     FLEXTableColumnHeaderSortType current) { | ||||
|     switch (current) { | ||||
|         case FLEXTableColumnHeaderSortTypeAsc: | ||||
|             return FLEXTableColumnHeaderSortTypeDesc; | ||||
|         case FLEXTableColumnHeaderSortTypeNone: | ||||
|         case FLEXTableColumnHeaderSortTypeDesc: | ||||
|             return FLEXTableColumnHeaderSortTypeAsc; | ||||
|     } | ||||
|      | ||||
|     return FLEXTableColumnHeaderSortTypeNone; | ||||
| } | ||||
|  | ||||
| @interface FLEXTableColumnHeader : UIView | ||||
|  | ||||
| @property (nonatomic) NSInteger index; | ||||
| @property (nonatomic, readonly) UILabel *titleLabel; | ||||
|  | ||||
| @property (nonatomic) FLEXTableColumnHeaderSortType sortType; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @@ -0,0 +1,78 @@ | ||||
| // | ||||
| //  FLEXTableContentHeaderCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/26. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableColumnHeader.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "UIFont+FLEX.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| static const CGFloat kMargin = 5; | ||||
| static const CGFloat kArrowWidth = 20; | ||||
|  | ||||
| @interface FLEXTableColumnHeader () | ||||
| @property (nonatomic, readonly) UILabel *arrowLabel; | ||||
| @property (nonatomic, readonly) UIView *lineView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableColumnHeader | ||||
|  | ||||
| - (instancetype)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|          | ||||
|         _titleLabel = [UILabel new]; | ||||
|         _titleLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|         [self addSubview:_titleLabel]; | ||||
|          | ||||
|         _arrowLabel = [UILabel new]; | ||||
|         _arrowLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|         [self addSubview:_arrowLabel]; | ||||
|          | ||||
|         _lineView = [UIView new]; | ||||
|         _lineView.backgroundColor = FLEXColor.hairlineColor; | ||||
|         [self addSubview:_lineView]; | ||||
|          | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setSortType:(FLEXTableColumnHeaderSortType)type { | ||||
|     _sortType = type; | ||||
|      | ||||
|     switch (type) { | ||||
|         case FLEXTableColumnHeaderSortTypeNone: | ||||
|             _arrowLabel.text = @""; | ||||
|             break; | ||||
|         case FLEXTableColumnHeaderSortTypeAsc: | ||||
|             _arrowLabel.text = @"⬆️"; | ||||
|             break; | ||||
|         case FLEXTableColumnHeaderSortTypeDesc: | ||||
|             _arrowLabel.text = @"⬇️"; | ||||
|             break; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGSize size = self.frame.size; | ||||
|      | ||||
|     self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height); | ||||
|     self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height); | ||||
|     self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4); | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat margins = kArrowWidth - 2 * kMargin; | ||||
|     size = CGSizeMake(size.width - margins, size.height); | ||||
|     CGFloat width = [_titleLabel sizeThatFits:size].width + margins; | ||||
|     return CGSizeMake(width, size.height); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,36 @@ | ||||
| // | ||||
| //  PTTableContentViewController.h | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "FLEXDatabaseManager.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXTableContentViewController : UIViewController | ||||
|  | ||||
| /// Display a mutable table with the given columns, rows, and name. | ||||
| /// | ||||
| /// @param columnNames self explanatory. | ||||
| /// @param rowData an array of rows, where each row is an array of column data. | ||||
| /// @param rowIDs an array of stringy row IDs. Required for deleting rows. | ||||
| /// @param tableName an optional name of the table being viewed, if any. Enables adding rows. | ||||
| /// @param databaseManager an optional manager to allow modifying the table. | ||||
| ///        Required for deleting rows. Required for adding rows if \c tableName is supplied. | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData | ||||
|                  rowIDs:(NSArray<NSString *> *)rowIDs | ||||
|               tableName:(NSString *)tableName | ||||
|                database:(id<FLEXDatabaseManager>)databaseManager; | ||||
|  | ||||
| /// Display an immutable table with the given columns and rows. | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,359 @@ | ||||
| // | ||||
| //  PTTableContentViewController.m | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableContentViewController.h" | ||||
| #import "FLEXTableRowDataViewController.h" | ||||
| #import "FLEXMultiColumnTableView.h" | ||||
| #import "FLEXWebViewController.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| @interface FLEXTableContentViewController () < | ||||
|     FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate | ||||
| > | ||||
| @property (nonatomic, readonly) NSArray<NSString *> *columns; | ||||
| @property (nonatomic) NSMutableArray<NSArray *> *rows; | ||||
| @property (nonatomic, readonly) NSString *tableName; | ||||
| @property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs; | ||||
| @property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager; | ||||
|  | ||||
| @property (nonatomic, readonly) BOOL canRefresh; | ||||
|  | ||||
| @property (nonatomic) FLEXMultiColumnTableView *multiColumnView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableContentViewController | ||||
|  | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData | ||||
|                  rowIDs:(NSArray<NSString *> *)rowIDs | ||||
|               tableName:(NSString *)tableName | ||||
|                database:(id<FLEXDatabaseManager>)databaseManager { | ||||
|     return [[self alloc] | ||||
|         initWithColumns:columnNames | ||||
|         rows:rowData | ||||
|         rowIDs:rowIDs | ||||
|         tableName:tableName | ||||
|         database:databaseManager | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| + (instancetype)columns:(NSArray<NSString *> *)cols | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData { | ||||
|     return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames | ||||
|                            rows:(NSArray<NSArray<NSString *> *> *)rowData | ||||
|                          rowIDs:(nullable NSArray<NSString *> *)rowIDs | ||||
|                       tableName:(nullable NSString *)tableName | ||||
|                        database:(nullable id<FLEXDatabaseManager>)databaseManager { | ||||
|     // Must supply all optional parameters as one, or none | ||||
|     BOOL all = rowIDs && tableName && databaseManager; | ||||
|     BOOL none = !rowIDs && !tableName && !databaseManager; | ||||
|     NSParameterAssert(all || none); | ||||
|  | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         self->_columns = columnNames.copy; | ||||
|         self->_rows = rowData.mutableCopy; | ||||
|         self->_rowIDs = rowIDs.mutableCopy; | ||||
|         self->_tableName = tableName.copy; | ||||
|         self->_databaseManager = databaseManager; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)loadView { | ||||
|     [super loadView]; | ||||
|      | ||||
|     [self.view addSubview:self.multiColumnView]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|     self.title = self.tableName; | ||||
|     [self.multiColumnView reloadData]; | ||||
|     [self setupToolbarItems]; | ||||
| } | ||||
|  | ||||
| - (FLEXMultiColumnTableView *)multiColumnView { | ||||
|     if (!_multiColumnView) { | ||||
|         _multiColumnView = [[FLEXMultiColumnTableView alloc] | ||||
|             initWithFrame:FLEXRectSetSize(CGRectZero, self.view.frame.size) | ||||
|         ]; | ||||
|          | ||||
|         _multiColumnView.dataSource = self; | ||||
|         _multiColumnView.delegate   = self; | ||||
|     } | ||||
|      | ||||
|     return _multiColumnView; | ||||
| } | ||||
|  | ||||
| - (BOOL)canRefresh { | ||||
|     return self.databaseManager && self.tableName; | ||||
| } | ||||
|  | ||||
| #pragma mark MultiColumnTableView DataSource | ||||
|  | ||||
| - (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     return self.columns.count; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     return self.rows.count; | ||||
| } | ||||
|  | ||||
| - (NSString *)columnTitle:(NSInteger)column { | ||||
|     return self.columns[column]; | ||||
| } | ||||
|  | ||||
| - (NSString *)rowTitle:(NSInteger)row { | ||||
|     return @(row).stringValue; | ||||
| } | ||||
|  | ||||
| - (NSArray *)contentForRow:(NSInteger)row { | ||||
|     return self.rows[row]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView | ||||
|       heightForContentCellInRow:(NSInteger)row { | ||||
|     return 40; | ||||
| } | ||||
|  | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView | ||||
|     minWidthForContentCellInColumn:(NSInteger)column { | ||||
|     return 100; | ||||
| } | ||||
|  | ||||
| - (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     return 40; | ||||
| } | ||||
|  | ||||
| - (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.rows.count]; | ||||
|     NSDictionary *attrs = @{ NSFontAttributeName : [UIFont systemFontOfSize:17.0] }; | ||||
|     CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14) | ||||
|         options:NSStringDrawingUsesLineFragmentOrigin | ||||
|         attributes:attrs context:nil | ||||
|     ].size; | ||||
|      | ||||
|     return size.width + 20; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark MultiColumnTableView Delegate | ||||
|  | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row { | ||||
|     NSArray<NSString *> *fields = [self.rows[row] flex_mapped:^id(NSString *field, NSUInteger idx) { | ||||
|         return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field]; | ||||
|     }]; | ||||
|      | ||||
|     NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) { | ||||
|         return [NSString stringWithFormat:@"'%@'", value]; | ||||
|     }]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title([@"Row " stringByAppendingString:@(row).stringValue]); | ||||
|         NSString *message = [fields componentsJoinedByString:@"\n\n"]; | ||||
|         make.message(message); | ||||
|         make.button(@"Copy").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = message; | ||||
|         }); | ||||
|         make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "]; | ||||
|         }); | ||||
|         make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIViewController *focusedRow = [FLEXTableRowDataViewController | ||||
|                 rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns] | ||||
|             ]; | ||||
|             [self.navigationController pushViewController:focusedRow animated:YES]; | ||||
|         }); | ||||
|          | ||||
|         // Option to delete row | ||||
|         BOOL hasRowID = self.rows.count && row < self.rows.count; | ||||
|         if (hasRowID && self.canRefresh) { | ||||
|             make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|                 NSString *deleteRow = [NSString stringWithFormat: | ||||
|                     @"DELETE FROM %@ WHERE rowid = %@", | ||||
|                     self.tableName, self.rowIDs[row] | ||||
|                 ]; | ||||
|                  | ||||
|                 [self executeStatementAndShowResult:deleteRow completion:^(BOOL success) { | ||||
|                     // Remove deleted row and reload view | ||||
|                     if (success) { | ||||
|                         [self reloadTableDataFromDB]; | ||||
|                     } | ||||
|                 }]; | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         make.button(@"Dismiss").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView | ||||
|     didSelectHeaderForColumn:(NSInteger)column | ||||
|                     sortType:(FLEXTableColumnHeaderSortType)sortType { | ||||
|      | ||||
|     NSArray<NSArray *> *sortContentData = [self.rows | ||||
|         sortedArrayWithOptions:NSSortStable | ||||
|         usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) { | ||||
|             id a = obj1[column], b = obj2[column]; | ||||
|             if (a == NSNull.null) { | ||||
|                 return NSOrderedAscending; | ||||
|             } | ||||
|             if (b == NSNull.null) { | ||||
|                 return NSOrderedDescending; | ||||
|             } | ||||
|          | ||||
|             if ([a respondsToSelector:@selector(compare:options:)] && | ||||
|                 [b respondsToSelector:@selector(compare:options:)]) { | ||||
|                 return [a compare:b options:NSNumericSearch]; | ||||
|             } | ||||
|              | ||||
|             if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) { | ||||
|                 return [a compare:b]; | ||||
|             } | ||||
|              | ||||
|             return NSOrderedSame; | ||||
|         } | ||||
|     ]; | ||||
|      | ||||
|     if (sortType == FLEXTableColumnHeaderSortTypeDesc) { | ||||
|         sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy; | ||||
|     } | ||||
|      | ||||
|     self.rows = sortContentData.mutableCopy; | ||||
|     [self.multiColumnView reloadData]; | ||||
| } | ||||
|  | ||||
| #pragma mark - About Transition | ||||
|  | ||||
| - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection | ||||
|               withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator { | ||||
|     [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator]; | ||||
|      | ||||
|     [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) { | ||||
|         if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) { | ||||
|             self.multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32); | ||||
|         } | ||||
|         else { | ||||
|             self.multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64); | ||||
|         } | ||||
|          | ||||
|         [self.view setNeedsLayout]; | ||||
|     } completion:nil]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Toolbar | ||||
|  | ||||
| - (void)setupToolbarItems { | ||||
|     // We do not support modifying realm databases | ||||
|     if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) { | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed)); | ||||
|     UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed)); | ||||
|  | ||||
|     // Only allow adding rows or deleting rows if we have a table name | ||||
|     trashButton.enabled = self.canRefresh; | ||||
|     addButton.enabled = self.canRefresh; | ||||
|      | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         addButton, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         [trashButton flex_withTintColor:UIColor.redColor], | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (void)trashPressed { | ||||
|     NSParameterAssert(self.tableName); | ||||
|  | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Delete All Rows"); | ||||
|         make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?"); | ||||
|          | ||||
|         make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName]; | ||||
|             [self executeStatementAndShowResult:deleteAll completion:^(BOOL success) { | ||||
|                 // Only dismiss on success | ||||
|                 if (success) { | ||||
|                     [self.navigationController popViewControllerAnimated:YES]; | ||||
|                 } | ||||
|             }]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)addPressed { | ||||
|     NSParameterAssert(self.tableName); | ||||
|  | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Add a New Row"); | ||||
|         make.message(@"Comma separate values to use in an INSERT statement.\n\n"); | ||||
|         make.message(@"INSERT INTO [table] VALUES (your_input)"); | ||||
|         make.textField(@"5, 'John Smith', 14,..."); | ||||
|         make.button(@"Insert").handler(^(NSArray<NSString *> *strings) { | ||||
|             NSString *statement = [NSString stringWithFormat: | ||||
|                 @"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0] | ||||
|             ]; | ||||
|  | ||||
|             [self executeStatementAndShowResult:statement completion:^(BOOL success) { | ||||
|                 if (success) { | ||||
|                     [self reloadTableDataFromDB]; | ||||
|                 } | ||||
|             }]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Helpers | ||||
|  | ||||
| - (void)executeStatementAndShowResult:(NSString *)statement | ||||
|                            completion:(void (^_Nullable)(BOOL success))completion { | ||||
|     NSParameterAssert(self.databaseManager); | ||||
|  | ||||
|     FLEXSQLResult *result = [self.databaseManager executeStatement:statement]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         if (result.isError) { | ||||
|             make.title(@"Error"); | ||||
|         } | ||||
|          | ||||
|         make.message(result.message ?: @"<no output>"); | ||||
|         make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) { | ||||
|             if (completion) { | ||||
|                 completion(!result.isError); | ||||
|             } | ||||
|         }); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)reloadTableDataFromDB { | ||||
|     if (!self.canRefresh) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName]; | ||||
|     NSArray<NSString *> *rowIDs = nil; | ||||
|     if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) { | ||||
|         rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName]; | ||||
|     } | ||||
|  | ||||
|     self.rows = rows.mutableCopy; | ||||
|     self.rowIDs = rowIDs.mutableCopy; | ||||
|     [self.multiColumnView reloadData]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXTableLeftCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/24. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @interface FLEXTableLeftCell : UITableViewCell | ||||
|  | ||||
| @property (nonatomic) UILabel *titlelabel; | ||||
|  | ||||
| + (instancetype)cellWithTableView:(UITableView *)tableView; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,33 @@ | ||||
| // | ||||
| //  FLEXTableLeftCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/24. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableLeftCell.h" | ||||
|  | ||||
| @implementation FLEXTableLeftCell | ||||
|  | ||||
| + (instancetype)cellWithTableView:(UITableView *)tableView { | ||||
|     static NSString *identifier = @"FLEXTableLeftCell"; | ||||
|     FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; | ||||
|      | ||||
|     if (!cell) { | ||||
|         cell = [[FLEXTableLeftCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; | ||||
|         UILabel *textLabel               = [UILabel new]; | ||||
|         textLabel.textAlignment          = NSTextAlignmentCenter; | ||||
|         textLabel.font                   = [UIFont systemFontOfSize:13.0]; | ||||
|         [cell.contentView addSubview:textLabel]; | ||||
|         cell.titlelabel = textLabel; | ||||
|     } | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|     self.titlelabel.frame = self.contentView.frame; | ||||
| } | ||||
| @end | ||||
| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  PTTableListViewController.h | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
|  | ||||
| @interface FLEXTableListViewController : FLEXFilteringTableViewController | ||||
|  | ||||
| + (BOOL)supportsExtension:(NSString *)extension; | ||||
| - (instancetype)initWithPath:(NSString *)path; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,165 @@ | ||||
| // | ||||
| //  PTTableListViewController.m | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableListViewController.h" | ||||
| #import "FLEXDatabaseManager.h" | ||||
| #import "FLEXSQLiteDatabaseManager.h" | ||||
| #import "FLEXRealmDatabaseManager.h" | ||||
| #import "FLEXTableContentViewController.h" | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXAlert.h" | ||||
| #import "FLEXMacros.h" | ||||
|  | ||||
| @interface FLEXTableListViewController () | ||||
| @property (nonatomic, readonly) id<FLEXDatabaseManager> dbm; | ||||
| @property (nonatomic, readonly) NSString *path; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXMutableListSection<NSString *> *tables; | ||||
|  | ||||
| + (NSArray<NSString *> *)supportedSQLiteExtensions; | ||||
| + (NSArray<NSString *> *)supportedRealmExtensions; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableListViewController | ||||
|  | ||||
| - (instancetype)initWithPath:(NSString *)path { | ||||
|     self = [super initWithStyle:UITableViewStyleGrouped]; | ||||
|     if (self) { | ||||
|         _path = path.copy; | ||||
|         _dbm = [self databaseManagerForFileAtPath:path]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     self.showsSearchBar = YES; | ||||
|      | ||||
|     // Compose query button // | ||||
|  | ||||
|     UIBarButtonItem *composeQuery = [[UIBarButtonItem alloc] | ||||
|         initWithBarButtonSystemItem:UIBarButtonSystemItemCompose | ||||
|         target:self | ||||
|         action:@selector(queryButtonPressed) | ||||
|     ]; | ||||
|     // Cannot run custom queries on realm databases | ||||
|     composeQuery.enabled = [self.dbm | ||||
|         respondsToSelector:@selector(executeStatement:) | ||||
|     ]; | ||||
|      | ||||
|     [self addToolbarItems:@[composeQuery]]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     _tables = [FLEXMutableListSection list:[self.dbm queryAllTables] | ||||
|         cellConfiguration:^(__kindof UITableViewCell *cell, NSString *tableName, NSInteger row) { | ||||
|             cell.textLabel.text = tableName; | ||||
|         } filterMatcher:^BOOL(NSString *filterText, NSString *tableName) { | ||||
|             return [tableName localizedCaseInsensitiveContainsString:filterText]; | ||||
|         } | ||||
|     ]; | ||||
|      | ||||
|     self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) { | ||||
|         NSArray *rows = [host.dbm queryAllDataInTable:tableName]; | ||||
|         NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName]; | ||||
|         NSArray *rowIDs = nil; | ||||
|         if ([host.dbm respondsToSelector:@selector(queryRowIDsInTable:)]) {         | ||||
|             rowIDs = [host.dbm queryRowIDsInTable:tableName]; | ||||
|         } | ||||
|         UIViewController *resultsScreen = [FLEXTableContentViewController | ||||
|             columns:columns rows:rows rowIDs:rowIDs tableName:tableName database:host.dbm | ||||
|         ]; | ||||
|         [host.navigationController pushViewController:resultsScreen animated:YES]; | ||||
|     }; | ||||
|      | ||||
|     return @[self.tables]; | ||||
| } | ||||
|  | ||||
| - (void)reloadData { | ||||
|     self.tables.customTitle = [NSString | ||||
|         stringWithFormat:@"Tables (%@)", @(self.tables.filteredList.count) | ||||
|     ]; | ||||
|      | ||||
|     [super reloadData]; | ||||
| } | ||||
|      | ||||
| - (void)queryButtonPressed { | ||||
|     FLEXSQLiteDatabaseManager *database = self.dbm; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Execute an SQL query"); | ||||
|         make.textField(nil); | ||||
|         make.button(@"Run").handler(^(NSArray<NSString *> *strings) { | ||||
|             FLEXSQLResult *result = [database executeStatement:strings[0]]; | ||||
|              | ||||
|             if (result.message) { | ||||
|                 [FLEXAlert showAlert:@"Message" message:result.message from:self]; | ||||
|             } else { | ||||
|                 UIViewController *resultsScreen = [FLEXTableContentViewController | ||||
|                     columns:result.columns rows:result.rows | ||||
|                 ]; | ||||
|                  | ||||
|                 [self.navigationController pushViewController:resultsScreen animated:YES]; | ||||
|             } | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|      | ||||
| - (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path { | ||||
|     NSString *pathExtension = path.pathExtension.lowercaseString; | ||||
|      | ||||
|     NSArray<NSString *> *sqliteExtensions = FLEXTableListViewController.supportedSQLiteExtensions; | ||||
|     if ([sqliteExtensions indexOfObject:pathExtension] != NSNotFound) { | ||||
|         return [FLEXSQLiteDatabaseManager managerForDatabase:path]; | ||||
|     } | ||||
|      | ||||
|     NSArray<NSString *> *realmExtensions = FLEXTableListViewController.supportedRealmExtensions; | ||||
|     if (realmExtensions != nil && [realmExtensions indexOfObject:pathExtension] != NSNotFound) { | ||||
|         return [FLEXRealmDatabaseManager managerForDatabase:path]; | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXTableListViewController | ||||
|  | ||||
| + (BOOL)supportsExtension:(NSString *)extension { | ||||
|     extension = extension.lowercaseString; | ||||
|      | ||||
|     NSArray<NSString *> *sqliteExtensions = FLEXTableListViewController.supportedSQLiteExtensions; | ||||
|     if (sqliteExtensions.count > 0 && [sqliteExtensions indexOfObject:extension] != NSNotFound) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     NSArray<NSString *> *realmExtensions = FLEXTableListViewController.supportedRealmExtensions; | ||||
|     if (realmExtensions.count > 0 && [realmExtensions indexOfObject:extension] != NSNotFound) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| + (NSArray<NSString *> *)supportedSQLiteExtensions { | ||||
|     return @[@"db", @"sqlite", @"sqlite3"]; | ||||
| } | ||||
|  | ||||
| + (NSArray<NSString *> *)supportedRealmExtensions { | ||||
|     if (NSClassFromString(@"RLMRealm") == nil) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     return @[@"realm"]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXTableRowDataViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Chaoshuai Lu on 7/8/20. | ||||
| // | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
|  | ||||
| @interface FLEXTableRowDataViewController : FLEXFilteringTableViewController | ||||
|  | ||||
| + (instancetype)rows:(NSDictionary<NSString *, id> *)rowData; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,54 @@ | ||||
| // | ||||
| //  FLEXTableRowDataViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Chaoshuai Lu on 7/8/20. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableRowDataViewController.h" | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "FLEXAlert.h" | ||||
|  | ||||
| @interface FLEXTableRowDataViewController () | ||||
| @property (nonatomic) NSDictionary<NSString *, NSString *> *rowsByColumn; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableRowDataViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)rows:(NSDictionary<NSString *, id> *)rowData { | ||||
|     FLEXTableRowDataViewController *controller = [self new]; | ||||
|     controller.rowsByColumn = rowData; | ||||
|     return controller; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     NSDictionary<NSString *, NSString *> *rowsByColumn = self.rowsByColumn; | ||||
|      | ||||
|     FLEXMutableListSection<NSString *> *section = [FLEXMutableListSection list:self.rowsByColumn.allKeys | ||||
|         cellConfiguration:^(UITableViewCell *cell, NSString *column, NSInteger row) { | ||||
|             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; | ||||
|             cell.textLabel.text = column; | ||||
|             cell.detailTextLabel.text = rowsByColumn[column].description; | ||||
|         } filterMatcher:^BOOL(NSString *filterText, NSString *column) { | ||||
|             return [column localizedCaseInsensitiveContainsString:filterText] || | ||||
|                 [rowsByColumn[column] localizedCaseInsensitiveContainsString:filterText]; | ||||
|         } | ||||
|     ]; | ||||
|      | ||||
|     section.selectionHandler = ^(UIViewController *host, NSString *column) { | ||||
|         UIPasteboard.generalPasteboard.string = rowsByColumn[column].description; | ||||
|         [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|             make.title(@"Column Copied to Clipboard"); | ||||
|             make.message(rowsByColumn[column].description); | ||||
|             make.button(@"Dismiss").cancelStyle(); | ||||
|         } showFrom:host]; | ||||
|     }; | ||||
|  | ||||
|     return @[section]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										21
									
								
								Tweaks/FLEX/GlobalStateExplorers/DatabaseBrowser/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Tweaks/FLEX/GlobalStateExplorers/DatabaseBrowser/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
|  | ||||
| FMDB | ||||
| Copyright (c) 2008-2014 Flying Meat Inc. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in | ||||
| all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
| THE SOFTWARE. | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn