/WordPress/Classes/Services/ThemeService.m

https://gitlab.com/jslee1/WordPress-iOS · Objective C · 407 lines · 286 code · 77 blank · 44 comment · 39 complexity · e88443ff266641c799565759446be9e9 MD5 · raw file

  1. #import "ThemeService.h"
  2. #import "Blog.h"
  3. #import "RemoteTheme.h"
  4. #import "Theme.h"
  5. #import "ThemeServiceRemote.h"
  6. #import "WPAccount.h"
  7. #import "ContextManager.h"
  8. /**
  9. * @brief Place unordered themes after loaded pages
  10. */
  11. const NSInteger ThemeOrderUnspecified = 0;
  12. const NSInteger ThemeOrderTrailing = 9999;
  13. @implementation ThemeService
  14. #pragma mark - Themes availability
  15. - (BOOL)blogSupportsThemeServices:(Blog *)blog
  16. {
  17. NSParameterAssert([blog isKindOfClass:[Blog class]]);
  18. return [blog supports:BlogFeatureWPComRESTAPI];
  19. }
  20. #pragma mark - Local queries: Creating themes
  21. /**
  22. * @brief Creates and initializes a new theme with the specified theme Id in the specified
  23. * context.
  24. * @details You should probably not call this method directly. Please read the documentation
  25. * for findOrCreateThemeWithId: first.
  26. *
  27. * @param themeId The ID of the new theme. Cannot be nil.
  28. * @param blog Blog being updated. May be nil for account.
  29. *
  30. * @returns The newly created and initialized object.
  31. */
  32. - (Theme *)newThemeWithId:(NSString *)themeId
  33. forBlog:(nullable Blog *)blog
  34. {
  35. NSParameterAssert([themeId isKindOfClass:[NSString class]]);
  36. NSEntityDescription *entityDescription = [NSEntityDescription entityForName:[Theme entityName]
  37. inManagedObjectContext:self.managedObjectContext];
  38. __block Theme *theme = nil;
  39. [self.managedObjectContext performBlockAndWait:^{
  40. theme = [[Theme alloc] initWithEntity:entityDescription
  41. insertIntoManagedObjectContext:self.managedObjectContext];
  42. if (blog) {
  43. theme.blog = blog;
  44. }
  45. }];
  46. return theme;
  47. }
  48. /**
  49. * @brief Obtains the theme with the specified ID if it exists, otherwise a new theme is
  50. * created and returned.
  51. *
  52. * @param themeId The ID of the theme to retrieve. Cannot be nil.
  53. * @param blog Blog being updated. May be nil for account.
  54. *
  55. * @returns The stored theme matching the specified ID if found, or nil if it's not found.
  56. */
  57. - (Theme *)findOrCreateThemeWithId:(NSString *)themeId
  58. forBlog:(nullable Blog *)blog
  59. {
  60. NSParameterAssert([themeId isKindOfClass:[NSString class]]);
  61. Theme *theme = [self findThemeWithId:themeId
  62. forBlog:blog];
  63. if (!theme) {
  64. theme = [self newThemeWithId:themeId
  65. forBlog:blog];
  66. }
  67. return theme;
  68. }
  69. #pragma mark - Local queries: finding themes
  70. - (Theme *)findThemeWithId:(NSString *)themeId
  71. forBlog:(nullable Blog *)blog
  72. {
  73. NSParameterAssert([themeId isKindOfClass:[NSString class]]);
  74. Theme *theme = nil;
  75. NSPredicate *predicate = nil;
  76. if (blog) {
  77. predicate = [NSPredicate predicateWithFormat:@"themeId == %@ AND blog == %@", themeId, blog];
  78. } else {
  79. predicate = [NSPredicate predicateWithFormat:@"themeId == %@ AND blog.@count == 0", themeId, blog];
  80. }
  81. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[Theme entityName]];
  82. fetchRequest.predicate = predicate;
  83. NSError *error = nil;
  84. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  85. if (results.count > 0) {
  86. theme = (Theme *)[results firstObject];
  87. NSAssert([theme isKindOfClass:[Theme class]],
  88. @"Expected a Theme object.");
  89. } else {
  90. NSAssert(error == nil,
  91. @"We shouldn't be getting errors here. This means something's internally broken.");
  92. }
  93. return theme;
  94. }
  95. - (NSArray *)findAccountThemes
  96. {
  97. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"blog.@count == 0"];
  98. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[Theme entityName]];
  99. fetchRequest.predicate = predicate;
  100. NSError *error = nil;
  101. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  102. return results;
  103. }
  104. #pragma mark - Remote queries: Getting theme info
  105. - (NSProgress *)getActiveThemeForBlog:(Blog *)blog
  106. success:(ThemeServiceThemeRequestSuccessBlock)success
  107. failure:(ThemeServiceFailureBlock)failure
  108. {
  109. NSParameterAssert([blog isKindOfClass:[Blog class]]);
  110. NSAssert([self blogSupportsThemeServices:blog],
  111. @"Do not call this method on unsupported blogs, check with blogSupportsThemeServices first.");
  112. if (blog.wordPressComRestApi == nil) {
  113. return nil;
  114. }
  115. ThemeServiceRemote *remote = [[ThemeServiceRemote alloc] initWithWordPressComRestApi:blog.wordPressComRestApi];
  116. NSProgress *progress = [remote getActiveThemeForBlogId:[blog dotComID]
  117. success:^(RemoteTheme *remoteTheme) {
  118. Theme *theme = [self themeFromRemoteTheme:remoteTheme
  119. forBlog:blog];
  120. [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{
  121. if (success) {
  122. success(theme);
  123. }
  124. }];
  125. } failure:failure];
  126. return progress;
  127. }
  128. - (NSProgress *)getPurchasedThemesForBlog:(Blog *)blog
  129. success:(ThemeServiceThemesRequestSuccessBlock)success
  130. failure:(ThemeServiceFailureBlock)failure
  131. {
  132. NSParameterAssert([blog isKindOfClass:[Blog class]]);
  133. NSAssert([self blogSupportsThemeServices:blog],
  134. @"Do not call this method on unsupported blogs, check with blogSupportsThemeServices first.");
  135. if (blog.wordPressComRestApi == nil) {
  136. return nil;
  137. }
  138. ThemeServiceRemote *remote = [[ThemeServiceRemote alloc] initWithWordPressComRestApi:blog.wordPressComRestApi];
  139. NSProgress *progress = [remote getPurchasedThemesForBlogId:[blog dotComID]
  140. success:^(NSArray *remoteThemes) {
  141. NSArray *themes = [self themesFromRemoteThemes:remoteThemes
  142. forBlog:blog];
  143. [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{
  144. if (success) {
  145. success(themes, NO);
  146. }
  147. }];
  148. } failure:failure];
  149. return progress;
  150. }
  151. - (NSProgress *)getThemeId:(NSString*)themeId
  152. forAccount:(WPAccount *)account
  153. success:(ThemeServiceThemeRequestSuccessBlock)success
  154. failure:(ThemeServiceFailureBlock)failure
  155. {
  156. NSParameterAssert([themeId isKindOfClass:[NSString class]]);
  157. NSParameterAssert(account.wordPressComRestApi != nil);
  158. if (account.wordPressComRestApi == nil) {
  159. return nil;
  160. }
  161. ThemeServiceRemote *remote = [[ThemeServiceRemote alloc] initWithWordPressComRestApi:account.wordPressComRestApi];
  162. NSProgress *progress = [remote getThemeId:themeId
  163. success:^(RemoteTheme *remoteTheme) {
  164. Theme *theme = [self themeFromRemoteTheme:remoteTheme
  165. forBlog:nil];
  166. [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{
  167. if (success) {
  168. success(theme);
  169. }
  170. }];
  171. } failure:failure];
  172. return progress;
  173. }
  174. - (NSProgress *)getThemesForAccount:(WPAccount *)account
  175. page:(NSInteger)page
  176. success:(ThemeServiceThemesRequestSuccessBlock)success
  177. failure:(ThemeServiceFailureBlock)failure
  178. {
  179. NSParameterAssert([account isKindOfClass:[WPAccount class]]);
  180. if (account.wordPressComRestApi == nil) {
  181. return nil;
  182. }
  183. ThemeServiceRemote *remote = [[ThemeServiceRemote alloc] initWithWordPressComRestApi:account.wordPressComRestApi];
  184. NSProgress *progress = [remote getThemesPage:page
  185. success:^(NSArray<RemoteTheme *> *remoteThemes, BOOL hasMore) {
  186. NSArray *themes = [self themesFromRemoteThemes:remoteThemes
  187. forBlog:nil];
  188. [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{
  189. if (success) {
  190. success(themes, hasMore);
  191. }
  192. }];
  193. } failure:failure];
  194. return progress;
  195. }
  196. - (NSProgress *)getThemesForBlog:(Blog *)blog
  197. page:(NSInteger)page
  198. sync:(BOOL)sync
  199. success:(ThemeServiceThemesRequestSuccessBlock)success
  200. failure:(ThemeServiceFailureBlock)failure
  201. {
  202. NSParameterAssert([blog isKindOfClass:[Blog class]]);
  203. NSAssert([self blogSupportsThemeServices:blog],
  204. @"Do not call this method on unsupported blogs, check with blogSupportsThemeServices first.");
  205. if (blog.wordPressComRestApi == nil) {
  206. return nil;
  207. }
  208. ThemeServiceRemote *remote = [[ThemeServiceRemote alloc] initWithWordPressComRestApi:blog.wordPressComRestApi];
  209. NSMutableSet *unsyncedThemes = sync ? [NSMutableSet setWithSet:blog.themes] : nil;
  210. NSProgress *progress = [remote getThemesForBlogId:[blog dotComID]
  211. page:page
  212. success:^(NSArray<RemoteTheme *> *remoteThemes, BOOL hasMore) {
  213. NSArray *themes = [self themesFromRemoteThemes:remoteThemes
  214. forBlog:blog];
  215. if (sync) {
  216. [unsyncedThemes minusSet:[NSSet setWithArray:themes]];
  217. for (Theme *deleteTheme in unsyncedThemes) {
  218. if (![blog.currentThemeId isEqualToString:deleteTheme.themeId]) {
  219. [self.managedObjectContext deleteObject:deleteTheme];
  220. }
  221. }
  222. }
  223. [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{
  224. if (success) {
  225. success(themes, hasMore);
  226. }
  227. }];
  228. } failure:failure];
  229. return progress;
  230. }
  231. #pragma mark - Remote queries: Activating themes
  232. - (NSProgress *)activateTheme:(Theme *)theme
  233. forBlog:(Blog *)blog
  234. success:(ThemeServiceThemeRequestSuccessBlock)success
  235. failure:(ThemeServiceFailureBlock)failure
  236. {
  237. NSParameterAssert([theme isKindOfClass:[Theme class]]);
  238. NSParameterAssert([theme.themeId isKindOfClass:[NSString class]]);
  239. NSParameterAssert([blog isKindOfClass:[Blog class]]);
  240. NSAssert([self blogSupportsThemeServices:blog],
  241. @"Do not call this method on unsupported blogs, check with blogSupportsThemeServices first.");
  242. if (blog.wordPressComRestApi == nil) {
  243. return nil;
  244. }
  245. ThemeServiceRemote *remote = [[ThemeServiceRemote alloc] initWithWordPressComRestApi:blog.wordPressComRestApi];
  246. NSProgress *progress = [remote activateThemeId:theme.themeId
  247. forBlogId:[blog dotComID]
  248. success:^(RemoteTheme *remoteTheme) {
  249. Theme *theme = [self themeFromRemoteTheme:remoteTheme
  250. forBlog:blog];
  251. [[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{
  252. if (success) {
  253. success(theme);
  254. }
  255. }];
  256. } failure:failure];
  257. return progress;
  258. }
  259. #pragma mark - Parsing the dictionary replies
  260. /**
  261. * @brief Updates our local theme matching the specified remote theme.
  262. * @details If the local theme does not exist, it is created.
  263. *
  264. * @param remoteTheme The remote theme containing the data to update locally.
  265. * Cannot be nil.
  266. * @param blog Blog being updated. May be nil for account.
  267. *
  268. * @returns The updated and matching local theme.
  269. */
  270. - (Theme *)themeFromRemoteTheme:(RemoteTheme *)remoteTheme
  271. forBlog:(nullable Blog *)blog
  272. {
  273. NSParameterAssert([remoteTheme isKindOfClass:[RemoteTheme class]]);
  274. Theme *theme = [self findOrCreateThemeWithId:remoteTheme.themeId
  275. forBlog:blog];
  276. if (remoteTheme.author) {
  277. theme.author = remoteTheme.author;
  278. theme.authorUrl = remoteTheme.authorUrl;
  279. }
  280. theme.demoUrl = remoteTheme.demoUrl;
  281. theme.details = remoteTheme.desc;
  282. theme.launchDate = remoteTheme.launchDate;
  283. theme.name = remoteTheme.name;
  284. if (remoteTheme.order != ThemeOrderUnspecified) {
  285. theme.order = @(remoteTheme.order);
  286. } else if (theme.order.integerValue == ThemeOrderUnspecified) {
  287. theme.order = @(ThemeOrderTrailing);
  288. }
  289. theme.popularityRank = remoteTheme.popularityRank;
  290. theme.previewUrl = remoteTheme.previewUrl;
  291. BOOL availableFree = remoteTheme.purchased.boolValue || remoteTheme.price.length == 0;
  292. theme.premium = @(!availableFree);
  293. theme.price = remoteTheme.price;
  294. theme.purchased = remoteTheme.purchased;
  295. theme.screenshotUrl = remoteTheme.screenshotUrl;
  296. theme.stylesheet = remoteTheme.stylesheet;
  297. theme.themeId = remoteTheme.themeId;
  298. theme.trendingRank = remoteTheme.trendingRank;
  299. theme.version = remoteTheme.version;
  300. if (blog && remoteTheme.active) {
  301. blog.currentThemeId = theme.themeId;
  302. }
  303. return theme;
  304. }
  305. /**
  306. * @brief Updates our local themes matching the specified remote themes.
  307. * @details If the local themes do not exist, they are created.
  308. *
  309. * @param remoteThemes An array with the remote themes containing the data to update
  310. * locally. Cannot be nil.
  311. * @param blog Blog being updated. May be nil for account.
  312. * @param ordered Whether to update displayed order
  313. *
  314. * @returns An array with the updated and matching local themes.
  315. */
  316. - (NSArray<Theme *> *)themesFromRemoteThemes:(NSArray<RemoteTheme *> *)remoteThemes
  317. forBlog:(nullable Blog *)blog
  318. {
  319. NSParameterAssert([remoteThemes isKindOfClass:[NSArray class]]);
  320. NSMutableArray *themes = [[NSMutableArray alloc] initWithCapacity:remoteThemes.count];
  321. [remoteThemes enumerateObjectsUsingBlock:^(RemoteTheme *remoteTheme, NSUInteger idx, BOOL *stop) {
  322. NSAssert([remoteTheme isKindOfClass:[RemoteTheme class]],
  323. @"Expected a remote theme.");
  324. Theme *theme = [self themeFromRemoteTheme:remoteTheme
  325. forBlog:blog];
  326. [themes addObject:theme];
  327. }];
  328. return [NSArray arrayWithArray:themes];
  329. }
  330. @end