From 50bdce9c4e8f2697f55b2f088983305c1fe7f2a9 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 12 Jan 2024 23:36:39 +0300 Subject: [PATCH] add caching next lessons for ios widget --- ios/Runner.xcodeproj/project.pbxproj | 6 + ios/UWidget/LessonDataProvider.swift | 58 +++++++- ios/UWidget/LessonRepository.swift | 55 ++++++++ ios/UWidget/UWidget.swift | 8 +- .../data/initialize_dependencies.dart | 1 + .../schedule/widget/schedule_page.dart | 1 - .../settings/widget/settings_page.dart | 133 ++++++++++++++++++ 7 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 ios/UWidget/LessonRepository.swift diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5847685..37d2dcc 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 4AC4552286D0AE5C2D060267 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B630AA0B7629E0911B1F13C6 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7C85D8B02B51D8EB002E371C /* LessonRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C85D8AF2B51D8EB002E371C /* LessonRepository.swift */; }; + 7C85D8B12B51D8EB002E371C /* LessonRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C85D8AF2B51D8EB002E371C /* LessonRepository.swift */; }; 7CBA2ACC2A9284E800F8DB4E /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CBA2ACB2A9284E800F8DB4E /* WidgetKit.framework */; }; 7CBA2ACE2A9284E800F8DB4E /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CBA2ACD2A9284E800F8DB4E /* SwiftUI.framework */; }; 7CBA2AD12A9284E800F8DB4E /* UWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CBA2AD02A9284E800F8DB4E /* UWidgetBundle.swift */; }; @@ -67,6 +69,7 @@ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7C85D8AF2B51D8EB002E371C /* LessonRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonRepository.swift; sourceTree = ""; }; 7CBA2ACA2A9284E800F8DB4E /* UWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = UWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7CBA2ACB2A9284E800F8DB4E /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 7CBA2ACD2A9284E800F8DB4E /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -141,6 +144,7 @@ 7CBA2AD62A9284E900F8DB4E /* Info.plist */, 7CBA2AE12A929F8700F8DB4E /* Lesson.swift */, 7CBA2AE42A92A04300F8DB4E /* LessonDataProvider.swift */, + 7C85D8AF2B51D8EB002E371C /* LessonRepository.swift */, ); path = UWidget; sourceTree = ""; @@ -377,6 +381,7 @@ buildActionMask = 2147483647; files = ( 7CBA2AE62A92A04300F8DB4E /* LessonDataProvider.swift in Sources */, + 7C85D8B12B51D8EB002E371C /* LessonRepository.swift in Sources */, 7CBA2AD12A9284E800F8DB4E /* UWidgetBundle.swift in Sources */, 7CBA2AE32A92A00700F8DB4E /* Lesson.swift in Sources */, 7CBA2AD32A9284E800F8DB4E /* UWidget.swift in Sources */, @@ -388,6 +393,7 @@ buildActionMask = 2147483647; files = ( 7CBA2AE52A92A04300F8DB4E /* LessonDataProvider.swift in Sources */, + 7C85D8B02B51D8EB002E371C /* LessonRepository.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 7CBA2AE22A929F8700F8DB4E /* Lesson.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, diff --git a/ios/UWidget/LessonDataProvider.swift b/ios/UWidget/LessonDataProvider.swift index 2539e89..a089f86 100644 --- a/ios/UWidget/LessonDataProvider.swift +++ b/ios/UWidget/LessonDataProvider.swift @@ -16,7 +16,7 @@ struct LessonResponse: Codable { } protocol DataProvider { - func fetchData(groupId: Int, completion: @escaping (Lesson?) -> Void) + func fetchData(groupId: Int, completion: @escaping ([Lesson]?) -> Void) // Add any other methods or properties that the data provider should have. } @@ -27,7 +27,7 @@ class ServerDataProvider: DataProvider { self.baseURL = baseURL } - func fetchData(groupId: Int, completion: @escaping (Lesson?) -> Void) { + func fetchData(groupId: Int, completion: @escaping ([Lesson]?) -> Void) { // Create a URL object guard let url = URL(string: "\(baseURL)/group/\(groupId)/lessons/next") else { print("Invalid URL") @@ -71,7 +71,7 @@ class ServerDataProvider: DataProvider { }) let lessonResponse = try decoder.decode(LessonResponse.self, from: data) - completion(lessonResponse.lessons.first) + completion(lessonResponse.lessons) } catch { print("Decoding error: \(error)") completion(nil) @@ -88,3 +88,55 @@ class ServerDataProvider: DataProvider { // Implement any other methods from the DataProvider protocol. } + +extension FileManager { + static func documentsDirectory() -> URL { + return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + } +} + + +struct LessonsRecord: Codable { + let lessons: [Lesson] + let savedDate: Date + let groupId: Int +} + +protocol ILocalLessonDataProvider { + func fetchData(groupId: Int) -> LessonsRecord? + func saveData(lessonsRecord: LessonsRecord) +} + +class LocalLessonDataProvider: ILocalLessonDataProvider { + func fetchData(groupId: Int) -> LessonsRecord? { + let url = getUrl(groupId: groupId) + + do { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return try decoder.decode(LessonsRecord.self, from: data) + } catch { + print("Error loading lessons from LocalLessonDataProvider: \(error)") + return nil + } + } + + func saveData(lessonsRecord: LessonsRecord) { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + + do { + let data = try encoder.encode(lessonsRecord) + let url = getUrl(groupId: lessonsRecord.groupId) + try data.write(to: url) + } catch { + print("Error saving lessons to LocalLessonDataProvider: \(error)") + } + } + + func getUrl(groupId: Int) -> URL { + return FileManager.documentsDirectory().appendingPathComponent("lessons_\(groupId)") + } +} + diff --git a/ios/UWidget/LessonRepository.swift b/ios/UWidget/LessonRepository.swift new file mode 100644 index 0000000..02a37bf --- /dev/null +++ b/ios/UWidget/LessonRepository.swift @@ -0,0 +1,55 @@ +// +// LessonRepository.swift +// Runner +// +// Created by Oleg on 12.01.2024. +// + +import Foundation + +protocol ILessonRepository { + func fetchData(groupId: Int, completion: @escaping (Lesson?) -> Void) +} + +class LessonRepository : ILessonRepository { + let remoteDataProvider: DataProvider + let localDataProvider: ILocalLessonDataProvider + + init(remoteDataProvider: DataProvider, localDataProvider: ILocalLessonDataProvider) { + self.remoteDataProvider = remoteDataProvider + self.localDataProvider = localDataProvider + } + + func fetchData(groupId: Int, completion: @escaping (Lesson?) -> Void) { + self.remoteDataProvider.fetchData(groupId: groupId) { lessons in + if (lessons == nil) { + let lessonsRecord = self.localDataProvider.fetchData(groupId: groupId) + + if (lessonsRecord == nil) { + completion(nil) + return + } + + let lessons = lessonsRecord?.lessons + + if (lessons == nil || lessons?.isEmpty ?? true) { + completion(nil) + return + } + + completion(lessons?.first) + return + } + + if let lessons = lessons { + let lessonsRecord = LessonsRecord( + lessons: lessons, savedDate: Date(), groupId: groupId + ) + + self.localDataProvider.saveData(lessonsRecord: lessonsRecord) + + completion(lessons.first) + } + } + } +} diff --git a/ios/UWidget/UWidget.swift b/ios/UWidget/UWidget.swift index 6be6489..5e86b6d 100644 --- a/ios/UWidget/UWidget.swift +++ b/ios/UWidget/UWidget.swift @@ -44,7 +44,13 @@ struct Provider: TimelineProvider { print("groupId \(groupId)") - let dataProvider: DataProvider = ServerDataProvider(baseURL: "https://roadmapik.com:5000") + let dataProvider: ILessonRepository = LessonRepository( + remoteDataProvider: ServerDataProvider( + baseURL: "https://roadmapik.com:5000" + ), + localDataProvider: LocalLessonDataProvider() + ) + dataProvider.fetchData(groupId: groupId) { lesson in let entry = SimpleEntry(date: Date(), lesson: lesson, isResponseFromServer: true) let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())! diff --git a/lib/feature/initialization/data/initialize_dependencies.dart b/lib/feature/initialization/data/initialize_dependencies.dart index dca2d83..069fe32 100644 --- a/lib/feature/initialization/data/initialize_dependencies.dart +++ b/lib/feature/initialization/data/initialize_dependencies.dart @@ -34,6 +34,7 @@ Future $initializeDependencies({ ); } } + return dependencies; } diff --git a/lib/feature/schedule/widget/schedule_page.dart b/lib/feature/schedule/widget/schedule_page.dart index 395f86b..75c804a 100644 --- a/lib/feature/schedule/widget/schedule_page.dart +++ b/lib/feature/schedule/widget/schedule_page.dart @@ -4,7 +4,6 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:l/l.dart'; import 'package:octopus/octopus.dart'; import 'package:uneconly/common/localization/localization.dart'; import 'package:uneconly/common/model/dependencies.dart'; diff --git a/lib/feature/settings/widget/settings_page.dart b/lib/feature/settings/widget/settings_page.dart index 9ecbdb4..6d5b78a 100644 --- a/lib/feature/settings/widget/settings_page.dart +++ b/lib/feature/settings/widget/settings_page.dart @@ -173,6 +173,20 @@ class _SettingsPageState extends State { // ], // ), + // Open ListSectionInsetExample widget + // CupertinoButton( + // onPressed: () { + // Navigator.of(context).push( + // CupertinoPageRoute( + // builder: (BuildContext context) { + // return const ListSectionInsetExample(); + // }, + // ), + // ); + // }, + // child: const Text('Open ListSectionInsetExample'), + // ), + Text('${AppLocalizations.of(context)!.theme}: '), const SizedBox(height: 10), // horizontal list of themes @@ -238,3 +252,122 @@ class _SettingsPageState extends State { ); } } + +class CupertinoListSectionInsetApp extends StatelessWidget { + const CupertinoListSectionInsetApp({super.key}); + + @override + Widget build(BuildContext context) { + return const CupertinoApp( + home: ListSectionInsetExample(), + ); + } +} + +class ListSectionInsetExample extends StatefulWidget { + const ListSectionInsetExample({super.key}); + + @override + State createState() => + _ListSectionInsetExampleState(); +} + +class _ListSectionInsetExampleState extends State { + bool _isNotificationsEnabled = true; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('ListSectionInsetExample'), + ), + body: CupertinoListSection.insetGrouped( + header: const Text('My Settings'), + children: [ + CupertinoListTile.notched( + title: const Text('Open pull request'), + leading: Container( + width: double.infinity, + height: double.infinity, + color: CupertinoColors.activeGreen, + ), + trailing: const CupertinoListTileChevron(), + onTap: () => Navigator.of(context).push( + CupertinoPageRoute( + builder: (BuildContext context) { + return const _SecondPage(text: 'Open pull request'); + }, + ), + ), + ), + CupertinoListTile.notched( + title: const Text('Push to master'), + leading: Container( + width: double.infinity, + height: double.infinity, + color: CupertinoColors.systemRed, + ), + additionalInfo: const Text('Not available'), + ), + CupertinoListTile.notched( + title: const Text('View last commit'), + leading: Container( + width: double.infinity, + height: double.infinity, + color: CupertinoColors.activeOrange, + ), + additionalInfo: const Text('12 days ago'), + trailing: const CupertinoListTileChevron(), + onTap: () => Navigator.of(context).push( + CupertinoPageRoute( + builder: (BuildContext context) { + return const _SecondPage(text: 'Last commit'); + }, + ), + ), + ), + CupertinoListTile.notched( + title: const Text('Notifications'), + leading: Container( + width: double.infinity, + height: double.infinity, + color: CupertinoColors.activeBlue, + ), + trailing: CupertinoSwitch( + value: _isNotificationsEnabled, + onChanged: (value) { + setState( + () { + _isNotificationsEnabled = value; + }, + ); + }, + ), + onTap: () => Navigator.of(context).push( + CupertinoPageRoute( + builder: (BuildContext context) { + return const _SecondPage(text: 'Last commit'); + }, + ), + ), + ), + ], + ), + ); + } +} + +class _SecondPage extends StatelessWidget { + const _SecondPage({required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Text(text), + ), + ); + } +}