mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-04-13 09:42:01 +00:00
Ading pull to refresh on feed detail in iphone app. Not done, but also added extra parameter in feed canonical model that gives seconds since last update.
This commit is contained in:
parent
bffb8a6d98
commit
442012fdeb
17 changed files with 126 additions and 32 deletions
|
@ -26,6 +26,7 @@ from utils.fields import AutoOneToOneField
|
|||
from utils.feed_functions import levenshtein_distance
|
||||
from utils.feed_functions import timelimit, TimeoutError
|
||||
from utils.feed_functions import relative_timesince
|
||||
from utils.feed_functions import seconds_timesince
|
||||
from utils.story_functions import pre_process_story
|
||||
from utils.diff import HTMLDiff
|
||||
|
||||
|
@ -71,6 +72,7 @@ class Feed(models.Model):
|
|||
'feed_link': self.feed_link,
|
||||
'num_subscribers': self.num_subscribers,
|
||||
'updated': relative_timesince(self.last_update),
|
||||
'updated_seconds_ago': seconds_timesince(self.last_update),
|
||||
'subs': self.num_subscribers,
|
||||
'favicon_color': self.favicon_color,
|
||||
'favicon_fetching': bool(not (self.favicon_not_found or self.favicon_color))
|
||||
|
|
|
@ -5,3 +5,5 @@ ServerAliveCountMax=6
|
|||
StrictHostKeyChecking=no
|
||||
Compression=yes
|
||||
ForwardAgent=yes
|
||||
ControlMaster auto
|
||||
ControlPath /tmp/ssh_mux_%h_%p_%r
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Path to your oh-my-zsh configuration.
|
||||
plugins=(git osx ruby gem github pip rails)
|
||||
export ZSH=$HOME/.oh-my-zsh
|
||||
|
||||
# Set to the name theme to load.
|
||||
|
@ -10,7 +11,8 @@ export CASE_SENSITIVE="true"
|
|||
export LC_COLLATE='C'
|
||||
source $ZSH/oh-my-zsh.sh
|
||||
|
||||
export PROMPT='%{$fg_bold[green]%}%n@%M:%{$fg_bold[blue]%}%~ $(git_prompt_info)%{$reset_color%}%(!.#.$) '
|
||||
|
||||
export DISABLE_AUTO_UPDATE="true"
|
||||
export PYTHONSTARTUP=$HOME/.pystartup
|
||||
export LSCOLORS='gxgxcxdxBxegedabagacad'
|
||||
|
||||
|
@ -32,7 +34,7 @@ zle -N expand-or-complete-with-dots
|
|||
bindkey "^I" expand-or-complete-with-dots
|
||||
unsetopt LIST_BEEP
|
||||
|
||||
PROMPT='%{$fg_bold[green]%}%n%{$reset_color%}%{$fg[yellow]%}@%M:%{$fg_bold[blue]%}%~%b $(git_prompt_info)%{$reset_color%}%(!.#.$) '
|
||||
PROMPT='%{$fg_bold[green]%}%n%{$reset_color%}%{$fg_bold[yellow]%}@%M:%{$fg_bold[blue]%}%~%b $(git_prompt_info)%{$reset_color%}%(!.#.$) '
|
||||
|
||||
ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg[red]%}‹%B"
|
||||
ZSH_THEME_GIT_PROMPT_SUFFIX="%b%{$fg[red]%}›%{$reset_color%}"
|
||||
|
|
|
@ -13,6 +13,7 @@ body {
|
|||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-rendering: optimizeLegibility;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
a, a:active, a:hover, a:visited, button {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "PullToRefreshView.h"
|
||||
|
||||
@class NewsBlurAppDelegate;
|
||||
|
||||
|
@ -25,6 +26,7 @@
|
|||
UISlider * feedScoreSlider;
|
||||
UIBarButtonItem * feedMarkReadButton;
|
||||
UISegmentedControl * intelligenceControl;
|
||||
PullToRefreshView *pull;
|
||||
}
|
||||
|
||||
- (void)fetchFeedDetail:(int)page;
|
||||
|
@ -34,6 +36,8 @@
|
|||
- (NSDictionary *)getStoryAtRow:(NSInteger)indexPathRow;
|
||||
- (void)checkScroll;
|
||||
- (void)markedAsRead;
|
||||
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view;
|
||||
- (NSDate *)pullToRefreshViewLastUpdated:(PullToRefreshView *)view;
|
||||
|
||||
@property (nonatomic, retain) IBOutlet NewsBlurAppDelegate *appDelegate;
|
||||
@property (nonatomic, retain) IBOutlet UITableView *storyTitlesTable;
|
||||
|
@ -41,6 +45,7 @@
|
|||
@property (nonatomic, retain) IBOutlet UISlider * feedScoreSlider;
|
||||
@property (nonatomic, retain) IBOutlet UIBarButtonItem * feedMarkReadButton;
|
||||
@property (nonatomic, retain) IBOutlet UISegmentedControl * intelligenceControl;
|
||||
@property (nonatomic, retain) PullToRefreshView *pull;
|
||||
|
||||
@property (nonatomic, retain) NSArray * stories;
|
||||
@property (nonatomic, retain) NSMutableData * jsonString;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#import "FeedDetailViewController.h"
|
||||
#import "NewsBlurAppDelegate.h"
|
||||
#import "FeedDetailTableCell.h"
|
||||
#import "PullToRefreshView.h"
|
||||
#import "ASIFormDataRequest.h"
|
||||
#import "NSString+HTML.h"
|
||||
#import "JSON.h"
|
||||
|
@ -25,6 +26,7 @@
|
|||
@synthesize pageFetching;
|
||||
@synthesize pageFinished;
|
||||
@synthesize intelligenceControl;
|
||||
@synthesize pull;
|
||||
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
||||
|
||||
|
@ -33,6 +35,13 @@
|
|||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
pull = [[PullToRefreshView alloc] initWithScrollView:self.storyTitlesTable];
|
||||
[pull setDelegate:self];
|
||||
[self.storyTitlesTable addSubview:pull];
|
||||
[super viewDidLoad];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
self.pageFinished = NO;
|
||||
self.title = [appDelegate.activeFeed objectForKey:@"feed_title"];
|
||||
|
@ -82,6 +91,7 @@
|
|||
[appDelegate release];
|
||||
[jsonString release];
|
||||
[intelligenceControl release];
|
||||
[pull release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
|
@ -188,6 +198,13 @@
|
|||
[error localizedDescription]);
|
||||
|
||||
self.pageFetching = NO;
|
||||
|
||||
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
|
||||
|
||||
// User clicking on another link before the page loads is OK.
|
||||
if ([error code] != NSURLErrorCancelled) {
|
||||
[NewsBlurAppDelegate informError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (UITableViewCell *)makeLoadingCell {
|
||||
|
@ -415,4 +432,19 @@
|
|||
return [appDelegate.activeFeedStories objectAtIndex:row];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark PullToRefresh
|
||||
|
||||
// called when the user pulls-to-refresh
|
||||
- (void)pullToRefreshViewShouldRefresh:(PullToRefreshView *)view {
|
||||
// [self fetchFeedList:NO];
|
||||
}
|
||||
|
||||
// called when the date shown needs to be updated, optional
|
||||
- (NSDate *)pullToRefreshViewLastUpdated:(PullToRefreshView *)view {
|
||||
// return self.lastUpdate;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
- (void)markActiveFeedAllRead;
|
||||
- (void)calculateStoryLocations;
|
||||
+ (int)computeStoryScore:(NSDictionary *)intelligence;
|
||||
+ (void)informError:(NSError *)error;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -283,4 +283,15 @@
|
|||
return score;
|
||||
}
|
||||
|
||||
+ (void)informError:(NSError *)error {
|
||||
NSString* localizedDescription = [error localizedDescription];
|
||||
UIAlertView* alertView = [[UIAlertView alloc]
|
||||
initWithTitle:@"Error"
|
||||
message:localizedDescription delegate:nil
|
||||
cancelButtonTitle:@"OK"
|
||||
otherButtonTitles:nil];
|
||||
[alertView show];
|
||||
[alertView release];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -178,6 +178,16 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
|
||||
NSLog(@"%@", [NSString stringWithFormat:@"Connection failed: %@", [error description]]);
|
||||
|
||||
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
|
||||
|
||||
[MBProgressHUD hideHUDForView:self.view animated:YES];
|
||||
[pull finishedLoading];
|
||||
|
||||
// User clicking on another link before the page loads is OK.
|
||||
if ([error code] != NSURLErrorCancelled) {
|
||||
[NewsBlurAppDelegate informError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
|
||||
|
@ -329,6 +339,8 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
FeedTableCell *cell = (FeedTableCell *)[tableView dequeueReusableCellWithIdentifier:FeedCellIdentifier];
|
||||
if (cell == nil) {
|
||||
cell = [[[FeedTableCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"FeedCellIdentifier"] autorelease];
|
||||
cell.appDelegate = (NewsBlurAppDelegate *)[[UIApplication sharedApplication] delegate];
|
||||
|
||||
}
|
||||
|
||||
NSString *folderName = [self.dictFoldersArray objectAtIndex:indexPath.section];
|
||||
|
@ -438,7 +450,10 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
// NSLog(@"Select Intelligence from %d to %d.", previousLevel, newLevel);
|
||||
[self updateFeedsWithIntelligence:previousLevel newLevel:newLevel];
|
||||
}
|
||||
// TODO: Refresh cells on screen to show correct unread pills.
|
||||
|
||||
for (UITableViewCell *cell in self.feedTitlesTable.visibleCells) {
|
||||
[cell setNeedsDisplay];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateFeedsWithIntelligence:(int)previousLevel newLevel:(int)newLevel {
|
||||
|
@ -493,14 +508,17 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
}
|
||||
}
|
||||
|
||||
BOOL isVisible = !![self.stillVisibleFeeds objectForKey:feedIdStr];
|
||||
BOOL notDeletedYetVisible = !deleted &&
|
||||
previousLevel != newLevel &&
|
||||
(maxScore < newLevel) &&
|
||||
[self.stillVisibleFeeds objectForKey:feedIdStr];
|
||||
isVisible;
|
||||
if (notDeletedYetVisible) {
|
||||
// NSLog(@"DELETING: %@ - %d - %d - %d - %d", [feed objectForKey:@"feed_title"], maxScore, newLevel, previousLevel, !deleted);
|
||||
[deleteIndexPaths addObject:indexPath];
|
||||
[self.stillVisibleFeeds removeObjectForKey:feedIdStr];
|
||||
} else if (deleted && isVisible) {
|
||||
[self.stillVisibleFeeds removeObjectForKey:feedIdStr];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -508,8 +526,8 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
for (id feedIdStr in [self.stillVisibleFeeds allKeys]) {
|
||||
NSDictionary *feed = [self.dictFeeds objectForKey:feedIdStr];
|
||||
int maxScore = [NewsBlurViewController computeMaxScoreForFeed:feed];
|
||||
// NSLog(@"Still visible: %@ - %d - %d - %d", [feed objectForKey:@"feed_title"], maxScore, newLevel, previousLevel);
|
||||
if (previousLevel != newLevel && maxScore < newLevel) {
|
||||
NSLog(@"Still visible: %@ - %d - %d - %d", [feed objectForKey:@"feed_title"], maxScore, newLevel, previousLevel);
|
||||
[deleteIndexPaths addObject:[self.stillVisibleFeeds objectForKey:feedIdStr]];
|
||||
[self.stillVisibleFeeds removeObjectForKey:feedIdStr];
|
||||
}
|
||||
|
@ -634,4 +652,15 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
[appDelegate reloadFeedsView];
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
|
||||
NSLog(@"%@", [NSString stringWithFormat:@"Connection failed: %@", [error description]]);
|
||||
|
||||
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
|
||||
|
||||
// User clicking on another link before the page loads is OK.
|
||||
if ([error code] != NSURLErrorCancelled) {
|
||||
[NewsBlurAppDelegate informError:error];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -52,6 +52,5 @@ static const CGFloat kButtonWidth = 48.0f;
|
|||
- (void)updateTitle:(UIWebView*)aWebView;
|
||||
- (void)updateAddress:(NSURLRequest*)request;
|
||||
- (void)updateButtons;
|
||||
- (void)informError:(NSError*)error;
|
||||
|
||||
@end
|
||||
|
|
|
@ -180,7 +180,7 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
|
||||
// User clicking on another link before the page loads is OK.
|
||||
if ([error code] != NSURLErrorCancelled) {
|
||||
[self informError:error];
|
||||
[NewsBlurAppDelegate informError:error];
|
||||
}
|
||||
}
|
||||
- (void)updateTitle:(UIWebView*)aWebView
|
||||
|
@ -200,17 +200,6 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
self.back.enabled = self.webView.canGoBack;
|
||||
// self.stop.enabled = self.webView.loading;
|
||||
}
|
||||
- (void)informError:(NSError *)error
|
||||
{
|
||||
NSString* localizedDescription = [error localizedDescription];
|
||||
UIAlertView* alertView = [[UIAlertView alloc]
|
||||
initWithTitle:@"Error"
|
||||
message:localizedDescription delegate:nil
|
||||
cancelButtonTitle:@"OK"
|
||||
otherButtonTitles:nil];
|
||||
[alertView show];
|
||||
[alertView release];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
// Releases the view if it doesn't have a superview.
|
||||
|
@ -248,9 +237,9 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
|
|||
|
||||
NSArray *buttonTitles;
|
||||
if ([[appDelegate.activeOriginalStoryURL absoluteString] isEqualToString:self.pageUrl.text]) {
|
||||
buttonTitles = [NSArray arrayWithObjects:@"Open story in Safari", nil];
|
||||
buttonTitles = [NSArray arrayWithObjects:@"Open Story in Safari", nil];
|
||||
} else {
|
||||
buttonTitles = [NSArray arrayWithObjects:@"Open this page in Safari", @"Open original in Safari", nil];
|
||||
buttonTitles = [NSArray arrayWithObjects:@"Open this Page in Safari", @"Open Original in Safari", nil];
|
||||
}
|
||||
for (id title in buttonTitles) {
|
||||
[options addButtonWithTitle:title];
|
||||
|
|
|
@ -308,7 +308,7 @@
|
|||
- (void)didReceiveMemoryWarning {
|
||||
// Releases the view if it doesn't have a superview.
|
||||
[super didReceiveMemoryWarning];
|
||||
|
||||
self.activeStoryId = nil;
|
||||
// Release any cached data, images, etc that aren't in use.
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
<string>English</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array/>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
@ -24,20 +26,26 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1</string>
|
||||
<string></string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array/>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<string>1.0b3</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainWindow</string>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIPrerenderedIcon</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array/>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -7,10 +7,14 @@
|
|||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "NewsBlurAppDelegate.h"
|
||||
#import "ABTableViewCell.h"
|
||||
|
||||
@class NewsBlurAppDelegate;
|
||||
|
||||
@interface FeedTableCell : ABTableViewCell {
|
||||
NewsBlurAppDelegate *appDelegate;
|
||||
|
||||
NSString *feedTitle;
|
||||
UIImage *feedFavicon;
|
||||
int _positiveCount;
|
||||
|
@ -21,6 +25,7 @@
|
|||
NSString *_negativeCountStr;
|
||||
}
|
||||
|
||||
@property (nonatomic, retain) NewsBlurAppDelegate *appDelegate;
|
||||
@property (nonatomic, retain) NSString *feedTitle;
|
||||
@property (nonatomic, retain) UIImage *feedFavicon;
|
||||
@property (assign, nonatomic) int positiveCount;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
// Copyright 2011 NewsBlur. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NewsBlurAppDelegate.h"
|
||||
#import "FeedTableCell.h"
|
||||
#import "ABTableViewCell.h"
|
||||
#import "UIView+TKCategory.h"
|
||||
|
@ -26,6 +27,7 @@ static CGFloat *psColors = nil;
|
|||
|
||||
@implementation FeedTableCell
|
||||
|
||||
@synthesize appDelegate;
|
||||
@synthesize feedTitle;
|
||||
@synthesize feedFavicon;
|
||||
@synthesize positiveCount = _positiveCount;
|
||||
|
@ -138,7 +140,7 @@ static CGFloat *psColors = nil;
|
|||
drawAtPoint:CGPointMake(rr.origin.x + x_pos, rr.origin.y + y_pos)
|
||||
withFont:indicatorFont];
|
||||
}
|
||||
if(_neutralCount > 0){
|
||||
if(_neutralCount > 0 && appDelegate.selectedIntelligence <= 0){
|
||||
[neutralBackgroundColor set];
|
||||
CGRect rr = CGRectMake(rect.size.width + rect.origin.x - psWidth - psPadding - ntOffset, 10, ntWidth, 18);
|
||||
[UIView drawRoundRectangleInRect:rr withRadius:5];
|
||||
|
@ -152,7 +154,7 @@ static CGFloat *psColors = nil;
|
|||
drawAtPoint:CGPointMake(rr.origin.x + x_pos, rr.origin.y + y_pos)
|
||||
withFont:indicatorFont];
|
||||
}
|
||||
if(_negativeCount > 0){
|
||||
if(_negativeCount > 0 && appDelegate.selectedIntelligence <= -1){
|
||||
[negativeBackgroundColor set];
|
||||
CGRect rr = CGRectMake(rect.size.width + rect.origin.x - psWidth - psPadding - ntWidth - ntPadding - ngOffset, 10, ngWidth, 18);
|
||||
[UIView drawRoundRectangleInRect:rr withRadius:5];
|
||||
|
|
|
@ -579,8 +579,8 @@
|
|||
"-all_load",
|
||||
"-ObjC",
|
||||
);
|
||||
PROVISIONING_PROFILE = "7B7D4B1E-A24A-4E63-9C14-F251D6C5B095";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "7B7D4B1E-A24A-4E63-9C14-F251D6C5B095";
|
||||
PROVISIONING_PROFILE = "9390CCB7-7064-41BA-A292-53CE11BB3BEB";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "9390CCB7-7064-41BA-A292-53CE11BB3BEB";
|
||||
RUN_CLANG_STATIC_ANALYZER = YES;
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
|
@ -597,8 +597,8 @@
|
|||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 4.0;
|
||||
OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1";
|
||||
PROVISIONING_PROFILE = "7B7D4B1E-A24A-4E63-9C14-F251D6C5B095";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "7B7D4B1E-A24A-4E63-9C14-F251D6C5B095";
|
||||
PROVISIONING_PROFILE = "9390CCB7-7064-41BA-A292-53CE11BB3BEB";
|
||||
"PROVISIONING_PROFILE[sdk=iphoneos*]" = "9390CCB7-7064-41BA-A292-53CE11BB3BEB";
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Release;
|
||||
|
|
|
@ -130,7 +130,13 @@ def relative_timeuntil(value):
|
|||
now = datetime.datetime.utcnow()
|
||||
|
||||
return _do_timesince(now, chunks, value)
|
||||
|
||||
|
||||
def seconds_timesince(value):
|
||||
now = datetime.datetime.utcnow()
|
||||
delta = now - value
|
||||
|
||||
return delta.days * 24 * 60 * 60 + delta.seconds
|
||||
|
||||
def format_relative_date(date, future=False):
|
||||
if not date or date < datetime.datetime(2010, 1, 1):
|
||||
return "Soon"
|
||||
|
|
Loading…
Add table
Reference in a new issue