From df81288341397cc9b10a9b10004a6cc5eed407d1 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 20 Aug 2023 18:38:39 +0400 Subject: [PATCH 1/9] added new SwiftUI views --- Simple Anki.xcodeproj/project.pbxproj | 84 ++++++++++++++----- .../xcschemes/Simple Anki.xcscheme | 2 +- .../xcschemes/Simple AnkiUITests.xcscheme | 2 +- Simple Anki/SwiftUI/DecksView.swift | 79 +++++++++++++++++ Simple Anki/SwiftUI/MainView.swift | 37 ++++++++ .../SwiftUI/Managers/RealmManager.swift | 68 +++++++++++++++ Simple Anki/SwiftUI/Models/CardSUI.swift | 25 ++++++ Simple Anki/SwiftUI/Models/DeckSUI.swift | 29 +++++++ Simple Anki/SwiftUI/NewCardView.swift | 31 +++++++ Simple Anki/SwiftUI/NewDeckView.swift | 62 ++++++++++++++ Simple Anki/SwiftUI/SettingsView.swift | 20 +++++ Simple Anki/SwiftUI/SimpleAnkiApp.swift | 17 ++++ 12 files changed, 432 insertions(+), 24 deletions(-) create mode 100644 Simple Anki/SwiftUI/DecksView.swift create mode 100644 Simple Anki/SwiftUI/MainView.swift create mode 100644 Simple Anki/SwiftUI/Managers/RealmManager.swift create mode 100644 Simple Anki/SwiftUI/Models/CardSUI.swift create mode 100644 Simple Anki/SwiftUI/Models/DeckSUI.swift create mode 100644 Simple Anki/SwiftUI/NewCardView.swift create mode 100644 Simple Anki/SwiftUI/NewDeckView.swift create mode 100644 Simple Anki/SwiftUI/SettingsView.swift create mode 100644 Simple Anki/SwiftUI/SimpleAnkiApp.swift diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index b49607f..ff92b59 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ C00648AC2682070200265EB8 /* EmailManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AB2682070200265EB8 /* EmailManager.swift */; }; C00648AF2682140800265EB8 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AE2682140800265EB8 /* Options.swift */; }; C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */; }; + C01290732A8F823700ECF9D7 /* RealmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01290722A8F823700ECF9D7 /* RealmManager.swift */; }; + C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01290742A920A9700ECF9D7 /* NewDeckView.swift */; }; C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348CA2861F47E002FFBDF /* UILabelExtension.swift */; }; C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C02348D2286343F4002FFBDF /* StoreKit.framework */; }; C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */; }; @@ -20,7 +22,14 @@ C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41C2282146BF005E9835 /* DateExtension.swift */; }; C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */; }; C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */; }; + C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */; }; + C067B6872A8C1251000AF881 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6862A8C1251000AF881 /* MainView.swift */; }; + C067B6892A8C139A000AF881 /* DecksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksView.swift */; }; + C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B68A2A8C13A5000AF881 /* SettingsView.swift */; }; C070008E263E86E0006DF020 /* RateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070008D263E86E0006DF020 /* RateManager.swift */; }; + C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581C2A924A0B0046F39D /* NewCardView.swift */; }; + C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903242A8D444F00EFC51C /* DeckSUI.swift */; }; + C08903272A8D474900EFC51C /* CardSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903262A8D474900EFC51C /* CardSUI.swift */; }; C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70E5F28211E620020D533 /* ReminderManager.swift */; }; C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4125EC0E0B005DE503 /* Deck.swift */; }; C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4625EC0E62005DE503 /* Card.swift */; }; @@ -50,13 +59,11 @@ C0C9BAB62848B9F20020E555 /* APKGModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAB52848B9F20020E555 /* APKGModel.swift */; }; C0C9BAB82848BA160020E555 /* ImportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAB72848BA160020E555 /* ImportViewController.swift */; }; C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BABD2848BD460020E555 /* APKGDatabase.swift */; }; - C0C9BAC02848BE2F0020E555 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C0C9BABF2848BE2F0020E555 /* GoogleService-Info.plist */; }; C0C9BAC22848BE960020E555 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = C0C9BAC12848BE960020E555 /* .swiftlint.yml */; }; C0C9BAC52848F4590020E555 /* build.yml in Resources */ = {isa = PBXBuildFile; fileRef = C0C9BAC42848F4590020E555 /* build.yml */; }; C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE86D266AAA9A00B83253 /* Utils.swift */; }; C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE86F266B7A2900B83253 /* PlayerManager.swift */; }; C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE871266BA50900B83253 /* URLExtension.swift */; }; - C0D1C1702815757A00350862 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = C0D1C16F2815757A00350862 /* FirebaseAnalytics */; }; C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1712815A57600350862 /* ReminderViewController.swift */; }; C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */; }; C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DBE8312673E9E00074DC13 /* HapticManager.swift */; }; @@ -102,6 +109,8 @@ C00648AB2682070200265EB8 /* EmailManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManager.swift; sourceTree = ""; }; C00648AE2682140800265EB8 /* Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = ""; }; + C01290722A8F823700ECF9D7 /* RealmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmManager.swift; sourceTree = ""; }; + C01290742A920A9700ECF9D7 /* NewDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckView.swift; sourceTree = ""; }; C02348CA2861F47E002FFBDF /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; C02348D2286343F4002FFBDF /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewController.swift; sourceTree = ""; }; @@ -111,7 +120,14 @@ C02D41C2282146BF005E9835 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSettingsCell.swift; sourceTree = ""; }; + C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnkiApp.swift; sourceTree = ""; }; + C067B6862A8C1251000AF881 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + C067B6882A8C139A000AF881 /* DecksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksView.swift; sourceTree = ""; }; + C067B68A2A8C13A5000AF881 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C070008D263E86E0006DF020 /* RateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateManager.swift; sourceTree = ""; }; + C074581C2A924A0B0046F39D /* NewCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardView.swift; sourceTree = ""; }; + C08903242A8D444F00EFC51C /* DeckSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckSUI.swift; sourceTree = ""; }; + C08903262A8D474900EFC51C /* CardSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardSUI.swift; sourceTree = ""; }; C0A70E5F28211E620020D533 /* ReminderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderManager.swift; sourceTree = ""; }; C0ADAD4125EC0E0B005DE503 /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; C0ADAD4625EC0E62005DE503 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; @@ -143,7 +159,6 @@ C0C9BAB52848B9F20020E555 /* APKGModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APKGModel.swift; sourceTree = ""; }; C0C9BAB72848BA160020E555 /* ImportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportViewController.swift; sourceTree = ""; }; C0C9BABD2848BD460020E555 /* APKGDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APKGDatabase.swift; sourceTree = ""; }; - C0C9BABF2848BE2F0020E555 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; C0C9BAC12848BE960020E555 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; C0C9BAC42848F4590020E555 /* build.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = build.yml; sourceTree = ""; }; C0CBE86D266AAA9A00B83253 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; @@ -180,7 +195,6 @@ C0DD10BB2823F6D10058F96B /* SwiftCSV in Frameworks */, C0B5FFAC25E98362001D8D83 /* Realm in Frameworks */, C0B5FFAA25E98362001D8D83 /* RealmSwift in Frameworks */, - C0D1C1702815757A00350862 /* FirebaseAnalytics in Frameworks */, C0E8477827F9C9EA006AEED1 /* SPIndicator in Frameworks */, C0B85477282ABA14009816D0 /* ZIPFoundation in Frameworks */, ); @@ -214,6 +228,14 @@ path = Cells; sourceTree = ""; }; + C01290712A8F822700ECF9D7 /* Managers */ = { + isa = PBXGroup; + children = ( + C01290722A8F823700ECF9D7 /* RealmManager.swift */, + ); + path = Managers; + sourceTree = ""; + }; C02348D1286343F4002FFBDF /* Frameworks */ = { isa = PBXGroup; children = ( @@ -238,6 +260,21 @@ path = Controllers; sourceTree = ""; }; + C067B6822A8C10D0000AF881 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + C01290712A8F822700ECF9D7 /* Managers */, + C08903232A8D443900EFC51C /* Models */, + C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */, + C067B6862A8C1251000AF881 /* MainView.swift */, + C067B6882A8C139A000AF881 /* DecksView.swift */, + C067B68A2A8C13A5000AF881 /* SettingsView.swift */, + C01290742A920A9700ECF9D7 /* NewDeckView.swift */, + C074581C2A924A0B0046F39D /* NewCardView.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; C078B5F9281EA89500DE5A86 /* Reminder */ = { isa = PBXGroup; children = ( @@ -256,6 +293,15 @@ path = Settings; sourceTree = ""; }; + C08903232A8D443900EFC51C /* Models */ = { + isa = PBXGroup; + children = ( + C08903242A8D444F00EFC51C /* DeckSUI.swift */, + C08903262A8D474900EFC51C /* CardSUI.swift */, + ); + path = Models; + sourceTree = ""; + }; C0ADAD5625EC19D3005DE503 /* Extensions */ = { isa = PBXGroup; children = ( @@ -324,6 +370,7 @@ C0B5FF7425E981A8001D8D83 /* Simple Anki */ = { isa = PBXGroup; children = ( + C067B6822A8C10D0000AF881 /* SwiftUI */, C0C9BAA72848B9410020E555 /* APKG */, C0F4FD4427BD86BF00814BD6 /* Protocols */, C0AF0E7B2614F1AD00853FA7 /* Managers */, @@ -336,7 +383,6 @@ C0B5FF7E25E981AF001D8D83 /* Assets.xcassets */, C0B5FF8025E981AF001D8D83 /* LaunchScreen.storyboard */, C0B5FF8325E981AF001D8D83 /* Info.plist */, - C0C9BABF2848BE2F0020E555 /* GoogleService-Info.plist */, C0FA7EA925F511FE00710F0D /* Constants.swift */, ); path = "Simple Anki"; @@ -484,7 +530,6 @@ C0B5FFA925E98362001D8D83 /* RealmSwift */, C0B5FFAB25E98362001D8D83 /* Realm */, C0E8477727F9C9EA006AEED1 /* SPIndicator */, - C0D1C16F2815757A00350862 /* FirebaseAnalytics */, C0DD10BA2823F6D10058F96B /* SwiftCSV */, C0B85473282AB18F009816D0 /* SQLite */, C0B85476282ABA14009816D0 /* ZIPFoundation */, @@ -535,8 +580,9 @@ C0B5FF6A25E981A8001D8D83 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1340; + LastUpgradeCheck = 1430; TargetAttributes = { C0B5FF7125E981A8001D8D83 = { CreatedOnToolsVersion = 12.4; @@ -563,7 +609,6 @@ packageReferences = ( C0B5FFA825E98362001D8D83 /* XCRemoteSwiftPackageReference "realm-cocoa" */, C0E8477627F9C9EA006AEED1 /* XCRemoteSwiftPackageReference "SPIndicator" */, - C0D1C16E2815757A00350862 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, C0DD10B92823F6D10058F96B /* XCRemoteSwiftPackageReference "SwiftCSV" */, C0B85472282AB18F009816D0 /* XCRemoteSwiftPackageReference "SQLite.swift" */, C0B85475282ABA14009816D0 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, @@ -588,7 +633,6 @@ C0C9BA672848A3B30020E555 /* (null) in Resources */, C0C9BAC52848F4590020E555 /* build.yml in Resources */, C028C4C8280C852400F6894E /* .gitignore in Resources */, - C0C9BAC02848BE2F0020E555 /* GoogleService-Info.plist in Resources */, C0B5FF8225E981AF001D8D83 /* LaunchScreen.storyboard in Resources */, C0B5FF7F25E981AF001D8D83 /* Assets.xcassets in Resources */, ); @@ -641,15 +685,20 @@ C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */, C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */, C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */, + C067B6892A8C139A000AF881 /* DecksView.swift in Sources */, C0FA7EAA25F511FE00710F0D /* Constants.swift in Sources */, C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, + C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */, C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */, C0B1275527C3F0E7008E412D /* DecksViewModel.swift in Sources */, C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */, + C01290732A8F823700ECF9D7 /* RealmManager.swift in Sources */, + C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */, C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */, C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */, C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */, + C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */, C0B1275E27C3FC4A008E412D /* DecksTableViewController.swift in Sources */, C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */, C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */, @@ -660,6 +709,8 @@ C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */, C070008E263E86E0006DF020 /* RateManager.swift in Sources */, C0C9BAA42848B9120020E555 /* ImportedCardCollectionViewCell.swift in Sources */, + C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */, + C067B6872A8C1251000AF881 /* MainView.swift in Sources */, C0B1275727C3FB7A008E412D /* CardsTableViewController.swift in Sources */, C00648AC2682070200265EB8 /* EmailManager.swift in Sources */, C0C081B425ED379600DF1083 /* StorageManager.swift in Sources */, @@ -675,11 +726,13 @@ C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */, C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */, C0C9BAB82848BA160020E555 /* ImportViewController.swift in Sources */, + C08903272A8D474900EFC51C /* CardSUI.swift in Sources */, C0B5FF7625E981A8001D8D83 /* AppDelegate.swift in Sources */, C00648AF2682140800265EB8 /* Options.swift in Sources */, C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */, C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, + C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */, C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */, C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */, ); @@ -1058,14 +1111,6 @@ minimumVersion = 0.9.9; }; }; - C0D1C16E2815757A00350862 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 8.0.0; - }; - }; C0DD10B92823F6D10058F96B /* XCRemoteSwiftPackageReference "SwiftCSV" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/swiftcsv/SwiftCSV.git"; @@ -1105,11 +1150,6 @@ package = C0B85475282ABA14009816D0 /* XCRemoteSwiftPackageReference "ZIPFoundation" */; productName = ZIPFoundation; }; - C0D1C16F2815757A00350862 /* FirebaseAnalytics */ = { - isa = XCSwiftPackageProductDependency; - package = C0D1C16E2815757A00350862 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; - productName = FirebaseAnalytics; - }; C0DD10BA2823F6D10058F96B /* SwiftCSV */ = { isa = XCSwiftPackageProductDependency; package = C0DD10B92823F6D10058F96B /* XCRemoteSwiftPackageReference "SwiftCSV" */; diff --git a/Simple Anki.xcodeproj/xcshareddata/xcschemes/Simple Anki.xcscheme b/Simple Anki.xcodeproj/xcshareddata/xcschemes/Simple Anki.xcscheme index ded1275..0d96bca 100644 --- a/Simple Anki.xcodeproj/xcshareddata/xcschemes/Simple Anki.xcscheme +++ b/Simple Anki.xcodeproj/xcshareddata/xcschemes/Simple Anki.xcscheme @@ -1,6 +1,6 @@ Void) { + do { + try realm?.write { + realm?.add(deck) + completion(true) + } + } catch { + print("Error: \(error)") + completion(false) + } + } + + func loadDecks() { + let allDecks = realm?.objects(Deck.self).sorted(byKeyPath: "dateCreated") + decks = [] + allDecks?.forEach { deck in + decks.append(deck) + } + } + + func getDecks() { + if let realm { + let allDecks = realm.objects(Deck.self).sorted(byKeyPath: "dateCreated") + decks = [] + allDecks.forEach { deck in + decks.append(deck) + } + } + } +} diff --git a/Simple Anki/SwiftUI/Models/CardSUI.swift b/Simple Anki/SwiftUI/Models/CardSUI.swift new file mode 100644 index 0000000..cce6db9 --- /dev/null +++ b/Simple Anki/SwiftUI/Models/CardSUI.swift @@ -0,0 +1,25 @@ +// +// Card.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + + import Foundation + import RealmSwift + +class Card: Object, ObjectKeyIdentifiable { + @Persisted(primaryKey: true) var _id: ObjectId + @Persisted var front: String + @Persisted var back: String + @Persisted var dateCreated: Date = Date() + @Persisted var audioName: String? + @Persisted var memorized: Bool = false + @Persisted(originProperty: "cards") var deck: LinkingObjects + + convenience init(front: String, back: String) { + self.init() + self.front = front + self.back = back + } +} diff --git a/Simple Anki/SwiftUI/Models/DeckSUI.swift b/Simple Anki/SwiftUI/Models/DeckSUI.swift new file mode 100644 index 0000000..405128b --- /dev/null +++ b/Simple Anki/SwiftUI/Models/DeckSUI.swift @@ -0,0 +1,29 @@ +// +// Deck.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import Foundation +import RealmSwift + +// enum Layout: String, PersistableEnum, CaseIterable { +// case frontToBack +// case backToFront +// case all +// } + +class Deck: Object, ObjectKeyIdentifiable { + @Persisted(primaryKey: true) var _id: ObjectId + @Persisted var name: String + @Persisted var dateCreated: Date = Date() + @Persisted var layout: String = "frontToBack" + @Persisted var autoplay: Bool = false + @Persisted var cards: List + + convenience init(name: String) { + self.init() + self.name = name + } +} diff --git a/Simple Anki/SwiftUI/NewCardView.swift b/Simple Anki/SwiftUI/NewCardView.swift new file mode 100644 index 0000000..5dc76c9 --- /dev/null +++ b/Simple Anki/SwiftUI/NewCardView.swift @@ -0,0 +1,31 @@ +// +// NewCardView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 20.08.2023. +// + +import SwiftUI + +struct NewCardView: View { + @Environment(\.dismiss) private var dismiss + @Binding var isPopoverPresented: Bool + + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + isPopoverPresented = false + } + } + } + } +} + +struct NewCardView_Previews: PreviewProvider { + @State static var isPresented: Bool = true + static var previews: some View { + NewCardView(isPopoverPresented: $isPresented) + } +} diff --git a/Simple Anki/SwiftUI/NewDeckView.swift b/Simple Anki/SwiftUI/NewDeckView.swift new file mode 100644 index 0000000..f9a346f --- /dev/null +++ b/Simple Anki/SwiftUI/NewDeckView.swift @@ -0,0 +1,62 @@ +// +// NewDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 20.08.2023. +// + +import SwiftUI + +struct NewDeckView: View { + @State private var deckNameText: String = "" + @FocusState private var isTextFieldFocused: Bool + @Binding var isPopoverPresented: Bool + + var body: some View { + NavigationView { + VStack { + TextField("Deck name", text: $deckNameText) + .background() + .font(.system(size: 35, weight: .bold)) + .padding(.top, 160) + .focused($isTextFieldFocused) + Spacer() + NavigationLink { + NewCardView(isPopoverPresented: $isPopoverPresented) + } label: { + HStack { + Text("Add cards") + Image(systemName: "arrow.right") + } + .padding(8) + .frame(maxWidth: 280) + } + .disabled(deckNameText.isEmpty) + .buttonStyle(.borderedProminent) + } + .onAppear { + isTextFieldFocused.toggle() + } + .navigationTitle(deckNameText) + .navigationBarTitleDisplayMode(.inline) + .padding() + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + if !deckNameText.isEmpty { + + } + isPopoverPresented.toggle() + } + } + } + } + } +} + +struct NewDeckView_Previews: PreviewProvider { + @State static var isPresented: Bool = true + static var previews: some View { + NewDeckView(isPopoverPresented: $isPresented) + } +} diff --git a/Simple Anki/SwiftUI/SettingsView.swift b/Simple Anki/SwiftUI/SettingsView.swift new file mode 100644 index 0000000..37cd6c9 --- /dev/null +++ b/Simple Anki/SwiftUI/SettingsView.swift @@ -0,0 +1,20 @@ +// +// SettingsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI + +struct SettingsView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView() + } +} diff --git a/Simple Anki/SwiftUI/SimpleAnkiApp.swift b/Simple Anki/SwiftUI/SimpleAnkiApp.swift new file mode 100644 index 0000000..c96dd25 --- /dev/null +++ b/Simple Anki/SwiftUI/SimpleAnkiApp.swift @@ -0,0 +1,17 @@ +// +// SimpleAnkiApp.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + + import SwiftUI + + @main + struct SimpleAnkiApp: App { + var body: some Scene { + WindowGroup { + MainView() + } + } + } From 4d42feb88fb7e5a9da28cb155ee9f31f9dcbc796 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 3 Sep 2023 11:53:19 +0400 Subject: [PATCH 2/9] added review view --- Simple Anki.xcodeproj/project.pbxproj | 68 ++++++++++++++++---- Simple Anki/SwiftUI/ReviewView.swift | 91 +++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 Simple Anki/SwiftUI/ReviewView.swift diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index ff92b59..7e7fc46 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -27,9 +27,19 @@ C067B6892A8C139A000AF881 /* DecksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksView.swift */; }; C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B68A2A8C13A5000AF881 /* SettingsView.swift */; }; C070008E263E86E0006DF020 /* RateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070008D263E86E0006DF020 /* RateManager.swift */; }; - C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581C2A924A0B0046F39D /* NewCardView.swift */; }; + C074581D2A924A0B0046F39D /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581C2A924A0B0046F39D /* CardView.swift */; }; + C074581F2A9293AA0046F39D /* CardsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581E2A9293AA0046F39D /* CardsView.swift */; }; + C07458232A938CF90046F39D /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458222A938CF90046F39D /* UserSettings.swift */; }; + C07458252A93943E0046F39D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458242A93943E0046F39D /* Constants.swift */; }; + C07458282A939FA40046F39D /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458272A939FA40046F39D /* LinkView.swift */; }; + C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */; }; + C0750ED02AA3A60400089B29 /* RecorderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ECF2AA3A60400089B29 /* RecorderManager.swift */; }; + C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED32AA3C01800089B29 /* ReviewView.swift */; }; + C0750ED62AA3C39400089B29 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED52AA3C39400089B29 /* CircleButton.swift */; }; + C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */; }; C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903242A8D444F00EFC51C /* DeckSUI.swift */; }; C08903272A8D474900EFC51C /* CardSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903262A8D474900EFC51C /* CardSUI.swift */; }; + C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0C6492A9DFC210015C65E /* SoundManager.swift */; }; C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70E5F28211E620020D533 /* ReminderManager.swift */; }; C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4125EC0E0B005DE503 /* Deck.swift */; }; C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4625EC0E62005DE503 /* Card.swift */; }; @@ -68,7 +78,6 @@ C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */; }; C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DBE8312673E9E00074DC13 /* HapticManager.swift */; }; C0DD10BB2823F6D10058F96B /* SwiftCSV in Frameworks */ = {isa = PBXBuildFile; productRef = C0DD10BA2823F6D10058F96B /* SwiftCSV */; }; - C0DDE0F32658608C0051059B /* RecorderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DDE0F22658608C0051059B /* RecorderManager.swift */; }; C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */; }; C0E8477827F9C9EA006AEED1 /* SPIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = C0E8477727F9C9EA006AEED1 /* SPIndicator */; }; C0F4FD1527BD82DF00814BD6 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */; }; @@ -84,7 +93,7 @@ C0F4FD5127BD87CE00814BD6 /* DecksScrenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD4F27BD87CE00814BD6 /* DecksScrenTests.swift */; }; C0F4FD5527BD87EB00814BD6 /* WaitUtillities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD5327BD87EB00814BD6 /* WaitUtillities.swift */; }; C0F4FD5627BD87EB00814BD6 /* RandomGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD5427BD87EB00814BD6 /* RandomGenerator.swift */; }; - C0FA7EAA25F511FE00710F0D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FA7EA925F511FE00710F0D /* Constants.swift */; }; + C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FA7EA925F511FE00710F0D /* ConstantsOld.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -125,9 +134,19 @@ C067B6882A8C139A000AF881 /* DecksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksView.swift; sourceTree = ""; }; C067B68A2A8C13A5000AF881 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C070008D263E86E0006DF020 /* RateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateManager.swift; sourceTree = ""; }; - C074581C2A924A0B0046F39D /* NewCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardView.swift; sourceTree = ""; }; + C074581C2A924A0B0046F39D /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; + C074581E2A9293AA0046F39D /* CardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsView.swift; sourceTree = ""; }; + C07458222A938CF90046F39D /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; + C07458242A93943E0046F39D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + C07458272A939FA40046F39D /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = ""; }; + C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManagerSUI.swift; sourceTree = ""; }; + C0750ECF2AA3A60400089B29 /* RecorderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderManager.swift; sourceTree = ""; }; + C0750ED32AA3C01800089B29 /* ReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewView.swift; sourceTree = ""; }; + C0750ED52AA3C39400089B29 /* CircleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleButton.swift; sourceTree = ""; }; + C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewManagerSUI.swift; sourceTree = ""; }; C08903242A8D444F00EFC51C /* DeckSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckSUI.swift; sourceTree = ""; }; C08903262A8D474900EFC51C /* CardSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardSUI.swift; sourceTree = ""; }; + C0A0C6492A9DFC210015C65E /* SoundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundManager.swift; sourceTree = ""; }; C0A70E5F28211E620020D533 /* ReminderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderManager.swift; sourceTree = ""; }; C0ADAD4125EC0E0B005DE503 /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; C0ADAD4625EC0E62005DE503 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; @@ -167,7 +186,6 @@ C0D1C1712815A57600350862 /* ReminderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderViewController.swift; sourceTree = ""; }; C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewCell.swift; sourceTree = ""; }; C0DBE8312673E9E00074DC13 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; - C0DDE0F22658608C0051059B /* RecorderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderManager.swift; sourceTree = ""; }; C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardViewController.swift; sourceTree = ""; }; C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; C0F4FD1B27BD84DA00814BD6 /* SettingsViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewCell.swift; sourceTree = ""; }; @@ -182,7 +200,7 @@ C0F4FD4F27BD87CE00814BD6 /* DecksScrenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecksScrenTests.swift; sourceTree = ""; }; C0F4FD5327BD87EB00814BD6 /* WaitUtillities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitUtillities.swift; sourceTree = ""; }; C0F4FD5427BD87EB00814BD6 /* RandomGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RandomGenerator.swift; sourceTree = ""; }; - C0FA7EA925F511FE00710F0D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + C0FA7EA925F511FE00710F0D /* ConstantsOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsOld.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -232,6 +250,11 @@ isa = PBXGroup; children = ( C01290722A8F823700ECF9D7 /* RealmManager.swift */, + C07458222A938CF90046F39D /* UserSettings.swift */, + C0A0C6492A9DFC210015C65E /* SoundManager.swift */, + C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */, + C0750ECF2AA3A60400089B29 /* RecorderManager.swift */, + C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */, ); path = Managers; sourceTree = ""; @@ -263,18 +286,31 @@ C067B6822A8C10D0000AF881 /* SwiftUI */ = { isa = PBXGroup; children = ( + C07458262A939F800046F39D /* UIComponents */, C01290712A8F822700ECF9D7 /* Managers */, C08903232A8D443900EFC51C /* Models */, C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */, C067B6862A8C1251000AF881 /* MainView.swift */, C067B6882A8C139A000AF881 /* DecksView.swift */, + C074581E2A9293AA0046F39D /* CardsView.swift */, C067B68A2A8C13A5000AF881 /* SettingsView.swift */, C01290742A920A9700ECF9D7 /* NewDeckView.swift */, - C074581C2A924A0B0046F39D /* NewCardView.swift */, + C074581C2A924A0B0046F39D /* CardView.swift */, + C07458242A93943E0046F39D /* Constants.swift */, + C0750ED32AA3C01800089B29 /* ReviewView.swift */, ); path = SwiftUI; sourceTree = ""; }; + C07458262A939F800046F39D /* UIComponents */ = { + isa = PBXGroup; + children = ( + C0750ED52AA3C39400089B29 /* CircleButton.swift */, + C07458272A939FA40046F39D /* LinkView.swift */, + ); + path = UIComponents; + sourceTree = ""; + }; C078B5F9281EA89500DE5A86 /* Reminder */ = { isa = PBXGroup; children = ( @@ -323,7 +359,6 @@ C0C081B325ED379600DF1083 /* StorageManager.swift */, C0AF0E7C2614F1E000853FA7 /* ReviewManager.swift */, C070008D263E86E0006DF020 /* RateManager.swift */, - C0DDE0F22658608C0051059B /* RecorderManager.swift */, C0DBE8312673E9E00074DC13 /* HapticManager.swift */, C00648AB2682070200265EB8 /* EmailManager.swift */, C0A70E5F28211E620020D533 /* ReminderManager.swift */, @@ -383,7 +418,7 @@ C0B5FF7E25E981AF001D8D83 /* Assets.xcassets */, C0B5FF8025E981AF001D8D83 /* LaunchScreen.storyboard */, C0B5FF8325E981AF001D8D83 /* Info.plist */, - C0FA7EA925F511FE00710F0D /* Constants.swift */, + C0FA7EA925F511FE00710F0D /* ConstantsOld.swift */, ); path = "Simple Anki"; sourceTree = ""; @@ -685,8 +720,9 @@ C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */, C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */, C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */, + C07458252A93943E0046F39D /* Constants.swift in Sources */, C067B6892A8C139A000AF881 /* DecksView.swift in Sources */, - C0FA7EAA25F511FE00710F0D /* Constants.swift in Sources */, + C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */, C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, @@ -694,11 +730,14 @@ C0B1275527C3F0E7008E412D /* DecksViewModel.swift in Sources */, C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */, C01290732A8F823700ECF9D7 /* RealmManager.swift in Sources */, - C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */, + C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */, + C0750ED02AA3A60400089B29 /* RecorderManager.swift in Sources */, + C074581D2A924A0B0046F39D /* CardView.swift in Sources */, C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */, C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */, C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */, C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */, + C0750ED62AA3C39400089B29 /* CircleButton.swift in Sources */, C0B1275E27C3FC4A008E412D /* DecksTableViewController.swift in Sources */, C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */, C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */, @@ -708,6 +747,7 @@ C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */, C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */, C070008E263E86E0006DF020 /* RateManager.swift in Sources */, + C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */, C0C9BAA42848B9120020E555 /* ImportedCardCollectionViewCell.swift in Sources */, C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */, C067B6872A8C1251000AF881 /* MainView.swift in Sources */, @@ -717,11 +757,12 @@ C0B6D153285DE18900E354BA /* OnboardingViewController.swift in Sources */, C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */, C0F4FD1527BD82DF00814BD6 /* UIViewExtension.swift in Sources */, - C0DDE0F32658608C0051059B /* RecorderManager.swift in Sources */, C0B1275D27C3FB91008E412D /* ReviewViewController.swift in Sources */, + C07458232A938CF90046F39D /* UserSettings.swift in Sources */, C0B6D156285E2DFD00E354BA /* FeatureView.swift in Sources */, C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */, C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */, + C074581F2A9293AA0046F39D /* CardsView.swift in Sources */, C0F4FD3127BD855A00814BD6 /* MainTabBarViewController.swift in Sources */, C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */, C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */, @@ -732,8 +773,11 @@ C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */, C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, + C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */, C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */, C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */, + C07458282A939FA40046F39D /* LinkView.swift in Sources */, + C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */, C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Simple Anki/SwiftUI/ReviewView.swift b/Simple Anki/SwiftUI/ReviewView.swift new file mode 100644 index 0000000..8045720 --- /dev/null +++ b/Simple Anki/SwiftUI/ReviewView.swift @@ -0,0 +1,91 @@ +// +// ReviewView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 02.09.2023. +// + +import SwiftUI + +struct ReviewView: View { + @Environment(\.dismiss) var dismiss + @State private var frontText: String = "" + @State private var backText: String = "" + @State private var isShowAnswer: Bool = false + @StateObject var reviewManager: ReviewManagerSUI + + var body: some View { + NavigationView { + ZStack { + VStack { + if reviewManager.isReviewing { + Text(reviewManager.currentCard?.front ?? "") + + if isShowAnswer { + Divider() + Text(reviewManager.currentCard?.back ?? "") + } + } else { + Text("Finished!") + .font(.system(size: 60, weight: .bold)) + } + } + .scaledToFit() + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + .onTapGesture { + SoundManager.shared.play(sound: reviewManager.currentCard?.audioName ?? "") + } + .font(.system(size: 40, weight: .medium)) + .padding() + .onAppear { + reviewManager.startReview() + } + + VStack { + Spacer() + if reviewManager.isReviewing { + Button { + if isShowAnswer { + reviewManager.nextCard() + isShowAnswer = false + } else { + isShowAnswer = true + } + HapticManagerSUI.shared.impact(style: .medium) + } label: { + Image(systemName: isShowAnswer ? "arrow.right.circle.fill" : "eye.circle.fill") + } + } else { + Button { + reviewManager.startReview() + isShowAnswer = false + HapticManagerSUI.shared.impact(style: .medium) + } label: { + Image(systemName: "repeat.circle.fill") + } + } + } + .font(.system(size: 70)) + .foregroundColor(.gray.opacity(0.7)) + .padding(.bottom) + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .foregroundColor(.gray) + } + } + } + } + } +} + +struct ReviewView_Previews: PreviewProvider { + static var previews: some View { + ReviewView(reviewManager: ReviewManagerSUI(deck: Deck.deck2)) + } +} From 7297b01b29175ae9e4b7e227d2ccb3508dba4a71 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Fri, 12 Jan 2024 20:29:35 +0400 Subject: [PATCH 3/9] new card view UI --- .../{Constants.swift => ConstantsOld.swift} | 0 Simple Anki/GoogleService-Info.plist | 34 ---- Simple Anki/Managers/RecorderManager.swift | 15 -- Simple Anki/SwiftUI/CircleButton.swift | 8 + Simple Anki/SwiftUI/DecksView.swift | 79 --------- .../SwiftUI/Managers/AudioRecorder.swift | 92 ++++++++++ .../SwiftUI/Managers/HapticManagerSUI.swift | 25 +++ .../SwiftUI/Managers/LocalFileManager.swift | 8 + .../SwiftUI/Managers/RealmManager.swift | 68 -------- .../SwiftUI/Managers/ReviewManagerSUI.swift | 8 + .../SwiftUI/Managers/SoundManager.swift | 8 + .../SwiftUI/Managers/UserSettings.swift | 8 + Simple Anki/SwiftUI/NewCardView.swift | 31 ---- Simple Anki/SwiftUI/SettingsView.swift | 20 --- .../UI/UIComponents/CardToolbarView.swift | 18 ++ .../UI/UIComponents/CircleButton.swift | 19 +++ .../UI/UIComponents/ImagePickerButton.swift | 18 ++ .../SwiftUI/UI/UIComponents/LinkView.swift | 39 +++++ .../UI/UIComponents/RecordingButton.swift | 64 +++++++ .../SwiftUI/UI/Views/CardPreviewView.swift | 96 +++++++++++ Simple Anki/SwiftUI/UI/Views/CardView.swift | 157 ++++++++++++++++++ Simple Anki/SwiftUI/UI/Views/CardsView.swift | 115 +++++++++++++ Simple Anki/SwiftUI/UI/Views/Constants.swift | 15 ++ .../SwiftUI/UI/Views/ContentView.swift | 63 +++++++ Simple Anki/SwiftUI/UI/Views/DecksView.swift | 101 +++++++++++ .../SwiftUI/{ => UI/Views}/MainView.swift | 9 +- .../SwiftUI/UI/Views/NewCardView.swift | 144 ++++++++++++++++ .../SwiftUI/{ => UI/Views}/NewDeckView.swift | 40 +++-- .../SwiftUI/{ => UI/Views}/ReviewView.swift | 29 +++- .../SwiftUI/UI/Views/SettingsView.swift | 50 ++++++ .../SwiftUI/ViewModels/CardViewModel.swift | 8 + 31 files changed, 1120 insertions(+), 269 deletions(-) rename Simple Anki/{Constants.swift => ConstantsOld.swift} (100%) delete mode 100644 Simple Anki/GoogleService-Info.plist delete mode 100644 Simple Anki/Managers/RecorderManager.swift create mode 100644 Simple Anki/SwiftUI/CircleButton.swift delete mode 100644 Simple Anki/SwiftUI/DecksView.swift create mode 100644 Simple Anki/SwiftUI/Managers/AudioRecorder.swift create mode 100644 Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift create mode 100644 Simple Anki/SwiftUI/Managers/LocalFileManager.swift delete mode 100644 Simple Anki/SwiftUI/Managers/RealmManager.swift create mode 100644 Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift create mode 100644 Simple Anki/SwiftUI/Managers/SoundManager.swift create mode 100644 Simple Anki/SwiftUI/Managers/UserSettings.swift delete mode 100644 Simple Anki/SwiftUI/NewCardView.swift delete mode 100644 Simple Anki/SwiftUI/SettingsView.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/CardToolbarView.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/CircleButton.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/LinkView.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/CardView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/CardsView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Constants.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/ContentView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/DecksView.swift rename Simple Anki/SwiftUI/{ => UI/Views}/MainView.swift (73%) create mode 100644 Simple Anki/SwiftUI/UI/Views/NewCardView.swift rename Simple Anki/SwiftUI/{ => UI/Views}/NewDeckView.swift (54%) rename Simple Anki/SwiftUI/{ => UI/Views}/ReviewView.swift (76%) create mode 100644 Simple Anki/SwiftUI/UI/Views/SettingsView.swift create mode 100644 Simple Anki/SwiftUI/ViewModels/CardViewModel.swift diff --git a/Simple Anki/Constants.swift b/Simple Anki/ConstantsOld.swift similarity index 100% rename from Simple Anki/Constants.swift rename to Simple Anki/ConstantsOld.swift diff --git a/Simple Anki/GoogleService-Info.plist b/Simple Anki/GoogleService-Info.plist deleted file mode 100644 index 3c83329..0000000 --- a/Simple Anki/GoogleService-Info.plist +++ /dev/null @@ -1,34 +0,0 @@ - - - - - CLIENT_ID - 592209129003-hlno590ddsr1q5oobc2tmnpor9og4nof.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.592209129003-hlno590ddsr1q5oobc2tmnpor9og4nof - API_KEY - AIzaSyCi4oR1kvtqg8KSeBCpZChhYuTHrxds6cQ - GCM_SENDER_ID - 592209129003 - PLIST_VERSION - 1 - BUNDLE_ID - nart.SimpleAnki - PROJECT_ID - simple-anki-166ea - STORAGE_BUCKET - simple-anki-166ea.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:592209129003:ios:bd6a5413b089da9568446e - - \ No newline at end of file diff --git a/Simple Anki/Managers/RecorderManager.swift b/Simple Anki/Managers/RecorderManager.swift deleted file mode 100644 index 5cda94d..0000000 --- a/Simple Anki/Managers/RecorderManager.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AudioManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.05.2021. -// - -import Foundation -import AVFoundation - -class RecorderManager: NSObject, AVAudioRecorderDelegate { - static let shared = RecorderManager() - - private override init() { } -} diff --git a/Simple Anki/SwiftUI/CircleButton.swift b/Simple Anki/SwiftUI/CircleButton.swift new file mode 100644 index 0000000..26e8626 --- /dev/null +++ b/Simple Anki/SwiftUI/CircleButton.swift @@ -0,0 +1,8 @@ +// +// CircleButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 02.09.2023. +// + +import Foundation diff --git a/Simple Anki/SwiftUI/DecksView.swift b/Simple Anki/SwiftUI/DecksView.swift deleted file mode 100644 index 06f1cb4..0000000 --- a/Simple Anki/SwiftUI/DecksView.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// DecksView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 16.08.2023. -// - -import SwiftUI - -struct DecksView: View { - @EnvironmentObject var realmManager: RealmManager - @State private var isPopoverPresented: Bool = false - - var body: some View { - NavigationView { - VStack { - if realmManager.decks.isEmpty { - Spacer() - - VStack(spacing: 10) { - Image(systemName: "tray") - .font(.system(size: 120, weight: .light)) - Text("There are no decks yet") - } - .foregroundColor(.gray.opacity(0.7)) - - Spacer() - - Button { - isPopoverPresented.toggle() - } label: { - Text("Add deck") - .padding(8) - .frame(maxWidth: 280) - } - .buttonStyle(.borderedProminent) - .padding(.bottom, 40) - .popover(isPresented: $isPopoverPresented) { - NewDeckView(isPopoverPresented: $isPopoverPresented) - } - } else { - List { - ForEach(realmManager.decks) { deck in - Text(deck.name) - } - } - .onAppear { - print(realmManager.decks) - } - } - } - .navigationTitle("Decks") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - print("Test") - } label: { - Image(systemName: "tray.and.arrow.down") - } - Button { - isPopoverPresented.toggle() - } label: { - Image(systemName: "plus") - } - .popover(isPresented: $isPopoverPresented) { - NewDeckView(isPopoverPresented: $isPopoverPresented) - } - } - } - } - } -} - -struct DecksView_Previews: PreviewProvider { - static var previews: some View { - DecksView() - .environmentObject(RealmManager()) - } -} diff --git a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift new file mode 100644 index 0000000..5668e84 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift @@ -0,0 +1,92 @@ +// +// RecorderManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 02.09.2023. +// + +import Foundation +import AVFoundation + +class AudioRecorder: ObservableObject { + private var audioRecorder: AVAudioRecorder? + + @Published var isRecording = false + @Published var audioURL: URL? + private var audioName: String? + + private var settings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue, + AVSampleRateKey: 44100.0, + AVNumberOfChannelsKey: 2 + ] + + private func setupAudioRecorder() { + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.record, mode: .default) + try audioSession.setActive(true) + + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + guard let audioName = audioName else { return } + let audioFileURL = documentsDirectory.appendingPathComponent("\(audioName).m4a") + + audioRecorder = try AVAudioRecorder(url: audioFileURL, settings: settings) + audioRecorder?.prepareToRecord() + } catch { + print("Error setting up audio recorder: \(error.localizedDescription)") + } + } + + func startRecording() { + self.setupAudioRecorder() + guard let audioRecorder = audioRecorder else { return } + + if !audioRecorder.isRecording { + do { + try AVAudioSession.sharedInstance().setActive(true) + audioRecorder.record() + isRecording = true + } catch { + print("Error starting recording: \(error.localizedDescription)") + } + } + } + + func stopRecording() { + if let audioRecorder = audioRecorder, audioRecorder.isRecording { + audioRecorder.stop() + isRecording = false + audioURL = audioRecorder.url + } + } + + func isMicAccessGranted() -> Bool { + var permissionCheck: Bool = false + switch AVAudioSession.sharedInstance().recordPermission { + case .granted: + permissionCheck = true + case .denied: + permissionCheck = false + case .undetermined: + AVAudioSession.sharedInstance().requestRecordPermission { granted in + permissionCheck = granted + } + @unknown default: + break + } + return permissionCheck + } +} + +extension AudioRecorder { + + func generateAudioName() { + self.audioName = UUID().uuidString + } + + func setAudioName(_ name: String) { + self.audioName = name + } +} diff --git a/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift b/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift new file mode 100644 index 0000000..b5f94dd --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift @@ -0,0 +1,25 @@ +// +// HapticManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 30.08.2023. +// + +import Foundation +import SwiftUI + + +class HapticManagerSUI { + + static let shared = HapticManagerSUI() + + func notification(type: UINotificationFeedbackGenerator.FeedbackType) { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(type) + } + + func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) { + let generator = UIImpactFeedbackGenerator(style: style) + generator.impactOccurred() + } +} diff --git a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift new file mode 100644 index 0000000..3488d7e --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift @@ -0,0 +1,8 @@ +// +// LocalFileManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 06.09.2023. +// + +import Foundation diff --git a/Simple Anki/SwiftUI/Managers/RealmManager.swift b/Simple Anki/SwiftUI/Managers/RealmManager.swift deleted file mode 100644 index 3af1e1f..0000000 --- a/Simple Anki/SwiftUI/Managers/RealmManager.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// RealmManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 18.08.2023. -// - -import Foundation -import RealmSwift - -protocol RealmProtocol { - func save(deck: Deck) - func save(_ card: Card, to deck: Deck) - func delete(deck: Deck) - func delete(card: Card) - func update(deck: Deck) - func update(card: Card) -} - -class RealmManager: ObservableObject { - private(set) var realm: Realm? - @Published private(set) var decks: [Deck] = [] - - init() { - openRealm() - loadDecks() - } - - func openRealm() { - do { - let config = Realm.Configuration(schemaVersion: 1) - Realm.Configuration.defaultConfiguration = config - realm = try Realm() - } catch { - print("Error opening Realm: \(error)") - } - } - - func save(deck: Deck, completion: @escaping (Bool) -> Void) { - do { - try realm?.write { - realm?.add(deck) - completion(true) - } - } catch { - print("Error: \(error)") - completion(false) - } - } - - func loadDecks() { - let allDecks = realm?.objects(Deck.self).sorted(byKeyPath: "dateCreated") - decks = [] - allDecks?.forEach { deck in - decks.append(deck) - } - } - - func getDecks() { - if let realm { - let allDecks = realm.objects(Deck.self).sorted(byKeyPath: "dateCreated") - decks = [] - allDecks.forEach { deck in - decks.append(deck) - } - } - } -} diff --git a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift new file mode 100644 index 0000000..e69f116 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift @@ -0,0 +1,8 @@ +// +// ReviewManagerSUI.swift +// Simple Anki +// +// Created by Астемир Бозиев on 03.09.2023. +// + +import Foundation diff --git a/Simple Anki/SwiftUI/Managers/SoundManager.swift b/Simple Anki/SwiftUI/Managers/SoundManager.swift new file mode 100644 index 0000000..d29674f --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/SoundManager.swift @@ -0,0 +1,8 @@ +// +// SoundManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 29.08.2023. +// + +import Foundation diff --git a/Simple Anki/SwiftUI/Managers/UserSettings.swift b/Simple Anki/SwiftUI/Managers/UserSettings.swift new file mode 100644 index 0000000..8a24c86 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/UserSettings.swift @@ -0,0 +1,8 @@ +// +// UserSettings.swift +// Simple Anki +// +// Created by Астемир Бозиев on 21.08.2023. +// + +import Foundation diff --git a/Simple Anki/SwiftUI/NewCardView.swift b/Simple Anki/SwiftUI/NewCardView.swift deleted file mode 100644 index 5dc76c9..0000000 --- a/Simple Anki/SwiftUI/NewCardView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// NewCardView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 20.08.2023. -// - -import SwiftUI - -struct NewCardView: View { - @Environment(\.dismiss) private var dismiss - @Binding var isPopoverPresented: Bool - - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Save") { - isPopoverPresented = false - } - } - } - } -} - -struct NewCardView_Previews: PreviewProvider { - @State static var isPresented: Bool = true - static var previews: some View { - NewCardView(isPopoverPresented: $isPresented) - } -} diff --git a/Simple Anki/SwiftUI/SettingsView.swift b/Simple Anki/SwiftUI/SettingsView.swift deleted file mode 100644 index 37cd6c9..0000000 --- a/Simple Anki/SwiftUI/SettingsView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SettingsView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 16.08.2023. -// - -import SwiftUI - -struct SettingsView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() - } -} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/CardToolbarView.swift b/Simple Anki/SwiftUI/UI/UIComponents/CardToolbarView.swift new file mode 100644 index 0000000..5826591 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/CardToolbarView.swift @@ -0,0 +1,18 @@ +// +// CardToolbarView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.01.2024. +// + +import SwiftUI + +struct CardToolbarView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + CardToolbarView() +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/CircleButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/CircleButton.swift new file mode 100644 index 0000000..1160289 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/CircleButton.swift @@ -0,0 +1,19 @@ +// +// CircleButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 02.09.2023. +// + +import Foundation +import SwiftUI + +struct CircleButton: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(8) + .background(.blue) + .foregroundStyle(.white) + .clipShape(Circle()) + } +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift new file mode 100644 index 0000000..7403e45 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift @@ -0,0 +1,18 @@ +// +// ImagePickerButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 12.01.2024. +// + +import SwiftUI + +struct ImagePickerButton: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + ImagePickerButton() +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/LinkView.swift b/Simple Anki/SwiftUI/UI/UIComponents/LinkView.swift new file mode 100644 index 0000000..0166c67 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/LinkView.swift @@ -0,0 +1,39 @@ +// +// LinkView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 21.08.2023. +// + +import SwiftUI + +struct LinkView: View { + let label: String + let urlString: String + let icon: Image + + var body: some View { + Link(destination: URL(string: urlString)!) { + HStack { + Label { + Text(label) + .foregroundColor(.primary) + } icon: { + icon + } + Spacer() + Image(systemName: "arrow.up.forward.app") + .foregroundColor(.pink) + .font(.system(size: 10)) + } + } + } +} + +struct LinkView_Previews: PreviewProvider { + static var previews: some View { + LinkView(label: "Review app", urlString: "test", icon: Image(systemName: "star")) + .previewLayout(.sizeThatFits) + .padding() + } +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift new file mode 100644 index 0000000..b0d7201 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift @@ -0,0 +1,64 @@ +// +// RecordingButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.09.2023. +// + +import SwiftUI +import RealmSwift +import AVFoundation + +struct RecordingButton: View { + @ObservedObject var audioRecorder: AudioRecorder + var audioName: String? + var perform: (URL?) -> Void + @State private var showAlert: Bool = false + + var body: some View { + Button { + switch AVAudioSession.sharedInstance().recordPermission { + case .granted: + if audioRecorder.isRecording { + audioRecorder.stopRecording() + perform(audioRecorder.audioURL) + } else { + if let audioName { + audioRecorder.setAudioName(audioName) + } else { + audioRecorder.generateAudioName() + } + audioRecorder.startRecording() + } + case .denied: + showAlert.toggle() + case .undetermined: + AVAudioSession.sharedInstance().requestRecordPermission { _ in } + @unknown default: + break + } + } label: { + if audioRecorder.isRecording { + Image(systemName: "stop.circle") + .symbolRenderingMode(.palette) + .foregroundStyle(.red, .blue) + } else { + Image(systemName: "mic.fill.badge.plus") + .symbolRenderingMode(.palette) + .foregroundStyle(.green, .blue) + } + } + .alert("No access", isPresented: $showAlert, actions: { + Button("Cancel", role: .cancel, action: {}) + Button("Open settings", role: .none) { + + } + }) + } +} + +struct RecordingButton_Previews: PreviewProvider { + static var previews: some View { + RecordingButton(audioRecorder: AudioRecorder(), perform: { _ in }) + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift b/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift new file mode 100644 index 0000000..a78e8e6 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift @@ -0,0 +1,96 @@ +// +// CardPreviewView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 12.01.2024. +// + +import SwiftUI + +struct CardPreviewView: View { + + @Binding var isPreviewPresented: Bool + + var front: String + var back: String? + var image: UIImage? + var audio: String? + + private var transaction: Transaction { + var transaction = Transaction() + transaction.disablesAnimations = true + return transaction + } + + init(front: String, back: String?, image: UIImage? = nil, audio: String? = nil, isPreviewPresented: Binding) { + self.front = front + self.back = back + self.image = image + self.audio = audio + self._isPreviewPresented = isPreviewPresented + } + + var body: some View { + NavigationView { + ZStack { + if let image { + VStack { + Image(uiImage: image) + .resizable() + .scaledToFill() + .clipShape(RoundedRectangle(cornerRadius: 10)) + .frame(width: 150, height: 150) + .padding(.top, 60) + Spacer() + } + } + + VStack { + Text(front) + + Group { + Divider() + Text(back ?? "") + } + } + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + .onTapGesture { + SoundManager.shared.play(sound: audio ?? "") + } + .font(.system(size: 40, weight: .medium)) + .padding() + + if let audio { + VStack { + Spacer() + + Button(action: { + + }, label: { + Image(systemName: "speaker.wave.2.circle") + .font(.system(size: 60, weight: .light)) + .foregroundStyle(.black) + }) + .padding(.bottom, 140) + } + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + withTransaction(transaction) { + isPreviewPresented.toggle() + } + } label: { + Text("Back") + } + } + } + } + } +} + +#Preview { + CardPreviewView(front: "Hello", back: "Привет", image: UIImage(systemName: "photo"), audio: "test", isPreviewPresented: .constant(true)) +} diff --git a/Simple Anki/SwiftUI/UI/Views/CardView.swift b/Simple Anki/SwiftUI/UI/Views/CardView.swift new file mode 100644 index 0000000..7821fcb --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/CardView.swift @@ -0,0 +1,157 @@ +// +// CardView.swift +// Simple Anki +// +// Created by Astemir Boziev on 08.09.2023. +// + +import SwiftUI +import RealmSwift +import SPIndicator +import PhotosUI + +struct CardView: View { + @ObservedRealmObject var card: Card + + @State private var recordingButtonSize: CGFloat = 18 + @State private(set) var image: UIImage? + @State private var selectedImage: PhotosPickerItem? + @State private var frontText: String = "" + @State private var backText: String = "" + @State private var isPreviewPresented: Bool = false + + private var transaction: Transaction { + var transaction = Transaction() + transaction.disablesAnimations = true + return transaction + } + + @FocusState private var isTextFieldFocused: Bool + @Environment(\.dismiss) private var dismiss + + @EnvironmentObject private var audioRecorder: AudioRecorder + + var body: some View { + // MARK: VSTACK START + VStack { + Spacer() + + VStack { + TextField("Front word", text: $frontText) + .padding(.bottom) + Divider() + TextField("Back word", text: $backText) + .padding(.top) + } + .focused($isTextFieldFocused) + .font(.system(size: 35, weight: .medium)) + .multilineTextAlignment(.center) + .padding() + .onAppear { + frontText = card.front + backText = card.back + isTextFieldFocused.toggle() + print(card) + } + + Spacer() + + // MARK: ZSTACK START + ZStack { + // MARK: HSTACK START + HStack { + Group { + ImagePickerButton(image: $image) + + Spacer() + + Button { + guard let image = image else { return } + guard !frontText.isEmpty else { return } + writeToDisk(image: image, imageName: frontText + UUID().uuidString) + } label: { + Image(systemName: "plus.circle.fill") + .font(.system(size: 35)) + } + .disabled(frontText.isEmpty) + } + .opacity(audioRecorder.isRecording ? 0 : 1) + .offset(x: audioRecorder.isRecording ? -200 : 0) + .animation(.easeInOut(duration: 0.3), value: audioRecorder.isRecording) + + Spacer() + + Button { + audioRecorder.isRecording.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") + .foregroundStyle(audioRecorder.isRecording ? .red : .blue) + .font(.system(size: audioRecorder.isRecording ? 35 : 18)) + .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) + } + .contextMenu { + Button { + print("delete audio") + } label: { + Label("Delete", systemImage: "trash") + } + } + } // MARK: HSTACK END + .padding(.horizontal) + .padding(.vertical, 7) + .frame(maxWidth: .infinity) + + Text("Recording...") + .padding(.bottom, 3) + .foregroundStyle(.gray) + .offset(x: audioRecorder.isRecording ? 0 : 100) + .opacity(audioRecorder.isRecording ? 1 : 0) + .animation(.easeInOut(duration: 0.3), value: audioRecorder.isRecording) + } // MARK: ZSTACK END + } // MARK: VSTACK END + .onChange(of: selectedImage) { + Task { + let data = try? await selectedImage?.loadTransferable(type: Data.self) + self.image = UIImage(data: data ?? Data()) + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Preview") { + withTransaction(transaction) { + isPreviewPresented.toggle() + } + } + .fullScreenCover(isPresented: $isPreviewPresented) { + CardPreviewView(front: frontText, back: backText, image: image, audio: card.audioName, isPreviewPresented: $isPreviewPresented) + } + } + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .foregroundStyle(.gray) + } + } + } + } + + private func writeToDisk(image: UIImage, imageName: String) { + let savePath = FileManager.documentsDirectory.appendingPathComponent("\(imageName).jpg") + if let jpegData = image.jpegData(compressionQuality: 0.5) { + try? jpegData.write(to: savePath, options: [.atomic, .completeFileProtection]) + print("Image saved") + } + } +} + +struct CardView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + CardView(card: Card.card) + .environmentObject(AudioRecorder()) + } + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/CardsView.swift b/Simple Anki/SwiftUI/UI/Views/CardsView.swift new file mode 100644 index 0000000..9175a8b --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/CardsView.swift @@ -0,0 +1,115 @@ +// +// CardsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 20.08.2023. +// + +import SwiftUI +import RealmSwift + +struct CardsView: View { + @State private var isNewCardPresented: Bool = false + @State private var isReviewPresented: Bool = false + @ObservedRealmObject var deck: Deck + + var body: some View { + VStack { + if deck.cards.isEmpty { + Spacer() + VStack(spacing: 10) { + Image(systemName: "rectangle.portrait.on.rectangle.portrait.angled") + .font(.system(size: 100, weight: .light)) + Text("There are no cards yet") + } + .foregroundColor(.gray.opacity(0.7)) + Spacer() + Button { + isNewCardPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Add card") + .padding(8) + .frame(maxWidth: 280) + } + .buttonStyle(.borderedProminent) + .padding(.bottom, 40) + .sheet(isPresented: $isNewCardPresented, content: { + NavigationView { + NewCardView(deck: deck) + .environmentObject(AudioRecorder()) + } + }) + } else { + ZStack { + List { + ForEach(deck.cards) { card in + NavigationLink { + CardView(card: card) + .environmentObject(AudioRecorder()) + } label: { + Text(card.front) + } + .swipeActions(edge: .trailing) { + Button { + guard let item = deck.thaw() else { return } + guard item.isInvalidated else { return } + item.realm?.delete(card) + } label: { + Image(systemName: "trash") + } + .tint(.red) + } + } + .onMove(perform: $deck.cards.move) + } + VStack { + Spacer() + HStack { + Spacer() + Button { + isReviewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: "rectangle.stack.badge.play.fill") + .font(.system(size: 30)) + .padding(8) + } + .padding() + .padding(.bottom) + .buttonStyle(CircleButton()) + .fullScreenCover(isPresented: $isReviewPresented) { + ReviewView(reviewManager: ReviewManagerSUI(deck: deck)) + } + } + } + + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + isNewCardPresented.toggle() + } label: { + Image(systemName: "plus.circle.fill") + } + .popover(isPresented: $isNewCardPresented) { + NavigationView { + NewCardView(deck: deck) + .environmentObject(AudioRecorder()) + } + } + } + } + .navigationTitle(deck.name) + } +} + +struct CardsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + CardsView(deck: Deck.deck2) + } + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/Constants.swift b/Simple Anki/SwiftUI/UI/Views/Constants.swift new file mode 100644 index 0000000..fc42999 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Constants.swift @@ -0,0 +1,15 @@ +// +// Constants.swift +// Simple Anki +// +// Created by Астемир Бозиев on 21.08.2023. +// + +import Foundation + +struct Constants { + struct Links { + static let writeReview = "https://itunes.apple.com/app/id1625870857?action=write-review" + static let igMeLink = "https://ig.me/m/astemirboziy" + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/ContentView.swift b/Simple Anki/SwiftUI/UI/Views/ContentView.swift new file mode 100644 index 0000000..da9bc23 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/ContentView.swift @@ -0,0 +1,63 @@ +// +// TransitionDemo.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.01.2024. +// + +import SwiftUI + +struct ContentView: View { + @State private var keyboardHeight: CGFloat = 0 + @State private var text: String = "" + + var body: some View { + ZStack(alignment: .bottom) { + // Your main content here + // ... + VStack { + TextField("test", text: $text) + CustomToolbar() + .padding(.bottom, keyboardHeight) + } + + + } + .edgesIgnoringSafeArea(.bottom) + .onAppear { + NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in + let value = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect + let height = value.height + keyboardHeight = height + } + + NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in + keyboardHeight = 0 + } + } + } +} + +struct CustomToolbar: View { + var body: some View { + HStack { + Button("Left") { + // Action for left button + } + Spacer() + Button("Middle") { + // Action for middle button + } + Spacer() + Button("Right") { + // Action for right button + } + } + .padding() + .background(Color.gray.opacity(0.2)) // Just for visibility + } +} + +#Preview { + ContentView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/DecksView.swift b/Simple Anki/SwiftUI/UI/Views/DecksView.swift new file mode 100644 index 0000000..82be725 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/DecksView.swift @@ -0,0 +1,101 @@ +// +// DecksView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI +import RealmSwift + +struct DecksView: View { + @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @State private var isPopoverPresented: Bool = false + + var body: some View { + NavigationView { + VStack { + if decks.isEmpty { + Spacer() + VStack(spacing: 10) { + Image(systemName: "tray") + .font(.system(size: 100, weight: .light)) + Text("There are no decks yet") + } + .foregroundColor(.gray.opacity(0.7)) + Spacer() + Button { + isPopoverPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Create deck") + .padding(8) + .frame(maxWidth: 280) + } + .buttonStyle(.borderedProminent) + .padding(.bottom, 40) + .sheet(isPresented: $isPopoverPresented, content: { + NewDeckView() + }) + } else { + List { + ForEach(decks) { deck in + NavigationLink { + CardsView(deck: deck) + } label: { + VStack(alignment: .leading) { + Text(deck.name) + cardsCount(of: deck) + .font(.system(size: 11)) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + $decks.remove(deck) + } label: { + Image(systemName: "trash") + } + .tint(.red) + } + } + } + } + } + .navigationTitle("Decks") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + print("Test") + } label: { + Image(systemName: "tray.and.arrow.down") + } + Button { + isPopoverPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: "plus.circle.fill") + } + .popover(isPresented: $isPopoverPresented) { + NewDeckView() + } + } + } + } + .navigationViewStyle(.stack) + } + + @ViewBuilder private func cardsCount(of deck: Deck) -> some View { + let cardCount = deck.cards.count + if cardCount == 0 { + Text("No cards") + } else { + Text("^[\(cardCount) card](inflect: true)") + } + } +} + +struct DecksView_Previews: PreviewProvider { + static var previews: some View { + DecksView() + } +} diff --git a/Simple Anki/SwiftUI/MainView.swift b/Simple Anki/SwiftUI/UI/Views/MainView.swift similarity index 73% rename from Simple Anki/SwiftUI/MainView.swift rename to Simple Anki/SwiftUI/UI/Views/MainView.swift index 50cfd3d..b3791a2 100644 --- a/Simple Anki/SwiftUI/MainView.swift +++ b/Simple Anki/SwiftUI/UI/Views/MainView.swift @@ -8,12 +8,17 @@ import SwiftUI struct MainView: View { - @StateObject var realmManager = RealmManager() + + init() { + let appearance = UITabBarAppearance() + appearance.configureWithOpaqueBackground() + UITabBar.appearance().standardAppearance = appearance + UITabBar.appearance().scrollEdgeAppearance = appearance + } var body: some View { TabView { DecksView() - .environmentObject(realmManager) .tabItem { Image(systemName: "tray.full") Text("Decks") diff --git a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift new file mode 100644 index 0000000..1c4f1b2 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift @@ -0,0 +1,144 @@ +// +// NewCardView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 20.08.2023. +// + +import SwiftUI +import RealmSwift + +enum FocusableField: Hashable { + case frontField + case backField +} + +struct NewCardView: View { + @ObservedRealmObject var deck: Deck + + @State private var frontText: String = "" + @State private var backText: String = "" + @State private var isAlerPresented: Bool = false + + @Environment(\.dismiss) private var dismiss + @FocusState private var focusedField: FocusableField? + + @State private var audioName: String? + + init(deck: Deck) { + self.deck = deck + } + + var body: some View { + VStack { + VStack { + VStack(spacing: 30) { + TextField("Front", text: $frontText) + .submitLabel(.next) + .focused($focusedField, equals: .frontField) + Divider() + TextField("Back", text: $backText) + .submitLabel(.done) + .focused($focusedField, equals: .backField) + } + .onAppear { + focusedField = .frontField + } + .onSubmit { + focusedField = .backField + } + .font(.system(size: 35, weight: .bold)) + .padding(.top, 50) + Divider() + HStack { + if let audio = audioName { + Button { + isAlerPresented.toggle() + } label: { + Image(systemName: "trash.fill") + } + .alert("Delete audio file?", isPresented: $isAlerPresented, actions: { + Button("Delete", role: .destructive) { + LocalFileManager.shared.delete(audio) + audioName = nil + } + }) + .tint(.red) + + Button { + SoundManager.shared.play(sound: audio) + } label: { + Image(systemName: "speaker.wave.3.fill") + } + } + Spacer() + RecordingButton(audioRecorder: AudioRecorder()) { audioURL in + audioName = audioURL?.lastPathComponent + } + } + .padding(.top, 8) + .font(.system(size: 20, weight: .bold)) + } + Spacer() + Button { + saveCard() + focusedField = .frontField + frontText.removeAll() + backText.removeAll() + audioName = nil + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Add") + .padding(8) + .frame(maxWidth: .infinity) + } + .disabled(frontText.isEmpty) + .buttonStyle(.borderedProminent) + } + .padding() + .navigationTitle("Add card to \(deck.name)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + if let audio = audioName { + LocalFileManager.shared.delete(audio) + } + dismiss() + } label: { + Image(systemName: "xmark") + .foregroundColor(.gray) + } + } + } + } + + private func saveCard() { + let front = frontText.trimmingCharacters(in: .whitespacesAndNewlines) + let back = backText.trimmingCharacters(in: .whitespacesAndNewlines) + let card = Card(front: front, back: back, audioName: audioName) + $deck.cards.append(card) + } +} + +struct GrowingButton: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(8) + .background(.blue) + .foregroundStyle(.white) + .clipShape(Circle()) + .scaleEffect(configuration.isPressed ? 2 : 1) + .animation(.easeOut(duration: 0.1), value: configuration.isPressed) + } +} + +struct NewCardView_Previews: PreviewProvider { + @State static var isPresented: Bool = true + static var previews: some View { + NavigationView { + NewCardView(deck: Deck.deck1) + .environmentObject(AudioRecorder()) + } + } +} diff --git a/Simple Anki/SwiftUI/NewDeckView.swift b/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift similarity index 54% rename from Simple Anki/SwiftUI/NewDeckView.swift rename to Simple Anki/SwiftUI/UI/Views/NewDeckView.swift index f9a346f..eac8c84 100644 --- a/Simple Anki/SwiftUI/NewDeckView.swift +++ b/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift @@ -6,57 +6,65 @@ // import SwiftUI +import RealmSwift struct NewDeckView: View { + @Environment(\.dismiss) private var dismiss + + @EnvironmentObject private var userSettings: UserSettings + @ObservedResults(Deck.self) var decks @State private var deckNameText: String = "" @FocusState private var isTextFieldFocused: Bool - @Binding var isPopoverPresented: Bool var body: some View { NavigationView { VStack { - TextField("Deck name", text: $deckNameText) - .background() + TextField("Name", text: $deckNameText) .font(.system(size: 35, weight: .bold)) .padding(.top, 160) .focused($isTextFieldFocused) + Divider() Spacer() - NavigationLink { - NewCardView(isPopoverPresented: $isPopoverPresented) + + Button { + $decks.append(Deck(name: deckNameText.trimmingCharacters(in: .whitespacesAndNewlines))) + HapticManagerSUI.shared.impact(style: .heavy) + dismiss() } label: { HStack { - Text("Add cards") - Image(systemName: "arrow.right") + Text("Create") } .padding(8) - .frame(maxWidth: 280) + .frame(maxWidth: .infinity) } - .disabled(deckNameText.isEmpty) .buttonStyle(.borderedProminent) } .onAppear { isTextFieldFocused.toggle() } - .navigationTitle(deckNameText) + .navigationTitle("New deck") .navigationBarTitleDisplayMode(.inline) .padding() .toolbar { ToolbarItem(placement: .confirmationAction) { - Button("Done") { - if !deckNameText.isEmpty { - - } - isPopoverPresented.toggle() + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .foregroundColor(.gray) } } } } + .preferredColorScheme(userSettings.colorScheme ? .dark : .light) } } struct NewDeckView_Previews: PreviewProvider { @State static var isPresented: Bool = true + static var previews: some View { - NewDeckView(isPopoverPresented: $isPresented) + NewDeckView() + .environmentObject(UserSettings()) } } diff --git a/Simple Anki/SwiftUI/ReviewView.swift b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift similarity index 76% rename from Simple Anki/SwiftUI/ReviewView.swift rename to Simple Anki/SwiftUI/UI/Views/ReviewView.swift index 8045720..c81f73b 100644 --- a/Simple Anki/SwiftUI/ReviewView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift @@ -20,10 +20,10 @@ struct ReviewView: View { VStack { if reviewManager.isReviewing { Text(reviewManager.currentCard?.front ?? "") - + if isShowAnswer { Divider() - Text(reviewManager.currentCard?.back ?? "") + Text(reviewManager.currentCard?.back ?? "No back text") } } else { Text("Finished!") @@ -34,13 +34,18 @@ struct ReviewView: View { .minimumScaleFactor(0.5) .multilineTextAlignment(.center) .onTapGesture { - SoundManager.shared.play(sound: reviewManager.currentCard?.audioName ?? "") + if reviewManager.isReviewing { + SoundManager.shared.play(sound: reviewManager.currentCard?.audioName ?? "") + } } .font(.system(size: 40, weight: .medium)) .padding() .onAppear { reviewManager.startReview() } + .onChange(of: reviewManager.currentCard) { _ in + playPronunciation() + } VStack { Spacer() @@ -67,7 +72,7 @@ struct ReviewView: View { } } .font(.system(size: 70)) - .foregroundColor(.gray.opacity(0.7)) + .foregroundColor(.gray.opacity(0.6)) .padding(.bottom) } .toolbar { @@ -79,9 +84,25 @@ struct ReviewView: View { .foregroundColor(.gray) } } + + ToolbarItem(placement: .navigationBarLeading) { + Menu { + Text("Tap on front word to play a sound") + } label: { + Image(systemName: "questionmark.circle") + .foregroundColor(.gray) + } + } } } } + + private func playPronunciation() { + if let audioName = reviewManager.currentCard?.audioName { + SoundManager.shared.play(sound: audioName) + } + } + } struct ReviewView_Previews: PreviewProvider { diff --git a/Simple Anki/SwiftUI/UI/Views/SettingsView.swift b/Simple Anki/SwiftUI/UI/Views/SettingsView.swift new file mode 100644 index 0000000..84d9d60 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/SettingsView.swift @@ -0,0 +1,50 @@ +// +// SettingsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject private var userSettings: UserSettings + var body: some View { + NavigationView { + List { + Section("appearance") { + HStack { + Label { + Text("Dark mode") + } icon: { + Image(systemName: "circle.lefthalf.filled") + } + Spacer() + Toggle(isOn: $userSettings.colorScheme) {} + } + } + + Section("Feedback") { + LinkView( + label: "Review app", + urlString: Constants.Links.writeReview, + icon: Image(systemName: "star") + ) + LinkView( + label: "Contact developer", + urlString: Constants.Links.igMeLink, + icon: Image(systemName: "envelope") + ) + } + } + .navigationTitle("Settings") + } + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView() + .environmentObject(UserSettings()) + } +} diff --git a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift new file mode 100644 index 0000000..37cf2c7 --- /dev/null +++ b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift @@ -0,0 +1,8 @@ +// +// CardViewModel.swift +// Simple Anki +// +// Created by Астемир Бозиев on 07.01.2024. +// + +import Foundation From a28ce7002e28a2cc2083f6d874df582b5ed4781a Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Thu, 18 Jan 2024 22:56:13 +0400 Subject: [PATCH 4/9] swiftui almost done --- Simple Anki.xcodeproj/project.pbxproj | 98 ++++++++++--- Simple Anki/SwiftUI/CircleButton.swift | 8 -- .../SwiftUI/Managers/AudioRecorder.swift | 106 ++++++++------ .../SwiftUI/Managers/HapticManagerSUI.swift | 3 +- .../SwiftUI/Managers/LocalFileManager.swift | 39 +++++ .../SwiftUI/Managers/ReviewManagerSUI.swift | 33 +++++ .../SwiftUI/Managers/SoundManager.swift | 36 +++++ .../SwiftUI/Managers/UserSettings.swift | 5 + Simple Anki/SwiftUI/Models/CardSUI.swift | 7 +- Simple Anki/SwiftUI/Models/DeckSUI.swift | 13 ++ Simple Anki/SwiftUI/SimpleAnkiApp.swift | 18 ++- .../UI/UIComponents/CardViewState.swift | 33 +++++ .../UI/UIComponents/ImagePickerButton.swift | 56 ++++++-- .../UI/UIComponents/RecordingButton.swift | 40 ++---- .../SwiftUI/UI/Views/CardPreviewView.swift | 3 +- Simple Anki/SwiftUI/UI/Views/CardView.swift | 119 ++++++++++----- Simple Anki/SwiftUI/UI/Views/CardsView.swift | 136 ++++++++++-------- .../SwiftUI/UI/Views/ContentView.swift | 53 +------ Simple Anki/SwiftUI/UI/Views/DecksView.swift | 66 +++++---- .../SwiftUI/UI/Views/NewCardView.swift | 6 +- .../SwiftUI/UI/Views/NewDeckView.swift | 7 +- Simple Anki/SwiftUI/UI/Views/ReviewView.swift | 10 +- .../SwiftUI/ViewModels/CardViewModel.swift | 35 +++++ 23 files changed, 631 insertions(+), 299 deletions(-) delete mode 100644 Simple Anki/SwiftUI/CircleButton.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/CardViewState.swift diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index 7e7fc46..dd3d4f2 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -7,38 +7,42 @@ objects = { /* Begin PBXBuildFile section */ + C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00513FA2B517E91005F5815 /* CardPreviewView.swift */; }; C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648A92682060F00265EB8 /* UIApplicationExtension.swift */; }; C00648AC2682070200265EB8 /* EmailManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AB2682070200265EB8 /* EmailManager.swift */; }; C00648AF2682140800265EB8 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AE2682140800265EB8 /* Options.swift */; }; C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */; }; - C01290732A8F823700ECF9D7 /* RealmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01290722A8F823700ECF9D7 /* RealmManager.swift */; }; C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01290742A920A9700ECF9D7 /* NewDeckView.swift */; }; C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348CA2861F47E002FFBDF /* UILabelExtension.swift */; }; C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C02348D2286343F4002FFBDF /* StoreKit.framework */; }; C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */; }; + C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */; }; C028C4C8280C852400F6894E /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = C028C4C7280C852400F6894E /* .gitignore */; }; C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C028C4C92811C0F000F6894E /* NewDeckViewController.swift */; }; C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C028C4CB2811C71500F6894E /* EmptyState.swift */; }; C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41C2282146BF005E9835 /* DateExtension.swift */; }; C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */; }; + C03A02612B5299EF00576543 /* CardViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A02602B5299EF00576543 /* CardViewState.swift */; }; + C05695D42B501AAA00033AF2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05695D32B501AAA00033AF2 /* ContentView.swift */; }; C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */; }; C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */; }; C067B6872A8C1251000AF881 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6862A8C1251000AF881 /* MainView.swift */; }; C067B6892A8C139A000AF881 /* DecksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksView.swift */; }; C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B68A2A8C13A5000AF881 /* SettingsView.swift */; }; C070008E263E86E0006DF020 /* RateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070008D263E86E0006DF020 /* RateManager.swift */; }; - C074581D2A924A0B0046F39D /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581C2A924A0B0046F39D /* CardView.swift */; }; + C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581C2A924A0B0046F39D /* NewCardView.swift */; }; C074581F2A9293AA0046F39D /* CardsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581E2A9293AA0046F39D /* CardsView.swift */; }; C07458232A938CF90046F39D /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458222A938CF90046F39D /* UserSettings.swift */; }; C07458252A93943E0046F39D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458242A93943E0046F39D /* Constants.swift */; }; C07458282A939FA40046F39D /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458272A939FA40046F39D /* LinkView.swift */; }; C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */; }; - C0750ED02AA3A60400089B29 /* RecorderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ECF2AA3A60400089B29 /* RecorderManager.swift */; }; + C0750ED02AA3A60400089B29 /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ECF2AA3A60400089B29 /* AudioRecorder.swift */; }; C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED32AA3C01800089B29 /* ReviewView.swift */; }; C0750ED62AA3C39400089B29 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED52AA3C39400089B29 /* CircleButton.swift */; }; C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */; }; C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903242A8D444F00EFC51C /* DeckSUI.swift */; }; C08903272A8D474900EFC51C /* CardSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903262A8D474900EFC51C /* CardSUI.swift */; }; + C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */; }; C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0C6492A9DFC210015C65E /* SoundManager.swift */; }; C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70E5F28211E620020D533 /* ReminderManager.swift */; }; C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4125EC0E0B005DE503 /* Deck.swift */; }; @@ -74,10 +78,14 @@ C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE86D266AAA9A00B83253 /* Utils.swift */; }; C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE86F266B7A2900B83253 /* PlayerManager.swift */; }; C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE871266BA50900B83253 /* URLExtension.swift */; }; + C0CC42532B5052C000F683CE /* CardToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CC42522B5052C000F683CE /* CardToolbarView.swift */; }; + C0CD12532AA9069400FE3BB6 /* LocalFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */; }; + C0CD12552AAB3F2700FE3BB6 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CD12542AAB3F2700FE3BB6 /* CardView.swift */; }; C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1712815A57600350862 /* ReminderViewController.swift */; }; C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */; }; C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DBE8312673E9E00074DC13 /* HapticManager.swift */; }; C0DD10BB2823F6D10058F96B /* SwiftCSV in Frameworks */ = {isa = PBXBuildFile; productRef = C0DD10BA2823F6D10058F96B /* SwiftCSV */; }; + C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */; }; C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */; }; C0E8477827F9C9EA006AEED1 /* SPIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = C0E8477727F9C9EA006AEED1 /* SPIndicator */; }; C0F4FD1527BD82DF00814BD6 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */; }; @@ -114,38 +122,42 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + C00513FA2B517E91005F5815 /* CardPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPreviewView.swift; sourceTree = ""; }; C00648A92682060F00265EB8 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; C00648AB2682070200265EB8 /* EmailManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManager.swift; sourceTree = ""; }; C00648AE2682140800265EB8 /* Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = ""; }; - C01290722A8F823700ECF9D7 /* RealmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmManager.swift; sourceTree = ""; }; C01290742A920A9700ECF9D7 /* NewDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckView.swift; sourceTree = ""; }; C02348CA2861F47E002FFBDF /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; C02348D2286343F4002FFBDF /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewController.swift; sourceTree = ""; }; + C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewModel.swift; sourceTree = ""; }; C028C4C7280C852400F6894E /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; C028C4C92811C0F000F6894E /* NewDeckViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckViewController.swift; sourceTree = ""; }; C028C4CB2811C71500F6894E /* EmptyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyState.swift; sourceTree = ""; }; C02D41C2282146BF005E9835 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; + C03A02602B5299EF00576543 /* CardViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewState.swift; sourceTree = ""; }; + C05695D32B501AAA00033AF2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSettingsCell.swift; sourceTree = ""; }; C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnkiApp.swift; sourceTree = ""; }; C067B6862A8C1251000AF881 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; C067B6882A8C139A000AF881 /* DecksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksView.swift; sourceTree = ""; }; C067B68A2A8C13A5000AF881 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C070008D263E86E0006DF020 /* RateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateManager.swift; sourceTree = ""; }; - C074581C2A924A0B0046F39D /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; + C074581C2A924A0B0046F39D /* NewCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardView.swift; sourceTree = ""; }; C074581E2A9293AA0046F39D /* CardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsView.swift; sourceTree = ""; }; C07458222A938CF90046F39D /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; C07458242A93943E0046F39D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; C07458272A939FA40046F39D /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = ""; }; C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManagerSUI.swift; sourceTree = ""; }; - C0750ECF2AA3A60400089B29 /* RecorderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderManager.swift; sourceTree = ""; }; + C0750ECF2AA3A60400089B29 /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = ""; }; C0750ED32AA3C01800089B29 /* ReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewView.swift; sourceTree = ""; }; C0750ED52AA3C39400089B29 /* CircleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleButton.swift; sourceTree = ""; }; C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewManagerSUI.swift; sourceTree = ""; }; C08903242A8D444F00EFC51C /* DeckSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckSUI.swift; sourceTree = ""; }; C08903262A8D474900EFC51C /* CardSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardSUI.swift; sourceTree = ""; }; + C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerButton.swift; sourceTree = ""; }; C0A0C6492A9DFC210015C65E /* SoundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundManager.swift; sourceTree = ""; }; C0A70E5F28211E620020D533 /* ReminderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderManager.swift; sourceTree = ""; }; C0ADAD4125EC0E0B005DE503 /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; @@ -183,9 +195,13 @@ C0CBE86D266AAA9A00B83253 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; C0CBE86F266B7A2900B83253 /* PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManager.swift; sourceTree = ""; }; C0CBE871266BA50900B83253 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; + C0CC42522B5052C000F683CE /* CardToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardToolbarView.swift; sourceTree = ""; }; + C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileManager.swift; sourceTree = ""; }; + C0CD12542AAB3F2700FE3BB6 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; C0D1C1712815A57600350862 /* ReminderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderViewController.swift; sourceTree = ""; }; C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewCell.swift; sourceTree = ""; }; C0DBE8312673E9E00074DC13 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; + C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingButton.swift; sourceTree = ""; }; C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardViewController.swift; sourceTree = ""; }; C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; C0F4FD1B27BD84DA00814BD6 /* SettingsViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewCell.swift; sourceTree = ""; }; @@ -249,12 +265,12 @@ C01290712A8F822700ECF9D7 /* Managers */ = { isa = PBXGroup; children = ( - C01290722A8F823700ECF9D7 /* RealmManager.swift */, C07458222A938CF90046F39D /* UserSettings.swift */, C0A0C6492A9DFC210015C65E /* SoundManager.swift */, C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */, - C0750ECF2AA3A60400089B29 /* RecorderManager.swift */, + C0750ECF2AA3A60400089B29 /* AudioRecorder.swift */, C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */, + C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */, ); path = Managers; sourceTree = ""; @@ -267,6 +283,23 @@ name = Frameworks; sourceTree = ""; }; + C025748A2B4B1CE900F8EE29 /* ViewModels */ = { + isa = PBXGroup; + children = ( + C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + C025748B2B4B1CFB00F8EE29 /* UI */ = { + isa = PBXGroup; + children = ( + C07458262A939F800046F39D /* UIComponents */, + C0750ED92AA492BD00089B29 /* Views */, + ); + path = UI; + sourceTree = ""; + }; C05AA2B025E9891E00E40346 /* Controllers */ = { isa = PBXGroup; children = ( @@ -286,18 +319,11 @@ C067B6822A8C10D0000AF881 /* SwiftUI */ = { isa = PBXGroup; children = ( - C07458262A939F800046F39D /* UIComponents */, + C025748B2B4B1CFB00F8EE29 /* UI */, + C025748A2B4B1CE900F8EE29 /* ViewModels */, C01290712A8F822700ECF9D7 /* Managers */, C08903232A8D443900EFC51C /* Models */, C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */, - C067B6862A8C1251000AF881 /* MainView.swift */, - C067B6882A8C139A000AF881 /* DecksView.swift */, - C074581E2A9293AA0046F39D /* CardsView.swift */, - C067B68A2A8C13A5000AF881 /* SettingsView.swift */, - C01290742A920A9700ECF9D7 /* NewDeckView.swift */, - C074581C2A924A0B0046F39D /* CardView.swift */, - C07458242A93943E0046F39D /* Constants.swift */, - C0750ED32AA3C01800089B29 /* ReviewView.swift */, ); path = SwiftUI; sourceTree = ""; @@ -307,10 +333,32 @@ children = ( C0750ED52AA3C39400089B29 /* CircleButton.swift */, C07458272A939FA40046F39D /* LinkView.swift */, + C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */, + C0CC42522B5052C000F683CE /* CardToolbarView.swift */, + C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */, + C03A02602B5299EF00576543 /* CardViewState.swift */, ); path = UIComponents; sourceTree = ""; }; + C0750ED92AA492BD00089B29 /* Views */ = { + isa = PBXGroup; + children = ( + C067B6862A8C1251000AF881 /* MainView.swift */, + C067B6882A8C139A000AF881 /* DecksView.swift */, + C074581E2A9293AA0046F39D /* CardsView.swift */, + C067B68A2A8C13A5000AF881 /* SettingsView.swift */, + C01290742A920A9700ECF9D7 /* NewDeckView.swift */, + C074581C2A924A0B0046F39D /* NewCardView.swift */, + C07458242A93943E0046F39D /* Constants.swift */, + C0750ED32AA3C01800089B29 /* ReviewView.swift */, + C0CD12542AAB3F2700FE3BB6 /* CardView.swift */, + C00513FA2B517E91005F5815 /* CardPreviewView.swift */, + C05695D32B501AAA00033AF2 /* ContentView.swift */, + ); + path = Views; + sourceTree = ""; + }; C078B5F9281EA89500DE5A86 /* Reminder */ = { isa = PBXGroup; children = ( @@ -725,14 +773,16 @@ C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */, + C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */, + C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */, C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */, C0B1275527C3F0E7008E412D /* DecksViewModel.swift in Sources */, C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */, - C01290732A8F823700ECF9D7 /* RealmManager.swift in Sources */, + C0CC42532B5052C000F683CE /* CardToolbarView.swift in Sources */, C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */, - C0750ED02AA3A60400089B29 /* RecorderManager.swift in Sources */, - C074581D2A924A0B0046F39D /* CardView.swift in Sources */, + C0750ED02AA3A60400089B29 /* AudioRecorder.swift in Sources */, + C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */, C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */, C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */, C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */, @@ -742,10 +792,12 @@ C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */, C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */, C0C9BAAB2848B9410020E555 /* APKGManager.swift in Sources */, + C0CD12532AA9069400FE3BB6 /* LocalFileManager.swift in Sources */, C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */, C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */, C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */, C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */, + C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */, C070008E263E86E0006DF020 /* RateManager.swift in Sources */, C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */, C0C9BAA42848B9120020E555 /* ImportedCardCollectionViewCell.swift in Sources */, @@ -755,7 +807,9 @@ C00648AC2682070200265EB8 /* EmailManager.swift in Sources */, C0C081B425ED379600DF1083 /* StorageManager.swift in Sources */, C0B6D153285DE18900E354BA /* OnboardingViewController.swift in Sources */, + C03A02612B5299EF00576543 /* CardViewState.swift in Sources */, C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */, + C05695D42B501AAA00033AF2 /* ContentView.swift in Sources */, C0F4FD1527BD82DF00814BD6 /* UIViewExtension.swift in Sources */, C0B1275D27C3FB91008E412D /* ReviewViewController.swift in Sources */, C07458232A938CF90046F39D /* UserSettings.swift in Sources */, @@ -774,10 +828,12 @@ C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */, + C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */, C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */, C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */, C07458282A939FA40046F39D /* LinkView.swift in Sources */, C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */, + C0CD12552AAB3F2700FE3BB6 /* CardView.swift in Sources */, C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -962,6 +1018,7 @@ INFOPLIST_FILE = "Simple Anki/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "Simple Anki"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -988,6 +1045,7 @@ INFOPLIST_FILE = "Simple Anki/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "Simple Anki"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Simple Anki/SwiftUI/CircleButton.swift b/Simple Anki/SwiftUI/CircleButton.swift deleted file mode 100644 index 26e8626..0000000 --- a/Simple Anki/SwiftUI/CircleButton.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// CircleButton.swift -// Simple Anki -// -// Created by Астемир Бозиев on 02.09.2023. -// - -import Foundation diff --git a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift index 5668e84..02db20d 100644 --- a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift +++ b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift @@ -8,12 +8,19 @@ import Foundation import AVFoundation -class AudioRecorder: ObservableObject { +class AudioRecorder: NSObject, ObservableObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { + @Published var isPlaybackReady = false + @Published var isRecording = false + private var audioRecorder: AVAudioRecorder? + private var audioPlayer: AVAudioPlayer? + private var fileName: String? - @Published var isRecording = false - @Published var audioURL: URL? - private var audioName: String? + init(fileName: String? = nil) { + self.fileName = fileName ?? UUID().uuidString + ".m4a" + super.init() + setupRecorder() + } private var settings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, @@ -22,71 +29,82 @@ class AudioRecorder: ObservableObject { AVNumberOfChannelsKey: 2 ] - private func setupAudioRecorder() { + private func setupRecorder() { let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.record, mode: .default) try audioSession.setActive(true) + guard let fileName = fileName else { return } + let fileURL = getDocumentsDirectory().appendingPathComponent("\(fileName)") - let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - guard let audioName = audioName else { return } - let audioFileURL = documentsDirectory.appendingPathComponent("\(audioName).m4a") - - audioRecorder = try AVAudioRecorder(url: audioFileURL, settings: settings) - audioRecorder?.prepareToRecord() + audioRecorder = try AVAudioRecorder(url: fileURL, settings: settings) + audioRecorder?.delegate = self } catch { print("Error setting up audio recorder: \(error.localizedDescription)") } } + private func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] + } + func startRecording() { - self.setupAudioRecorder() - guard let audioRecorder = audioRecorder else { return } - - if !audioRecorder.isRecording { - do { - try AVAudioSession.sharedInstance().setActive(true) - audioRecorder.record() - isRecording = true - } catch { - print("Error starting recording: \(error.localizedDescription)") - } - } + audioRecorder?.record() + isRecording = true + } + + func stopRecording(completion: @escaping (String?) -> Void) { + audioRecorder?.stop() + isRecording = false + isPlaybackReady = true + completion(fileName ?? nil) } - func stopRecording() { - if let audioRecorder = audioRecorder, audioRecorder.isRecording { - audioRecorder.stop() - isRecording = false - audioURL = audioRecorder.url + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if flag { + isPlaybackReady = true } } - func isMicAccessGranted() -> Bool { - var permissionCheck: Bool = false - switch AVAudioSession.sharedInstance().recordPermission { + func checkRecordPermission(completion: @escaping (Bool) -> Void) { + switch AVAudioApplication.shared.recordPermission { case .granted: - permissionCheck = true + completion(true) case .denied: - permissionCheck = false + completion(false) case .undetermined: - AVAudioSession.sharedInstance().requestRecordPermission { granted in - permissionCheck = granted + AVAudioApplication.requestRecordPermission { granted in + DispatchQueue.main.async { + completion(granted) + } } - @unknown default: - break + default: + completion(false) } - return permissionCheck } -} -extension AudioRecorder { + func playRecording() { + guard isPlaybackReady, let url = audioRecorder?.url else { return } - func generateAudioName() { - self.audioName = UUID().uuidString + do { + audioPlayer = try AVAudioPlayer(contentsOf: url) + audioPlayer?.delegate = self + audioPlayer?.play() + } catch { + print("Failed to initialize the audio player: \(error.localizedDescription)") + } } - func setAudioName(_ name: String) { - self.audioName = name + func deleteRecording() { + guard let fileName = fileName else { return } + + let url = getDocumentsDirectory().appendingPathComponent("\(fileName)") + + do { + try FileManager.default.removeItem(at: url) + } catch { + print(error) + } } } diff --git a/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift b/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift index b5f94dd..b53fa01 100644 --- a/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift +++ b/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift @@ -8,11 +8,12 @@ import Foundation import SwiftUI - class HapticManagerSUI { static let shared = HapticManagerSUI() + private init() { } + func notification(type: UINotificationFeedbackGenerator.FeedbackType) { let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(type) diff --git a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift index 3488d7e..1f3dc00 100644 --- a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift +++ b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift @@ -6,3 +6,42 @@ // import Foundation + +class LocalFileManager { + + static let shared = LocalFileManager() + + private init() { } + + func delete(_ fileName: String?) { + guard let fileName = fileName else { return } + do { + try FileManager.default.removeItem(at: Self.documentsDirectory.appendingPathComponent(fileName)) + } catch { + print(error) + } + } + + func delete(at path: URL?) { + guard let path = path else { return } + do { + try FileManager.default.removeItem(at: path) + } catch { + print(error) + } + } +} + +extension LocalFileManager { + static var documentsDirectory: URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] + } +} + +extension FileManager { + static var documentsDirectory: URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + return paths[0] + } +} diff --git a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift index e69f116..ccb27cc 100644 --- a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift +++ b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift @@ -6,3 +6,36 @@ // import Foundation +import RealmSwift + +class ReviewManagerSUI: ObservableObject { + @Published var currentCard: Card? + @Published var isReviewing = false + + private var cards: List + private var currentIndex = 0 + + init(cards: List) { + self.cards = cards + } + + func startReview() { + guard !cards.isEmpty else { + return + } + + currentIndex = 0 + currentCard = cards[currentIndex] + isReviewing = true + } + + func nextCard() { + currentIndex += 1 + + if currentIndex < cards.count { + currentCard = cards[currentIndex] + } else { + isReviewing = false + } + } +} diff --git a/Simple Anki/SwiftUI/Managers/SoundManager.swift b/Simple Anki/SwiftUI/Managers/SoundManager.swift index d29674f..34adb52 100644 --- a/Simple Anki/SwiftUI/Managers/SoundManager.swift +++ b/Simple Anki/SwiftUI/Managers/SoundManager.swift @@ -6,3 +6,39 @@ // import Foundation +import AVKit + +class SoundManager { + + static let shared = SoundManager() + private var player: AVAudioPlayer? + + private init() { + try? AVAudioSession.sharedInstance().setCategory(.playback) + try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) + } + + func play(sound: String) { + let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let fullUrl = documentDirectory.appendingPathComponent(sound) + + do { + self.player = try AVAudioPlayer(contentsOf: fullUrl) + self.player?.prepareToPlay() + self.player?.play() + } catch let error { + print("Error playing sound: \(error.localizedDescription)") + print("Error playing sound: \(error)") + } + } + + func play(sound: URL) { + do { + self.player = try AVAudioPlayer(contentsOf: sound) + self.player?.prepareToPlay() + self.player?.play() + } catch let error { + print("Error playing sound: \(error.localizedDescription)") + } + } +} diff --git a/Simple Anki/SwiftUI/Managers/UserSettings.swift b/Simple Anki/SwiftUI/Managers/UserSettings.swift index 8a24c86..fb42aa6 100644 --- a/Simple Anki/SwiftUI/Managers/UserSettings.swift +++ b/Simple Anki/SwiftUI/Managers/UserSettings.swift @@ -6,3 +6,8 @@ // import Foundation +import SwiftUI + +class UserSettings: ObservableObject { + @AppStorage("colorScheme") var colorScheme: Bool = false +} diff --git a/Simple Anki/SwiftUI/Models/CardSUI.swift b/Simple Anki/SwiftUI/Models/CardSUI.swift index cce6db9..ac8bfaa 100644 --- a/Simple Anki/SwiftUI/Models/CardSUI.swift +++ b/Simple Anki/SwiftUI/Models/CardSUI.swift @@ -17,9 +17,14 @@ class Card: Object, ObjectKeyIdentifiable { @Persisted var memorized: Bool = false @Persisted(originProperty: "cards") var deck: LinkingObjects - convenience init(front: String, back: String) { + convenience init(front: String, back: String, audioName: String? = nil) { self.init() self.front = front self.back = back + self.audioName = audioName } } + +extension Card { + static var card: Card = Card(front: "front", back: "back") +} diff --git a/Simple Anki/SwiftUI/Models/DeckSUI.swift b/Simple Anki/SwiftUI/Models/DeckSUI.swift index 405128b..e60d296 100644 --- a/Simple Anki/SwiftUI/Models/DeckSUI.swift +++ b/Simple Anki/SwiftUI/Models/DeckSUI.swift @@ -27,3 +27,16 @@ class Deck: Object, ObjectKeyIdentifiable { self.name = name } } + +extension Deck { + static let deck1 = Deck(name: "Test") + static let deck2 = Deck(value: [ + "name": "Greetings", + "cards": [ + Card(front: "Hello", back: "Привет"), + Card(front: "Good day", back: "Добрый день") + ] + ] as [String : Any]) + + static let decks = [deck1, deck2] +} diff --git a/Simple Anki/SwiftUI/SimpleAnkiApp.swift b/Simple Anki/SwiftUI/SimpleAnkiApp.swift index c96dd25..83cf543 100644 --- a/Simple Anki/SwiftUI/SimpleAnkiApp.swift +++ b/Simple Anki/SwiftUI/SimpleAnkiApp.swift @@ -5,13 +5,23 @@ // Created by Астемир Бозиев on 16.08.2023. // - import SwiftUI +import SwiftUI +import RealmSwift + +@main +struct SimpleAnkiApp: SwiftUI.App { + @StateObject var userSettings = UserSettings() - @main - struct SimpleAnkiApp: App { var body: some Scene { WindowGroup { +// ContentView() MainView() + .environment(\.realmConfiguration, Realm.Configuration(schemaVersion: 1)) + .preferredColorScheme(userSettings.colorScheme ? .dark : .light) + .environmentObject(userSettings) + .onAppear { + print(NSHomeDirectory()) + } } } - } +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/CardViewState.swift b/Simple Anki/SwiftUI/UI/UIComponents/CardViewState.swift new file mode 100644 index 0000000..01bdb72 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/CardViewState.swift @@ -0,0 +1,33 @@ +// +// CardViewState.swift +// Simple Anki +// +// Created by Астемир Бозиев on 13.01.2024. +// + +import Foundation +import SwiftUI + +// enum CardViewState: Identifiable, View { +// case new +// case update(Card) +// +// var id: String { +// switch self { +// case .new: +// "new" +// case .update(let card): +// "update" +// } +// } +// +// var body: some View { +// switch self { +// case .new: +// return CardView(viewModel: CardViewModel()) +// case .update(let card): +// return CardView(viewModel: CardViewModel(card: card)) +// } +// } +// +// } diff --git a/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift index 7403e45..876a8fb 100644 --- a/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift +++ b/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift @@ -1,18 +1,58 @@ +//// +//// ImagePickerButton.swift +//// Simple Anki +//// +//// Created by Астемир Бозиев on 12.01.2024. +//// // -// ImagePickerButton.swift -// Simple Anki -// -// Created by Астемир Бозиев on 12.01.2024. -// - import SwiftUI +import PhotosUI struct ImagePickerButton: View { + + @Binding var image: UIImage? + @State private var selectedImage: PhotosPickerItem? + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + PhotosPicker(selection: $selectedImage, matching: .images) { + Image(systemName: "photo") + .overlay { + if let image { + Image(uiImage: image) + .resizable() + .scaledToFill() + .frame(width: 30, height: 30) + .clipShape(RoundedRectangle(cornerRadius: 3)) + .contextMenu { + Button(role: .destructive) { + clearSelection() + } label: { + Label("Delete", systemImage: "trash") + } + + } + } + } + } + .onChange(of: selectedImage) { + selectImage() + } + + } + + private func selectImage() { + Task { + let data = try? await selectedImage?.loadTransferable(type: Data.self) + self.image = UIImage(data: data ?? Data()) + } + } + + private func clearSelection() { + self.image = nil + self.selectedImage = nil } } #Preview { - ImagePickerButton() + ImagePickerButton(image: .constant(UIImage(data: Data()))) } diff --git a/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift index b0d7201..a4354bc 100644 --- a/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift +++ b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift @@ -11,42 +11,26 @@ import AVFoundation struct RecordingButton: View { @ObservedObject var audioRecorder: AudioRecorder - var audioName: String? - var perform: (URL?) -> Void @State private var showAlert: Bool = false var body: some View { Button { - switch AVAudioSession.sharedInstance().recordPermission { - case .granted: - if audioRecorder.isRecording { - audioRecorder.stopRecording() - perform(audioRecorder.audioURL) - } else { - if let audioName { - audioRecorder.setAudioName(audioName) + if audioRecorder.isRecording { +// audioRecorder.stopRecording() + } else { + audioRecorder.checkRecordPermission { granted in + if granted { + audioRecorder.startRecording() } else { - audioRecorder.generateAudioName() + showAlert.toggle() } - audioRecorder.startRecording() } - case .denied: - showAlert.toggle() - case .undetermined: - AVAudioSession.sharedInstance().requestRecordPermission { _ in } - @unknown default: - break } } label: { - if audioRecorder.isRecording { - Image(systemName: "stop.circle") - .symbolRenderingMode(.palette) - .foregroundStyle(.red, .blue) - } else { - Image(systemName: "mic.fill.badge.plus") - .symbolRenderingMode(.palette) - .foregroundStyle(.green, .blue) - } + Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") + .foregroundStyle(audioRecorder.isRecording ? .red : .blue) + .font(.system(size: audioRecorder.isRecording ? 35 : 18)) + .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) } .alert("No access", isPresented: $showAlert, actions: { Button("Cancel", role: .cancel, action: {}) @@ -59,6 +43,6 @@ struct RecordingButton: View { struct RecordingButton_Previews: PreviewProvider { static var previews: some View { - RecordingButton(audioRecorder: AudioRecorder(), perform: { _ in }) + RecordingButton(audioRecorder: AudioRecorder()) } } diff --git a/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift b/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift index a78e8e6..9225152 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift +++ b/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift @@ -53,12 +53,13 @@ struct CardPreviewView: View { Text(back ?? "") } } + .lineLimit(2) .minimumScaleFactor(0.5) .multilineTextAlignment(.center) .onTapGesture { SoundManager.shared.play(sound: audio ?? "") } - .font(.system(size: 40, weight: .medium)) + .font(.system(size: 35, weight: .medium)) .padding() if let audio { diff --git a/Simple Anki/SwiftUI/UI/Views/CardView.swift b/Simple Anki/SwiftUI/UI/Views/CardView.swift index 7821fcb..a08dd83 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/CardView.swift @@ -7,18 +7,15 @@ import SwiftUI import RealmSwift -import SPIndicator import PhotosUI struct CardView: View { - @ObservedRealmObject var card: Card + @State var viewModel: CardViewModel + @ObservedRealmObject var deck: Deck - @State private var recordingButtonSize: CGFloat = 18 - @State private(set) var image: UIImage? @State private var selectedImage: PhotosPickerItem? - @State private var frontText: String = "" - @State private var backText: String = "" @State private var isPreviewPresented: Bool = false + @State private var showAlert: Bool = false private var transaction: Transaction { var transaction = Transaction() @@ -28,7 +25,6 @@ struct CardView: View { @FocusState private var isTextFieldFocused: Bool @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var audioRecorder: AudioRecorder var body: some View { @@ -37,10 +33,10 @@ struct CardView: View { Spacer() VStack { - TextField("Front word", text: $frontText) + TextField("Front word", text: $viewModel.frontWord) .padding(.bottom) Divider() - TextField("Back word", text: $backText) + TextField("Back word", text: $viewModel.backWord) .padding(.top) } .focused($isTextFieldFocused) @@ -48,10 +44,7 @@ struct CardView: View { .multilineTextAlignment(.center) .padding() .onAppear { - frontText = card.front - backText = card.back isTextFieldFocused.toggle() - print(card) } Spacer() @@ -61,19 +54,23 @@ struct CardView: View { // MARK: HSTACK START HStack { Group { - ImagePickerButton(image: $image) + ImagePickerButton(image: $viewModel.image) Spacer() Button { - guard let image = image else { return } - guard !frontText.isEmpty else { return } - writeToDisk(image: image, imageName: frontText + UUID().uuidString) + guard !viewModel.frontWord.isEmpty else { return } + if viewModel.updating { + update() + } else { + addCard() + viewModel.clear() + } +// writeToDisk(image: image, imageName: viewModel.frontWord + UUID().uuidString) } label: { Image(systemName: "plus.circle.fill") .font(.system(size: 35)) } - .disabled(frontText.isEmpty) } .opacity(audioRecorder.isRecording ? 0 : 1) .offset(x: audioRecorder.isRecording ? -200 : 0) @@ -81,21 +78,51 @@ struct CardView: View { Spacer() - Button { - audioRecorder.isRecording.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") - .foregroundStyle(audioRecorder.isRecording ? .red : .blue) - .font(.system(size: audioRecorder.isRecording ? 35 : 18)) - .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) - } - .contextMenu { + if let fileName = viewModel.audioName { Button { - print("delete audio") + Task { + SoundManager.shared.play(sound: fileName) + } } label: { - Label("Delete", systemImage: "trash") + Image(systemName: "play.circle") + .font(.system(size: 18)) } + .contextMenu { + Button(role: .destructive) { + audioRecorder.deleteRecording() + } label: { + Label("Delete", systemImage: "trash") + } + } + } else { + Button { + HapticManagerSUI.shared.impact(style: .heavy) + + if audioRecorder.isRecording { + audioRecorder.stopRecording { fileName in + viewModel.audioName = fileName + } + } else { + audioRecorder.checkRecordPermission { granted in + if granted { + audioRecorder.startRecording() + } else { + showAlert.toggle() + } + } + } + } label: { + Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") + .foregroundStyle(audioRecorder.isRecording ? .red : .blue) + .font(.system(size: audioRecorder.isRecording ? 35 : 18)) + .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) + } + .alert("No access", isPresented: $showAlert, actions: { + Button("Cancel", role: .cancel, action: {}) + Button("Open settings", role: .none) { + + } + }) } } // MARK: HSTACK END .padding(.horizontal) @@ -113,7 +140,7 @@ struct CardView: View { .onChange(of: selectedImage) { Task { let data = try? await selectedImage?.loadTransferable(type: Data.self) - self.image = UIImage(data: data ?? Data()) + viewModel.image = UIImage(data: data ?? Data()) } } .toolbar { @@ -123,10 +150,12 @@ struct CardView: View { isPreviewPresented.toggle() } } + .disabled(viewModel.incomplete) .fullScreenCover(isPresented: $isPreviewPresented) { - CardPreviewView(front: frontText, back: backText, image: image, audio: card.audioName, isPreviewPresented: $isPreviewPresented) + CardPreviewView(front: viewModel.frontWord, back: viewModel.backWord, image: viewModel.image, audio: viewModel.audioName, isPreviewPresented: $isPreviewPresented) } } + ToolbarItem(placement: .cancellationAction) { Button { dismiss() @@ -138,6 +167,32 @@ struct CardView: View { } } + private func update() { + guard let cardID = viewModel.id else { return } + guard let card = deck.cards.first(where: {$0._id == cardID }) else { return } + + do { + let realm = try Realm() + try realm.write { + card.thaw()?.front = viewModel.frontWord + card.thaw()?.back = viewModel.backWord + card.thaw()?.audioName = viewModel.audioName + card.thaw()?.memorized = viewModel.memorized + } + } catch { + print("Error: \(error)") + } + } + + private func addCard() { + let card = Card( + front: viewModel.frontWord, + back: viewModel.backWord, + audioName: viewModel.audioName + ) + $deck.cards.append(card) + } + private func writeToDisk(image: UIImage, imageName: String) { let savePath = FileManager.documentsDirectory.appendingPathComponent("\(imageName).jpg") if let jpegData = image.jpegData(compressionQuality: 0.5) { @@ -150,7 +205,7 @@ struct CardView: View { struct CardView_Previews: PreviewProvider { static var previews: some View { NavigationView { - CardView(card: Card.card) + CardView(viewModel: CardViewModel(), deck: Deck.deck1) .environmentObject(AudioRecorder()) } } diff --git a/Simple Anki/SwiftUI/UI/Views/CardsView.swift b/Simple Anki/SwiftUI/UI/Views/CardsView.swift index 9175a8b..d29fcf2 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardsView.swift +++ b/Simple Anki/SwiftUI/UI/Views/CardsView.swift @@ -9,80 +9,77 @@ import SwiftUI import RealmSwift struct CardsView: View { - @State private var isNewCardPresented: Bool = false + @State private var isCardViewPresented: Bool = false @State private var isReviewPresented: Bool = false @ObservedRealmObject var deck: Deck var body: some View { - VStack { + ZStack { if deck.cards.isEmpty { - Spacer() - VStack(spacing: 10) { - Image(systemName: "rectangle.portrait.on.rectangle.portrait.angled") - .font(.system(size: 100, weight: .light)) - Text("There are no cards yet") - } - .foregroundColor(.gray.opacity(0.7)) - Spacer() - Button { - isNewCardPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Add card") - .padding(8) - .frame(maxWidth: 280) - } - .buttonStyle(.borderedProminent) - .padding(.bottom, 40) - .sheet(isPresented: $isNewCardPresented, content: { - NavigationView { - NewCardView(deck: deck) - .environmentObject(AudioRecorder()) + ContentUnavailableView(label: { + Label("No cards", systemImage: "rectangle.portrait.on.rectangle.portrait.angled") + }, description: { + Text("Cards will appear here.") + }, actions: { + Button { + isCardViewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Add card") } + .controlSize(.regular) + .buttonStyle(.borderedProminent) }) } else { - ZStack { - List { - ForEach(deck.cards) { card in - NavigationLink { - CardView(card: card) - .environmentObject(AudioRecorder()) - } label: { - Text(card.front) - } - .swipeActions(edge: .trailing) { - Button { - guard let item = deck.thaw() else { return } - guard item.isInvalidated else { return } - item.realm?.delete(card) - } label: { - Image(systemName: "trash") - } - .tint(.red) - } + List { + ForEach(deck.cards) { card in + NavigationLink { + CardView(viewModel: CardViewModel(card: card), deck: deck) + .environmentObject(AudioRecorder(fileName: card.audioName)) + .navigationBarBackButtonHidden() + } label: { + Text(card.front) } - .onMove(perform: $deck.cards.move) - } - VStack { - Spacer() - HStack { - Spacer() + .swipeActions(edge: .trailing) { Button { - isReviewPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) + remove(card) } label: { - Image(systemName: "rectangle.stack.badge.play.fill") - .font(.system(size: 30)) - .padding(8) - } - .padding() - .padding(.bottom) - .buttonStyle(CircleButton()) - .fullScreenCover(isPresented: $isReviewPresented) { - ReviewView(reviewManager: ReviewManagerSUI(deck: deck)) + Image(systemName: "trash") } + .tint(.red) } } + .onMove(perform: $deck.cards.move) + } + VStack { + Spacer() + HStack { + Button { + + } label: { + Image(systemName: "gearshape") + .padding(.vertical, 8) + } + .tint(.blue) + .buttonStyle(.bordered) +// .confirmationDialog("Test", isPresented: .constant(true)) { +// +// } + + Button { + isReviewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Review") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .fullScreenCover(isPresented: $isReviewPresented) { + ReviewView(reviewManager: ReviewManagerSUI(cards: deck.cards)) + } + } + .padding() } } @@ -90,20 +87,33 @@ struct CardsView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { - isNewCardPresented.toggle() + isCardViewPresented.toggle() } label: { Image(systemName: "plus.circle.fill") } - .popover(isPresented: $isNewCardPresented) { + .sheet(isPresented: $isCardViewPresented) { NavigationView { - NewCardView(deck: deck) + CardView(viewModel: CardViewModel(), deck: deck) .environmentObject(AudioRecorder()) } } } } + .toolbar(.hidden, for: .tabBar) .navigationTitle(deck.name) } + + private func remove(_ card: Card) { + guard let index = findIndex(of: card) else { return } + let audioName = deck.cards[index].audioName + + $deck.cards.remove(at: index) + LocalFileManager.shared.delete(audioName) + } + + private func findIndex(of card: Card) -> Int? { + return deck.cards.firstIndex(of: card) + } } struct CardsView_Previews: PreviewProvider { diff --git a/Simple Anki/SwiftUI/UI/Views/ContentView.swift b/Simple Anki/SwiftUI/UI/Views/ContentView.swift index da9bc23..bb4fca0 100644 --- a/Simple Anki/SwiftUI/UI/Views/ContentView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ContentView.swift @@ -7,57 +7,6 @@ import SwiftUI -struct ContentView: View { - @State private var keyboardHeight: CGFloat = 0 - @State private var text: String = "" - - var body: some View { - ZStack(alignment: .bottom) { - // Your main content here - // ... - VStack { - TextField("test", text: $text) - CustomToolbar() - .padding(.bottom, keyboardHeight) - } - - - } - .edgesIgnoringSafeArea(.bottom) - .onAppear { - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { notification in - let value = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect - let height = value.height - keyboardHeight = height - } - - NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in - keyboardHeight = 0 - } - } - } -} - -struct CustomToolbar: View { - var body: some View { - HStack { - Button("Left") { - // Action for left button - } - Spacer() - Button("Middle") { - // Action for middle button - } - Spacer() - Button("Right") { - // Action for right button - } - } - .padding() - .background(Color.gray.opacity(0.2)) // Just for visibility - } -} - #Preview { - ContentView() + Text("Test") } diff --git a/Simple Anki/SwiftUI/UI/Views/DecksView.swift b/Simple Anki/SwiftUI/UI/Views/DecksView.swift index 82be725..e71d6ee 100644 --- a/Simple Anki/SwiftUI/UI/Views/DecksView.swift +++ b/Simple Anki/SwiftUI/UI/Views/DecksView.swift @@ -10,32 +10,28 @@ import RealmSwift struct DecksView: View { @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks - @State private var isPopoverPresented: Bool = false + @State private var isNewDeckViewPresented: Bool = false var body: some View { NavigationView { VStack { if decks.isEmpty { - Spacer() - VStack(spacing: 10) { - Image(systemName: "tray") - .font(.system(size: 100, weight: .light)) - Text("There are no decks yet") - } - .foregroundColor(.gray.opacity(0.7)) - Spacer() - Button { - isPopoverPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Create deck") - .padding(8) - .frame(maxWidth: 280) - } - .buttonStyle(.borderedProminent) - .padding(.bottom, 40) - .sheet(isPresented: $isPopoverPresented, content: { - NewDeckView() + ContentUnavailableView(label: { + Label("No decks", systemImage: "tray") + }, description: { + Text("Your decks will appear here.") + }, actions: { + Button { + isNewDeckViewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Add deck") + } + .controlSize(.regular) + .buttonStyle(.borderedProminent) + .sheet(isPresented: $isNewDeckViewPresented, content: { + NewDeckView() + }) }) } else { List { @@ -51,7 +47,7 @@ struct DecksView: View { } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - $decks.remove(deck) + remove(deck) } label: { Image(systemName: "trash") } @@ -70,12 +66,12 @@ struct DecksView: View { Image(systemName: "tray.and.arrow.down") } Button { - isPopoverPresented.toggle() + isNewDeckViewPresented.toggle() HapticManagerSUI.shared.impact(style: .heavy) } label: { Image(systemName: "plus.circle.fill") } - .popover(isPresented: $isPopoverPresented) { + .sheet(isPresented: $isNewDeckViewPresented) { NewDeckView() } } @@ -86,10 +82,24 @@ struct DecksView: View { @ViewBuilder private func cardsCount(of deck: Deck) -> some View { let cardCount = deck.cards.count - if cardCount == 0 { - Text("No cards") - } else { - Text("^[\(cardCount) card](inflect: true)") + Text(cardCount == 0 ? "No cards" : "^[\(cardCount) card](inflect: true)") + } + + private func remove(_ deck: Deck) { + + do { + let realm = try Realm() + guard let deckToDelete = realm.objects(Deck.self).first(where: { $0._id == deck._id }) else { return } + deckToDelete.cards.forEach { card in + LocalFileManager.shared.delete(card.audioName) + } + + try realm.write { + realm.delete(deckToDelete.cards) + realm.delete(deckToDelete) + } + } catch { + print("Error: \(error)") } } } diff --git a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift index 1c4f1b2..af195e1 100644 --- a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift @@ -72,9 +72,9 @@ struct NewCardView: View { } } Spacer() - RecordingButton(audioRecorder: AudioRecorder()) { audioURL in - audioName = audioURL?.lastPathComponent - } +// RecordingButton(audioRecorder: AudioRecorder()) { audioURL in +// audioName = audioURL?.lastPathComponent +// } } .padding(.top, 8) .font(.system(size: 20, weight: .bold)) diff --git a/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift b/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift index eac8c84..11aca3e 100644 --- a/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift +++ b/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift @@ -27,7 +27,7 @@ struct NewDeckView: View { Spacer() Button { - $decks.append(Deck(name: deckNameText.trimmingCharacters(in: .whitespacesAndNewlines))) + addDeck() HapticManagerSUI.shared.impact(style: .heavy) dismiss() } label: { @@ -58,6 +58,11 @@ struct NewDeckView: View { } .preferredColorScheme(userSettings.colorScheme ? .dark : .light) } + + private func addDeck() { + let deck = Deck(name: deckNameText.trimmingCharacters(in: .whitespacesAndNewlines)) + $decks.append(deck) + } } struct NewDeckView_Previews: PreviewProvider { diff --git a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift index c81f73b..d455e3f 100644 --- a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift @@ -9,8 +9,7 @@ import SwiftUI struct ReviewView: View { @Environment(\.dismiss) var dismiss - @State private var frontText: String = "" - @State private var backText: String = "" + @State private var isShowAnswer: Bool = false @StateObject var reviewManager: ReviewManagerSUI @@ -21,10 +20,11 @@ struct ReviewView: View { if reviewManager.isReviewing { Text(reviewManager.currentCard?.front ?? "") - if isShowAnswer { + Group { Divider() Text(reviewManager.currentCard?.back ?? "No back text") } + .opacity(isShowAnswer ? 1 : 0) } else { Text("Finished!") .font(.system(size: 60, weight: .bold)) @@ -43,7 +43,7 @@ struct ReviewView: View { .onAppear { reviewManager.startReview() } - .onChange(of: reviewManager.currentCard) { _ in + .onChange(of: reviewManager.currentCard) { playPronunciation() } @@ -107,6 +107,6 @@ struct ReviewView: View { struct ReviewView_Previews: PreviewProvider { static var previews: some View { - ReviewView(reviewManager: ReviewManagerSUI(deck: Deck.deck2)) + ReviewView(reviewManager: ReviewManagerSUI(cards: Deck.deck2.cards)) } } diff --git a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift index 37cf2c7..80aea1b 100644 --- a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift +++ b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift @@ -6,3 +6,38 @@ // import Foundation +import SwiftUI +import RealmSwift +import Observation + +@Observable +final class CardViewModel { + var frontWord: String = "" + var backWord: String = "" + var memorized: Bool = false + var audioName: String? + var image: UIImage? + var id: ObjectId? + + var incomplete: Bool { frontWord.isEmpty } + + @ObservationIgnored + var updating: Bool { id != nil } + + init() {} + + init(card: Card) { + self.frontWord = card.front + self.backWord = card.back + self.memorized = card.memorized + self.audioName = card.audioName + self.image = UIImage(data: Data()) + self.id = card._id + } + + func clear() { + frontWord = "" + backWord = "" + audioName = nil + } +} From 3e2015dfc61e87c2243a7a70980ed482a8169fa3 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 28 Jan 2024 22:59:39 +0400 Subject: [PATCH 5/9] Audio recorder is fixed --- Simple Anki.xcodeproj/project.pbxproj | 16 ++- Simple Anki/AppDelegate.swift | 8 +- .../CardsTableViewController.swift | 9 +- .../Cells/DatePickerViewCell.swift | 4 - .../Decks/DecksTableViewController.swift | 2 - .../Decks/Import/ImportViewController.swift | 6 -- ...mportedCardsCollectionViewController.swift | 5 - .../MainTabBarViewController.swift | 4 - .../Reminder/ReminderViewController.swift | 11 +-- .../Reminder/WeekdaysViewController.swift | 3 - .../SettingsTableViewController.swift | 13 +-- Simple Anki/Models/Card.swift | 26 ++--- Simple Anki/Models/Deck.swift | 24 ++--- Simple Anki/Models/Options.swift | 2 +- .../SwiftUI/Managers/AudioRecorder.swift | 70 +++++++------ .../SwiftUI/Managers/LocalFileManager.swift | 23 +++++ .../SwiftUI/Managers/SoundManager.swift | 13 ++- Simple Anki/SwiftUI/SimpleAnkiApp.swift | 1 - .../UI/Repositories/CardRepository.swift | 75 ++++++++++++++ .../UI/UIComponents/RecordingButton.swift | 73 +++++++------- .../SwiftUI/UI/Views/CardPreviewView.swift | 25 ++--- Simple Anki/SwiftUI/UI/Views/CardView.swift | 99 +++++++++---------- Simple Anki/SwiftUI/UI/Views/CardsView.swift | 67 ++++++------- Simple Anki/SwiftUI/UI/Views/DecksView.swift | 27 +++-- Simple Anki/SwiftUI/UI/Views/MainView.swift | 24 +++-- .../SwiftUI/UI/Views/NewCardView.swift | 6 -- .../SwiftUI/UI/Views/NewDeckView.swift | 75 -------------- .../SwiftUI/ViewModels/CardViewModel.swift | 29 +++++- Simple Anki/ViewController.swift | 5 - Simple AnkiUITests/Screens/DecksScreen.swift | 2 +- 30 files changed, 379 insertions(+), 368 deletions(-) create mode 100644 Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift delete mode 100644 Simple Anki/SwiftUI/UI/Views/NewDeckView.swift diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index dd3d4f2..090f9e1 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -11,8 +11,8 @@ C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648A92682060F00265EB8 /* UIApplicationExtension.swift */; }; C00648AC2682070200265EB8 /* EmailManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AB2682070200265EB8 /* EmailManager.swift */; }; C00648AF2682140800265EB8 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AE2682140800265EB8 /* Options.swift */; }; + C00BD2642B5A8EA900ACD977 /* CardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00BD2632B5A8EA900ACD977 /* CardRepository.swift */; }; C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */; }; - C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01290742A920A9700ECF9D7 /* NewDeckView.swift */; }; C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348CA2861F47E002FFBDF /* UILabelExtension.swift */; }; C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C02348D2286343F4002FFBDF /* StoreKit.framework */; }; C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */; }; @@ -126,8 +126,8 @@ C00648A92682060F00265EB8 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; C00648AB2682070200265EB8 /* EmailManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManager.swift; sourceTree = ""; }; C00648AE2682140800265EB8 /* Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; + C00BD2632B5A8EA900ACD977 /* CardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepository.swift; sourceTree = ""; }; C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = ""; }; - C01290742A920A9700ECF9D7 /* NewDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckView.swift; sourceTree = ""; }; C02348CA2861F47E002FFBDF /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; C02348D2286343F4002FFBDF /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewController.swift; sourceTree = ""; }; @@ -262,6 +262,14 @@ path = Cells; sourceTree = ""; }; + C00BD2622B5A8E9400ACD977 /* Repositories */ = { + isa = PBXGroup; + children = ( + C00BD2632B5A8EA900ACD977 /* CardRepository.swift */, + ); + path = Repositories; + sourceTree = ""; + }; C01290712A8F822700ECF9D7 /* Managers */ = { isa = PBXGroup; children = ( @@ -294,6 +302,7 @@ C025748B2B4B1CFB00F8EE29 /* UI */ = { isa = PBXGroup; children = ( + C00BD2622B5A8E9400ACD977 /* Repositories */, C07458262A939F800046F39D /* UIComponents */, C0750ED92AA492BD00089B29 /* Views */, ); @@ -348,7 +357,6 @@ C067B6882A8C139A000AF881 /* DecksView.swift */, C074581E2A9293AA0046F39D /* CardsView.swift */, C067B68A2A8C13A5000AF881 /* SettingsView.swift */, - C01290742A920A9700ECF9D7 /* NewDeckView.swift */, C074581C2A924A0B0046F39D /* NewCardView.swift */, C07458242A93943E0046F39D /* Constants.swift */, C0750ED32AA3C01800089B29 /* ReviewView.swift */, @@ -772,7 +780,6 @@ C067B6892A8C139A000AF881 /* DecksView.swift in Sources */, C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, - C01290752A920A9700ECF9D7 /* NewDeckView.swift in Sources */, C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */, C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */, C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, @@ -806,6 +813,7 @@ C0B1275727C3FB7A008E412D /* CardsTableViewController.swift in Sources */, C00648AC2682070200265EB8 /* EmailManager.swift in Sources */, C0C081B425ED379600DF1083 /* StorageManager.swift in Sources */, + C00BD2642B5A8EA900ACD977 /* CardRepository.swift in Sources */, C0B6D153285DE18900E354BA /* OnboardingViewController.swift in Sources */, C03A02612B5299EF00576543 /* CardViewState.swift in Sources */, C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */, diff --git a/Simple Anki/AppDelegate.swift b/Simple Anki/AppDelegate.swift index d39de54..1b17f00 100644 --- a/Simple Anki/AppDelegate.swift +++ b/Simple Anki/AppDelegate.swift @@ -7,18 +7,14 @@ import UIKit import RealmSwift -import FirebaseCore -import Firebase -import FirebaseAnalytics -@main +// @main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - FirebaseApp.configure() RateManager.incrementLaunchCount() // let config = Realm.Configuration( // schemaVersion: 3, @@ -61,4 +57,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } -} + } diff --git a/Simple Anki/Controllers/CardsTableViewController.swift b/Simple Anki/Controllers/CardsTableViewController.swift index 622d552..b2dbe3c 100644 --- a/Simple Anki/Controllers/CardsTableViewController.swift +++ b/Simple Anki/Controllers/CardsTableViewController.swift @@ -7,7 +7,6 @@ import UIKit import RealmSwift -import FirebaseAnalytics class CardsTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { private lazy var tableView: UITableView = { @@ -116,7 +115,7 @@ class CardsTableViewController: UIViewController, UITableViewDataSource, UITable @objc private func reviewButtonTouchUpInside() { let reviewVC = ReviewViewController() - guard let deck = cards?[0].parentDeck.first else { return } + guard let deck = cards?[0].deck.first else { return } guard let cardsToReview = cards?.where({ $0.memorized == false }) else { return } reviewVC.reviewManager = ReviewManager( layout: deck.layout, @@ -172,12 +171,6 @@ class CardsTableViewController: UIViewController, UITableViewDataSource, UITable default: break } - if let deck = selectedDeck { - Analytics.logEvent("deck_layout", parameters: [ - "layout": deck.layout as NSObject, - "name": deck.name - ]) - } alert.addAction(frontToBack) alert.addAction(backToFront) diff --git a/Simple Anki/Controllers/Cells/DatePickerViewCell.swift b/Simple Anki/Controllers/Cells/DatePickerViewCell.swift index de6a8ad..24f4816 100644 --- a/Simple Anki/Controllers/Cells/DatePickerViewCell.swift +++ b/Simple Anki/Controllers/Cells/DatePickerViewCell.swift @@ -34,10 +34,6 @@ class DatePickerViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - override func prepareForReuse() { - super.prepareForReuse() - } - override func layoutSubviews() { super.layoutSubviews() datePicker.frame = CGRect(x: 0, y: 0, width: contentView.frame.size.width, height: 200) diff --git a/Simple Anki/Controllers/Decks/DecksTableViewController.swift b/Simple Anki/Controllers/Decks/DecksTableViewController.swift index 6adf433..1b7156c 100644 --- a/Simple Anki/Controllers/Decks/DecksTableViewController.swift +++ b/Simple Anki/Controllers/Decks/DecksTableViewController.swift @@ -7,7 +7,6 @@ import UIKit import RealmSwift -import FirebaseAnalytics class DecksTableViewController: UITableViewController { @@ -30,7 +29,6 @@ class DecksTableViewController: UITableViewController { } @objc private func didTapImport() { - Analytics.logEvent("import_deck_tapped", parameters: nil) let importVC = ImportViewController() importVC.reloadData = { [weak self] in self?.reload() diff --git a/Simple Anki/Controllers/Decks/Import/ImportViewController.swift b/Simple Anki/Controllers/Decks/Import/ImportViewController.swift index 139662b..e5ff62a 100644 --- a/Simple Anki/Controllers/Decks/Import/ImportViewController.swift +++ b/Simple Anki/Controllers/Decks/Import/ImportViewController.swift @@ -8,7 +8,6 @@ import UIKit import UniformTypeIdentifiers import SwiftCSV -import FirebaseAnalytics enum ImportFileType: String { case csv @@ -138,7 +137,6 @@ class ImportViewController: UIViewController { @objc func didTapCloseButton() { dismiss(animated: true) - Analytics.logEvent("close_import", parameters: nil) } @objc func didTapSemiColonButton() { @@ -209,10 +207,6 @@ class ImportViewController: UIViewController { @objc private func didTapChooseFileButton() { self.presentDocumentPicker() - // MARK: Log event - Analytics.logEvent("import_deck_tapped", parameters: [ - "file_type" : self.selectedFile.rawValue as NSObject - ]) } @objc private func didChangeSegmentedControl() { diff --git a/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift b/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift index 65fb7e0..94c0d47 100644 --- a/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift +++ b/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift @@ -6,7 +6,6 @@ // import UIKit -import FirebaseAnalytics class ImportedCardsCollectionViewController: UIViewController { @@ -43,10 +42,6 @@ class ImportedCardsCollectionViewController: UIViewController { newCard.back = card.back newDeck.cards.append(newCard) } - Analytics.logEvent("impored_deck", parameters: [ - "name" : deckName as NSObject, - "cards_number" : cards.count as NSObject - ]) StorageManager.save(newDeck) reloadData?() self.view.window?.rootViewController?.dismiss(animated: true) diff --git a/Simple Anki/Controllers/MainTabBarViewController.swift b/Simple Anki/Controllers/MainTabBarViewController.swift index 56ba4f7..559fd54 100644 --- a/Simple Anki/Controllers/MainTabBarViewController.swift +++ b/Simple Anki/Controllers/MainTabBarViewController.swift @@ -15,10 +15,6 @@ class MainTabBarViewController: UITabBarController { // showOnboardingScreen() } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - private func configureTabBar() { let decksVC = UINavigationController(rootViewController: DecksTableViewController()) let settingsVC = UINavigationController(rootViewController: SettingsViewController()) diff --git a/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift b/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift index a96d2e6..d2af9ef 100644 --- a/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift +++ b/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift @@ -6,7 +6,6 @@ // import UIKit -import FirebaseAnalytics class ReminderViewController: UIViewController { @@ -19,7 +18,7 @@ class ReminderViewController: UIViewController { return table }() - var models = [Section]() + var models = [SectionOld]() let remonderOn = UserDefaults.standard.bool(forKey: K.UserDefaultsKeys.reminder) let reminderTime = UserDefaults.standard.string(forKey: K.UserDefaultsKeys.reminderTime) ?? "00:00" let reminderIcon = ReminderManager.shared.isReminderOn() ? UIImage(systemName: "bell") : UIImage(systemName: "bell.slash") @@ -51,7 +50,7 @@ class ReminderViewController: UIViewController { } private func configure() { - models.append(Section(title: "", options: [ + models.append(SectionOld(title: "", options: [ .switchCell(model: SwitchOption( title: K.Settings.reminderOn, icon: reminderIcon, @@ -171,9 +170,6 @@ extension ReminderViewController: SwitchViewCellDelegate { ReminderManager.shared.setReminderOff() switchCell.iconImageView.image = UIImage(systemName: "bell.slash") } - Analytics.logEvent("reminder", parameters: [ - "is_on" : switchCell.mySwitch.isOn as NSObject - ]) } } @@ -184,8 +180,5 @@ extension ReminderViewController: DatePickerViewCellDelegate { dateFormatter.dateFormat = "HH:mm" let timeString = dateFormatter.string(from: datePickerCell.datePicker.date) UserDefaults.standard.set(timeString, forKey: K.UserDefaultsKeys.reminderTime) - Analytics.logEvent("reminder_time", parameters: [ - "time" : timeString as NSObject - ]) } } diff --git a/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift b/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift index f5fc28b..c8c73d5 100644 --- a/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift +++ b/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift @@ -6,7 +6,6 @@ // import UIKit -import FirebaseAnalytics class WeekdaysViewController: UIViewController { @@ -50,11 +49,9 @@ extension WeekdaysViewController: UITableViewDelegate, UITableViewDataSource { if !ReminderManager.shared.isDayInReminder(index: indexPath.row) { ReminderManager.shared.addWeekdayToReminder(index: indexPath.row) cell.accessoryType = .checkmark - Analytics.logEvent("weekday_\(indexPath.row)", parameters: ["on" : true as NSObject]) } else { ReminderManager.shared.deleteDayFromReminder(index: indexPath.row) cell.accessoryType = .none - Analytics.logEvent("weekday_\(indexPath.row)", parameters: ["off" : true as NSObject]) } tableView.deselectRow(at: indexPath, animated: true) } diff --git a/Simple Anki/Controllers/Settings/SettingsTableViewController.swift b/Simple Anki/Controllers/Settings/SettingsTableViewController.swift index 2fc7db7..b93b798 100644 --- a/Simple Anki/Controllers/Settings/SettingsTableViewController.swift +++ b/Simple Anki/Controllers/Settings/SettingsTableViewController.swift @@ -7,7 +7,6 @@ import UIKit import StoreKit -import FirebaseAnalytics class SettingsViewController: UIViewController { @@ -18,7 +17,7 @@ class SettingsViewController: UIViewController { return table }() - var models = [Section]() + var models = [SectionOld]() let darkMode = UserDefaults.standard.bool(forKey: K.UserDefaultsKeys.darkMode) @@ -34,7 +33,7 @@ class SettingsViewController: UIViewController { } func configure() { - models.append(Section(title: K.Settings.appearence, options: [ + models.append(SectionOld(title: K.Settings.appearence, options: [ .switchCell(model: SwitchOption( title: K.Settings.darkMode, icon: UIImage(systemName: K.Icon.lefthalf), @@ -42,7 +41,7 @@ class SettingsViewController: UIViewController { handler: nil)) ])) - models.append(Section(title: K.Settings.support, options: [ + models.append(SectionOld(title: K.Settings.support, options: [ .staticCell(model: Option(title: K.Settings.rateThisApp, icon: UIImage(systemName: K.Icon.star)) { RateManager.rateApp() }), @@ -58,7 +57,7 @@ class SettingsViewController: UIViewController { ])) - models.append(Section(title: K.Settings.notifications, options: [ + models.append(SectionOld(title: K.Settings.notifications, options: [ .staticCell(model: Option(title: "Reminder", icon: UIImage(systemName: K.Icon.bell), handler: { ReminderManager.shared.notificationCenter.requestAuthorization(options: [.alert, .sound]) { permissionGranted, error in if let error = error { @@ -126,7 +125,6 @@ extension SettingsViewController: UITableViewDelegate { switch type.self { case .staticCell(let model): - Analytics.logEvent(model.title, parameters: nil) model.handler?() default: break @@ -186,8 +184,5 @@ extension SettingsViewController: SwitchViewCellDelegate { UserDefaults.standard.set(false, forKey: K.UserDefaultsKeys.darkMode) view.window?.overrideUserInterfaceStyle = .light } - Analytics.logEvent("dark_mode", parameters: [ - "is_on" : switchCell.mySwitch.isOn as NSObject - ]) } } diff --git a/Simple Anki/Models/Card.swift b/Simple Anki/Models/Card.swift index de32a4a..b3e65af 100644 --- a/Simple Anki/Models/Card.swift +++ b/Simple Anki/Models/Card.swift @@ -8,16 +8,16 @@ import Foundation import RealmSwift -class Card: Object { - @objc dynamic var _id: ObjectId = ObjectId.generate() - @objc dynamic var front: String = "" - @objc dynamic var back: String = "" - @objc dynamic var dateCreated: Date = Date() - @objc dynamic var audioName: String? - @objc dynamic var memorized: Bool = false - var parentDeck = LinkingObjects(fromType: Deck.self, property: "cards") - - override static func primaryKey() -> String? { - return "_id" - } -} +// class Card: Object { +// @objc dynamic var _id: ObjectId = ObjectId.generate() +// @objc dynamic var front: String = "" +// @objc dynamic var back: String = "" +// @objc dynamic var dateCreated: Date = Date() +// @objc dynamic var audioName: String? +// @objc dynamic var memorized: Bool = false +// var parentDeck = LinkingObjects(fromType: Deck.self, property: "cards") +// +// override static func primaryKey() -> String? { +// return "_id" +// } +// } diff --git a/Simple Anki/Models/Deck.swift b/Simple Anki/Models/Deck.swift index 36a2483..5c60a8d 100644 --- a/Simple Anki/Models/Deck.swift +++ b/Simple Anki/Models/Deck.swift @@ -8,15 +8,15 @@ import Foundation import RealmSwift -class Deck: Object { - @objc dynamic var _id: ObjectId = ObjectId.generate() - @objc dynamic var name: String = "" - @objc dynamic var dateCreated: Date = Date() - @objc dynamic var layout: String = "frontToBack" - @objc dynamic var autoplay: Bool = false - let cards = List() - - override static func primaryKey() -> String? { - return "_id" - } -} +// class Deck: Object { +// @objc dynamic var _id: ObjectId = ObjectId.generate() +// @objc dynamic var name: String = "" +// @objc dynamic var dateCreated: Date = Date() +// @objc dynamic var layout: String = "frontToBack" +// @objc dynamic var autoplay: Bool = false +// let cards = List() +// +// override static func primaryKey() -> String? { +// return "_id" +// } +// } diff --git a/Simple Anki/Models/Options.swift b/Simple Anki/Models/Options.swift index 44fae8e..32799f4 100644 --- a/Simple Anki/Models/Options.swift +++ b/Simple Anki/Models/Options.swift @@ -35,7 +35,7 @@ enum SwitchType { case reminder } -struct Section { +struct SectionOld { let title: String let options: [OptionType] } diff --git a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift index 02db20d..ee5af72 100644 --- a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift +++ b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift @@ -8,16 +8,18 @@ import Foundation import AVFoundation -class AudioRecorder: NSObject, ObservableObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { - @Published var isPlaybackReady = false - @Published var isRecording = false +@Observable +class AudioRecorder: NSObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { + var isPlaybackReady = false + var isRecording = false private var audioRecorder: AVAudioRecorder? - private var audioPlayer: AVAudioPlayer? - private var fileName: String? + private var player: AVAudioPlayer? + private var fileName: String + private var audioSession = AVAudioSession.sharedInstance() - init(fileName: String? = nil) { - self.fileName = fileName ?? UUID().uuidString + ".m4a" + init(fileName: String) { + self.fileName = fileName super.init() setupRecorder() } @@ -30,12 +32,12 @@ class AudioRecorder: NSObject, ObservableObject, AVAudioRecorderDelegate, AVAudi ] private func setupRecorder() { - let audioSession = AVAudioSession.sharedInstance() + do { - try audioSession.setCategory(.record, mode: .default) - try audioSession.setActive(true) - guard let fileName = fileName else { return } - let fileURL = getDocumentsDirectory().appendingPathComponent("\(fileName)") + try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + + let fileURL = FileManager.documentsDirectory.appendingPathComponent(fileName) audioRecorder = try AVAudioRecorder(url: fileURL, settings: settings) audioRecorder?.delegate = self @@ -44,21 +46,23 @@ class AudioRecorder: NSObject, ObservableObject, AVAudioRecorderDelegate, AVAudi } } - private func getDocumentsDirectory() -> URL { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - return paths[0] - } - func startRecording() { audioRecorder?.record() isRecording = true } - func stopRecording(completion: @escaping (String?) -> Void) { + func stopRecording(completion: @escaping (String) -> Void) { audioRecorder?.stop() + + do { + try audioSession.setActive(false) + } catch { + print(error.localizedDescription) + } + isRecording = false isPlaybackReady = true - completion(fileName ?? nil) + completion(fileName) } func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { @@ -84,27 +88,31 @@ class AudioRecorder: NSObject, ObservableObject, AVAudioRecorderDelegate, AVAudi } } - func playRecording() { - guard isPlaybackReady, let url = audioRecorder?.url else { return } + func deleteRecording() { + let url = FileManager.documentsDirectory.appendingPathComponent(fileName) do { - audioPlayer = try AVAudioPlayer(contentsOf: url) - audioPlayer?.delegate = self - audioPlayer?.play() + try FileManager.default.removeItem(at: url) } catch { - print("Failed to initialize the audio player: \(error.localizedDescription)") + print(error) } } - func deleteRecording() { - guard let fileName = fileName else { return } + func setName(_ name: String) { + fileName = name + } - let url = getDocumentsDirectory().appendingPathComponent("\(fileName)") + func play(sound: String) { + let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let fullUrl = documentDirectory.appendingPathComponent(sound) do { - try FileManager.default.removeItem(at: url) - } catch { - print(error) + self.player = try AVAudioPlayer(contentsOf: fullUrl) + self.player?.prepareToPlay() + self.player?.play() + } catch let error { + print("Error playing sound: \(error.localizedDescription)") + print("Error playing sound: \(error)") } } } diff --git a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift index 1f3dc00..aee7d42 100644 --- a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift +++ b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift @@ -44,4 +44,27 @@ extension FileManager { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0] } + + func delete(_ fileName: String?) { + + guard let fileName = fileName else { return } + let path = Self.documentsDirectory.appendingPathComponent(fileName) + + do { + try Self.default.removeItem(at: path) + } catch { + print(error) + } + } + + func delete(at path: URL?) { + + guard let path = path else { return } + + do { + try Self.default.removeItem(at: path) + } catch { + print(error) + } + } } diff --git a/Simple Anki/SwiftUI/Managers/SoundManager.swift b/Simple Anki/SwiftUI/Managers/SoundManager.swift index 34adb52..c77fa2b 100644 --- a/Simple Anki/SwiftUI/Managers/SoundManager.swift +++ b/Simple Anki/SwiftUI/Managers/SoundManager.swift @@ -12,10 +12,19 @@ class SoundManager { static let shared = SoundManager() private var player: AVAudioPlayer? + private var audioSession = AVAudioSession.sharedInstance() private init() { - try? AVAudioSession.sharedInstance().setCategory(.playback) - try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) + setupPlayer() + } + + private func setupPlayer() { + do { + try audioSession.setCategory(.playback) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + print(error.localizedDescription) + } } func play(sound: String) { diff --git a/Simple Anki/SwiftUI/SimpleAnkiApp.swift b/Simple Anki/SwiftUI/SimpleAnkiApp.swift index 83cf543..a7519a0 100644 --- a/Simple Anki/SwiftUI/SimpleAnkiApp.swift +++ b/Simple Anki/SwiftUI/SimpleAnkiApp.swift @@ -14,7 +14,6 @@ struct SimpleAnkiApp: SwiftUI.App { var body: some Scene { WindowGroup { -// ContentView() MainView() .environment(\.realmConfiguration, Realm.Configuration(schemaVersion: 1)) .preferredColorScheme(userSettings.colorScheme ? .dark : .light) diff --git a/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift b/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift new file mode 100644 index 0000000..7a94939 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift @@ -0,0 +1,75 @@ +// +// CardRepository.swift +// Simple Anki +// +// Created by Астемир Бозиев on 19.01.2024. +// + +import Foundation +import RealmSwift + +protocol Repository { + associatedtype Item + + func getCard(by id: ObjectId) -> Item? + func add(card: Item) + func update(card: Item) + func delete(by cardID: ObjectId) +} + +class CardRepository: Repository { + + typealias Item = Card + + private var realm: Realm = try! Realm() + private var deck: Deck + + init(deck: Deck) { + self.deck = deck + } + + func getCard(by id: ObjectId) -> Card? { + return realm.object(ofType: Card.self, forPrimaryKey: id) + } + + func add(card: Card) { + guard let deck = getDeck(by: deck._id) else { return } + try! realm.write { + deck.cards.append(card) + } + } + + func update(card: Card) { + guard let cardToUpdate = getCard(by: card._id) else { return } + + try! realm.write { + cardToUpdate.front = card.front + cardToUpdate.back = card.back + cardToUpdate.audioName = card.audioName + cardToUpdate.memorized = card.memorized + } + } + + func delete(by cardID: ObjectId) { + guard let index = getCardIndex(by: cardID) else { return } + + let audioName = deck.cards[index].audioName + + try! realm.write { + deck.cards.remove(at: index) + } + + LocalFileManager.shared.delete(audioName) + } +} + +extension CardRepository { + + private func getDeck(by id: ObjectId) -> Deck? { + return realm.object(ofType: Deck.self, forPrimaryKey: deck._id) + } + + private func getCardIndex(by cardID: ObjectId) -> Int? { + return deck.cards.firstIndex(where: { $0._id == cardID }) + } +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift index a4354bc..84ff7a7 100644 --- a/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift +++ b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift @@ -9,40 +9,39 @@ import SwiftUI import RealmSwift import AVFoundation -struct RecordingButton: View { - @ObservedObject var audioRecorder: AudioRecorder - @State private var showAlert: Bool = false - - var body: some View { - Button { - if audioRecorder.isRecording { -// audioRecorder.stopRecording() - } else { - audioRecorder.checkRecordPermission { granted in - if granted { - audioRecorder.startRecording() - } else { - showAlert.toggle() - } - } - } - } label: { - Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") - .foregroundStyle(audioRecorder.isRecording ? .red : .blue) - .font(.system(size: audioRecorder.isRecording ? 35 : 18)) - .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) - } - .alert("No access", isPresented: $showAlert, actions: { - Button("Cancel", role: .cancel, action: {}) - Button("Open settings", role: .none) { - - } - }) - } -} - -struct RecordingButton_Previews: PreviewProvider { - static var previews: some View { - RecordingButton(audioRecorder: AudioRecorder()) - } -} +// struct RecordingButton: View { +// @State private var showAlert: Bool = false +// +// var body: some View { +// Button { +// if audioRecorder.isRecording { +//// audioRecorder.stopRecording() +// } else { +// audioRecorder.checkRecordPermission { granted in +// if granted { +// audioRecorder.startRecording() +// } else { +// showAlert.toggle() +// } +// } +// } +// } label: { +// Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") +// .foregroundStyle(audioRecorder.isRecording ? .red : .blue) +// .font(.system(size: audioRecorder.isRecording ? 35 : 18)) +// .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) +// } +// .alert("No access", isPresented: $showAlert, actions: { +// Button("Cancel", role: .cancel, action: {}) +// Button("Open settings", role: .none) { +// +// } +// }) +// } +// } +// +// struct RecordingButton_Previews: PreviewProvider { +// static var previews: some View { +// RecordingButton(audioRecorder: AudioRecorder()) +// } +// } diff --git a/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift b/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift index 9225152..a373c37 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift +++ b/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift @@ -62,20 +62,21 @@ struct CardPreviewView: View { .font(.system(size: 35, weight: .medium)) .padding() - if let audio { - VStack { - Spacer() - - Button(action: { + VStack { + Spacer() - }, label: { - Image(systemName: "speaker.wave.2.circle") - .font(.system(size: 60, weight: .light)) - .foregroundStyle(.black) - }) - .padding(.bottom, 140) - } + Button(action: { + Task { + SoundManager.shared.play(sound: audio ?? "") + } + }, label: { + Image(systemName: "speaker.wave.2.circle") + .font(.system(size: 60, weight: .light)) + .foregroundStyle(.black) + }) + .padding(.bottom, 140) } + .opacity(audio != nil ? 1 : 0) } .toolbar { ToolbarItem(placement: .confirmationAction) { diff --git a/Simple Anki/SwiftUI/UI/Views/CardView.swift b/Simple Anki/SwiftUI/UI/Views/CardView.swift index a08dd83..1c4c846 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/CardView.swift @@ -9,13 +9,19 @@ import SwiftUI import RealmSwift import PhotosUI +enum FocusableField: Hashable { + case frontField + case backField +} + struct CardView: View { @State var viewModel: CardViewModel - @ObservedRealmObject var deck: Deck + @State var recorder: AudioRecorder @State private var selectedImage: PhotosPickerItem? @State private var isPreviewPresented: Bool = false @State private var showAlert: Bool = false + @FocusState private var focusedField: FocusableField? private var transaction: Transaction { var transaction = Transaction() @@ -23,9 +29,7 @@ struct CardView: View { return transaction } - @FocusState private var isTextFieldFocused: Bool @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var audioRecorder: AudioRecorder var body: some View { // MARK: VSTACK START @@ -35,16 +39,22 @@ struct CardView: View { VStack { TextField("Front word", text: $viewModel.frontWord) .padding(.bottom) + .submitLabel(.next) + .focused($focusedField, equals: .frontField) + .onSubmit { + focusedField = .backField + } Divider() TextField("Back word", text: $viewModel.backWord) .padding(.top) + .submitLabel(.done) + .focused($focusedField, equals: .backField) } - .focused($isTextFieldFocused) .font(.system(size: 35, weight: .medium)) .multilineTextAlignment(.center) .padding() .onAppear { - isTextFieldFocused.toggle() + focusedField = .frontField } Spacer() @@ -61,35 +71,35 @@ struct CardView: View { Button { guard !viewModel.frontWord.isEmpty else { return } if viewModel.updating { - update() + viewModel.updateCard() } else { - addCard() + viewModel.addCard() viewModel.clear() + recorder.setName(UUID().uuidString + ".m4a") } -// writeToDisk(image: image, imageName: viewModel.frontWord + UUID().uuidString) + HapticManagerSUI.shared.impact(style: .heavy) } label: { - Image(systemName: "plus.circle.fill") - .font(.system(size: 35)) + Text(viewModel.updating ? "Update" : "Save") } + .controlSize(.extraLarge) + .buttonStyle(.borderedProminent) } - .opacity(audioRecorder.isRecording ? 0 : 1) - .offset(x: audioRecorder.isRecording ? -200 : 0) - .animation(.easeInOut(duration: 0.3), value: audioRecorder.isRecording) + .opacity(recorder.isRecording ? 0 : 1) + .offset(x: recorder.isRecording ? -200 : 0) + .animation(.easeInOut(duration: 0.3), value: recorder.isRecording) Spacer() if let fileName = viewModel.audioName { Button { - Task { - SoundManager.shared.play(sound: fileName) - } + SoundManager.shared.play(sound: fileName) } label: { Image(systemName: "play.circle") .font(.system(size: 18)) } .contextMenu { Button(role: .destructive) { - audioRecorder.deleteRecording() + deleteAudioAndUpdateCard() } label: { Label("Delete", systemImage: "trash") } @@ -98,23 +108,23 @@ struct CardView: View { Button { HapticManagerSUI.shared.impact(style: .heavy) - if audioRecorder.isRecording { - audioRecorder.stopRecording { fileName in + if recorder.isRecording { + recorder.stopRecording { fileName in viewModel.audioName = fileName } } else { - audioRecorder.checkRecordPermission { granted in + recorder.checkRecordPermission { granted in if granted { - audioRecorder.startRecording() + recorder.startRecording() } else { showAlert.toggle() } } } } label: { - Image(systemName: audioRecorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") - .foregroundStyle(audioRecorder.isRecording ? .red : .blue) - .font(.system(size: audioRecorder.isRecording ? 35 : 18)) + Image(systemName: recorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") + .foregroundStyle(recorder.isRecording ? .red : .blue) + .font(.system(size: recorder.isRecording ? 35 : 18)) .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) } .alert("No access", isPresented: $showAlert, actions: { @@ -132,9 +142,9 @@ struct CardView: View { Text("Recording...") .padding(.bottom, 3) .foregroundStyle(.gray) - .offset(x: audioRecorder.isRecording ? 0 : 100) - .opacity(audioRecorder.isRecording ? 1 : 0) - .animation(.easeInOut(duration: 0.3), value: audioRecorder.isRecording) + .offset(x: recorder.isRecording ? 0 : 100) + .opacity(recorder.isRecording ? 1 : 0) + .animation(.easeInOut(duration: 0.3), value: recorder.isRecording) } // MARK: ZSTACK END } // MARK: VSTACK END .onChange(of: selectedImage) { @@ -167,32 +177,6 @@ struct CardView: View { } } - private func update() { - guard let cardID = viewModel.id else { return } - guard let card = deck.cards.first(where: {$0._id == cardID }) else { return } - - do { - let realm = try Realm() - try realm.write { - card.thaw()?.front = viewModel.frontWord - card.thaw()?.back = viewModel.backWord - card.thaw()?.audioName = viewModel.audioName - card.thaw()?.memorized = viewModel.memorized - } - } catch { - print("Error: \(error)") - } - } - - private func addCard() { - let card = Card( - front: viewModel.frontWord, - back: viewModel.backWord, - audioName: viewModel.audioName - ) - $deck.cards.append(card) - } - private func writeToDisk(image: UIImage, imageName: String) { let savePath = FileManager.documentsDirectory.appendingPathComponent("\(imageName).jpg") if let jpegData = image.jpegData(compressionQuality: 0.5) { @@ -200,13 +184,18 @@ struct CardView: View { print("Image saved") } } + + private func deleteAudioAndUpdateCard() { + viewModel.audioName = nil + recorder.deleteRecording() + viewModel.updateCard() + } } struct CardView_Previews: PreviewProvider { static var previews: some View { NavigationView { - CardView(viewModel: CardViewModel(), deck: Deck.deck1) - .environmentObject(AudioRecorder()) + CardView(viewModel: CardViewModel(repositry: CardRepository(deck: Deck.deck1)), recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a")) } } } diff --git a/Simple Anki/SwiftUI/UI/Views/CardsView.swift b/Simple Anki/SwiftUI/UI/Views/CardsView.swift index d29fcf2..bc9c9f4 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardsView.swift +++ b/Simple Anki/SwiftUI/UI/Views/CardsView.swift @@ -34,9 +34,8 @@ struct CardsView: View { List { ForEach(deck.cards) { card in NavigationLink { - CardView(viewModel: CardViewModel(card: card), deck: deck) - .environmentObject(AudioRecorder(fileName: card.audioName)) - .navigationBarBackButtonHidden() + CardView(viewModel: CardViewModel(card: card, repository: CardRepository(deck: deck)), recorder: AudioRecorder(fileName: card.audioName != nil ? card.audioName! : UUID().uuidString + ".m4a")) + .navigationBarBackButtonHidden() } label: { Text(card.front) } @@ -51,37 +50,7 @@ struct CardsView: View { } .onMove(perform: $deck.cards.move) } - VStack { - Spacer() - HStack { - Button { - - } label: { - Image(systemName: "gearshape") - .padding(.vertical, 8) - } - .tint(.blue) - .buttonStyle(.bordered) -// .confirmationDialog("Test", isPresented: .constant(true)) { -// -// } - - Button { - isReviewPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Review") - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .fullScreenCover(isPresented: $isReviewPresented) { - ReviewView(reviewManager: ReviewManagerSUI(cards: deck.cards)) - } - } - .padding() - - } + .listStyle(.plain) } } .toolbar { @@ -93,8 +62,34 @@ struct CardsView: View { } .sheet(isPresented: $isCardViewPresented) { NavigationView { - CardView(viewModel: CardViewModel(), deck: deck) - .environmentObject(AudioRecorder()) + CardView(viewModel: CardViewModel(repositry: CardRepository(deck: deck)), recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a")) + } + } + } + } + .toolbar { + ToolbarItemGroup(placement: .bottomBar) { + HStack { + Button { + + } label: { + Image(systemName: "gearshape") + .padding(.vertical, 8) + } + .tint(.blue) + .buttonStyle(.bordered) + + Button { + isReviewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Review") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .fullScreenCover(isPresented: $isReviewPresented) { + ReviewView(reviewManager: ReviewManagerSUI(cards: deck.cards)) } } } diff --git a/Simple Anki/SwiftUI/UI/Views/DecksView.swift b/Simple Anki/SwiftUI/UI/Views/DecksView.swift index e71d6ee..ca5b2b6 100644 --- a/Simple Anki/SwiftUI/UI/Views/DecksView.swift +++ b/Simple Anki/SwiftUI/UI/Views/DecksView.swift @@ -11,6 +11,7 @@ import RealmSwift struct DecksView: View { @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks @State private var isNewDeckViewPresented: Bool = false + @State private var deckName: String = "" var body: some View { NavigationView { @@ -29,9 +30,6 @@ struct DecksView: View { } .controlSize(.regular) .buttonStyle(.borderedProminent) - .sheet(isPresented: $isNewDeckViewPresented, content: { - NewDeckView() - }) }) } else { List { @@ -55,6 +53,7 @@ struct DecksView: View { } } } + .listStyle(.plain) } } .navigationTitle("Decks") @@ -71,8 +70,15 @@ struct DecksView: View { } label: { Image(systemName: "plus.circle.fill") } - .sheet(isPresented: $isNewDeckViewPresented) { - NewDeckView() + .alert("New deck", isPresented: $isNewDeckViewPresented) { + TextField("Enter deck name", text: $deckName) + Button("Add") { + addDeck() + deckName = "" + } + Button("Cancel", role: .cancel) {} + } message: { + } } } @@ -80,6 +86,11 @@ struct DecksView: View { .navigationViewStyle(.stack) } + private func addDeck() { + let deck = Deck(name: deckName.trimmingCharacters(in: .whitespacesAndNewlines)) + $decks.append(deck) + } + @ViewBuilder private func cardsCount(of deck: Deck) -> some View { let cardCount = deck.cards.count Text(cardCount == 0 ? "No cards" : "^[\(cardCount) card](inflect: true)") @@ -104,8 +115,6 @@ struct DecksView: View { } } -struct DecksView_Previews: PreviewProvider { - static var previews: some View { - DecksView() - } +#Preview { + DecksView() } diff --git a/Simple Anki/SwiftUI/UI/Views/MainView.swift b/Simple Anki/SwiftUI/UI/Views/MainView.swift index b3791a2..6e76959 100644 --- a/Simple Anki/SwiftUI/UI/Views/MainView.swift +++ b/Simple Anki/SwiftUI/UI/Views/MainView.swift @@ -9,12 +9,12 @@ import SwiftUI struct MainView: View { - init() { - let appearance = UITabBarAppearance() - appearance.configureWithOpaqueBackground() - UITabBar.appearance().standardAppearance = appearance - UITabBar.appearance().scrollEdgeAppearance = appearance - } +// init() { +// let appearance = UITabBarAppearance() +// appearance.configureWithOpaqueBackground() +// UITabBar.appearance().standardAppearance = appearance +// UITabBar.appearance().scrollEdgeAppearance = appearance +// } var body: some View { TabView { @@ -35,8 +35,12 @@ struct MainView: View { } } -struct MainView_Previews: PreviewProvider { - static var previews: some View { - MainView() - } +#Preview { + MainView() } + +// struct MainView_Previews: PreviewProvider { +// static var previews: some View { +// MainView() +// } +// } diff --git a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift index af195e1..4c7ef1e 100644 --- a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift @@ -8,11 +8,6 @@ import SwiftUI import RealmSwift -enum FocusableField: Hashable { - case frontField - case backField -} - struct NewCardView: View { @ObservedRealmObject var deck: Deck @@ -138,7 +133,6 @@ struct NewCardView_Previews: PreviewProvider { static var previews: some View { NavigationView { NewCardView(deck: Deck.deck1) - .environmentObject(AudioRecorder()) } } } diff --git a/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift b/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift deleted file mode 100644 index 11aca3e..0000000 --- a/Simple Anki/SwiftUI/UI/Views/NewDeckView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// NewDeckView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 20.08.2023. -// - -import SwiftUI -import RealmSwift - -struct NewDeckView: View { - @Environment(\.dismiss) private var dismiss - - @EnvironmentObject private var userSettings: UserSettings - @ObservedResults(Deck.self) var decks - @State private var deckNameText: String = "" - @FocusState private var isTextFieldFocused: Bool - - var body: some View { - NavigationView { - VStack { - TextField("Name", text: $deckNameText) - .font(.system(size: 35, weight: .bold)) - .padding(.top, 160) - .focused($isTextFieldFocused) - Divider() - Spacer() - - Button { - addDeck() - HapticManagerSUI.shared.impact(style: .heavy) - dismiss() - } label: { - HStack { - Text("Create") - } - .padding(8) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - } - .onAppear { - isTextFieldFocused.toggle() - } - .navigationTitle("New deck") - .navigationBarTitleDisplayMode(.inline) - .padding() - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button { - dismiss() - } label: { - Image(systemName: "xmark") - .foregroundColor(.gray) - } - } - } - } - .preferredColorScheme(userSettings.colorScheme ? .dark : .light) - } - - private func addDeck() { - let deck = Deck(name: deckNameText.trimmingCharacters(in: .whitespacesAndNewlines)) - $decks.append(deck) - } -} - -struct NewDeckView_Previews: PreviewProvider { - @State static var isPresented: Bool = true - - static var previews: some View { - NewDeckView() - .environmentObject(UserSettings()) - } -} diff --git a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift index 80aea1b..12b2e8c 100644 --- a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift +++ b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift @@ -24,15 +24,22 @@ final class CardViewModel { @ObservationIgnored var updating: Bool { id != nil } - init() {} + @ObservationIgnored + private var cardRepository: CardRepository + + init(repositry: CardRepository) { + self.cardRepository = repositry + } - init(card: Card) { + init(card: Card, repository: CardRepository) { self.frontWord = card.front self.backWord = card.back self.memorized = card.memorized self.audioName = card.audioName self.image = UIImage(data: Data()) self.id = card._id + + self.cardRepository = repository } func clear() { @@ -40,4 +47,22 @@ final class CardViewModel { backWord = "" audioName = nil } + + func addCard() { + let card = Card(front: frontWord, back: backWord, audioName: audioName) + cardRepository.add(card: card) + } + + func updateCard() { + guard let id = id else { return } + let card = Card(front: frontWord, back: backWord, audioName: audioName) + card._id = id + card.memorized = memorized + cardRepository.update(card: card) + } + + func deleteCard() { + guard let id = id else { return } + cardRepository.delete(by: id) + } } diff --git a/Simple Anki/ViewController.swift b/Simple Anki/ViewController.swift index b6036ce..936b589 100644 --- a/Simple Anki/ViewController.swift +++ b/Simple Anki/ViewController.swift @@ -9,9 +9,4 @@ import UIKit class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - } diff --git a/Simple AnkiUITests/Screens/DecksScreen.swift b/Simple AnkiUITests/Screens/DecksScreen.swift index 4bfede1..e874e64 100644 --- a/Simple AnkiUITests/Screens/DecksScreen.swift +++ b/Simple AnkiUITests/Screens/DecksScreen.swift @@ -11,7 +11,7 @@ import XCTest class DecksScreen: BaseScreen { - private lazy var addButton = app.navigationBars["Decks"]/*@START_MENU_TOKEN@*/.buttons["addButton"]/*[[".buttons[\"add\"]",".buttons[\"addButton\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ + private lazy var addButton = app.navigationBars["Decks"].buttons["addButton"] private lazy var noDecksStaticText = app.staticTexts["There are no decks yet"] private lazy var addNewDeckAlert = AddNewDeckAlert() lazy var baseElement = addButton From 5c276a9c2a1f4a9445905712b8a19cf30a8bc4f6 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 11 Feb 2024 15:24:33 +0400 Subject: [PATCH 6/9] almost done --- .swiftlint.yml | 17 +- Simple Anki.xcodeproj/project.pbxproj | 95 ++++++- .../github-fill.symbolset/Contents.json | 12 + .../github-fill.symbolset/github-fill.svg | 101 ++++++++ Simple Anki/ConstantsOld.swift | 2 +- Simple Anki/Extensions/DateExtension.swift | 6 - Simple Anki/Managers/ReminderManager.swift | 7 +- .../SwiftUI/Managers/AudioRecorder.swift | 24 +- .../SwiftUI/Managers/LocalFileManager.swift | 5 +- .../SwiftUI/Managers/ReminderManagerSUI.swift | 163 ++++++++++++ .../SwiftUI/Managers/ReviewManagerSUI.swift | 17 +- .../SwiftUI/Managers/SoundManager.swift | 47 ++-- Simple Anki/SwiftUI/Models/CardSUI.swift | 5 +- Simple Anki/SwiftUI/Models/DeckSUI.swift | 2 +- Simple Anki/SwiftUI/SimpleAnkiApp.swift | 10 +- .../UI/Repositories/CardRepository.swift | 10 +- .../UI/UIComponents/DelimeterButton.swift | 33 +++ .../UI/UIComponents/LayoutButton.swift | 47 ++++ .../SwiftUI/UI/UIComponents/TabItemView.swift | 43 ++++ .../UI/Views/{ => Card}/CardPreviewView.swift | 0 .../UI/Views/{ => Card}/CardView.swift | 23 +- .../SwiftUI/UI/Views/Card/CardsView.swift | 135 ++++++++++ Simple Anki/SwiftUI/UI/Views/CardsView.swift | 120 --------- .../SwiftUI/UI/Views/ContentView.swift | 232 +++++++++++++++++- .../UI/Views/Deck/CreateDeckView.swift | 69 ++++++ .../UI/Views/Deck/DeckSettingsView.swift | 78 ++++++ .../SwiftUI/UI/Views/Deck/DecksView.swift | 167 +++++++++++++ .../UI/Views/Deck/ImportDeckView.swift | 96 ++++++++ .../UI/Views/Deck/ImportedDeckView.swift | 61 +++++ .../SwiftUI/UI/Views/Deck/NewDeckView.swift | 161 ++++++++++++ Simple Anki/SwiftUI/UI/Views/DecksView.swift | 120 --------- Simple Anki/SwiftUI/UI/Views/MainView.swift | 60 +++-- .../SwiftUI/UI/Views/NewCardView.swift | 138 ----------- Simple Anki/SwiftUI/UI/Views/ReviewView.swift | 27 +- .../UI/Views/Settings/ReminderView.swift | 84 +++++++ .../UI/Views/Settings/SettingsView.swift | 98 ++++++++ .../SwiftUI/UI/Views/SettingsView.swift | 50 ---- .../SwiftUI/ViewModels/CardViewModel.swift | 7 + 38 files changed, 1830 insertions(+), 542 deletions(-) create mode 100644 Simple Anki/Assets.xcassets/github-fill.symbolset/Contents.json create mode 100644 Simple Anki/Assets.xcassets/github-fill.symbolset/github-fill.svg create mode 100644 Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/DelimeterButton.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/LayoutButton.swift create mode 100644 Simple Anki/SwiftUI/UI/UIComponents/TabItemView.swift rename Simple Anki/SwiftUI/UI/Views/{ => Card}/CardPreviewView.swift (100%) rename Simple Anki/SwiftUI/UI/Views/{ => Card}/CardView.swift (88%) create mode 100644 Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift delete mode 100644 Simple Anki/SwiftUI/UI/Views/CardsView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Deck/ImportDeckView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift delete mode 100644 Simple Anki/SwiftUI/UI/Views/DecksView.swift delete mode 100644 Simple Anki/SwiftUI/UI/Views/NewCardView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift create mode 100644 Simple Anki/SwiftUI/UI/Views/Settings/SettingsView.swift delete mode 100644 Simple Anki/SwiftUI/UI/Views/SettingsView.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index cd7560c..438dd5f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,6 +5,7 @@ disabled_rules: # rule identifiers turned on by default to exclude from running - control_statement - function_body_length - line_length + - identifier_name opt_in_rules: # some rules are turned off by default, so you need to opt-in - empty_count # Find all the available rules by running: `swiftlint rules` @@ -48,13 +49,13 @@ type_name: error: 50 excluded: iPhone # excluded via string allowed_symbols: ["_"] # these are allowed in type names -identifier_name: - min_length: # only min_length - error: 3 # only error - excluded: # excluded via string array - - id - - _id - - URL - - GlobalAPIKey +#identifier_name: +# min_length: # only min_length +# error: 3 # only error +# excluded: # excluded via string array +# - id +# - _id +# - URL +# - GlobalAPIKey reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging) diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index 090f9e1..0892463 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348CA2861F47E002FFBDF /* UILabelExtension.swift */; }; C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C02348D2286343F4002FFBDF /* StoreKit.framework */; }; C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */; }; + C02404082B6835930017EF2E /* TabItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02404072B6835930017EF2E /* TabItemView.swift */; }; C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */; }; C028C4C8280C852400F6894E /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = C028C4C7280C852400F6894E /* .gitignore */; }; C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C028C4C92811C0F000F6894E /* NewDeckViewController.swift */; }; @@ -23,14 +24,20 @@ C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41C2282146BF005E9835 /* DateExtension.swift */; }; C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */; }; C03A02612B5299EF00576543 /* CardViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A02602B5299EF00576543 /* CardViewState.swift */; }; + C03B91C82B6FA60500EA483F /* DeckSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */; }; + C03B91CA2B6FA82400EA483F /* LayoutButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B91C92B6FA82400EA483F /* LayoutButton.swift */; }; C05695D42B501AAA00033AF2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05695D32B501AAA00033AF2 /* ContentView.swift */; }; + C05BCFF02B78CD3F0076CAD9 /* CreateDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFEF2B78CD3F0076CAD9 /* CreateDeckView.swift */; }; + C05BCFF22B78CF4D0076CAD9 /* ImportDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */; }; + C05BCFF42B78D04D0076CAD9 /* DelimeterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF32B78D04D0076CAD9 /* DelimeterButton.swift */; }; + C05BCFF62B78D12C0076CAD9 /* ImportedDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */; }; C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */; }; C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */; }; C067B6872A8C1251000AF881 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6862A8C1251000AF881 /* MainView.swift */; }; C067B6892A8C139A000AF881 /* DecksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksView.swift */; }; C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B68A2A8C13A5000AF881 /* SettingsView.swift */; }; C070008E263E86E0006DF020 /* RateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070008D263E86E0006DF020 /* RateManager.swift */; }; - C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581C2A924A0B0046F39D /* NewCardView.swift */; }; + C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0730B162B77F26A00B43D42 /* NewDeckView.swift */; }; C074581F2A9293AA0046F39D /* CardsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581E2A9293AA0046F39D /* CardsView.swift */; }; C07458232A938CF90046F39D /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458222A938CF90046F39D /* UserSettings.swift */; }; C07458252A93943E0046F39D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458242A93943E0046F39D /* Constants.swift */; }; @@ -43,6 +50,7 @@ C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903242A8D444F00EFC51C /* DeckSUI.swift */; }; C08903272A8D474900EFC51C /* CardSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903262A8D474900EFC51C /* CardSUI.swift */; }; C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */; }; + C09AE07E2B6A726B00DB3E28 /* Pow in Frameworks */ = {isa = PBXBuildFile; productRef = C09AE07D2B6A726B00DB3E28 /* Pow */; }; C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0C6492A9DFC210015C65E /* SoundManager.swift */; }; C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70E5F28211E620020D533 /* ReminderManager.swift */; }; C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4125EC0E0B005DE503 /* Deck.swift */; }; @@ -101,6 +109,8 @@ C0F4FD5127BD87CE00814BD6 /* DecksScrenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD4F27BD87CE00814BD6 /* DecksScrenTests.swift */; }; C0F4FD5527BD87EB00814BD6 /* WaitUtillities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD5327BD87EB00814BD6 /* WaitUtillities.swift */; }; C0F4FD5627BD87EB00814BD6 /* RandomGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD5427BD87EB00814BD6 /* RandomGenerator.swift */; }; + C0F730C22B7424D900127CB4 /* ReminderManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F730C12B7424D900127CB4 /* ReminderManagerSUI.swift */; }; + C0F730C42B752F7100127CB4 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F730C32B752F7100127CB4 /* ReminderView.swift */; }; C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FA7EA925F511FE00710F0D /* ConstantsOld.swift */; }; /* End PBXBuildFile section */ @@ -131,6 +141,7 @@ C02348CA2861F47E002FFBDF /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; C02348D2286343F4002FFBDF /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewController.swift; sourceTree = ""; }; + C02404072B6835930017EF2E /* TabItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemView.swift; sourceTree = ""; }; C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewModel.swift; sourceTree = ""; }; C028C4C7280C852400F6894E /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; C028C4C92811C0F000F6894E /* NewDeckViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckViewController.swift; sourceTree = ""; }; @@ -138,14 +149,20 @@ C02D41C2282146BF005E9835 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; C03A02602B5299EF00576543 /* CardViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewState.swift; sourceTree = ""; }; + C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckSettingsView.swift; sourceTree = ""; }; + C03B91C92B6FA82400EA483F /* LayoutButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutButton.swift; sourceTree = ""; }; C05695D32B501AAA00033AF2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + C05BCFEF2B78CD3F0076CAD9 /* CreateDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDeckView.swift; sourceTree = ""; }; + C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportDeckView.swift; sourceTree = ""; }; + C05BCFF32B78D04D0076CAD9 /* DelimeterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelimeterButton.swift; sourceTree = ""; }; + C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedDeckView.swift; sourceTree = ""; }; C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSettingsCell.swift; sourceTree = ""; }; C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnkiApp.swift; sourceTree = ""; }; C067B6862A8C1251000AF881 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; C067B6882A8C139A000AF881 /* DecksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksView.swift; sourceTree = ""; }; C067B68A2A8C13A5000AF881 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C070008D263E86E0006DF020 /* RateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateManager.swift; sourceTree = ""; }; - C074581C2A924A0B0046F39D /* NewCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardView.swift; sourceTree = ""; }; + C0730B162B77F26A00B43D42 /* NewDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckView.swift; sourceTree = ""; }; C074581E2A9293AA0046F39D /* CardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsView.swift; sourceTree = ""; }; C07458222A938CF90046F39D /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; C07458242A93943E0046F39D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; @@ -216,6 +233,8 @@ C0F4FD4F27BD87CE00814BD6 /* DecksScrenTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecksScrenTests.swift; sourceTree = ""; }; C0F4FD5327BD87EB00814BD6 /* WaitUtillities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaitUtillities.swift; sourceTree = ""; }; C0F4FD5427BD87EB00814BD6 /* RandomGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RandomGenerator.swift; sourceTree = ""; }; + C0F730C12B7424D900127CB4 /* ReminderManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderManagerSUI.swift; sourceTree = ""; }; + C0F730C32B752F7100127CB4 /* ReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = ""; }; C0FA7EA925F511FE00710F0D /* ConstantsOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsOld.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -225,6 +244,7 @@ buildActionMask = 2147483647; files = ( C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */, + C09AE07E2B6A726B00DB3E28 /* Pow in Frameworks */, C0B85474282AB18F009816D0 /* SQLite in Frameworks */, C0DD10BB2823F6D10058F96B /* SwiftCSV in Frameworks */, C0B5FFAC25E98362001D8D83 /* Realm in Frameworks */, @@ -279,6 +299,7 @@ C0750ECF2AA3A60400089B29 /* AudioRecorder.swift */, C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */, C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */, + C0F730C12B7424D900127CB4 /* ReminderManagerSUI.swift */, ); path = Managers; sourceTree = ""; @@ -337,6 +358,38 @@ path = SwiftUI; sourceTree = ""; }; + C0730B182B77F28900B43D42 /* Deck */ = { + isa = PBXGroup; + children = ( + C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */, + C0730B162B77F26A00B43D42 /* NewDeckView.swift */, + C067B6882A8C139A000AF881 /* DecksView.swift */, + C05BCFEF2B78CD3F0076CAD9 /* CreateDeckView.swift */, + C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */, + C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */, + ); + path = Deck; + sourceTree = ""; + }; + C0730B192B77F2A200B43D42 /* Card */ = { + isa = PBXGroup; + children = ( + C00513FA2B517E91005F5815 /* CardPreviewView.swift */, + C0CD12542AAB3F2700FE3BB6 /* CardView.swift */, + C074581E2A9293AA0046F39D /* CardsView.swift */, + ); + path = Card; + sourceTree = ""; + }; + C0730B1A2B77F2B700B43D42 /* Settings */ = { + isa = PBXGroup; + children = ( + C0F730C32B752F7100127CB4 /* ReminderView.swift */, + C067B68A2A8C13A5000AF881 /* SettingsView.swift */, + ); + path = Settings; + sourceTree = ""; + }; C07458262A939F800046F39D /* UIComponents */ = { isa = PBXGroup; children = ( @@ -346,6 +399,9 @@ C0CC42522B5052C000F683CE /* CardToolbarView.swift */, C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */, C03A02602B5299EF00576543 /* CardViewState.swift */, + C02404072B6835930017EF2E /* TabItemView.swift */, + C03B91C92B6FA82400EA483F /* LayoutButton.swift */, + C05BCFF32B78D04D0076CAD9 /* DelimeterButton.swift */, ); path = UIComponents; sourceTree = ""; @@ -353,15 +409,12 @@ C0750ED92AA492BD00089B29 /* Views */ = { isa = PBXGroup; children = ( + C0730B1A2B77F2B700B43D42 /* Settings */, + C0730B192B77F2A200B43D42 /* Card */, + C0730B182B77F28900B43D42 /* Deck */, C067B6862A8C1251000AF881 /* MainView.swift */, - C067B6882A8C139A000AF881 /* DecksView.swift */, - C074581E2A9293AA0046F39D /* CardsView.swift */, - C067B68A2A8C13A5000AF881 /* SettingsView.swift */, - C074581C2A924A0B0046F39D /* NewCardView.swift */, C07458242A93943E0046F39D /* Constants.swift */, C0750ED32AA3C01800089B29 /* ReviewView.swift */, - C0CD12542AAB3F2700FE3BB6 /* CardView.swift */, - C00513FA2B517E91005F5815 /* CardPreviewView.swift */, C05695D32B501AAA00033AF2 /* ContentView.swift */, ); path = Views; @@ -624,6 +677,7 @@ C0DD10BA2823F6D10058F96B /* SwiftCSV */, C0B85473282AB18F009816D0 /* SQLite */, C0B85476282ABA14009816D0 /* ZIPFoundation */, + C09AE07D2B6A726B00DB3E28 /* Pow */, ); productName = "Simple Anki"; productReference = C0B5FF7225E981A8001D8D83 /* Simple Anki.app */; @@ -703,6 +757,7 @@ C0DD10B92823F6D10058F96B /* XCRemoteSwiftPackageReference "SwiftCSV" */, C0B85472282AB18F009816D0 /* XCRemoteSwiftPackageReference "SQLite.swift" */, C0B85475282ABA14009816D0 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, + C09AE07C2B6A726B00DB3E28 /* XCRemoteSwiftPackageReference "Pow" */, ); productRefGroup = C0B5FF7325E981A8001D8D83 /* Products */; projectDirPath = ""; @@ -774,13 +829,16 @@ C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */, C0C9BAB62848B9F20020E555 /* APKGModel.swift in Sources */, C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */, + C0F730C42B752F7100127CB4 /* ReminderView.swift in Sources */, C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */, C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */, + C0F730C22B7424D900127CB4 /* ReminderManagerSUI.swift in Sources */, C07458252A93943E0046F39D /* Constants.swift in Sources */, C067B6892A8C139A000AF881 /* DecksView.swift in Sources */, C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */, + C05BCFF22B78CF4D0076CAD9 /* ImportDeckView.swift in Sources */, C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */, C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */, @@ -788,8 +846,8 @@ C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */, C0CC42532B5052C000F683CE /* CardToolbarView.swift in Sources */, C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */, + C05BCFF42B78D04D0076CAD9 /* DelimeterButton.swift in Sources */, C0750ED02AA3A60400089B29 /* AudioRecorder.swift in Sources */, - C074581D2A924A0B0046F39D /* NewCardView.swift in Sources */, C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */, C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */, C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */, @@ -803,6 +861,7 @@ C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */, C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */, C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */, + C03B91C82B6FA60500EA483F /* DeckSettingsView.swift in Sources */, C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */, C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */, C070008E263E86E0006DF020 /* RateManager.swift in Sources */, @@ -824,21 +883,26 @@ C0B6D156285E2DFD00E354BA /* FeatureView.swift in Sources */, C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */, C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */, + C05BCFF02B78CD3F0076CAD9 /* CreateDeckView.swift in Sources */, C074581F2A9293AA0046F39D /* CardsView.swift in Sources */, C0F4FD3127BD855A00814BD6 /* MainTabBarViewController.swift in Sources */, C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */, C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */, C0C9BAB82848BA160020E555 /* ImportViewController.swift in Sources */, C08903272A8D474900EFC51C /* CardSUI.swift in Sources */, + C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */, C0B5FF7625E981A8001D8D83 /* AppDelegate.swift in Sources */, C00648AF2682140800265EB8 /* Options.swift in Sources */, C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */, C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */, + C03B91CA2B6FA82400EA483F /* LayoutButton.swift in Sources */, C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */, C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */, + C05BCFF62B78D12C0076CAD9 /* ImportedDeckView.swift in Sources */, C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */, + C02404082B6835930017EF2E /* TabItemView.swift in Sources */, C07458282A939FA40046F39D /* LinkView.swift in Sources */, C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */, C0CD12552AAB3F2700FE3BB6 /* CardView.swift in Sources */, @@ -1197,6 +1261,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + C09AE07C2B6A726B00DB3E28 /* XCRemoteSwiftPackageReference "Pow" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/EmergeTools/Pow.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.3; + }; + }; C0B5FFA825E98362001D8D83 /* XCRemoteSwiftPackageReference "realm-cocoa" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/realm-cocoa"; @@ -1240,6 +1312,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + C09AE07D2B6A726B00DB3E28 /* Pow */ = { + isa = XCSwiftPackageProductDependency; + package = C09AE07C2B6A726B00DB3E28 /* XCRemoteSwiftPackageReference "Pow" */; + productName = Pow; + }; C0B5FFA925E98362001D8D83 /* RealmSwift */ = { isa = XCSwiftPackageProductDependency; package = C0B5FFA825E98362001D8D83 /* XCRemoteSwiftPackageReference "realm-cocoa" */; diff --git a/Simple Anki/Assets.xcassets/github-fill.symbolset/Contents.json b/Simple Anki/Assets.xcassets/github-fill.symbolset/Contents.json new file mode 100644 index 0000000..f304e7b --- /dev/null +++ b/Simple Anki/Assets.xcassets/github-fill.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "github-fill.svg", + "idiom" : "universal" + } + ] +} diff --git a/Simple Anki/Assets.xcassets/github-fill.symbolset/github-fill.svg b/Simple Anki/Assets.xcassets/github-fill.symbolset/github-fill.svg new file mode 100644 index 0000000..455db6d --- /dev/null +++ b/Simple Anki/Assets.xcassets/github-fill.symbolset/github-fill.svg @@ -0,0 +1,101 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.5.0 + Requires Xcode 15 or greater + Generated from github-fill + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Simple Anki/ConstantsOld.swift b/Simple Anki/ConstantsOld.swift index 0ce44b7..1e014e5 100644 --- a/Simple Anki/ConstantsOld.swift +++ b/Simple Anki/ConstantsOld.swift @@ -46,5 +46,5 @@ struct K { } static let email = "help@simpleanki.com" - static let appURL = "https://apple.co/2Sx5VC1" + static let appURL = "https://apple.co/3Sxml7z" } diff --git a/Simple Anki/Extensions/DateExtension.swift b/Simple Anki/Extensions/DateExtension.swift index 006d072..117f28d 100644 --- a/Simple Anki/Extensions/DateExtension.swift +++ b/Simple Anki/Extensions/DateExtension.swift @@ -6,9 +6,3 @@ // import Foundation - -extension Date { - func component(_ component: Calendar.Component) -> Int { - Calendar.current.component(component, from: self) - } -} diff --git a/Simple Anki/Managers/ReminderManager.swift b/Simple Anki/Managers/ReminderManager.swift index d854ed3..735b2cf 100644 --- a/Simple Anki/Managers/ReminderManager.swift +++ b/Simple Anki/Managers/ReminderManager.swift @@ -7,8 +7,9 @@ import Foundation import UserNotifications +import SwiftUI -struct Weekday { +struct Weekday: Identifiable, Codable { let id: Int let name: String } @@ -47,6 +48,10 @@ class ReminderManager { UserDefaults.standard.set(weekdays[index].id, forKey: weekdays[index].name) } + func addWeekdayToReminder(weekday: Weekday) { + UserDefaults.standard.set(weekday.id, forKey: weekday.name) + } + func deleteDayFromReminder(index: Int) { UserDefaults.standard.set(0, forKey: weekdays[index].name) } diff --git a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift index ee5af72..69ec5d1 100644 --- a/Simple Anki/SwiftUI/Managers/AudioRecorder.swift +++ b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift @@ -13,26 +13,28 @@ class AudioRecorder: NSObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { var isPlaybackReady = false var isRecording = false + @ObservationIgnored private var audioRecorder: AVAudioRecorder? + @ObservationIgnored private var player: AVAudioPlayer? + @ObservationIgnored private var fileName: String + @ObservationIgnored private var audioSession = AVAudioSession.sharedInstance() init(fileName: String) { self.fileName = fileName super.init() - setupRecorder() } - private var settings: [String: Any] = [ + private let settings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue, AVSampleRateKey: 44100.0, AVNumberOfChannelsKey: 2 ] - private func setupRecorder() { - + func startRecording() { do { try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) @@ -41,25 +43,17 @@ class AudioRecorder: NSObject, AVAudioRecorderDelegate, AVAudioPlayerDelegate { audioRecorder = try AVAudioRecorder(url: fileURL, settings: settings) audioRecorder?.delegate = self + audioRecorder?.prepareToRecord() + audioRecorder?.record() + isRecording = true } catch { print("Error setting up audio recorder: \(error.localizedDescription)") } } - func startRecording() { - audioRecorder?.record() - isRecording = true - } - func stopRecording(completion: @escaping (String) -> Void) { audioRecorder?.stop() - do { - try audioSession.setActive(false) - } catch { - print(error.localizedDescription) - } - isRecording = false isPlaybackReady = true completion(fileName) diff --git a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift index aee7d42..52c8fd4 100644 --- a/Simple Anki/SwiftUI/Managers/LocalFileManager.swift +++ b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift @@ -46,12 +46,9 @@ extension FileManager { } func delete(_ fileName: String?) { - guard let fileName = fileName else { return } - let path = Self.documentsDirectory.appendingPathComponent(fileName) - do { - try Self.default.removeItem(at: path) + try Self.default.removeItem(at: Self.documentsDirectory.appendingPathComponent(fileName)) } catch { print(error) } diff --git a/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift b/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift new file mode 100644 index 0000000..6f8b6f4 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift @@ -0,0 +1,163 @@ +// +// ReminderManagerSUI.swift +// Simple Anki +// +// Created by Астемир Бозиев on 08.02.2024. +// + +import Foundation +import UserNotifications + +@Observable +class ReminderViewModel { + + @ObservationIgnored + private let weekdayKey: String = "weekdays" + @ObservationIgnored + private let timeKey: String = "selectedTime" + @ObservationIgnored + private let reminerStateKey: String = "reminderState" + + var selectedTime: Date = .now + + var isReminderOn: Bool = false + + var weekdays: [Weekday] = [] { + didSet { + saveSelectedWeekdays() + } + } + + private var notificationService: NotificationService + + init() { + notificationService = NotificationService() + fetchWeekdays() + fetchTime() + fetchReminderState() + } + + func addWeekdayToReminder(weekday: Weekday) { + weekdays.append(weekday) + } + + func isWeekdayInReminder(weekday: Weekday) -> Bool { + return weekdays.contains { $0.name == weekday.name } + } + + func removeNotificationFromReminder(weekday: Weekday) { + guard let index = findIndex(of: weekday) else { return } + weekdays.remove(at: index) + notificationService.removeNotification(id: weekday.name) + + } + + private func findIndex(of weekday: Weekday) -> Int? { + return weekdays.firstIndex(where: { $0.name == weekday.name }) + } +} + +extension ReminderViewModel { + + private func fetchWeekdays() { + if let data = UserDefaults.standard.data(forKey: weekdayKey), + let decoded = try? JSONDecoder().decode([Weekday].self, from: data) { + weekdays = decoded + } + } + + func saveSelectedWeekdays() { + if let encoded = try? JSONEncoder().encode(weekdays) { + UserDefaults.standard.set(encoded, forKey: weekdayKey) + } + } + + func fetchTime() { + if let time = UserDefaults.standard.object(forKey: timeKey) as? Date { + selectedTime = time + } + } + + func saveSelectedTime() { + UserDefaults.standard.setValue(selectedTime, forKey: timeKey) + } + + func saveReminderState() { + UserDefaults.standard.setValue(isReminderOn, forKey: reminerStateKey) + } + + func turnOffNotifications() { + notificationService.removeAllNotifications() + weekdays = [] + } + + func fetchReminderState() { + guard let reminderState = UserDefaults.standard.object(forKey: reminerStateKey) as? Bool else { return } + isReminderOn = reminderState + } + + func scheduleReminder(for weekday: Weekday) async { + notificationService.removeNotification(id: weekday.name) + let notifcation = notificationService.notificationsCredentials(weekday: weekday) + let dateComponents = notificationService.buildDate(from: selectedTime, and: notifcation) + do { + let request = try notificationService.notificationRequest(for: dateComponents, notification: notifcation) + try await UNUserNotificationCenter.current().add(request) + } catch { + print(error.localizedDescription) + } + } +} + +extension Date { + func component(_ component: Calendar.Component) -> Int { + Calendar.current.component(component, from: self) + } +} + +class NotificationService { + + enum NotificationError: Error { + case error + } + + private func notiicationContent(from notification: CustomNotification) -> UNMutableNotificationContent { + let content = UNMutableNotificationContent() + content.sound = .default + content.title = notification.title + content.body = notification.body + return content + } + + func removeNotification(id: String) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id]) + + } + + func removeAllNotifications() { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + } + + func notificationsCredentials(weekday: Weekday) -> CustomNotification { + return CustomNotification( + title: "Review time!", + body: "Your decks are wating to be reviewed.", + weekday: weekday + ) + } + + func notificationRequest(for date: DateComponents, notification: CustomNotification) throws -> UNNotificationRequest { + let content = notiicationContent(from: notification) + let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true) + return UNNotificationRequest(identifier: notification.weekday.name, content: content, trigger: trigger) + } + + func buildDate(from date: Date, and notification: CustomNotification) -> DateComponents { + return DateComponents( + timeZone: TimeZone.current, + hour: date.component(.hour), + minute: date.component(.minute), + weekday: notification.weekday.id + ) + } +} diff --git a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift index ccb27cc..81777fc 100644 --- a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift +++ b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift @@ -11,29 +11,32 @@ import RealmSwift class ReviewManagerSUI: ObservableObject { @Published var currentCard: Card? @Published var isReviewing = false + var isAutoplayOn: Bool { + return deck.autoplay + } - private var cards: List private var currentIndex = 0 + private var deck: Deck - init(cards: List) { - self.cards = cards + init(deck: Deck) { + self.deck = deck } func startReview() { - guard !cards.isEmpty else { + guard !deck.cards.isEmpty else { return } currentIndex = 0 - currentCard = cards[currentIndex] + currentCard = deck.cards[currentIndex] isReviewing = true } func nextCard() { currentIndex += 1 - if currentIndex < cards.count { - currentCard = cards[currentIndex] + if currentIndex < deck.cards.count { + currentCard = deck.cards[currentIndex] } else { isReviewing = false } diff --git a/Simple Anki/SwiftUI/Managers/SoundManager.swift b/Simple Anki/SwiftUI/Managers/SoundManager.swift index c77fa2b..e3ec0a2 100644 --- a/Simple Anki/SwiftUI/Managers/SoundManager.swift +++ b/Simple Anki/SwiftUI/Managers/SoundManager.swift @@ -19,35 +19,40 @@ class SoundManager { } private func setupPlayer() { - do { - try audioSession.setCategory(.playback) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - } catch { - print(error.localizedDescription) + DispatchQueue.global(qos: .background).async { + do { + try self.audioSession.setCategory(.playAndRecord) + try self.audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + print(error.localizedDescription) + } } } func play(sound: String) { - let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let fullUrl = documentDirectory.appendingPathComponent(sound) - - do { - self.player = try AVAudioPlayer(contentsOf: fullUrl) - self.player?.prepareToPlay() - self.player?.play() - } catch let error { - print("Error playing sound: \(error.localizedDescription)") - print("Error playing sound: \(error)") + let url = FileManager.documentsDirectory.appendingPathComponent(sound) + print(url) + + DispatchQueue.global(qos: .background).async { + do { + self.player = try AVAudioPlayer(contentsOf: url) + self.player?.play() + } catch { + print("Error playing sound: \(error.localizedDescription)") + print("Error playing sound: \(error)") + } } } func play(sound: URL) { - do { - self.player = try AVAudioPlayer(contentsOf: sound) - self.player?.prepareToPlay() - self.player?.play() - } catch let error { - print("Error playing sound: \(error.localizedDescription)") + DispatchQueue.global(qos: .background).async { + do { + self.player = try AVAudioPlayer(contentsOf: sound) + self.player?.prepareToPlay() + self.player?.play() + } catch let error { + print("Error playing sound: \(error.localizedDescription)") + } } } } diff --git a/Simple Anki/SwiftUI/Models/CardSUI.swift b/Simple Anki/SwiftUI/Models/CardSUI.swift index ac8bfaa..2a48081 100644 --- a/Simple Anki/SwiftUI/Models/CardSUI.swift +++ b/Simple Anki/SwiftUI/Models/CardSUI.swift @@ -12,16 +12,19 @@ class Card: Object, ObjectKeyIdentifiable { @Persisted(primaryKey: true) var _id: ObjectId @Persisted var front: String @Persisted var back: String + @Persisted var image: String? @Persisted var dateCreated: Date = Date() @Persisted var audioName: String? @Persisted var memorized: Bool = false + @Persisted var shuffled: Bool = false @Persisted(originProperty: "cards") var deck: LinkingObjects - convenience init(front: String, back: String, audioName: String? = nil) { + convenience init(front: String, back: String, audioName: String? = nil, image: String? = nil) { self.init() self.front = front self.back = back self.audioName = audioName + self.image = image } } diff --git a/Simple Anki/SwiftUI/Models/DeckSUI.swift b/Simple Anki/SwiftUI/Models/DeckSUI.swift index e60d296..14fe7fa 100644 --- a/Simple Anki/SwiftUI/Models/DeckSUI.swift +++ b/Simple Anki/SwiftUI/Models/DeckSUI.swift @@ -17,7 +17,7 @@ import RealmSwift class Deck: Object, ObjectKeyIdentifiable { @Persisted(primaryKey: true) var _id: ObjectId @Persisted var name: String - @Persisted var dateCreated: Date = Date() + @Persisted(indexed: true) var dateCreated: Date = Date() @Persisted var layout: String = "frontToBack" @Persisted var autoplay: Bool = false @Persisted var cards: List diff --git a/Simple Anki/SwiftUI/SimpleAnkiApp.swift b/Simple Anki/SwiftUI/SimpleAnkiApp.swift index a7519a0..e4ce582 100644 --- a/Simple Anki/SwiftUI/SimpleAnkiApp.swift +++ b/Simple Anki/SwiftUI/SimpleAnkiApp.swift @@ -15,9 +15,15 @@ struct SimpleAnkiApp: SwiftUI.App { var body: some Scene { WindowGroup { MainView() - .environment(\.realmConfiguration, Realm.Configuration(schemaVersion: 1)) + .environment(\.realmConfiguration, Realm.Configuration(schemaVersion: 2, migrationBlock: { migration, oldSchemaVersion in + if oldSchemaVersion < 2 { + migration.enumerateObjects(ofType: Card.className()) { _, newObject in + newObject!["shuffled"] = false + newObject!["image"] = nil + } + } + })) .preferredColorScheme(userSettings.colorScheme ? .dark : .light) - .environmentObject(userSettings) .onAppear { print(NSHomeDirectory()) } diff --git a/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift b/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift index 7a94939..6c9f525 100644 --- a/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift +++ b/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift @@ -11,7 +11,7 @@ import RealmSwift protocol Repository { associatedtype Item - func getCard(by id: ObjectId) -> Item? + func fetchCard(by id: ObjectId) -> Item? func add(card: Item) func update(card: Item) func delete(by cardID: ObjectId) @@ -28,19 +28,19 @@ class CardRepository: Repository { self.deck = deck } - func getCard(by id: ObjectId) -> Card? { + func fetchCard(by id: ObjectId) -> Card? { return realm.object(ofType: Card.self, forPrimaryKey: id) } func add(card: Card) { - guard let deck = getDeck(by: deck._id) else { return } + guard let deck = fetchDeck(by: deck._id) else { return } try! realm.write { deck.cards.append(card) } } func update(card: Card) { - guard let cardToUpdate = getCard(by: card._id) else { return } + guard let cardToUpdate = fetchCard(by: card._id) else { return } try! realm.write { cardToUpdate.front = card.front @@ -65,7 +65,7 @@ class CardRepository: Repository { extension CardRepository { - private func getDeck(by id: ObjectId) -> Deck? { + private func fetchDeck(by id: ObjectId) -> Deck? { return realm.object(ofType: Deck.self, forPrimaryKey: deck._id) } diff --git a/Simple Anki/SwiftUI/UI/UIComponents/DelimeterButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/DelimeterButton.swift new file mode 100644 index 0000000..5b24cb9 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/DelimeterButton.swift @@ -0,0 +1,33 @@ +// +// DelimeterButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.02.2024. +// + +import SwiftUI +import SwiftCSV + +struct DelimeterButton: View { + var title: String + var delimeter: CSVDelimiter + @Binding var selectedDelimeter: CSVDelimiter + var body: some View { + Button(action: { + selectedDelimeter = delimeter + }, label: { + HStack { + Text(title) + Spacer() + Image(systemName: "checkmark") + .foregroundStyle(.blue) + .opacity(selectedDelimeter == delimeter ? 1: 0) + } + }) + .tint(.primary) + } +} + +#Preview { + DelimeterButton(title: "Comma", delimeter: .comma, selectedDelimeter: .constant(.comma)) +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/LayoutButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/LayoutButton.swift new file mode 100644 index 0000000..2ee62ea --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/LayoutButton.swift @@ -0,0 +1,47 @@ +// +// LayoutButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 04.02.2024. +// + +import SwiftUI +import RealmSwift + +struct LayoutButton: View { + var labelText: String + var layout: String + + @ObservedRealmObject var deck: Deck + @Environment(\.realm) var realm + + var body: some View { + Button { + do { + let thawedDeck = deck.thaw() + try realm.write { + thawedDeck?.layout = layout + } + } catch { + print(error) + } + HapticManagerSUI.shared.impact(style: .light) + } label: { + HStack { + Text(labelText) + + Spacer() + + Image(systemName: "checkmark") + .foregroundStyle(.blue) + .opacity(deck.layout == layout ? 1 : 0) + } + } + .tint(.primary) + } +} + +#Preview { + LayoutButton(labelText: "Front to Back", layout: K.Layout.frontToBack, deck: Deck.deck1) + .padding() +} diff --git a/Simple Anki/SwiftUI/UI/UIComponents/TabItemView.swift b/Simple Anki/SwiftUI/UI/UIComponents/TabItemView.swift new file mode 100644 index 0000000..00cb440 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/TabItemView.swift @@ -0,0 +1,43 @@ +// +// TabItemView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 29.01.2024. +// + +import SwiftUI + +struct TabItemView: View { + let title: String + let icon: String + let tab: Tab + + @Binding var selectedTab: Tab + + init(tab: Tab, title: String, icon: String, selectedTab: Binding) { + self.tab = tab + self.title = title + self.icon = icon + self._selectedTab = selectedTab + } + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 24)) + Text(title) + .font(.system(size: 11)) + } + .foregroundColor(selectedTab == tab ? .blue : .secondary) + } + .frame(width: 65, height: 42) + .onTapGesture { + selectedTab = tab + } + } +} + +#Preview { + TabItemView(tab: .first, title: "Decks", icon: "tray.full", selectedTab: .constant(.first)) +} diff --git a/Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardPreviewView.swift similarity index 100% rename from Simple Anki/SwiftUI/UI/Views/CardPreviewView.swift rename to Simple Anki/SwiftUI/UI/Views/Card/CardPreviewView.swift diff --git a/Simple Anki/SwiftUI/UI/Views/CardView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift similarity index 88% rename from Simple Anki/SwiftUI/UI/Views/CardView.swift rename to Simple Anki/SwiftUI/UI/Views/Card/CardView.swift index 1c4c846..64aab75 100644 --- a/Simple Anki/SwiftUI/UI/Views/CardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift @@ -8,6 +8,7 @@ import SwiftUI import RealmSwift import PhotosUI +import Pow enum FocusableField: Hashable { case frontField @@ -21,6 +22,7 @@ struct CardView: View { @State private var selectedImage: PhotosPickerItem? @State private var isPreviewPresented: Bool = false @State private var showAlert: Bool = false + @State private var saveButtonTapped: Bool = false @FocusState private var focusedField: FocusableField? private var transaction: Transaction { @@ -36,6 +38,7 @@ struct CardView: View { VStack { Spacer() + // MARK: VSTACK START VStack { TextField("Front word", text: $viewModel.frontWord) .padding(.bottom) @@ -49,7 +52,7 @@ struct CardView: View { .padding(.top) .submitLabel(.done) .focused($focusedField, equals: .backField) - } + } // MARK: VSTACK END .font(.system(size: 35, weight: .medium)) .multilineTextAlignment(.center) .padding() @@ -65,6 +68,7 @@ struct CardView: View { HStack { Group { ImagePickerButton(image: $viewModel.image) + .hidden() Spacer() @@ -77,10 +81,19 @@ struct CardView: View { viewModel.clear() recorder.setName(UUID().uuidString + ".m4a") } + withAnimation { + saveButtonTapped.toggle() + } HapticManagerSUI.shared.impact(style: .heavy) } label: { Text(viewModel.updating ? "Update" : "Save") } + .changeEffect( + .rise(origin: UnitPoint(x: 0.45, y: -11)) { + Text(viewModel.updating ? "Updated" : "Added") + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .foregroundStyle(.blue) + }, value: saveButtonTapped) .controlSize(.extraLarge) .buttonStyle(.borderedProminent) } @@ -115,7 +128,9 @@ struct CardView: View { } else { recorder.checkRecordPermission { granted in if granted { - recorder.startRecording() + DispatchQueue.global(qos: .background).async { + recorder.startRecording() + } } else { showAlert.toggle() } @@ -166,8 +181,12 @@ struct CardView: View { } } + ToolbarItem(placement: .principal) { + } + ToolbarItem(placement: .cancellationAction) { Button { + FileManager.default.delete(viewModel.audioName) dismiss() } label: { Image(systemName: "xmark") diff --git a/Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift new file mode 100644 index 0000000..3031398 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift @@ -0,0 +1,135 @@ +// +// CardsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 20.08.2023. +// + +import SwiftUI +import RealmSwift + +struct CardsView: View { + @State private var isCardViewPresented: Bool = false + @State private var isReviewPresented: Bool = false + @State private var isLayoutDialogPresented: Bool = false + @ObservedRealmObject var deck: Deck + @Environment(\.realm) var realm + + var body: some View { + ZStack { + if deck.cards.isEmpty { + ContentUnavailableView(label: { + Label("No cards", systemImage: "rectangle.portrait.on.rectangle.portrait.angled") + }, description: { + Text("Cards will appear here.") + }, actions: { + Button { + isCardViewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Add card") + } + .controlSize(.regular) + .buttonStyle(.borderedProminent) + }) + } else { + VStack(spacing: 0) { + List { + ForEach(deck.cards) { card in + NavigationLink { + CardView( + viewModel: CardViewModel(card: card, repository: CardRepository(deck: deck)), + recorder: AudioRecorder(fileName: card.audioName != nil ? card.audioName! : UUID().uuidString + ".m4a") + ) + .navigationBarBackButtonHidden() + } label: { + Text(card.front) + } + .swipeActions(edge: .trailing) { + Button { + remove(card) + } label: { + Image(systemName: "trash") + } + .tint(.red) + } + } + .onMove(perform: $deck.cards.move) + } + .listStyle(.plain) + + Divider() + + HStack(spacing: 10) { + Button { + isLayoutDialogPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: "gearshape") + .padding(.vertical, 8) + .padding(.horizontal, 4) + } + .tint(.blue) + .buttonStyle(.bordered) + .sheet(isPresented: $isLayoutDialogPresented, content: { + DeckSettingsView(deck: deck) + .interactiveDismissDisabled(deck.name.isEmpty) + }) + + Button { + isReviewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Review") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .fullScreenCover(isPresented: $isReviewPresented) { + ReviewView(reviewManager: ReviewManagerSUI(deck: deck)) + } + + } + .padding() + .padding(.top) + .frame(height: 50) + } + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + isCardViewPresented.toggle() + } label: { + Image(systemName: "plus.circle.fill") + } + .sheet(isPresented: $isCardViewPresented) { + NavigationView { + CardView(viewModel: CardViewModel(repositry: CardRepository(deck: deck)), recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a")) + } + } + } + } + .navigationTitle(deck.name) + } + + private func remove(_ card: Card) { + guard let index = findIndex(of: card) else { return } + let audioName = deck.cards[index].audioName + + $deck.cards.remove(at: index) + LocalFileManager.shared.delete(audioName) + } + + private func findIndex(of card: Card) -> Int? { + return deck.cards.firstIndex(of: card) + } +} + +struct CardsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + CardsView(deck: Deck.deck2) + } + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/CardsView.swift b/Simple Anki/SwiftUI/UI/Views/CardsView.swift deleted file mode 100644 index bc9c9f4..0000000 --- a/Simple Anki/SwiftUI/UI/Views/CardsView.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// CardsView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 20.08.2023. -// - -import SwiftUI -import RealmSwift - -struct CardsView: View { - @State private var isCardViewPresented: Bool = false - @State private var isReviewPresented: Bool = false - @ObservedRealmObject var deck: Deck - - var body: some View { - ZStack { - if deck.cards.isEmpty { - ContentUnavailableView(label: { - Label("No cards", systemImage: "rectangle.portrait.on.rectangle.portrait.angled") - }, description: { - Text("Cards will appear here.") - }, actions: { - Button { - isCardViewPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Add card") - } - .controlSize(.regular) - .buttonStyle(.borderedProminent) - }) - } else { - List { - ForEach(deck.cards) { card in - NavigationLink { - CardView(viewModel: CardViewModel(card: card, repository: CardRepository(deck: deck)), recorder: AudioRecorder(fileName: card.audioName != nil ? card.audioName! : UUID().uuidString + ".m4a")) - .navigationBarBackButtonHidden() - } label: { - Text(card.front) - } - .swipeActions(edge: .trailing) { - Button { - remove(card) - } label: { - Image(systemName: "trash") - } - .tint(.red) - } - } - .onMove(perform: $deck.cards.move) - } - .listStyle(.plain) - } - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - isCardViewPresented.toggle() - } label: { - Image(systemName: "plus.circle.fill") - } - .sheet(isPresented: $isCardViewPresented) { - NavigationView { - CardView(viewModel: CardViewModel(repositry: CardRepository(deck: deck)), recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a")) - } - } - } - } - .toolbar { - ToolbarItemGroup(placement: .bottomBar) { - HStack { - Button { - - } label: { - Image(systemName: "gearshape") - .padding(.vertical, 8) - } - .tint(.blue) - .buttonStyle(.bordered) - - Button { - isReviewPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Review") - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .fullScreenCover(isPresented: $isReviewPresented) { - ReviewView(reviewManager: ReviewManagerSUI(cards: deck.cards)) - } - } - } - } - .toolbar(.hidden, for: .tabBar) - .navigationTitle(deck.name) - } - - private func remove(_ card: Card) { - guard let index = findIndex(of: card) else { return } - let audioName = deck.cards[index].audioName - - $deck.cards.remove(at: index) - LocalFileManager.shared.delete(audioName) - } - - private func findIndex(of card: Card) -> Int? { - return deck.cards.firstIndex(of: card) - } -} - -struct CardsView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - CardsView(deck: Deck.deck2) - } - } -} diff --git a/Simple Anki/SwiftUI/UI/Views/ContentView.swift b/Simple Anki/SwiftUI/UI/Views/ContentView.swift index bb4fca0..60ebc97 100644 --- a/Simple Anki/SwiftUI/UI/Views/ContentView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ContentView.swift @@ -7,6 +7,236 @@ import SwiftUI +// enum TabbedItems: Int, CaseIterable { +// case home = 0 +// case favorite +// +// var title: String { +// switch self { +// case .home: +// return "Decks" +// case .favorite: +// return "Settings" +// } +// } +// +// var iconName: String { +// switch self { +// case .home: +// return "tray.full.fill" +// case .favorite: +// return "gear" +// } +// } +// } +// +// struct MainTabbedView: View { +// +// @State var selectedTab = 0 +// @State var next: Bool = false +// +// var body: some View { +// +// ZStack(alignment: .bottom) { +// TabView(selection: $selectedTab) { +// NavigationStack { +// Button("go to next") { +// next.toggle() +// }.navigationDestination(isPresented: $next) { +// DecksView() +// } +// +// +// } +// +// Text("Settings") +// .tag(1) +// } +// +// ZStack { +// HStack { +// ForEach((TabbedItems.allCases), id: \.self) { item in +// Button { +// selectedTab = item.rawValue +// } label: { +// customTabItem(imageName: item.iconName, title: item.title, isActive: (selectedTab == item.rawValue)) +// } +// } +// } +// .padding(6) +// } +// .frame(maxWidth: .infinity, maxHeight: 70) +// .background(.cyan.opacity(0.2)) +// .cornerRadius(35) +// .padding(.horizontal, 26) +// .offset(x: next == true ? -400 : 0) +// .animation(.bouncy(duration: 0.2), value: next) +// } +// } +// } +// +// extension MainTabbedView { +// func customTabItem(imageName: String, title: String, isActive: Bool) -> some View { +// VStack(spacing: 5) { +// Spacer() +// Image(systemName: imageName) +// .renderingMode(.template) +// .foregroundColor(isActive ? .black : .gray) +// Text(title) +// .font(.system(size: 10)) +// .foregroundColor(isActive ? .black : .gray) +// Spacer() +// } +// .padding() +// .frame(width: 100, height: 60) +// .background(isActive ? .cyan.opacity(0.4) : .clear) +// .cornerRadius(30) +// } +// } + +struct FirstView: View { + var body: some View { + List { + ForEach(0..<20) { item in + NavigationLink { + ThirdView() + } label: { + Text("\(item)") + } + + } + } + .listStyle(.plain) + .navigationTitle("First title") + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + } +} + +struct SecondView: View { + var body: some View { + List { + ForEach(0..<20) { item in + NavigationLink { + ThirdView() + } label: { + Text("\(item)") + } + } + } + .navigationTitle("Title") + } +} + +struct ThirdView: View { + var body: some View { + VStack { + List { + ForEach(0..<20) { _ in + Text("Third View with tabBar hidden") + .font(.headline) + } + } + .listStyle(.plain) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) + + Divider() + + HStack { + Button(action: {}, label: { + Text("Button") + }) + } + .padding(0) + .frame(height: 42) + } + .navigationTitle("Cards") + } +} + +enum Tab: Int { + case first, second, third +} + +struct TabBarView: View { + + @State private var selectedTab: Tab = .first + + var body: some View { + VStack(spacing: 0) { + ZStack { + switch selectedTab { + case .first: + NavigationStack { + VStack(spacing: 0) { +// DecksView() + TabBarView() + } + } + case .second: + NavigationStack { + SettingsView() + } + case .third: + Text("Third") + } + } + + if selectedTab != .first { + TabBarView() + } + } + } + + @ViewBuilder + func TabBarView() -> some View { + VStack(spacing: 0) { + Divider() + + HStack { + Spacer() + TabItemView(tab: .first, title: "Decks", icon: "tray.full.fill", selectedTab: $selectedTab) + Spacer(minLength: 144) + TabItemView(tab: .second, title: "Settings", icon: "gear", selectedTab: $selectedTab) + Spacer() + } + .padding(.top, 8) + + } + .frame(height: 50) + } +} + +struct TabItemView2: View { + let title: String + let icon: String + let tab: Tab + + @Binding var selectedTab: Tab + + init(tab: Tab, title: String, icon: String, selectedTab: Binding) { + self.tab = tab + self.title = title + self.icon = icon + self._selectedTab = selectedTab + } + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 24)) + Text(title) + .font(.system(size: 11)) + } + .foregroundColor(selectedTab == tab ? .blue : .secondary) + } + .frame(width: 65, height: 42) + .onTapGesture { + selectedTab = tab + } + } +} + #Preview { - Text("Test") + TabBarView() } diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift new file mode 100644 index 0000000..27a347c --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift @@ -0,0 +1,69 @@ +// +// CreateDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.02.2024. +// + +import SwiftUI +import RealmSwift + +struct CreateDeckView: View { + @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @State private var deckName: String = "" + @FocusState private var isFocused: Bool + @Environment(\.dismiss) var dismiss + + var body: some View { + List { + Section("Deck name") { + TextField("Enter deck name", text: $deckName) + .focused($isFocused) + .submitLabel(.done) + .onSubmit { + createDeck() + } + .onAppear { + isFocused = true + } + } + + Section { + Button(action: { + createDeck() + HapticManagerSUI.shared.impact(style: .heavy) + dismiss() + }, label: { + HStack { + Spacer() + Text("Create deck") + Spacer() + } + }) + .disabled(deckName.isEmpty) + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(action: { + dismiss() + }, label: { + Text("Done") + }) + } + } + } + + private func createDeck() { + let name = deckName.trimmingCharacters(in: .whitespacesAndNewlines) + let deck = Deck(name: name) + $decks.append(deck) + deckName = "" + } +} + +#Preview { + NavigationStack { + CreateDeckView() + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift new file mode 100644 index 0000000..9323ae2 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift @@ -0,0 +1,78 @@ +// +// DeckSettingsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 04.02.2024. +// + +import SwiftUI +import RealmSwift + +struct DeckSettingsView: View { + @ObservedRealmObject var deck: Deck + @Environment(\.dismiss) var dismiss + @Environment(\.realm) var realm + + var body: some View { + NavigationStack { + VStack { + List { + Section("Name") { + TextField("Enter name", text: $deck.name) + } + Section("Layout") { + LayoutButton(labelText: "Front to Back", layout: K.Layout.frontToBack, deck: deck) + LayoutButton(labelText: "Back to Front", layout: K.Layout.backToFront, deck: deck) + LayoutButton(labelText: "Combined", layout: K.Layout.all, deck: deck) + } + + Section("Pronunciation") { + Toggle(isOn: $deck.autoplay, label: { + Label("Autoplay", systemImage: deck.autoplay ? "speaker.wave.2" : "speaker.slash") + }) + } + + Section { + Button(action: { + remove(deck: deck) + }, label: { + Label("Delete deck", systemImage: "trash") + .foregroundStyle(.red) + }) + .tint(.red) + } + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + .disabled(deck.name.isEmpty) + } + } + .navigationTitle("Deck settings") + .navigationBarTitleDisplayMode(.inline) + } + } + + private func remove(deck: Deck) { + guard let deckToDelete = realm.objects(Deck.self).first(where: { $0._id == deck._id }) else { return } + do { + deckToDelete.cards.forEach { card in + LocalFileManager.shared.delete(card.audioName) + } + try realm.write { + realm.delete(deckToDelete.cards) + realm.delete(deckToDelete) + } + } catch { + print("Error: \(error)") + } + + } +} + +#Preview { + DeckSettingsView(deck: Deck.deck1) +} diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift new file mode 100644 index 0000000..781927c --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift @@ -0,0 +1,167 @@ +// +// DecksView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI +import RealmSwift + +struct DecksView: View { + @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @State private var isNewDeckViewPresented: Bool = false + @State private var isDeleteDialogPresented = false + + @Environment(\.realm) var realm + + var body: some View { + NavigationStack { + VStack { + if decks.isEmpty { + ContentUnavailableView(label: { + Label("No decks", systemImage: "tray") + }, description: { + Text("Your decks will appear here.") + }, actions: { + Button { + isNewDeckViewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Text("Add deck") + } + .sheet(isPresented: $isNewDeckViewPresented) { + NewDeckView() + } + .controlSize(.regular) + .buttonStyle(.borderedProminent) + }) + } else { + List { + ForEach(decks) { deck in + NavigationLink { + CardsView(deck: deck) + } label: { + VStack(alignment: .leading) { + Text(deck.name) + cardsCount(of: deck) + .font(.system(size: 11)) + } + } + } + .onDelete(perform: { indexSet in + for index in indexSet { + remove(by: decks[index]._id) + } + }) + } + .listStyle(.plain) + } + } + .navigationTitle("Decks") + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { +// Button { +// isImportDeckViewPresented.toggle() +// HapticManagerSUI.shared.impact(style: .heavy) +// } label: { +// Image(systemName: "tray.and.arrow.down") +// } +// .sheet(isPresented: $isImportDeckViewPresented) { +// ImportDeckView() +// } + + Button { + isNewDeckViewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: "plus.circle.fill") + } + .sheet(isPresented: $isNewDeckViewPresented) { + NewDeckView() + .presentationDetents([.medium]) + } + } + } + } + } + + @ViewBuilder + private func ImportDeckView() -> some View { + NavigationStack { + VStack { + Spacer() + Text("Hello world") + .padding(.bottom, 40) + Spacer() + Button(action: { + + }, label: { + Text("Choose file...") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + }) + .buttonStyle(.borderedProminent) + } + .padding() + + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + // dismiss + }, label: { + Image(systemName: "xmark") + }) + .tint(.secondary) + } + ToolbarItem(placement: .principal) { +// Picker("What is your favorite color?", selection: $selectedType) { +// Text("APKG").tag(0) +// Text("CSV").tag(1) +// } +// .frame(maxWidth: 150) +// .pickerStyle(.segmented) + } + } + } + .presentationDetents([.medium]) + } + + @ViewBuilder + private func cardsCount(of deck: Deck) -> some View { + let cardCount = deck.cards.count + Text(cardCount == 0 ? "No cards" : "^[\(cardCount) card](inflect: true)") + } + +// private func addDeck() { +// guard !deckName.isEmpty else { return } +// +// let name = deckName.trimmingCharacters(in: .whitespacesAndNewlines) +// let deck = Deck(name: name) +// $decks.append(deck) +// deckName = "" +// isNewDeckViewPresented = false +// } + + private func remove(by deckID: ObjectId) { + + do { + + if let deckToDelete = realm.object(ofType: Deck.self, forPrimaryKey: deckID) { + deckToDelete.cards.forEach { card in + LocalFileManager.shared.delete(card.audioName) + } + try realm.write { + realm.delete(deckToDelete.cards) + realm.delete(deckToDelete) + } + } + } catch { + print("Error: \(error)") + } + } +} + +#Preview { + DecksView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/ImportDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/ImportDeckView.swift new file mode 100644 index 0000000..25a1ad2 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/ImportDeckView.swift @@ -0,0 +1,96 @@ +// +// ImportDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.02.2024. +// + +import SwiftUI +import SwiftCSV +import RealmSwift + +struct ImportDeckView: View { + @State private var deckName: String = "" + @State private var errorMessage: String = "" + @State private var parseError: Error? + @State private var isAlertPresented: Bool = false + @State private var isFileImporterPresented: Bool = false + @State private var isImportedCardsViewPresented: Bool = false + @State private var selectedDelimeter: CSVDelimiter = .character("d") + @State private var importedCards: RealmSwift.List = RealmSwift.List() + + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack { + List { + Section { + DelimeterButton(title: "Comma", delimeter: .comma, selectedDelimeter: $selectedDelimeter) + DelimeterButton(title: "Semicolon", delimeter: .semicolon, selectedDelimeter: $selectedDelimeter) + DelimeterButton(title: "Tab", delimeter: .tab, selectedDelimeter: $selectedDelimeter) + } header: { + Text("Select delimeter") + } footer: { + HStack(spacing: 3) { + Text("More information about") + Link("CSV files.", destination: URL(string: "https://en.wikipedia.org/wiki/Comma-separated_values")!) + .font(.footnote) + } + } + + Section { + Button(action: { + isFileImporterPresented = true + }, label: { + Text("Choose file...") + }) + .disabled(selectedDelimeter == .character("d")) + .fileImporter( + isPresented: $isFileImporterPresented, + allowedContentTypes: [.commaSeparatedText, .database], + allowsMultipleSelection: false + ) { result in + switch result { + case .success(let urls): + deckName = urls[0].lastPathComponent + do { + try parseCSV(from: urls[0]) + isImportedCardsViewPresented = true + } catch { + isAlertPresented = true + errorMessage = "Could not parse the imported file." + } + case .failure(let failure): + isAlertPresented = true + errorMessage = failure.localizedDescription + } + } + .alert("Error", isPresented: $isAlertPresented, actions: { + Button("OK") { } + }, message: { + Text(errorMessage) + }) + .sheet(isPresented: $isImportedCardsViewPresented) { + ImportedDeckView(deckName: $deckName, importedCards: $importedCards) + } + } + } + } + } + + private func parseCSV(from url: URL) throws { + do { + let csv = try EnumeratedCSV(url: url, delimiter: selectedDelimeter) + for row in csv.rows { + guard !row.isEmpty, row.count >= 2 else { continue } + importedCards.append(Card(front: row[0], back: row[1])) + } + } catch { + throw CSVParseError.generic(message: "Could not parse the csv file.") + } + } +} + +#Preview { + ImportDeckView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift new file mode 100644 index 0000000..ca81f82 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift @@ -0,0 +1,61 @@ +// +// ImportedDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.02.2024. +// + +import SwiftUI +import RealmSwift + +struct ImportedDeckView: View { + @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @Binding var deckName: String + @Binding var importedCards: RealmSwift.List + + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + List { + Section("Edit name") { + TextField("Deck name", text: $deckName) + } + Section("Cards") { + ForEach(importedCards) { card in + Button(action: { + + }, label: { + Text("\(card.front) - \(card.back)") + }) + } + .onDelete(perform: { indexSet in + importedCards.remove(atOffsets: indexSet) + }) + } + } + .navigationTitle("Imported deck") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(action: { + addDeck() + dismiss() + }, label: { + Text("Done") + }) + } + } + } + } + + private func addDeck() { + let deck = Deck(name: deckName) + deck.cards = importedCards + $decks.append(deck) + } +} + +#Preview { + ImportedDeckView(deckName: .constant("Deck name"), importedCards: .constant(List())) +} diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift new file mode 100644 index 0000000..7867723 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift @@ -0,0 +1,161 @@ +// +// NewDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 10.02.2024. +// + +import SwiftUI +import RealmSwift +import SwiftCSV + +enum Delimeter: String { + case comma = "," + case semicolon = ";" + case tab = "\t" + case none = "" +} + +struct NewDeckView: View { + @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @State private var deckName: String = "" + @State private var selectedSegment: Int = 0 + @State private var isFileImporterPresented: Bool = false + @State private var isImportedCardsViewPresented: Bool = false + @State private var selectedDelimeter: CSVDelimiter = .character("d") + @State private var importedCards: RealmSwift.List = RealmSwift.List() + + @Environment(\.dismiss) var dismiss + @Environment(\.realm) var realm + + var body: some View { + NavigationStack { + ZStack { + if selectedSegment == 0 { + CreateDeckView() + } else { + ImportDeckView() + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Picker("test", selection: $selectedSegment) { + Text("Create").tag(0) + Text("Import").tag(1) + } + .frame(width: 200) + .pickerStyle(.segmented) + } + } + } + } + + @ViewBuilder + private func ImportDeckView() -> some View { + VStack { + List { + Section { + DelimeterButton(title: "Comma", delimeter: .comma) + DelimeterButton(title: "Semicolon", delimeter: .semicolon) + DelimeterButton(title: "Tab", delimeter: .tab) + } header: { + Text("Select delimeter") + } footer: { + HStack(spacing: 3) { + Text("More information about") + Link("CSV files.", destination: URL(string: "https://en.wikipedia.org/wiki/Comma-separated_values")!) + .font(.caption2) + } + } + + Section { + Button(action: { + isFileImporterPresented = true + }, label: { + Text("Choose file...") + }) + .disabled(selectedDelimeter == .character("d")) + .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.commaSeparatedText], allowsMultipleSelection: false) { result in + switch result { + case .success(let success): + print(success) + deckName = success[0].lastPathComponent + do { + let csv = try CSV(url: success[0], delimiter: selectedDelimeter) + for row in csv.rows { + guard !row.isEmpty, row.count >= 2 else { continue } +// let front = self.cleanString(row[0]) +// let back = self.cleanString(row[1]) + importedCards.append(Card(front: row[0], back: row[1])) +// dismiss() + isImportedCardsViewPresented = true + } + } catch { +// self.showAlert(with: "Error", message: "Could not import deck") + print(error) + } + case .failure(let failure): + print(failure) + } + } + .sheet(isPresented: $isImportedCardsViewPresented) { + NavigationStack { + List { + Section("Edit name") { + TextField("Deck name", text: $deckName) + } + Section("Cards") { + ForEach(importedCards) { card in + Button(action: { + + }, label: { + Text("\(card.front) - \(card.back)") + }) + } + .onDelete(perform: { indexSet in + importedCards.remove(atOffsets: indexSet) + }) + } + } + .navigationTitle("Imported deck") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(action: { + var deck = Deck(name: deckName) + deck.cards = importedCards + $decks.append(deck) + dismiss() + }, label: { + Text("Done") + }) + } + } + } + } + } + } + } + } + + @ViewBuilder + private func DelimeterButton(title: String, delimeter: CSVDelimiter) -> some View { + Button(action: { + selectedDelimeter = delimeter + }, label: { + HStack { + Text(title) + Spacer() + Image(systemName: "checkmark") + .foregroundStyle(.blue) + .opacity(selectedDelimeter == delimeter ? 1: 0) + } + }) + .tint(.primary) + } +} + +#Preview { + NewDeckView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/DecksView.swift b/Simple Anki/SwiftUI/UI/Views/DecksView.swift deleted file mode 100644 index ca5b2b6..0000000 --- a/Simple Anki/SwiftUI/UI/Views/DecksView.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// DecksView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 16.08.2023. -// - -import SwiftUI -import RealmSwift - -struct DecksView: View { - @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks - @State private var isNewDeckViewPresented: Bool = false - @State private var deckName: String = "" - - var body: some View { - NavigationView { - VStack { - if decks.isEmpty { - ContentUnavailableView(label: { - Label("No decks", systemImage: "tray") - }, description: { - Text("Your decks will appear here.") - }, actions: { - Button { - isNewDeckViewPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Add deck") - } - .controlSize(.regular) - .buttonStyle(.borderedProminent) - }) - } else { - List { - ForEach(decks) { deck in - NavigationLink { - CardsView(deck: deck) - } label: { - VStack(alignment: .leading) { - Text(deck.name) - cardsCount(of: deck) - .font(.system(size: 11)) - } - } - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button { - remove(deck) - } label: { - Image(systemName: "trash") - } - .tint(.red) - } - } - } - .listStyle(.plain) - } - } - .navigationTitle("Decks") - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - print("Test") - } label: { - Image(systemName: "tray.and.arrow.down") - } - Button { - isNewDeckViewPresented.toggle() - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Image(systemName: "plus.circle.fill") - } - .alert("New deck", isPresented: $isNewDeckViewPresented) { - TextField("Enter deck name", text: $deckName) - Button("Add") { - addDeck() - deckName = "" - } - Button("Cancel", role: .cancel) {} - } message: { - - } - } - } - } - .navigationViewStyle(.stack) - } - - private func addDeck() { - let deck = Deck(name: deckName.trimmingCharacters(in: .whitespacesAndNewlines)) - $decks.append(deck) - } - - @ViewBuilder private func cardsCount(of deck: Deck) -> some View { - let cardCount = deck.cards.count - Text(cardCount == 0 ? "No cards" : "^[\(cardCount) card](inflect: true)") - } - - private func remove(_ deck: Deck) { - - do { - let realm = try Realm() - guard let deckToDelete = realm.objects(Deck.self).first(where: { $0._id == deck._id }) else { return } - deckToDelete.cards.forEach { card in - LocalFileManager.shared.delete(card.audioName) - } - - try realm.write { - realm.delete(deckToDelete.cards) - realm.delete(deckToDelete) - } - } catch { - print("Error: \(error)") - } - } -} - -#Preview { - DecksView() -} diff --git a/Simple Anki/SwiftUI/UI/Views/MainView.swift b/Simple Anki/SwiftUI/UI/Views/MainView.swift index 6e76959..4aea577 100644 --- a/Simple Anki/SwiftUI/UI/Views/MainView.swift +++ b/Simple Anki/SwiftUI/UI/Views/MainView.swift @@ -8,39 +8,49 @@ import SwiftUI struct MainView: View { - -// init() { -// let appearance = UITabBarAppearance() -// appearance.configureWithOpaqueBackground() -// UITabBar.appearance().standardAppearance = appearance -// UITabBar.appearance().scrollEdgeAppearance = appearance -// } + @State private var selectedTab: Tab = .first var body: some View { - TabView { - DecksView() - .tabItem { - Image(systemName: "tray.full") - Text("Decks") + VStack(spacing: 0) { + ZStack { + switch selectedTab { + case .first: + NavigationStack { + VStack(spacing: 0) { + DecksView() + TabBarView() + } + } + case .second: + SettingsView() + case .third: + Text("Third") } - .tag(1) + } - SettingsView() - .tabItem { - Image(systemName: "gear") - Text("Settings") - } - .tag(2) + if selectedTab != .first { + TabBarView() + } + } + } + + @ViewBuilder + func TabBarView() -> some View { + VStack(spacing: 0) { + Divider() + HStack { + Spacer() + TabItemView(tab: .first, title: "Decks", icon: "tray.full.fill", selectedTab: $selectedTab) + Spacer(minLength: 144) + TabItemView(tab: .second, title: "Settings", icon: "gear", selectedTab: $selectedTab) + Spacer() + } + .padding(.top, 8) } + .frame(height: 50) } } #Preview { MainView() } - -// struct MainView_Previews: PreviewProvider { -// static var previews: some View { -// MainView() -// } -// } diff --git a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift b/Simple Anki/SwiftUI/UI/Views/NewCardView.swift deleted file mode 100644 index 4c7ef1e..0000000 --- a/Simple Anki/SwiftUI/UI/Views/NewCardView.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// NewCardView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 20.08.2023. -// - -import SwiftUI -import RealmSwift - -struct NewCardView: View { - @ObservedRealmObject var deck: Deck - - @State private var frontText: String = "" - @State private var backText: String = "" - @State private var isAlerPresented: Bool = false - - @Environment(\.dismiss) private var dismiss - @FocusState private var focusedField: FocusableField? - - @State private var audioName: String? - - init(deck: Deck) { - self.deck = deck - } - - var body: some View { - VStack { - VStack { - VStack(spacing: 30) { - TextField("Front", text: $frontText) - .submitLabel(.next) - .focused($focusedField, equals: .frontField) - Divider() - TextField("Back", text: $backText) - .submitLabel(.done) - .focused($focusedField, equals: .backField) - } - .onAppear { - focusedField = .frontField - } - .onSubmit { - focusedField = .backField - } - .font(.system(size: 35, weight: .bold)) - .padding(.top, 50) - Divider() - HStack { - if let audio = audioName { - Button { - isAlerPresented.toggle() - } label: { - Image(systemName: "trash.fill") - } - .alert("Delete audio file?", isPresented: $isAlerPresented, actions: { - Button("Delete", role: .destructive) { - LocalFileManager.shared.delete(audio) - audioName = nil - } - }) - .tint(.red) - - Button { - SoundManager.shared.play(sound: audio) - } label: { - Image(systemName: "speaker.wave.3.fill") - } - } - Spacer() -// RecordingButton(audioRecorder: AudioRecorder()) { audioURL in -// audioName = audioURL?.lastPathComponent -// } - } - .padding(.top, 8) - .font(.system(size: 20, weight: .bold)) - } - Spacer() - Button { - saveCard() - focusedField = .frontField - frontText.removeAll() - backText.removeAll() - audioName = nil - HapticManagerSUI.shared.impact(style: .heavy) - } label: { - Text("Add") - .padding(8) - .frame(maxWidth: .infinity) - } - .disabled(frontText.isEmpty) - .buttonStyle(.borderedProminent) - } - .padding() - .navigationTitle("Add card to \(deck.name)") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button { - if let audio = audioName { - LocalFileManager.shared.delete(audio) - } - dismiss() - } label: { - Image(systemName: "xmark") - .foregroundColor(.gray) - } - } - } - } - - private func saveCard() { - let front = frontText.trimmingCharacters(in: .whitespacesAndNewlines) - let back = backText.trimmingCharacters(in: .whitespacesAndNewlines) - let card = Card(front: front, back: back, audioName: audioName) - $deck.cards.append(card) - } -} - -struct GrowingButton: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(8) - .background(.blue) - .foregroundStyle(.white) - .clipShape(Circle()) - .scaleEffect(configuration.isPressed ? 2 : 1) - .animation(.easeOut(duration: 0.1), value: configuration.isPressed) - } -} - -struct NewCardView_Previews: PreviewProvider { - @State static var isPresented: Bool = true - static var previews: some View { - NavigationView { - NewCardView(deck: Deck.deck1) - } - } -} diff --git a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift index d455e3f..23ad59b 100644 --- a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift @@ -16,6 +16,17 @@ struct ReviewView: View { var body: some View { NavigationView { ZStack { + if let image = reviewManager.currentCard?.image { + VStack { + Image(uiImage: UIImage(imageLiteralResourceName: image)) + .resizable() + .scaledToFill() + .clipShape(RoundedRectangle(cornerRadius: 10)) + .frame(width: 150, height: 150) + .padding(.top, 60) + Spacer() + } + } VStack { if reviewManager.isReviewing { Text(reviewManager.currentCard?.front ?? "") @@ -35,16 +46,21 @@ struct ReviewView: View { .multilineTextAlignment(.center) .onTapGesture { if reviewManager.isReviewing { - SoundManager.shared.play(sound: reviewManager.currentCard?.audioName ?? "") + playPronunciation() } } .font(.system(size: 40, weight: .medium)) .padding() .onAppear { reviewManager.startReview() + if reviewManager.isAutoplayOn { + playPronunciation() + } } .onChange(of: reviewManager.currentCard) { - playPronunciation() + if reviewManager.isAutoplayOn { + playPronunciation() + } } VStack { @@ -99,14 +115,15 @@ struct ReviewView: View { private func playPronunciation() { if let audioName = reviewManager.currentCard?.audioName { - SoundManager.shared.play(sound: audioName) + DispatchQueue.global(qos: .background).async { + SoundManager.shared.play(sound: audioName) + } } } - } struct ReviewView_Previews: PreviewProvider { static var previews: some View { - ReviewView(reviewManager: ReviewManagerSUI(cards: Deck.deck2.cards)) + ReviewView(reviewManager: ReviewManagerSUI(deck: Deck.deck2)) } } diff --git a/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift b/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift new file mode 100644 index 0000000..01e9d6e --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift @@ -0,0 +1,84 @@ +// +// ReminderView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 08.02.2024. +// + +import SwiftUI + +struct ReminderView: View { + @State private var viewModel = ReminderViewModel() + @Environment(\.dismiss) var dismiss + + var body: some View { + NavigationStack { + List { + Section { + Toggle("Turn on", systemImage: "bell", isOn: $viewModel.isReminderOn) + .onChange(of: viewModel.isReminderOn) { + if !viewModel.isReminderOn { + viewModel.turnOffNotifications() + } + } + + DatePicker(selection: $viewModel.selectedTime, displayedComponents: .hourAndMinute) { + Label("Choose time", systemImage: "clock") + } + .onChange(of: viewModel.selectedTime) { + if viewModel.isReminderOn { + viewModel.saveSelectedTime() + } + } + } footer: { + Text("Notification will be sent at specified time.") + } + + Section { + ForEach(ReminderManager.shared.weekdays) { weekday in + Button { + if viewModel.isWeekdayInReminder(weekday: weekday) { + viewModel.removeNotificationFromReminder(weekday: weekday) + } else { + viewModel.addWeekdayToReminder(weekday: weekday) + Task { + await viewModel.scheduleReminder(for: weekday) + } + } + } label: { + HStack { + Text(weekday.name) + Spacer() + Image(systemName: "checkmark") + .foregroundStyle(.blue) + .opacity(viewModel.isWeekdayInReminder(weekday: weekday) ? 1 : 0) + } + } + .tint(.primary) + } + .disabled(!viewModel.isReminderOn) + } header: { + Text("Repeat every") + } footer: { + Text("Notification will be sent on the selected days.") + } + } + .navigationTitle("Reminder") + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + viewModel.saveReminderState() + dismiss() + } label: { + Text("Done") + } + } + } + .navigationBarTitleDisplayMode(.inline) + } + } +} + +#Preview { + ReminderView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/Settings/SettingsView.swift b/Simple Anki/SwiftUI/UI/Views/Settings/SettingsView.swift new file mode 100644 index 0000000..e541504 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Settings/SettingsView.swift @@ -0,0 +1,98 @@ +// +// SettingsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI +import UserNotifications + +struct SettingsView: View { + @StateObject var userSettings = UserSettings() + @State private var reminderViewModel = ReminderViewModel() + @State private var isPresented: Bool = false + @State private var isReminderViewPresented: Bool = false + + var body: some View { + NavigationStack { + List { + Section("appearance") { + HStack { + Label { + Text("Dark mode") + } icon: { + Image(systemName: "circle.lefthalf.filled") + } + Spacer() + Toggle(isOn: $userSettings.colorScheme) {} + } + } + + Section("Notifications") { + Button(action: { + Task { + do { + let success = try await UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert]) + if success { + isReminderViewPresented = true + } + } catch { + print(error.localizedDescription) + } + } + }, label: { + HStack { + Label("Set up reminder", systemImage: "bell") + } + }) + .tint(.primary) + .sheet(isPresented: $isReminderViewPresented) { + ReminderView() + } + } + + Section("Feedback") { + Button(action: { + RateManager.rateApp() + }, label: { + Label("Review this app", systemImage: "star") + }) + .tint(.primary) + + Button(action: { + isPresented = true + }, label: { + Label("Contact developer", systemImage: "message") + }) + .tint(.primary) + .confirmationDialog("", isPresented: $isPresented, actions: { + Link("Facebook", destination: URL(string: "https://www.facebook.com/astemirboziy")!) + Link("Telegram", destination: URL(string: "https://t.me/nart_kenobi")!) + Link("Instagram", destination: URL(string: "https://www.instagram.com/astemirboziy")!) + Link("Email", destination: URL(string: "mailto:astemirboziy@gmail.com")!) + }, message: { + Text("Your insights are invaluable to me! Please feel free to suggest a feature, report a bug, or share your thoughts — I'm all ears!") + }) + + ShareLink(item: URL(string: K.appURL)!, preview: SharePreview("Simple Anki: \(K.appURL)", image: Image("SimpleAnki"))) { + Label("Share Simple Anki", systemImage: "square.and.arrow.up") + } + .tint(.primary) + } + Section { + LinkView(label: "Github", urlString: "https://github.com/bootuz/simpleAnki", icon: Image("github-fill")) + } header: { + Text("Source code") + } footer: { + Text("Simple Anki is now open source.") + } + } + .navigationTitle("Settings") + } + } +} + +#Preview { + SettingsView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/SettingsView.swift b/Simple Anki/SwiftUI/UI/Views/SettingsView.swift deleted file mode 100644 index 84d9d60..0000000 --- a/Simple Anki/SwiftUI/UI/Views/SettingsView.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// SettingsView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 16.08.2023. -// - -import SwiftUI - -struct SettingsView: View { - @EnvironmentObject private var userSettings: UserSettings - var body: some View { - NavigationView { - List { - Section("appearance") { - HStack { - Label { - Text("Dark mode") - } icon: { - Image(systemName: "circle.lefthalf.filled") - } - Spacer() - Toggle(isOn: $userSettings.colorScheme) {} - } - } - - Section("Feedback") { - LinkView( - label: "Review app", - urlString: Constants.Links.writeReview, - icon: Image(systemName: "star") - ) - LinkView( - label: "Contact developer", - urlString: Constants.Links.igMeLink, - icon: Image(systemName: "envelope") - ) - } - } - .navigationTitle("Settings") - } - } -} - -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() - .environmentObject(UserSettings()) - } -} diff --git a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift index 12b2e8c..1a79b0a 100644 --- a/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift +++ b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift @@ -49,12 +49,14 @@ final class CardViewModel { } func addCard() { + cleanWords() let card = Card(front: frontWord, back: backWord, audioName: audioName) cardRepository.add(card: card) } func updateCard() { guard let id = id else { return } + cleanWords() let card = Card(front: frontWord, back: backWord, audioName: audioName) card._id = id card.memorized = memorized @@ -65,4 +67,9 @@ final class CardViewModel { guard let id = id else { return } cardRepository.delete(by: id) } + + private func cleanWords() { + frontWord = frontWord.trimmingCharacters(in: .whitespacesAndNewlines) + backWord = backWord.trimmingCharacters(in: .whitespacesAndNewlines) + } } From 2a00af1eb1361d2d2479c18e98f9765efec46956 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 11 Feb 2024 16:26:38 +0400 Subject: [PATCH 7/9] almost done --- Simple Anki.xcodeproj/project.pbxproj | 284 +---------- Simple Anki/APKG/APKGDatabase.swift | 4 +- Simple Anki/AppDelegate.swift | 60 --- .../AppIcon 2.appiconset/1024.png | Bin 0 -> 348300 bytes .../120-1.png | Bin .../120.png | Bin .../180.png | Bin .../40.png | Bin .../58.png | Bin .../60.png | Bin .../80.png | Bin .../87.png | Bin .../AppIcon 2.appiconset/Contents.json | 67 +++ .../AppIcon.appiconset/Contents.json | 52 +- .../Base.lproj/LaunchScreen.storyboard | 23 +- .../CardsTableViewController.swift | 384 --------------- .../Controllers/Cells/BaseSettingsCell.swift | 65 --- .../Cells/DatePickerViewCell.swift | 53 -- .../Controllers/Cells/SettingsViewCell.swift | 27 - .../Cells/SwitchTableViewCell.swift | 58 --- .../Decks/DecksTableViewController.swift | 256 ---------- .../Controllers/Decks/DecksViewModel.swift | 51 -- .../Decks/Import/ImportViewController.swift | 323 ------------ .../ImportedCardCollectionViewCell.swift | 127 ----- ...mportedCardsCollectionViewController.swift | 74 --- .../Decks/NewDeckViewController.swift | 123 ----- .../LaunchScreenViewController.swift | 76 --- .../MainTabBarViewController.swift | 60 --- .../Controllers/NewCardViewController.swift | 463 ------------------ .../OnboardingViewController.swift | 120 ----- .../Controllers/ReviewViewController.swift | 221 --------- .../Reminder/ReminderViewController.swift | 184 ------- .../Reminder/WeekdaysViewController.swift | 58 --- .../SettingsTableViewController.swift | 188 ------- Simple Anki/Extensions/DateExtension.swift | 8 - .../Extensions/UIApplicationExtension.swift | 22 - .../Extensions/UIButtonExtension.swift | 54 -- Simple Anki/Extensions/UILabelExtension.swift | 45 -- Simple Anki/Extensions/UIViewExtension.swift | 52 -- Simple Anki/Extensions/URLExtension.swift | 14 - Simple Anki/Managers/EmailManager.swift | 31 -- Simple Anki/Managers/HapticManager.swift | 21 - Simple Anki/Managers/PlayerManager.swift | 28 -- Simple Anki/Managers/ReminderManager.swift | 126 ----- Simple Anki/Managers/ReviewManager.swift | 76 --- Simple Anki/Managers/StorageManager.swift | 95 ---- Simple Anki/Managers/Utils.swift | 46 -- Simple Anki/Models/Card.swift | 23 - Simple Anki/Models/Deck.swift | 22 - Simple Anki/Models/Options.swift | 41 -- Simple Anki/Protocols/EmptyState.swift | 18 - Simple Anki/SceneDelegate.swift | 48 -- .../Managers/OnboardingManager.swift | 0 .../{ => SwiftUI}/Managers/RateManager.swift | 0 .../SwiftUI/Managers/ReminderManagerSUI.swift | 21 + .../SwiftUI/Managers/ReviewManagerSUI.swift | 21 +- .../SwiftUI/Managers/SoundManager.swift | 4 +- .../Models/{CardSUI.swift => Card.swift} | 1 - .../Models/{DeckSUI.swift => Deck.swift} | 1 + Simple Anki/SwiftUI/SimpleAnkiApp.swift | 4 +- .../SwiftUI/UI/Views/Card/CardView.swift | 16 +- .../UI/Views/Deck/DeckSettingsView.swift | 6 + Simple Anki/SwiftUI/UI/Views/ReviewView.swift | 16 +- .../UI/Views/Settings/ReminderView.swift | 2 +- Simple Anki/UIComponents/FeatureView.swift | 78 --- 65 files changed, 161 insertions(+), 4180 deletions(-) delete mode 100644 Simple Anki/AppDelegate.swift create mode 100644 Simple Anki/Assets.xcassets/AppIcon 2.appiconset/1024.png rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/120-1.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/120.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/180.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/40.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/58.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/60.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/80.png (100%) rename Simple Anki/Assets.xcassets/{AppIcon.appiconset => AppIcon 2.appiconset}/87.png (100%) create mode 100644 Simple Anki/Assets.xcassets/AppIcon 2.appiconset/Contents.json delete mode 100644 Simple Anki/Controllers/CardsTableViewController.swift delete mode 100644 Simple Anki/Controllers/Cells/BaseSettingsCell.swift delete mode 100644 Simple Anki/Controllers/Cells/DatePickerViewCell.swift delete mode 100644 Simple Anki/Controllers/Cells/SettingsViewCell.swift delete mode 100644 Simple Anki/Controllers/Cells/SwitchTableViewCell.swift delete mode 100644 Simple Anki/Controllers/Decks/DecksTableViewController.swift delete mode 100644 Simple Anki/Controllers/Decks/DecksViewModel.swift delete mode 100644 Simple Anki/Controllers/Decks/Import/ImportViewController.swift delete mode 100644 Simple Anki/Controllers/Decks/Import/ImportedCardCollectionViewCell.swift delete mode 100644 Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift delete mode 100644 Simple Anki/Controllers/Decks/NewDeckViewController.swift delete mode 100644 Simple Anki/Controllers/LaunchScreenViewController.swift delete mode 100644 Simple Anki/Controllers/MainTabBarViewController.swift delete mode 100644 Simple Anki/Controllers/NewCardViewController.swift delete mode 100644 Simple Anki/Controllers/OnboardingViewController.swift delete mode 100644 Simple Anki/Controllers/ReviewViewController.swift delete mode 100644 Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift delete mode 100644 Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift delete mode 100644 Simple Anki/Controllers/Settings/SettingsTableViewController.swift delete mode 100644 Simple Anki/Extensions/DateExtension.swift delete mode 100644 Simple Anki/Extensions/UIApplicationExtension.swift delete mode 100644 Simple Anki/Extensions/UIButtonExtension.swift delete mode 100644 Simple Anki/Extensions/UILabelExtension.swift delete mode 100644 Simple Anki/Extensions/UIViewExtension.swift delete mode 100644 Simple Anki/Extensions/URLExtension.swift delete mode 100644 Simple Anki/Managers/EmailManager.swift delete mode 100644 Simple Anki/Managers/HapticManager.swift delete mode 100644 Simple Anki/Managers/PlayerManager.swift delete mode 100644 Simple Anki/Managers/ReminderManager.swift delete mode 100644 Simple Anki/Managers/ReviewManager.swift delete mode 100644 Simple Anki/Managers/StorageManager.swift delete mode 100644 Simple Anki/Managers/Utils.swift delete mode 100644 Simple Anki/Models/Card.swift delete mode 100644 Simple Anki/Models/Deck.swift delete mode 100644 Simple Anki/Models/Options.swift delete mode 100644 Simple Anki/Protocols/EmptyState.swift delete mode 100644 Simple Anki/SceneDelegate.swift rename Simple Anki/{ => SwiftUI}/Managers/OnboardingManager.swift (100%) rename Simple Anki/{ => SwiftUI}/Managers/RateManager.swift (100%) rename Simple Anki/SwiftUI/Models/{CardSUI.swift => Card.swift} (95%) rename Simple Anki/SwiftUI/Models/{DeckSUI.swift => Deck.swift} (96%) delete mode 100644 Simple Anki/UIComponents/FeatureView.swift diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index 0892463..2593e4c 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -8,21 +8,11 @@ /* Begin PBXBuildFile section */ C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00513FA2B517E91005F5815 /* CardPreviewView.swift */; }; - C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648A92682060F00265EB8 /* UIApplicationExtension.swift */; }; - C00648AC2682070200265EB8 /* EmailManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AB2682070200265EB8 /* EmailManager.swift */; }; - C00648AF2682140800265EB8 /* Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00648AE2682140800265EB8 /* Options.swift */; }; C00BD2642B5A8EA900ACD977 /* CardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00BD2632B5A8EA900ACD977 /* CardRepository.swift */; }; - C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */; }; - C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348CA2861F47E002FFBDF /* UILabelExtension.swift */; }; C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C02348D2286343F4002FFBDF /* StoreKit.framework */; }; - C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */; }; C02404082B6835930017EF2E /* TabItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02404072B6835930017EF2E /* TabItemView.swift */; }; C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */; }; C028C4C8280C852400F6894E /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = C028C4C7280C852400F6894E /* .gitignore */; }; - C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C028C4C92811C0F000F6894E /* NewDeckViewController.swift */; }; - C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C028C4CB2811C71500F6894E /* EmptyState.swift */; }; - C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41C2282146BF005E9835 /* DateExtension.swift */; }; - C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */; }; C03A02612B5299EF00576543 /* CardViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A02602B5299EF00576543 /* CardViewState.swift */; }; C03B91C82B6FA60500EA483F /* DeckSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */; }; C03B91CA2B6FA82400EA483F /* LayoutButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B91C92B6FA82400EA483F /* LayoutButton.swift */; }; @@ -31,7 +21,6 @@ C05BCFF22B78CF4D0076CAD9 /* ImportDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */; }; C05BCFF42B78D04D0076CAD9 /* DelimeterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF32B78D04D0076CAD9 /* DelimeterButton.swift */; }; C05BCFF62B78D12C0076CAD9 /* ImportedDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */; }; - C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */; }; C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */; }; C067B6872A8C1251000AF881 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6862A8C1251000AF881 /* MainView.swift */; }; C067B6892A8C139A000AF881 /* DecksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksView.swift */; }; @@ -47,59 +36,31 @@ C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED32AA3C01800089B29 /* ReviewView.swift */; }; C0750ED62AA3C39400089B29 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED52AA3C39400089B29 /* CircleButton.swift */; }; C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */; }; - C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903242A8D444F00EFC51C /* DeckSUI.swift */; }; - C08903272A8D474900EFC51C /* CardSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903262A8D474900EFC51C /* CardSUI.swift */; }; + C08903252A8D444F00EFC51C /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903242A8D444F00EFC51C /* Deck.swift */; }; + C08903272A8D474900EFC51C /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08903262A8D474900EFC51C /* Card.swift */; }; C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */; }; C09AE07E2B6A726B00DB3E28 /* Pow in Frameworks */ = {isa = PBXBuildFile; productRef = C09AE07D2B6A726B00DB3E28 /* Pow */; }; C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A0C6492A9DFC210015C65E /* SoundManager.swift */; }; - C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70E5F28211E620020D533 /* ReminderManager.swift */; }; - C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4125EC0E0B005DE503 /* Deck.swift */; }; - C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0ADAD4625EC0E62005DE503 /* Card.swift */; }; - C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF0E7C2614F1E000853FA7 /* ReviewManager.swift */; }; - C0B1275527C3F0E7008E412D /* DecksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B1275427C3F0E7008E412D /* DecksViewModel.swift */; }; - C0B1275727C3FB7A008E412D /* CardsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B1275627C3FB7A008E412D /* CardsTableViewController.swift */; }; - C0B1275D27C3FB91008E412D /* ReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B1275C27C3FB91008E412D /* ReviewViewController.swift */; }; - C0B1275E27C3FC4A008E412D /* DecksTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B1275327C3F0D4008E412D /* DecksTableViewController.swift */; }; - C0B5FF7625E981A8001D8D83 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B5FF7525E981A8001D8D83 /* AppDelegate.swift */; }; - C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B5FF7725E981A8001D8D83 /* SceneDelegate.swift */; }; C0B5FF7F25E981AF001D8D83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C0B5FF7E25E981AF001D8D83 /* Assets.xcassets */; }; C0B5FF8225E981AF001D8D83 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C0B5FF8025E981AF001D8D83 /* LaunchScreen.storyboard */; }; C0B5FF8D25E981AF001D8D83 /* Simple_AnkiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B5FF8C25E981AF001D8D83 /* Simple_AnkiTests.swift */; }; C0B5FFAA25E98362001D8D83 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = C0B5FFA925E98362001D8D83 /* RealmSwift */; }; C0B5FFAC25E98362001D8D83 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = C0B5FFAB25E98362001D8D83 /* Realm */; }; C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B6D150285DE01A00E354BA /* OnboardingManager.swift */; }; - C0B6D153285DE18900E354BA /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B6D152285DE18900E354BA /* OnboardingViewController.swift */; }; - C0B6D156285E2DFD00E354BA /* FeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B6D155285E2DFD00E354BA /* FeatureView.swift */; }; C0B85474282AB18F009816D0 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C0B85473282AB18F009816D0 /* SQLite */; }; C0B85477282ABA14009816D0 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C0B85476282ABA14009816D0 /* ZIPFoundation */; }; - C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C043E228206B1F00A8AC6D /* WeekdaysViewController.swift */; }; - C0C081B425ED379600DF1083 /* StorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C081B325ED379600DF1083 /* StorageManager.swift */; }; C0C9BA672848A3B30020E555 /* (null) in Resources */ = {isa = PBXBuildFile; }; - C0C9BAA42848B9120020E555 /* ImportedCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAA12848B9120020E555 /* ImportedCardCollectionViewCell.swift */; }; - C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAA22848B9120020E555 /* ImportedCardsCollectionViewController.swift */; }; C0C9BAAB2848B9410020E555 /* APKGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAA82848B9410020E555 /* APKGManager.swift */; }; C0C9BAB62848B9F20020E555 /* APKGModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAB52848B9F20020E555 /* APKGModel.swift */; }; - C0C9BAB82848BA160020E555 /* ImportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BAB72848BA160020E555 /* ImportViewController.swift */; }; C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C9BABD2848BD460020E555 /* APKGDatabase.swift */; }; C0C9BAC22848BE960020E555 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = C0C9BAC12848BE960020E555 /* .swiftlint.yml */; }; C0C9BAC52848F4590020E555 /* build.yml in Resources */ = {isa = PBXBuildFile; fileRef = C0C9BAC42848F4590020E555 /* build.yml */; }; - C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE86D266AAA9A00B83253 /* Utils.swift */; }; - C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE86F266B7A2900B83253 /* PlayerManager.swift */; }; - C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CBE871266BA50900B83253 /* URLExtension.swift */; }; C0CC42532B5052C000F683CE /* CardToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CC42522B5052C000F683CE /* CardToolbarView.swift */; }; C0CD12532AA9069400FE3BB6 /* LocalFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */; }; C0CD12552AAB3F2700FE3BB6 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CD12542AAB3F2700FE3BB6 /* CardView.swift */; }; - C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1712815A57600350862 /* ReminderViewController.swift */; }; - C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */; }; - C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DBE8312673E9E00074DC13 /* HapticManager.swift */; }; C0DD10BB2823F6D10058F96B /* SwiftCSV in Frameworks */ = {isa = PBXBuildFile; productRef = C0DD10BA2823F6D10058F96B /* SwiftCSV */; }; C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */; }; - C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */; }; C0E8477827F9C9EA006AEED1 /* SPIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = C0E8477727F9C9EA006AEED1 /* SPIndicator */; }; - C0F4FD1527BD82DF00814BD6 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */; }; - C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD1B27BD84DA00814BD6 /* SettingsViewCell.swift */; }; - C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD2827BD855A00814BD6 /* SettingsTableViewController.swift */; }; - C0F4FD3127BD855A00814BD6 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD2927BD855A00814BD6 /* MainTabBarViewController.swift */; }; C0F4FD3727BD862E00814BD6 /* Simple_AnkiUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD3227BD862D00814BD6 /* Simple_AnkiUITestsLaunchTests.swift */; }; C0F4FD3927BD862E00814BD6 /* Simple_AnkiUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD3427BD862E00814BD6 /* Simple_AnkiUITests.swift */; }; C0F4FD4727BD877600814BD6 /* BaseScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F4FD4627BD877600814BD6 /* BaseScreen.swift */; }; @@ -133,21 +94,11 @@ /* Begin PBXFileReference section */ C00513FA2B517E91005F5815 /* CardPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPreviewView.swift; sourceTree = ""; }; - C00648A92682060F00265EB8 /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; - C00648AB2682070200265EB8 /* EmailManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManager.swift; sourceTree = ""; }; - C00648AE2682140800265EB8 /* Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Options.swift; sourceTree = ""; }; C00BD2632B5A8EA900ACD977 /* CardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepository.swift; sourceTree = ""; }; - C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButtonExtension.swift; sourceTree = ""; }; - C02348CA2861F47E002FFBDF /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; C02348D2286343F4002FFBDF /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenViewController.swift; sourceTree = ""; }; C02404072B6835930017EF2E /* TabItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemView.swift; sourceTree = ""; }; C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewModel.swift; sourceTree = ""; }; C028C4C7280C852400F6894E /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; - C028C4C92811C0F000F6894E /* NewDeckViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckViewController.swift; sourceTree = ""; }; - C028C4CB2811C71500F6894E /* EmptyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyState.swift; sourceTree = ""; }; - C02D41C2282146BF005E9835 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; - C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; C03A02602B5299EF00576543 /* CardViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardViewState.swift; sourceTree = ""; }; C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckSettingsView.swift; sourceTree = ""; }; C03B91C92B6FA82400EA483F /* LayoutButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutButton.swift; sourceTree = ""; }; @@ -156,7 +107,6 @@ C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportDeckView.swift; sourceTree = ""; }; C05BCFF32B78D04D0076CAD9 /* DelimeterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelimeterButton.swift; sourceTree = ""; }; C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedDeckView.swift; sourceTree = ""; }; - C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSettingsCell.swift; sourceTree = ""; }; C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnkiApp.swift; sourceTree = ""; }; C067B6862A8C1251000AF881 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; C067B6882A8C139A000AF881 /* DecksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksView.swift; sourceTree = ""; }; @@ -172,21 +122,11 @@ C0750ED32AA3C01800089B29 /* ReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewView.swift; sourceTree = ""; }; C0750ED52AA3C39400089B29 /* CircleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleButton.swift; sourceTree = ""; }; C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewManagerSUI.swift; sourceTree = ""; }; - C08903242A8D444F00EFC51C /* DeckSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeckSUI.swift; sourceTree = ""; }; - C08903262A8D474900EFC51C /* CardSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardSUI.swift; sourceTree = ""; }; + C08903242A8D444F00EFC51C /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; + C08903262A8D474900EFC51C /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerButton.swift; sourceTree = ""; }; C0A0C6492A9DFC210015C65E /* SoundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundManager.swift; sourceTree = ""; }; - C0A70E5F28211E620020D533 /* ReminderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderManager.swift; sourceTree = ""; }; - C0ADAD4125EC0E0B005DE503 /* Deck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deck.swift; sourceTree = ""; }; - C0ADAD4625EC0E62005DE503 /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; - C0AF0E7C2614F1E000853FA7 /* ReviewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewManager.swift; sourceTree = ""; }; - C0B1275327C3F0D4008E412D /* DecksTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksTableViewController.swift; sourceTree = ""; }; - C0B1275427C3F0E7008E412D /* DecksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksViewModel.swift; sourceTree = ""; }; - C0B1275627C3FB7A008E412D /* CardsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsTableViewController.swift; sourceTree = ""; }; - C0B1275C27C3FB91008E412D /* ReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewViewController.swift; sourceTree = ""; }; C0B5FF7225E981A8001D8D83 /* Simple Anki.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Simple Anki.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - C0B5FF7525E981A8001D8D83 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - C0B5FF7725E981A8001D8D83 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; C0B5FF7E25E981AF001D8D83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C0B5FF8125E981AF001D8D83 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; C0B5FF8325E981AF001D8D83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -196,34 +136,16 @@ C0B5FF9325E981B0001D8D83 /* Simple AnkiUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Simple AnkiUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C0B5FF9925E981B0001D8D83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C0B6D150285DE01A00E354BA /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; - C0B6D152285DE18900E354BA /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; - C0B6D155285E2DFD00E354BA /* FeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureView.swift; sourceTree = ""; }; - C0C043E228206B1F00A8AC6D /* WeekdaysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekdaysViewController.swift; sourceTree = ""; }; - C0C081B325ED379600DF1083 /* StorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageManager.swift; sourceTree = ""; }; C0C9BA722848AC300020E555 /* linter.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = linter.yml; sourceTree = ""; }; - C0C9BAA12848B9120020E555 /* ImportedCardCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportedCardCollectionViewCell.swift; sourceTree = ""; }; - C0C9BAA22848B9120020E555 /* ImportedCardsCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportedCardsCollectionViewController.swift; sourceTree = ""; }; C0C9BAA82848B9410020E555 /* APKGManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APKGManager.swift; sourceTree = ""; }; C0C9BAB52848B9F20020E555 /* APKGModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APKGModel.swift; sourceTree = ""; }; - C0C9BAB72848BA160020E555 /* ImportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportViewController.swift; sourceTree = ""; }; C0C9BABD2848BD460020E555 /* APKGDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APKGDatabase.swift; sourceTree = ""; }; C0C9BAC12848BE960020E555 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; C0C9BAC42848F4590020E555 /* build.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = build.yml; sourceTree = ""; }; - C0CBE86D266AAA9A00B83253 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - C0CBE86F266B7A2900B83253 /* PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManager.swift; sourceTree = ""; }; - C0CBE871266BA50900B83253 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; C0CC42522B5052C000F683CE /* CardToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardToolbarView.swift; sourceTree = ""; }; C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFileManager.swift; sourceTree = ""; }; C0CD12542AAB3F2700FE3BB6 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; - C0D1C1712815A57600350862 /* ReminderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderViewController.swift; sourceTree = ""; }; - C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerViewCell.swift; sourceTree = ""; }; - C0DBE8312673E9E00074DC13 /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = ""; }; C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingButton.swift; sourceTree = ""; }; - C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCardViewController.swift; sourceTree = ""; }; - C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; - C0F4FD1B27BD84DA00814BD6 /* SettingsViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewCell.swift; sourceTree = ""; }; - C0F4FD2827BD855A00814BD6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; - C0F4FD2927BD855A00814BD6 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; C0F4FD3227BD862D00814BD6 /* Simple_AnkiUITestsLaunchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Simple_AnkiUITestsLaunchTests.swift; sourceTree = ""; }; C0F4FD3427BD862E00814BD6 /* Simple_AnkiUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Simple_AnkiUITests.swift; path = "../../../ZBS/Simple Anki/Simple AnkiUITests/Simple_AnkiUITests.swift"; sourceTree = ""; }; C0F4FD4627BD877600814BD6 /* BaseScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseScreen.swift; sourceTree = ""; }; @@ -271,17 +193,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - C00648A62681F26000265EB8 /* Cells */ = { - isa = PBXGroup; - children = ( - C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */, - C0F4FD1B27BD84DA00814BD6 /* SettingsViewCell.swift */, - C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */, - C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; C00BD2622B5A8E9400ACD977 /* Repositories */ = { isa = PBXGroup; children = ( @@ -293,6 +204,8 @@ C01290712A8F822700ECF9D7 /* Managers */ = { isa = PBXGroup; children = ( + C070008D263E86E0006DF020 /* RateManager.swift */, + C0B6D150285DE01A00E354BA /* OnboardingManager.swift */, C07458222A938CF90046F39D /* UserSettings.swift */, C0A0C6492A9DFC210015C65E /* SoundManager.swift */, C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */, @@ -330,22 +243,6 @@ path = UI; sourceTree = ""; }; - C05AA2B025E9891E00E40346 /* Controllers */ = { - isa = PBXGroup; - children = ( - C00648A62681F26000265EB8 /* Cells */, - C078B5FA281EA8B300DE5A86 /* Settings */, - C0B1275227C3F0CB008E412D /* Decks */, - C0B1275627C3FB7A008E412D /* CardsTableViewController.swift */, - C0F4FD2927BD855A00814BD6 /* MainTabBarViewController.swift */, - C0B1275C27C3FB91008E412D /* ReviewViewController.swift */, - C0E8477427F9AEF4006AEED1 /* NewCardViewController.swift */, - C0B6D152285DE18900E354BA /* OnboardingViewController.swift */, - C02348D628671E16002FFBDF /* LaunchScreenViewController.swift */, - ); - path = Controllers; - sourceTree = ""; - }; C067B6822A8C10D0000AF881 /* SwiftUI */ = { isa = PBXGroup; children = ( @@ -420,73 +317,15 @@ path = Views; sourceTree = ""; }; - C078B5F9281EA89500DE5A86 /* Reminder */ = { - isa = PBXGroup; - children = ( - C0C043E228206B1F00A8AC6D /* WeekdaysViewController.swift */, - C0D1C1712815A57600350862 /* ReminderViewController.swift */, - ); - path = Reminder; - sourceTree = ""; - }; - C078B5FA281EA8B300DE5A86 /* Settings */ = { - isa = PBXGroup; - children = ( - C0F4FD2827BD855A00814BD6 /* SettingsTableViewController.swift */, - C078B5F9281EA89500DE5A86 /* Reminder */, - ); - path = Settings; - sourceTree = ""; - }; C08903232A8D443900EFC51C /* Models */ = { isa = PBXGroup; children = ( - C08903242A8D444F00EFC51C /* DeckSUI.swift */, - C08903262A8D474900EFC51C /* CardSUI.swift */, + C08903242A8D444F00EFC51C /* Deck.swift */, + C08903262A8D474900EFC51C /* Card.swift */, ); path = Models; sourceTree = ""; }; - C0ADAD5625EC19D3005DE503 /* Extensions */ = { - isa = PBXGroup; - children = ( - C0CBE871266BA50900B83253 /* URLExtension.swift */, - C00648A92682060F00265EB8 /* UIApplicationExtension.swift */, - C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */, - C02D41C2282146BF005E9835 /* DateExtension.swift */, - C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */, - C02348CA2861F47E002FFBDF /* UILabelExtension.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - C0AF0E7B2614F1AD00853FA7 /* Managers */ = { - isa = PBXGroup; - children = ( - C0CBE86D266AAA9A00B83253 /* Utils.swift */, - C0CBE86F266B7A2900B83253 /* PlayerManager.swift */, - C0C081B325ED379600DF1083 /* StorageManager.swift */, - C0AF0E7C2614F1E000853FA7 /* ReviewManager.swift */, - C070008D263E86E0006DF020 /* RateManager.swift */, - C0DBE8312673E9E00074DC13 /* HapticManager.swift */, - C00648AB2682070200265EB8 /* EmailManager.swift */, - C0A70E5F28211E620020D533 /* ReminderManager.swift */, - C0B6D150285DE01A00E354BA /* OnboardingManager.swift */, - ); - path = Managers; - sourceTree = ""; - }; - C0B1275227C3F0CB008E412D /* Decks */ = { - isa = PBXGroup; - children = ( - C0C9BA8B2848B73C0020E555 /* Import */, - C0B1275427C3F0E7008E412D /* DecksViewModel.swift */, - C0B1275327C3F0D4008E412D /* DecksTableViewController.swift */, - C028C4C92811C0F000F6894E /* NewDeckViewController.swift */, - ); - path = Decks; - sourceTree = ""; - }; C0B5FF6925E981A8001D8D83 = { isa = PBXGroup; children = ( @@ -516,14 +355,6 @@ children = ( C067B6822A8C10D0000AF881 /* SwiftUI */, C0C9BAA72848B9410020E555 /* APKG */, - C0F4FD4427BD86BF00814BD6 /* Protocols */, - C0AF0E7B2614F1AD00853FA7 /* Managers */, - C0ADAD5625EC19D3005DE503 /* Extensions */, - C0CD246E25F168DB00D7191B /* Models */, - C0B6D154285E2DD100E354BA /* UIComponents */, - C05AA2B025E9891E00E40346 /* Controllers */, - C0B5FF7525E981A8001D8D83 /* AppDelegate.swift */, - C0B5FF7725E981A8001D8D83 /* SceneDelegate.swift */, C0B5FF7E25E981AF001D8D83 /* Assets.xcassets */, C0B5FF8025E981AF001D8D83 /* LaunchScreen.storyboard */, C0B5FF8325E981AF001D8D83 /* Info.plist */, @@ -554,14 +385,6 @@ path = "Simple AnkiUITests"; sourceTree = ""; }; - C0B6D154285E2DD100E354BA /* UIComponents */ = { - isa = PBXGroup; - children = ( - C0B6D155285E2DFD00E354BA /* FeatureView.swift */, - ); - path = UIComponents; - sourceTree = ""; - }; C0C9BA702848AC300020E555 /* .github */ = { isa = PBXGroup; children = ( @@ -579,16 +402,6 @@ path = workflows; sourceTree = ""; }; - C0C9BA8B2848B73C0020E555 /* Import */ = { - isa = PBXGroup; - children = ( - C0C9BAA12848B9120020E555 /* ImportedCardCollectionViewCell.swift */, - C0C9BAA22848B9120020E555 /* ImportedCardsCollectionViewController.swift */, - C0C9BAB72848BA160020E555 /* ImportViewController.swift */, - ); - path = Import; - sourceTree = ""; - }; C0C9BAA72848B9410020E555 /* APKG */ = { isa = PBXGroup; children = ( @@ -599,24 +412,6 @@ path = APKG; sourceTree = ""; }; - C0CD246E25F168DB00D7191B /* Models */ = { - isa = PBXGroup; - children = ( - C00648AE2682140800265EB8 /* Options.swift */, - C0ADAD4125EC0E0B005DE503 /* Deck.swift */, - C0ADAD4625EC0E62005DE503 /* Card.swift */, - ); - path = Models; - sourceTree = ""; - }; - C0F4FD4427BD86BF00814BD6 /* Protocols */ = { - isa = PBXGroup; - children = ( - C028C4CB2811C71500F6894E /* EmptyState.swift */, - ); - path = Protocols; - sourceTree = ""; - }; C0F4FD4527BD876600814BD6 /* Screens */ = { isa = PBXGroup; children = ( @@ -826,87 +621,48 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */, C0C9BAB62848B9F20020E555 /* APKGModel.swift in Sources */, - C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */, C0F730C42B752F7100127CB4 /* ReminderView.swift in Sources */, - C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */, - C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */, C0F730C22B7424D900127CB4 /* ReminderManagerSUI.swift in Sources */, C07458252A93943E0046F39D /* Constants.swift in Sources */, C067B6892A8C139A000AF881 /* DecksView.swift in Sources */, C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, - C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */, C05BCFF22B78CF4D0076CAD9 /* ImportDeckView.swift in Sources */, C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */, - C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, - C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */, - C0B1275527C3F0E7008E412D /* DecksViewModel.swift in Sources */, - C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */, C0CC42532B5052C000F683CE /* CardToolbarView.swift in Sources */, C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */, C05BCFF42B78D04D0076CAD9 /* DelimeterButton.swift in Sources */, C0750ED02AA3A60400089B29 /* AudioRecorder.swift in Sources */, - C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */, - C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */, - C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */, C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */, C0750ED62AA3C39400089B29 /* CircleButton.swift in Sources */, - C0B1275E27C3FC4A008E412D /* DecksTableViewController.swift in Sources */, - C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */, - C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */, C0C9BAAB2848B9410020E555 /* APKGManager.swift in Sources */, C0CD12532AA9069400FE3BB6 /* LocalFileManager.swift in Sources */, - C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */, - C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */, - C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */, C03B91C82B6FA60500EA483F /* DeckSettingsView.swift in Sources */, - C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */, C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */, C070008E263E86E0006DF020 /* RateManager.swift in Sources */, C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */, - C0C9BAA42848B9120020E555 /* ImportedCardCollectionViewCell.swift in Sources */, C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */, C067B6872A8C1251000AF881 /* MainView.swift in Sources */, - C0B1275727C3FB7A008E412D /* CardsTableViewController.swift in Sources */, - C00648AC2682070200265EB8 /* EmailManager.swift in Sources */, - C0C081B425ED379600DF1083 /* StorageManager.swift in Sources */, C00BD2642B5A8EA900ACD977 /* CardRepository.swift in Sources */, - C0B6D153285DE18900E354BA /* OnboardingViewController.swift in Sources */, C03A02612B5299EF00576543 /* CardViewState.swift in Sources */, - C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */, C05695D42B501AAA00033AF2 /* ContentView.swift in Sources */, - C0F4FD1527BD82DF00814BD6 /* UIViewExtension.swift in Sources */, - C0B1275D27C3FB91008E412D /* ReviewViewController.swift in Sources */, C07458232A938CF90046F39D /* UserSettings.swift in Sources */, - C0B6D156285E2DFD00E354BA /* FeatureView.swift in Sources */, C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */, - C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */, C05BCFF02B78CD3F0076CAD9 /* CreateDeckView.swift in Sources */, C074581F2A9293AA0046F39D /* CardsView.swift in Sources */, - C0F4FD3127BD855A00814BD6 /* MainTabBarViewController.swift in Sources */, - C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */, - C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */, - C0C9BAB82848BA160020E555 /* ImportViewController.swift in Sources */, - C08903272A8D474900EFC51C /* CardSUI.swift in Sources */, + C08903272A8D474900EFC51C /* Card.swift in Sources */, C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */, - C0B5FF7625E981A8001D8D83 /* AppDelegate.swift in Sources */, - C00648AF2682140800265EB8 /* Options.swift in Sources */, - C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */, - C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */, C03B91CA2B6FA82400EA483F /* LayoutButton.swift in Sources */, C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */, - C08903252A8D444F00EFC51C /* DeckSUI.swift in Sources */, + C08903252A8D444F00EFC51C /* Deck.swift in Sources */, C05BCFF62B78D12C0076CAD9 /* ImportedDeckView.swift in Sources */, - C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */, C02404082B6835930017EF2E /* TabItemView.swift in Sources */, C07458282A939FA40046F39D /* LinkView.swift in Sources */, C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */, C0CD12552AAB3F2700FE3BB6 /* CardView.swift in Sources */, - C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1012,7 +768,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1067,7 +823,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -1085,7 +841,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KHNA6PF8QV; INFOPLIST_FILE = "Simple Anki/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "Simple Anki"; @@ -1095,10 +851,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = nart.SimpleAnki; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1112,7 +872,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KHNA6PF8QV; INFOPLIST_FILE = "Simple Anki/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "Simple Anki"; @@ -1122,10 +882,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.3; PRODUCT_BUNDLE_IDENTIFIER = nart.SimpleAnki; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; diff --git a/Simple Anki/APKG/APKGDatabase.swift b/Simple Anki/APKG/APKGDatabase.swift index c29bfd5..7fe6e54 100644 --- a/Simple Anki/APKG/APKGDatabase.swift +++ b/Simple Anki/APKG/APKGDatabase.swift @@ -81,8 +81,8 @@ class APKGDatabase { while let row = try rowIterator.failableNext() { let data = fetch(col: Queries.flds, row: row) let cleanedData = data - .replaceOccurrences(of: "<[^>]+>", with: "") - .replaceOccurrences(of: "[\\[].*?[\\]]", with: "") + .replacingOccurrences(of: "<[^>]+>", with: "") + .replacingOccurrences(of: "[\\[].*?[\\]]", with: "") .components(separatedBy: "\u{1F}") let result = Dictionary(uniqueKeysWithValues: zip(headers, cleanedData)) guard let front = result["Front"], diff --git a/Simple Anki/AppDelegate.swift b/Simple Anki/AppDelegate.swift deleted file mode 100644 index 1b17f00..0000000 --- a/Simple Anki/AppDelegate.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// AppDelegate.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit -import RealmSwift - -// @main -class AppDelegate: UIResponder, UIApplicationDelegate { - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - RateManager.incrementLaunchCount() -// let config = Realm.Configuration( -// schemaVersion: 3, -// migrationBlock: { migration, oldSchemaVersion in -// if (oldSchemaVersion < 3) { -// migration.enumerateObjects(ofType: Card.className()) { _, newObject in -// newObject!["_id"] = ObjectId.generate() -// newObject!["memorized"] = false -// } -// -// migration.enumerateObjects(ofType: Deck.className()) { _, newObject in -// newObject!["_id"] = ObjectId.generate() -// newObject!["autoplay"] = false -// } -// } -// } -// ) - - do { - StorageManager.realm = try Realm() - } catch { - print(error.localizedDescription) - } - return true - } - - // MARK: UISceneSession Lifecycle - - func application( - _ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions - ) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - - } - - } diff --git a/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/1024.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3b867080647f7cd2b91536c5153051e0660b9caf GIT binary patch literal 348300 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
2aq{r`}Pu2u1})71gNhh zL1#a*{!ApT8SZx>NID1d>>cfvIf+_3fk&}gyy$)4cPw;ih1ax=iVuH|n-7K~NJme`Jw-&uHz;&*89k*f zu|;dD;TMSe^)JmZ61A?~s!lq%ziE4*S2J9v5mjJUm$%j&nQ=Em#HoIC5@d$fK#&C8 z^vFKz-&?2nPZ6m^Wx5CVcH}a1{a??Lw-VqHm8PmBjoEVjy2n8D350-{0YLsU+g#iq z%(swoJV(ytq`AV5MGsqoVacjbztb)PN4SgjtIpet)$PIO%X;To0+@!|9Gx|@6d3*A zR*FoDT5A#ri%$QX2f1}z;LT=BR1xgc@|8Gn6Pt;y&?-R(B<=-V9{R)Z695S)^eg6L zWPmtFtt&-(^-AL69Xrs#M$BV|NfG1hGy*-G1xu;GN!sWy1Q(gn$gbDtT#d`%_4Cbh z=!rlK`ZI1a3%M-SjM}a@a&cicDY^GfWy&J~c}#J`dzvcKz_?C*_(v*bg@10)imj>K@97YDYJc!?gL_-OVfg!i@$ z*uhMxQsValV;1fY$#+l>?@L~)AMW3AEA|!FPseT;!? z?8jgzI+=?v?cb-y1fP@65cbcdMYc?NY@S`d5INax#zJUe%^h0>?*uaofXJ;=Hs3 zxyfHzn18;_l?akQV!%k!DzR$c^iKHe=v^CGpB`ky*#?a#+&hwFVXsvKuGm^9K96z! zGCFlSkN)F$x;b8wTgA@vmbg;#;J2&<^t4|= z|C8PRAR=@Z_5WLR|7D)W?+=o3Q9j7yyPFrk=a6wo61A0D4_bQ=f8J;u7{_i&Of^fxh7HO; z*y66?_(_TELW)*gX7q6l9kOAH5+0v`7$4aPdo z;|k`n@Tre}D5X(a>=&f(fim1-zRC-=1Hmiv*~RLKQw8+0QX~5tg)E9#RvQ-n^- zpD&4i6ju-~(miueTq6%$Bm7s}<(!QdP%00oR8O&$fy2V5I4(p-{OVrWta>rh8!z#2 zxYlF%@T5h|ls|;0UfeZOh%lbm!YbJR=`Z1*H8U9c>!t8;^$s~YJbMq!?#o_hIjvC- z|2zjCuY24%w~U$nCk=lGU#tuZP-B(AAF~FHWA)95Nb?;<`aUKkK{JzNzyxCVyup~o z6)ppNWH<2H?e)<8?cdAp;xzX{8Xzjdfu{y>$vw;7WM*!JnPKCcg`YiiJL88VvX4J0 zI`gkEL0JQOWuS6Li`71$>r5IYp9kd}xl4{qJkWcobEFn20N+h{vmvu(oqqYXNn~j* ziK`i_zGH}BKHy;jYE2(vD0vZFds@VUdh_2v0{k#nKz{$~&x0z78XxDWUDEzdzK7v8 zgt7NlC(VwJMm4bL-g2uM z3y-FLTF7McI@;_t-H*}aD3`msvCm66XO^|?0k%-p7Ptw`R&xS?!_4&gmMJ+!;SE+J zV{Z8G0&fW)16a1`NcEUBXiN0oHA)Kh{XVPNxX{~7RstjWIo4T);0r-ZM+!z@_@k_; zQ}NL+1s7MGlf9Vwlhu*2o{`NgMkEBIxIgy039mXv7-hUu*(lH1{0tkk@D0seTii5N zX^{1teH}RJ!i=Ogj|1G9L-R(TQ_kk^brj->1iH7LGRXD`V!~0YM^P!(MMlY%4+zRvhwS${n+#_#sX+0k?`P?Z(aeLAaB?fK(!ZMxad=-j~jfC6F+L4)b~y9+7EKpvs)9zI|{L-Y|(>h z+Fv6&=ALl2xT6LeCu!DQ#A*^75op-F(`eyPE=D0c>t*1q(&vV3)^!bx)(2672%Z`I z@gE#q79~O6kd-HujUC88i~G5AV>wTVrK+gM17*-&VDEVVcRFVGSX^L2Ujcbq4jD!C zKW(1A`n~1vu7em;8*u5d0j;&(`rPcrV6LCJ<1O(&lR;=n;8xVq;A~=k$U9}yfy1~g zLj4=!e_dX~_-pM8K@EV91`N*ei6WDOqdmrFYyf5NTz^rb4V@yvcbU0Rw-n;sVk4#Y zbwBwBp~dw35^XHPh_EJeyooQ#r^cl4RyUV}_g>vq^!Mz}iDW|icmI{2_KyErAbWy? z-#2O3$V2cM_RS0AhUt&Dh8YqU5Q4*{_C4}>TwHxX=4T98+9*=vgstWcK>dCk3$WN; z$tHC?aBo4+a%*u3S_EGS)dXrk9g$>-uQAThgZM(M=)LgP-9elcitE^85Q$1hhaV2# zarv8ZXXQZN$BG(?rywAS;5)NKK$p+%UA_l!GTOM<3Gj|*PhC0W!IyoD9!S7Nk@|^B zvOl0 zn;A~oJg`>`Xrg%UtXFwv^N@`cMMXO6xus7 zt(ezc<2p>WU77^1x;6HvD;5VHUpfQv3kzm-c{`UQN^~cojkBg@&QZefHTzFoizdG9 zf=>g$?`Rv>R-BVU;C@@rzLwTHSjW8e@Df8LW(~Xaqz`m&W8q3tX#Uz30;lV+jV3B4{^xeC7({J4~>dO36_RrREX^-O`6&E=J!J_<7Dc(04YqlFL*o; zNuR{JIowYXMWJ?XXEDTmFP#ly04bjplCtL-zj3QSV5S*3gqdXCw&V%X!{}E7AG&}S z?Q6PW8Nv(-gc0UT?Pq1QXq`k#`;Xi-XKGngN>Scaz!-8YF~kMs&{*TPnO8i@9!~Vx z)per+FuplFo+lJXfToe+qKTQ_`?b8iHZb5_WARh(?~3!RQPU^Se>zxWjy_zJm_1^v z@09`OkeVg--dO*}owDnN-@<=7EKT}7cS#UzUQ_|(@2cN( zT*@9YEVV%1fM@^a%;d~e7n{Fr?o25uf-Oo2_`N~CA2V>I+4nu9G%n7<`1cUfBL?vzd`rIs#fB%~#!yJKk( zkS=Lym6nhWk&y20?v4d^pZ#9<^Lz0heAx3oGjrz5sWh9ulWQXnlqtd{(AQJvq9c@N z=9B#nS>7n~V!)wZB?mHXt!>?@G6R$0K>=2m$+ZNS8n5UH3JdYbuXkjvY#LL&ZV*)o zv|18>88eMr!1f6()M|)ho}T-CP4zEmp&k=wCeCw6?WCp|^)0Zb`2^>k%JMT}V(f_` z;8HJ`T5Ux0rn7ON%D9t4ex8Gz1W4{Lc+2m#$U%Oqh48hi8z33sCi2+dC(Qtf@^})+}h5KXYpyw|qoSsW7w!3mt=S+nL z-wvWp?&`u&ttMsJrBmC}v1&k5=%-;Lb8RF}es_;ZRYvNG&MS*X>pyHv?|tX-mQrzA z^4U2U)%2zcHxl;+_NJk7?;^;jViZ?us|v9?QX;c{`tHvu;uJw}P>io4L)3LE=QF>R zhmzoT85>0iiVTHXdNm{iVrNm$Gy7fvnIM+U>cGS&zkNM`76lvRB2J!w)2ls|psNT- zHp;ZI3}&}#AiqouPU?Z&aLw!12l6qSbt2ChOBp{6{w61ZwUV0nUN1wy4k%rRyp3az zx9dds)xcfpd{~v(>+x~IHMtDH;^QzAb|@!Ko4gx?gb2lcB{BPUimf@&1Bn1=MmiS|V=y+s7a6k1TZt{CoS(^MdI%Q6$ zxZyWeAyIE*TmpUTjVaM#`95=Vy%U7OJ(hK^b7l*Y05s{Wt-aBDimqJ`+Gvx}u8b-8 z_?v!QXPs!BB1o|n z%S}!Nb3B?@IiUG&l*rn}i$8LY<~R6k2`08&+k%3tCf`e+=D|WC<+E7W*u``@#L>DD z=XqgfgnGK&L1l(k9n-R%u1*=4CK)Iw5SnYckWya~1<*;!c%ZQTo%ra5m z^?^?4w!FdgFOf*zG4l8P%J2u(Y-Y1E>%rRFzd_4QdN6Hqt~V+|i?}Ph2Um3egc4|C zS6hF=|23NGoJPruW?&&9EV)zQwe|*|dZ$0~G#DwEGi*GZ9?KVD-fhZA$^gd5CBwXN?YL+FNS zEh5z2L$+wnZ{UD7^Q~im7tc$2M5bX?nBHq*$g!nB)fl{raIvNr7n=8pyvy%8Y9{6j74TsHv-K z=6B2KAa?iQ@(&jw%Y$)0e$V8^gj>9V2@%BToJ$(E^fLf4a zs6+WJx0`AFPxwxRGhtto7<8LA6wYbtiG zNLX~YHm21*j@V0;+*s6Jg3*kgWUNL=V6ES{9RC^N%{ndS#>Asyir421;b+&rd3(Ub zy{Cj8Bz_tkE%}-}T@)*^@-77?;1#k{Tts&v3uYk8NEpa@x1~Ub3b?u;{ei8a@_XE& zY(bY1Sbjo2K6Wiln-+_H>AR&G>U(zT)7f9?*!w7S7H0F)G}K)?Ba}bPl@TLu=A?+{ zDjNFK=JU3Y!0bIx$TKtTs%kmR_q^{d*wt;u?gi6F;GVW0WK%nJ1~71glNW!P%nDG6 z>edrsyj<}RB+7r2^RNFTgphqD%pyw=qYj<*CTZa1Tg@xJ$)|d4{3jKk(vIGUyuA;=o%u9($&yb?B+ zC{+Dxk9Ypf;Tg%JIehBQI45{I$&&uZ`$Fs648npwgo%(T_Bz4xEvk1b8E!D_zjZkDqKt^x0pJ^n3$KOCp;xF|2hH z>zP^<{cJt}Af!MjB1w(g3cpc47vULfXA+fph;HQ)$Iw=* zkq0F(fByX0RR-UXjHG?ImV|0^->nhrbW3{du!eYnw`JefN$AxYfdf3SE9&so=1yb~ z;Q<;hA?%`X*2l>g+3{tLAObx3E|eus*(JVFDS!?e>7qJ317^YYj4D-h$a9LHqhu%T zX!f*HJv5L{;6v=?Rq&5opx5y;Vhh7kdi=M_$qrQ;B$GZiK!OoF*xNoFGE+PBw#O#$@_M6HQ9U)D5s zbgO|iuHZ4JJ6FehikOLX`Qd6dU4-;3edftIX8eAH{E*MqborlQH`8o=rue&wwC}Sp z@rIScm6f}wCdicH+>8MqL9WT!bF6lh{Z8nxwo1gD#=i60NNtYRb4Qlbs0`|MK!&x& zeAYUq0A&PBhaKOiqRk zlLmuAIrRs`$cg~vIc&vp^(mUz5WuIYpo79oONQ+0MsYOp9&Z1ywomXyPx)EAH^VxZ z=7{-Vp}`egB@#V67cXS?;>v}-$W@FfbOPolNud#N`zYCpdOL;08uRK$rxt6cf-Uv+ zqB?NImL$U2<#^z2Ga=9=KF5)vzwc)q^sk(NVb(Bxz{{NJs*K3Vb7BMN-%^6(4|leJ zO}c$`Onkcp-uHFjB!+f*~2!RS+N=U za>n=w`Yt=^oXWX=xX;n?M$-4oz!B8y{^U8n$FHNJ0l0Hdv|NZhbJ{u-m66ZTw}uuL zoyCNa)B}vV%9zbU$VfefXL{jX!CfJ&#$qNw79-pw7aT2N#)KXNdqt}y&j|(!JZ|}T ze4UK9Q+i6RLY-iAN%m{)f)5VsJp{vH%=e>}j~huGqUY-ZM6iEe$6-2Yxtk7A`lnSz zn;ucdOFIhA^t8iWY@t%WQqfx~T3p!4!$c4@_-) zJrpPxppr?(G2n+IhhqB4o?}xvoM70_FBSSI7G!_WpIipcvk&TR2(@O!f*1_EI`}1b z+J1@n2IDzFU_FGwK7nHdZ3MA~5BU_lbLd6g`{m`=h2k6S7TG-E=0*)ypK^^GAB{7PSJX=_n?R>7#@<|dO?yKyg9 zX-rr#k-7-k-L!o%g}t{vOZ((1a@*GG#}099pQ}c`ph^F|pbC*~UsvdW=Ye-mSpS(A z+#iY%c2FsH;j`yQ^PKinbCdJm`|||DoQnxyc>;kE`!nC0{HEZx;LaOl#bal3ADCWF zw84KIm|me!C)`HBo35RKJ+oAAZDCWtSq2k)uQOyrrh{o7g(Ps*TaJ#*wj!r^S60ZS zjTihnny1bpV#hy!Bmha1pgc%if30wyMlJA(g1BhjQVKb_YcVYRyENwaI5jN6k>woQJoD;*6b}ldTy1y&hIz6~t zZA*EbM@E)ikoaRKiBwO~%ge8z-vRH?g_PQVrizinM+N{@37?wgtHq=2+{|oq^X%w~ zr~)ce7evn)R(rmL$__J1wV1zrhK9li2H;>dn|*RYqX4Bmn^0e%eP@Fz*80 z+FG%(x_+TYuCyQ4W_wbVoHFF1yfZoh!&iz=1;bOG)P@Nq#)NLSBvtNW8q|?ZD_>Yf zV96A3@^k$0?<+IQ7UH6N7O3sykHI{d>PAn^k^0pr>N)AgLmxW~*!G<+ zU|%8B;)SR}8J^!C@ccxGt(Y4T8r!l$ZptgSX1zGdYL)Y^J)0|OS|aVif8^jnLRF;f zp|*uZ!Dbi_Q1d$`1a-s2uN$LRpmQDwbhQ zzymn5CVmp!{aFFk1e*XF`Q%QCx7=)tY@eP$ky_~i0;#t(A5g%!z6)>0d3NFTjWmfZ z2VPD>p@#j0KK;94=bJ?t>zf|n*4~St3R_x|sjW9VpqyIF0)R}9%Q^L00Qy{owQh!< z7W!AOlX^T)Wik=p6bY&q?&23jP^#%8zww@+BtW%_N)7c|Qsg6zG%EnQ@YG@Q0A+JdaOnWOoNodB1@B_!ssAp}%Bcc6nThGJ zmzpk5do`nFh)QarbY&CY?~k3{`O(u==SM;{QG00KhswfO;|%Ft+ZUNJ(ja>1<32-gzk=5q?boUUri9bZ3voTS1k<5Ps@qsp7qOx&j z(cb-swXh1pclU#8ItlrCp4D}}O^(&6V^Zx-o|UEk&q{xrvs0tUe(@_}m!qQmU3C<) zNkD31a0>~Kk!ck-G(Y(MmrG(N#26_@uecObs|6>gZMbGsvQe}?y=sNqYl8(xc5y;bl|3Sb|Mx>kU;LjVMk-rgzhX~>(^Uvu7>;IO` zY0>UTEL!Kbz0sa9GRM3YvXaKJ<&&h+c+cxyM#`pd#DkWW?6z4Tbs+T?+p#ONl`n7s6mb|?^(z4qp@uvNK6J!R71!;oww5NgVoIfk8!V`J1$F(0+%%(u)TEQ<16kQH7$PO4QlTbVkN*DASKTXJ_6v>f1 ziKPAgb8#mfyr?&blfEM0V#?rA93;|#YEb$|*1XhHAn z63RVF6KH=>xzTJF(ovoiMlKr)qD{X3r$rl?KSgFT%cQ3)qj-`caj=>LE+9l#BBa9Z zk5w->8yoU+y}c}*W>8}NVN(|5(#V~`$bvQS(*};q->d^g0>G5DbL-{}!;bbc5GU;6 z;G_kW{eP7>uwmbM^tdqq`*6Ryu96W7!9v@Q(%06vZ;Cd*{Z9MFt747f zf_`b%K{mv6#HrGa+aiO4O>-)Ls2m^CTgS$!BLHw13HwUD(;P$|^$)ok4YwYdqwv+4 zGBsg|-DLe3Yb0oz@nkuI{1+?+l<-O&sZ445v*>Sq>Zmzp@6Tu~s@q&ne$H5&ezJP_ z3`;v?b7XmT9`UWjJuy)sz3?wbaoPO4w9dv8ApTBnLia{rq($uuH1;HrKr9uC9%c+= z<$I@4>V}rNpeFGVbM3>KMQ{R^WoA+{cjVe@$_Pq~^Vy}n*0VwD*b=aS0;y#Zc)4+u zR;I9%yzBJEI!A(OxG;?*x`M*%=Jl#i>z*}0R1@GevDfOw^FNusiOhI2^@HHdo5j%% zDY6&pM7y)v;+vQ5JeKlq8J+4TtA%m}=@O+Y)QP=JWptUBEo zQ@CULQuOUMo$UEk!VK5#=_7D7ijv`$=J?E|Q0(Ao*t|+U*U}t=yTO#ZJ}Tzgr*ORQq z=2OPckyZ0+LqlW|w+EfLcTbCwk=bfa-Ii~aLf88tRsI)CrO#?JtuDZmdH2zKrQQ}G6?a_j*9t5B388oCtZ53UT5hs0V<_1vZlM@{%3oPcl zZbK~IY!FIAU#_Ybs0HQ95wdy$nUtVwYy^(HdWwj)6H4jptRNzrm{h=*UIMy5Yl4qj zIs4WjC=aC8jLk>;#P_9yNZOLNLchJ11X|2Gr1_&&f--+SsyvbyVZPnPI3hk2H}HZ$5N+KH^#`+N#u#^T2}6$Nr+iV!qDi(){$jajr|?yZf*`3uEd0ofDk#h`6_DCva-rI;d$6 zbS+20qZR#}@rV1`1=p8ErEf1FxZ<@#`ngfXJ+1wTRmv9sn+GRELpe6Y{VwGT(|c)p z?)@KM@G0NTnPEQ|{`mw4HQ_EY8Zn|6+3j^LS1jPk!qx6&?Ed(0{1im~<7s*v(+8>}cr^bDoyE>ii|yAZjwTSR`H& zEb!dm&uMHYo6P)gW_Hc*mULS3YoNii;APli+)$agzaRqn!6{)u^?CRkh>~mC1oOKg zx#s5>9;kxkO<#@s1Bea0pElhk6!OeJ3SU+n7e%7|?lu z6c;MjQ$-YyF{pxB=QxM1p}$7wJ!F<~Tc>01ICe*hg$ zTF~}Ywx%s@CrqJHKVz)Q7U!d+5G0qjCeQYMhF3C-A#@r;uq zI!e)8e|f*6HZ}5AFgigpX`53Y_JEDtY8o|EveAz8nY}>>y!w_6*l=`(Oy$=``*;=| zydUK8bSvvxzlaL1(f&!|G3h-zC1X2BOy@SDO$sUHqg$nyu;<#%NO^HZjO)<~*E4dG zY?cnrShr6wg*HsQ;C|T(rvg8CV=N}`4ep_yF^%7z*cBdB*vd3ZvfC1(y~wGfc0VN| z{If}BEr8;g_}F?TESc0rN1C}ZATH1oa{DQ=rOfp=k7fR z7Ib+I4kVlKk=Xt>hoN}sS?MeTTEDuJ#PIICYTa0lP^oDl7y?bC9efLI4COG& z$-Gz)1Y#^^+P^DW^M$dlh^k8p5*t>Z^<`dN^CiF=g6!uE9coMiIVaB;96xw)t`xs{RM!?wfaD)X>d!l`f-Ji@4SQ{WXg{T;$K^o^vr8)GGgH8yJ%pl7jXS3*%Y$ zCZzH?$L1aP6sKLX4yxGQUkv^##O%FzKvPM4v79 z1aALH?@mcx9=*j(Y~eI!xh3$ML+${>NgX@q*#M`GHQ0cRva0blp@6MfUlGB)o(Ie> zpJ*zr@9U2-d_Gg4!2_vK)58Vz7smvb`%yQ#T%Y5$!ziRMuV3-8bv1KoU7Bm zOS=)fG2!h%3C~SDn%PB5D4tG}vcA@o>5#>O?rm738_Z5a5hLTE`k}B?@`SLz1W^K) zX&3NU%re|fN4=xC(PLDyT}poUwX$SY>+az?p)WIX=S^jzpw)!Ne`3Lk{#d{@iU~}{qMyG|DFLZ5 z4!09b=AwLLyw1Jub-T*SJ1Ccum$kf2daJ!vWS&gbR{iA1pf4@CdnzKY9o6=E8e^rCtn))`BQ12;2UR*y=ZpLTHjUD2L_pDbg))6cz zYHbCs=g>3TNjF??MC!VKvkl=PM+zuI>hBTWO^_5+KP*T}R@Jkr52)+xc@QB?1T-=> zrZ<#>F^F@i5jvc!d@}l)oL2JC8vMLTiz8v{KO#?%Q_pO>FPOn%YHhJrA^A*$Ww?*I zL;+2QnxL%euwY(WJ+_ZaE-0XroWS<@)+q$)cYy{qvJJOt!A@-U63YHV7bVWyaf$BP ztLiti*V2+c3#?D?5~V@rI6^%`Okv#&oGbv>5nLq`9&^ylFWgj(9!DdAm*VIJSpuqC z^!SO=TCTLiYbmy%HpbvxO$nvMbwl)%JwvypDdCh~F9Q$VpHtrZbY9cp@n~klO@y3q zt0aT{3)RQfQZuEv2w!gRxFr2(t*P}T47gBP#VAUfNk!z({xLZ@s^f;%xpQkH?^S%( zh=+d5d9iWD@8p-o`~ zRNko8jo$i1d~8;p6YYJ-E)aX6>X0)gDRzBL>^@`P-6uTyq$L%V6x#(y zXC)gmO?-jHkcDUhF6C)Vf4sOvxNi*o7Q9&dx3zTKs7o2Bxd>?LCA|FP9aK;T8n;qK z_%|Ur1G2bt&{}ny7=h>c@O$+Q`_(IReM94Txl9p^apviDJM8eC*mEk~O*do&o`*qP zaKAl8cuGz7t;~{PAX9X~%;uc-uxD$u9UtQ+c!DENL)T)YodA#9Vzm>1XIwD)Ii5l%7BH~*j z61fq}hzj-Zs0+^{IyJjrlP$84P(!S<0bj)pN{%Mgfq(wpI?W;6q6d6u z{#xB15@tkNJahjxMy#*NCbI$FAHV9q`$n)h0u2;^;2dscKw?37+b9yRSIcej?o45K z=QrdSKip`pOtCbFMkM?dBr*X0UY> zm#e!V@R!b~_qkx(2k@PL3o6lnE9!~wu$2LAx8f2YXwcrF-89gxJt3&y#IPWuts*{4 ztW{WIHQU6kf=i!UruWl764teE!7uYtcCZ)Hsttl3j@;0Le$!-JP&F{bgY?zDEppDs zKA1A9FACUB1ZzINBCvRy+cmMpf(|mX@_cDM#iqhJs0gir>;m3?Dh>@m(;!T?szDyY zGgY@oyv2m%Q*!$w*N>72e~IH(j4hSHNf|M#bd3SUBctl_=jRQW0b9FrKCmR}W)bR; ztJ};7Hj(ih21Xy67GT{+Pye75;Kw~xoOdU=GtG~7(5YEaSdh@?+|BMR4+bp_#RcMc zuq}F`jHSrGtcfnaL>^TK21=#CuIcY_5d6#kcIO!OV6wsbOpyDPI1p=TSPhG9^v`p5 zsC?}FwwBOZCXFqnvXe&VhY1v=(sXSg*@I?-rqA8)BA}k`dZWL@%O44C?QPMczdrx%vvXKL}AP}01@n|##sC?g=DI?@xUkwGY!jQ}kc89s}wy^)ZInDeqhGJAvlsblJWhMqFB>?xQR(yf0o*?w@0QSaxM*ewi z4hD(Eo#Y96Um^fjR*D{+QcDpP5|~@gVUd}K6aw=v1)6VIf^g(zh(}zj(Z}KRr`}%J z&3i!|f!j{?_7)+Wpst;S&vlk_@^un+O_2Zl8v*43&e;H80AvgR+p5l_yL=bb2=KTx zoE-XI3&e`UR{COp0)QEt_6>DW5&D7!71DKX!s{l!EjdB&_lP8pTNDMol z)ruMzmBI@(s#7^JHewL{sSQjE59;ONA^RIbET$@ z-xAGWN7sNsiv2f)2}XxPAQjp3USLoTymHL2TlYJhtM1owy@JGIhEwPh`d`H3H_O9R z0>GGG#Sb7J17Kh@u;JcRzwNbt9bmx5EaS;y0eP(bMC~}H0_%lBU37%j5QJ@e?XEl| zfWn}b3^VaX(OtguY1wPj9mJr5*M_QL#(L5ZZ4t>E(XwVjL+laSJQT4ha{j79Pz+L0I2_$u|AuB&qNit_}Y)XOCq?}?`|&Rf3^9PRRWmC0AS$=6qr5B zpGbA9Vw?T-ep9t?f70;innUuy45(imch%@jmY}Gy2O9cUNCvSA8cxpmV*?y7s2gHS zgOA)N4<)~w##U*mkCdgyadj+VKij)ETJ}!9ew%>!#kV_Q3M-j8yXM z85=h-Z4IukNC3Eip|Jp${??(2j1q9MA;E#6D06`iwG=*&a8}O&X`93_^_md(QOWTp z%%l|jLWX&w-C)q^ImpQS;VHugzzupxqg*;4BGfZug$sv2wc=Mm1lrnivh#_brlgD5 zFoq#yY{e}a><}7;YEjuQIseZKFvp^ZbNjHzR+c9MWhnzI=-9tl541F?#dF`);88%+ zvM}Um=!%AD*i-z$?8Bm;#URQ7*%kMQ72Zwa%nssicgTX)#fT8`aOsuJ6prto&>p{Q z5~ja=IC~%bqfBPr4fmER!Q5+y|7Tvu%hBblf4mZ|J?gDN0%lXB6>Hby|3&uT#2`kH zUp{;jU6al^fP4tG!9QfuegAnb6d`{i({#AqNR1Q2KhF1@kyS19=?N5Q(0>zNXIEyH zc_{s8;})LqwL&r}wz0mEN${B&lJRjWxV|$bSn=DRuPN9EJUAHRutW^m)v!4(yf3%? zs6O%cg%}}?B`1Q2*(#ecX*{E=N;$2;-?ERXL7Jueyp@JG&h*~f_YLrpmEu4_%Gswi zY5oa`^cmBk@&hw*#!KHN=jVnu5G#v0Lg4vvMhE%dZ6?x2P0H&sAmYW5tl^j*so6fs z+!Q$Tuih!u+$Wd;ck5YF#}MYv0Yy+OI6AOq5v*(sW%VW#J|uh5;IS&H-bg8tRA}Gy z4-q}X*5-P|FZL-AL}~+=$`W1WdCP|$OA`Ifd}rU~(5G#cs3*7W16C4Gm>vf))F)-2 zzpM0>caHn%xA(81aZ0Zsy8ArHJI%>|NxX!S7GI$l{Iw_pMdw6n76Xd4BUu*O&i*^{ zzwvJ54H&i_=xOiiLp^9qS!x93K>7N?!ByxaWR}Qa?7reL>|aW;RD(P{UPB@m_k(F^ z8#+)z?@kJ}z_BQQcIb`cai=#n#{XdkOeVm8;v(4OF)yJD_El7xy=|L${>$5m665=K zT8Rm?glJs{p9K?v9)hH#cN-m27<$l%$POhUnyM+{T0S`@VdpZuc@H|8c5-kq(q{`Ax14)2^%EYK*rrNy8(6_Op!{yL z8yrzAP5A*FI*n?8$zFErP1S03CySi0$ysUK=s8(?G07J1ASTbQa|JWt?xmjNb0yz{ z565!vd9#jtE-JGlF`yIk?Y(6Oj;bcu7u8p@sC0gh6356vfG5W<3Dq?%W9WSAoH_{^ z(@)^7!rJFw0-8a_xDN?yGUO4|oJO5_OlCfejdyd@&1+de2XzIPxxYAVPFGktWMk_L zr^sX?)RzT&7F*4J?{BD{b|+8-=_E-pc@v(Lx4X}kza-II+d_=TY*Um?t;s}{vdJV; z9O^zLk(l7nC|50XXy7Hb_bLr^1T@)Os^*MO(&RkxVqbld#ea6<{l+XF@afAqscMZ} zfcp4#n2V{r*E(O=rr)k4Y&~|Lv=L{(UMF?0o?rxD5t=P|uPFkr%ed2lPA--pbux_q zjWM&Z6ai0=VZgTDaiC% z%vM*2P+3_eoJmPaEG3kd&t=c$wvvO>KRf9JpR~gFU-fey(84I-GD_)Mw8>zMtH|G& zdjz+P_*I+QDu`pv8GFxn71fq6CVcJr+AUlA3qWKC6QDQXNzqdpmiNLRf8~JCc*WI5 z^r8Ed9H1@%A*+_7zn^U$pVLCSJumMP7^XAQ6{f35E+%j!xHbIA4LK=?kY1i)J?B!P zw0XHx=6O4ySG@yz8JoK;zlk4Z5bYuQg?iFvfc<`~_r~1LP7~$j9N`9JwNlyifvh|{ z4=WK1j=AvdCrHfD56xhgO?J&c)VF{?`3+(ZmNxXA(n0eQNwOio z&;wbY)(jKKt|Kt|4nW5xsoS^J!?35Dra~(I9+}#(Hyv_B;ob|Dr1D1!kvhn%JPFEXnh~nA}MXR29K8Oz;vkqVDVaB&Q$588r1z>@vpGJ;#axl zPQn$b6Z`_6k1T%tE@>cpPb>raNIobl^#d$QOj)r_AY&T)dyq-<0}Wmu+!coY@gL}Z z;go;IR3vU8b+J_w@s0f#?#91eudip@Pi2h^XVK+`O2Gu4VX+E1_U2g>Qb}y}RzG}i zst0MnkWRhlGm^58G;&&k^GrFQbquHtItY+}G>+{uB#Ttlr#K=Ft0c%v^98b?#7|mi z4}t_;-bD0__&pZr2$Wun?zV;_nNFfHS;KYkLRb-~9)2~A)pR5YgvL(ci`@)xw*}c# zI=M%{U%}tUD6I8~*}N1$teW< zO%q0}^(O<7+rF_#!{B4WRd0*C?IGu|2iF7A7S#WLXUp|&=D&t&UKNw6!f^45f9EUf ztjNBM5yI-Z$>Gtw&!*(JK+AYrrL0!k0JTw`Fdk%Nf*;lRRt`acua|x0NidcRg5m=d z0X5$d=2Fy!mEcM9i+|~G!E@!gIm_B=d{q3>>J3@&#bPCnoXlO1>sl z>^OwXuH?8exz_&8>^p8Ovao;yKb-H(BF52w%8yB!uCwqN(4ccMWSTv(84N`|&Wci8QCT9$K?Q(~(ZTula9tiU+XY4Pg*6`^oIb90;(7Xwf4Zw*0Cs z*Oh^+3NPaStQnM@J4@zAFP#r=0O`g!*SB*G|ATo7O!0np~D^SO0~r4r9%hGnrEpc1QF})pK02^_IaMg3D|&vxx?#T?zH&< zzY%xEufJQ_ab8?0lV{)lfi82u=L#s1t!+KdvXQ+ z2?f4IGLC#lJJ7^eAS@|ailzAdpK8Un@Hs9OX0{To?bZ{rn4U}_tW^$8O1VpVMnm4v z&@q{uxhHx(MdCE3jkgyz?+BO&qc|6PpOMQZWxr?UN8+<&V~hV9JcVe1 zr;j|KZI97q8XxfZ6*SIn(D}c#um2Li?10H2UOYf88pJMZcAlv?k8TxQ#lZsjgJ(P` z%!o>%2mtmhz$8Jx+mX>KT4-dht~ZCEW@CgSU_KYL+`t!7y|^3_j~oplMG{*KYG@yx zFQ!+T*cs8O5kOOpcd+-~0P7|8`@3iNg5o{r?1xQ;7h;k9q>vp*1K-0NymwIT$Xbrp z&5(EJU;yx3o~SGFxBPBJTD1vs5j7BZ<9^6VaHU~XddpM1PQza&O7_?E6q=5f{GsW{ z+1{VACokP6l?6e&A87nNKPxmo2e~Mqm~?=D;QBM={N9`5U7bJY=}zBloXPO%EHqq) z)&38z@`xhIJk?C!@&A%kG&u1X0Yd8Cxu$SyvdW*uPd>jbXfNm%h)K=J++AGDBfg=^ z?u(x>zEu2!6f)0iW>H<%*G?|ic=NjToKJe{6I25VC0{%wtP|!IpQV@O24aI@YuaQD z0Cg+~zk4}K&G#Mseg|8Wf%M*Uxe>t64)w7jVMQF=zxbF#ZBCsjoc4IUn+0f8sBB?g z=ay0?1BnXk7?S!E6xU>(4mfz}+;JI8$+~+U7O#Sv?nmm-RmaTS+0;Za4{L?OI0rHw z%h+~83B*wO|8|zyo(35LyFb{)QiyMA{pX+*GJT$Lneq-q%7NEBZcWg}S6MfX+cJE7 z$XSidMk%-D|7bYW2}la8X~phW&kN;o1w*FzplxDiFSqM&gw4?e0Qq#W)GybUkR1HB zlN*vcH85c_9t+JI)~yX3S5D+cPHU0LB&xtXHxI1prJOFVA5$SOT;|%Sx4B!uo|v+= zPk|P9M&gXQp(^%e+9-<@nB&PTYAmW$ORR}DNd!ce97*VT`;aj7^B>T@55$33w<66f z5r;(lU7Rih05&#?^7%OyoCl>pGAGR4TYKTE09QJyM-J$oes2Dt+%emto3s?jS!+PE zR1ptxg@NVB3EE{_NF4DjtUZ;&F~MB3Zc(DxN}p`yZWK{?!OGUntl>83@Eio(51CH zAe;!$DW=xy@#1HHgFCE29;ES<*;TJdPFMaL=kjLUUrlKHvh3)l2t*!4XI=J4Nb$S& zS~J9ay>NW(g&8s6RDv%ggDU@p!~#v1B)TMd4afE!ZkO;%$PHH_14wp1&kH5U)!<%XvKK&9v92IfoH_CG{)Av-)^SZlbrs*f2W%n?4z`R3bgV7 zG3)IA<6wF9F~10Z*vs22;8-N*;bE-n@ZdEvJ&GoH($70;>Ayu;CioT`fHty7;mbV* z0KYXI!fC8!Co6UE)}+@<1czGvF`hzqV0U};vR2^XQ)duc8iMU0-dly*l85Uy31%Ot z&lHzBF*!3?E1+a}-H&S>!}NW@mWMV~H_Py^!^w{u3CH0NOTN_^yMD2wSF747&a3eB zc0|Eo{ZEPblO(|WTU66aV&{;wwD*~tqHQ5yGx{5kmsFu$eZ+Qc%!>9L2uMAVOV0Gf9RY!Ynqu`dYa=A-PZ1&wPoSLFAvk9e@0zpjF6(?=$Mn&jV9BbAiBl*`mvo z2ZLb{--#aKt#owha9<|l!yQ>`- zHDGgFrXb2vj72&;vGT5noiFeKM2ULR(-%pFcoMN_7{8}k=|5tEB-xoDIE}uv2ZmB> zVy=vJiXb1hP5wNO_5O3oKlw5C_H%*mt5Bp5VGjnhoC<3b&H=%B7JeP@>jRcl!dyk+Q=lMTrq_H-ANjm0 z|8BT~Z7-F~Zo)OPPr6kK5pkUOqili9PbarP>S1zS`N*ET#e4Bg!g0@5kX5Yj11r+{=z*8tMe zDcvEBbSp@AcSv_P4BuS$^SyszAA9e$)|vM_On?;@z-CC=raq#~bi>)XhZI^aW@ytJ zYo&R zY4PODvh9^{IUVw^anvUaN```4`p9ko+PxK>W-jk# z?<3AY7^^!8iX?8Dd4hLm00SWV>9Xk?8K5j66dQyw2Jq)HiwBdOPI^3US^brV3Wk%h zZ#-GS^krBgFn&AXL7|FwqFK<%GOBNt>?uLDx87{_2%#G_ePiFSO0XzuOYvGN$x zF~j&>PI}n7D^KF}+J*%r(}nYQ!`PWN=+8cDt#EIjIh(gwjLI%4;{G5qqlD?!V{O#e zpf7;=xXEm$1E1|dN*~pSrk3-p8tO;Fml6BKU6L$hf0&De^|&nE-~7B}_kTk9w%D(Tud5&p3 z0%+C*6c?>L*YB6ik-$3TL|*i|6n^0i?6qxziQ+IJ(y&|zg$TlhauCHfehsuSca3=# zzKQws1jA>lX6)z&e}-nWwFMG!3GBjc9@q2HdS8}C3lA`WfcGSfD37`__=LYfaasng zDCRwgFjaiGe)JUlJrMxwQt|02p>?|& z!?Z}MEB=h^6sC*p@R=BGj4Zs?iM+ViL&)+!yrWr>r>+fe!4c&|zx+!=(A?{+I|O+| zHu9i-g$rUHt&ogsg@W5+MnlhEDVvVeI*UZ@jIl-3RDVIR(MBXcp+jzY1-_T#;J_}J zCXtDB{;XI2yxDA30L9e~(=N>jT@{1Cxk_2`hXkHtj9fcg{Bl6^&9i*!TR3LdALO;| zC4C;***lY%9__N8m8JR3U{Ix*r3%bJVLg4U2IJzR3?L@Qu#3 zz}PpkDDp+!V&8OhJo zk$~*x3qH|#xYF>`PcsSMCNbm&7;z(MguMkHK=L0dLS4LPx&wEn#0nYGoTR_ritRjo zbY23Wvl8o{<{H#}b(wx4WRdO#A$Vhjv#MsjU>;!fi$7#}{-sJHNYr5k4D(gHiE~2A z{~J?`xLTFoESsdG2yp5Kq{z7D1}t*DFHJHw6-C{YK~i;H<)MQ`frds@{JXR&PV%0fAPZ z*?rUN#ogx7+MS=%=PeS!54o=gM9puA(ufO3S+|X@C6Mp=C0hw+yNEAY1WajC^2@wV z_D+c(Q@c;=9ywXxH8yPz_%pR?@NSL=w~#%n^+&6K!#Nj2H&l&2Bk*Dj1Q<3Sn0p{M zI$VOG)n$0JU%=_Y;i_C-@DxWVC3!b%JOGR@n`R$$56`&iGx1}vxX7;mVhk&VL^C+` zKiP+^!1kzfeh;hOqdX+mIMUQ{Feh4mhyk>*(zKg{;#x$VOKnG zA=m~)D1!$nuy+|=C+WtBoCDAyt}S(3dk?x7IqO;EHm9-ahM6|xMtEH}nU@}@?F;}! zB@^ocp5Gy6rU#6`-iJmP*^V~u|4M)b8^{BiQ2^Qq0D8banJ^#{W4qsHYV_mTw=4N% zKcr6QI;|Kq1DleZ($uu_gM)qaw$99-C}X3u@cDI0ErjXp$rgUlQO z@%YTYNJo>>&4&DQvC0*WuI?iYeCg(G5{ovmszt>9Re~MK?m^U$E8}er&|6St;t5~1 zE(9#m-Xxj7U_x~v`midX(o7;q`AFO=wm#mi1Ifrx0J~Yab4#JnQV6iqo~C~JG^0iloLkmhuB@d($|WC379lwy0po9ytij}VXn!|wa(n3x+!!L*u$BWF)gn3tvtGZuph0;# z^I*0ZYsCuU1@utNKtMP(1lUL#9zPRLcE_%YY2;BS#{lovs)KB;kvX+V2U>i3V_1JE zi8@jMt<%aed*$7TMfS}=I-he=3|zmD{eHgBhy7mjmV{jP@xX2M2(qltd%3AZD7}Wa zbNCn^ACg)O(Er{w;G%_4XpDOGKxZxI>Ub4zJ`m|b7chQ$ zgu6m%*tc8Rv`68mX|qkSkpqn?z?Y#AFi{j6&|nK-tV~`Bd;jPABc5pcp`TTji&mSl z%!8WLTG#7+yjbW*6`f=)YH9s;rKq`U;}wKPWd)I8VfKV31+A3)%Gmd(Kcn^AItS9O z8uNaU5Zhe7JJt-671qLRZ@Oj|a~Pa_8|y2j^g8#*7HB@|VBYexHP2kahPvLO9G0H> zfyh49=vNp6$SKC=EZpnGI3r_@JOvlv(Zw$kk^JvnHGVZZj=cswS}3`#$V(8ICN$=@ z;zol8r2L?{$M;h(oev26AN}MgCm>6C-^G{pdlI*MSU~QaksOey%Y03I#f~#FW#GX* zy>KdEB#k2-y(7t~MqE>^`5r$FOEx~&ilC1b$|y1Z5K~T!^i+sMcNZcgg_#yoqMvtt zUD~xt#nMGRt%}%v)Xam!#5o}zlKC(QBr>IUAFieTtdzr(i%;bH8MTdNJ2EG1N#k3n z<4*JOX8(gizIxKpV84R_DT#Ot#`QRiu0rAz{b>@~fUV!QUC(Iiq>|p7z7I=ax@w!W)PSt-n*Sor1&LwzJ`39HIRQIzPVn7#d{a5O)RE5)7-NUD zkNfxaMRZY#?re0ee)0{~{aY*Wr#$v{vQEWViz?$=@sLV@wNleVkx^eGCA(!(kPL0QC z9@@odf8gQpnr_tid@zI|TTojKblS4^T!R4axj;6^a9bVOG6SHqZNUD1UQo>H%=-jN zrlH=lhKHFOdc}J{8m=Z_RgDgPSpVM$2O4$o2ZdE57-Zgih>WvJYxidTKP^D&!9b++Oh>uO?#A!_T8Yqp4 zR5>BPP?siSy}bj|LnVJ(WwsI3_DKFD3p5!xIkCc3CNmAr#GI!rz@GBO?H0=3K3pPg zJ7zCKyZ8lVC6pRszS0cH9%7J@6*YGaTK+CZ-@|xW%mAiL4e?)hdEymmEdX*FL2vrS zvHnX4?6$J+pvG(-=&x%Ms{bDu>72y?h#CI`uEA8TXES>cEAnULnoK>n02P1{Y;b-@ zYq#|$bj@Ks$!75}YtBm`lg`LeWbQe|OWRSZHV)8OQ`s7D*hlC{rR9V(jgHAP=?Exy z6z-6>gb?W2bU4$ZN!eM@@7FetR*xSyS_cK}JB;I{{IZ1l&hy)rJk`KVbjMg{UELR-pPH ze9oG;c@-PG@wZ>Dh9@1ftCGOowm>c{OBpCkx`89|oV+?GJb_9R1g1h|&m($XpD)%#+3K{5HYWo2wk2FrFup z@$K`XYBB1=`%}bCO<-f^DW=m_Q%6Y&Xkgst_0H|T*jNg@yD#AGS~D)nvA9OaF=uUf&{dF(I2xol&uCC?1LcYN9cVxN-VUqG+OAvZi2Po?D7Z&^}_*aKIH=_o}|x zeLj5wEn@ee#2BOlGyi(kFkfZCS^1X~f&_W3!o_s@~Q^@ zxv-CIn@NC@RMDd{?BJ>4ST7U!;YnYGRPQ5<#!S*%BRTJTTXkt`6nA+N*2GDQqQ_-ISf^wrk4Gmo~{Wtp!TPBZ*5i+sHjxvf71GYP=E+O$V4E+`=5T* z*}+o7dkNbU#pNNlj$`fHz}FaR=>jR;DBtT0X1Byg%ho1nDH5&CqmmeTtHEuG!06KH z&mp=OYksSaYGIQXqzVy@PFIZ=Vteh>jCIMrAJgs%ud+}H?N`@B3GMY6Py#fUYj)KV zpTq4%e}HV**!6II!<~s!7O-*?*~HQhczn-}aJaonyx~X1p*H_ds~*zMbdw$U3=mkj z|GG*^v~x8eH+R8OGy;B*<&&(JtcylCu1fD(-U&FI#P$61kLd!-2y63zPl7FFhU6cB3nX;uziW8$xVRCyhs)GNmgNgE9D6Gyu>LK& z3Cn%zu6nO#QfKPzpaYbfVoJa>LT1f+BSyY&c4k9asDziN>RafA^VGj5IVo{8Zt>av zta}KPX>+8+AMwA46k}|T_vQcHATj@QRsJ(j1o)o#NaJ?Xf*{Ye-Rh_jz;9!PO`Jk@di_=Em^c_zM7RldK0x92YNzYp_zg# zZPeLp4XEv1(ukuUOzEmvAa>^HsZ7EvEWJ@$DcZ!t16g(LD;Whaml|Q8T81(+G0Pt{ zfAEUjw9a{4vJ4$!EM(VVq=4!)_qG3lk{rSJU=B_6Y5~RFl6S$a4TdHN=f4pdut4YQ zQ4D}(U92ff(>c(wIRsq&bla0;3JDkOZVsk6g*yQELw{1a$^g0ItOw>juMK>qb5E2d za>e2R-HY+ZLZa4F$apeixI4KaGkYe91Gv6}FOcr0^BX8N|N78V`{!*Vn7N4`dIii_ z1d}#{>8q$B$lNutQ>J~@4>QOmJxrT>#n@+T#)}?R*bs{!QR=1%{xq^(i_Qx~$j*Nw zZ{A0B5;(vDWA8o!?EPi}+D`uuQ~5vyJYS3LR!8Wj132Ze|AvBHzDlYRj}h&w|3-{R zFKL`X0k=B@ik1gFiWRn6qdOc2|HF&k&#g0gy%uNb>J@UavtE9g=z4?DkXRfgK+Pw% z!y9-Nxa7X(qi8k^J8189R&-4rqO(Z4(#yE|>T9cNeWehik1#$w67YawC17(CV4tnu znLMtb5O6{{l^;-!S$PMgXmWF?Km2mTxfhq;h43s7XosIjLB6<5GF~rqL?yiX<=A)k ztO6QwRB20MUh9esATQYSi>dEtV{xohiB$ftR)=ZE9-2K_zCQ2*b2)0#->dqpPG3gD zo-;N(v=%ru$@-^$HWSN(t`S_bkCci*g|NgHybWIIr|2wOuS}}(l3TMy#3R9lx^8n! z^MBh{vE@L#g6U;?Z1b))BeFm<0{3gHWT0bGEgB8!1S6-WMTv0impJjc0m>pZGBUM2 z20xP-dBjJ_!fjP=vn0QLi`u>Hro4BIVkr}IzygP7UxHg2;UqO<`EjumdM5sX4DR$l zBp9F865R0)_uuInNfPxpFjHeX^OJm*DK#7>p!K`ecr1qlrHmAU|Oo$CU3{y`FW$T zQ$kbBH_oR&i-L2AZ3~;(ZKUv^3X;hH%VneitwHu#h+qWN+dZs!k{_^4QXdfH8C;iwb^q=Z^ z-us13u03Mt$RVCO?Fd>4^LaT^k3*}cW=E508B0^+;e-<#zfWxlq^K<7ZuZQbnQWs0 z@Fba3lmC)rj(}F{tFUD^v;U2I?Vl`w9&YITxf=R88xWbryRz&3r^=hQ{Tnj*Usd{B zx{$OA2_haKVxt~%)P8lK#c+YVwo}VO-4WDVgk)GxlQR!0GTJuEu$6{TYvFbKK|2f5y-KS?eOlVb^p|lL2ba}af zjXN9AV#RpK;B^cHwt|mr(B9ve4LBC%5=$u|c}FJUyWB^!=ZS`%Z=ry<&MQJ5lf(^C zpL0xwiQNNVu2TR?z%5DQGvrGR^zE>-Q0N}tZ{_o_zkf7yrV|t@oTk(>eEuB$ZXS1;BxXI=oWLGNSu; z&OI7eUg)D>L}4%wHWObqL7%7U6HN@-?%p1#X#yyKIJN*V^zYh4IBP*WCIi5*OX)kI zw2P)5@UN+l1~IgoF>;3s={Ez=w=I*w$>E1Gu+bDrO{fuM+}MCdPM-sUhQOR?u%sB7 z)ffW%v3KIW;Mb6E2qO3?5&(ioqrZ{3e@0!Hf@}l6qta9*l9F_?d*ttM2nLz1zHxsW zh!tb&&%8*BPM)d(AaLsMB9=NS>xe6~!LV#ngM(;uXYQ=-G#hJOdNNk6bA8~2)+|>p zjspE9fREehd)*z};MO(nb_sfsW6OjNwmN3a%pAdN#{OsU4|2um?f>7dy2yDEOo^ZVwk^3veX_M#+EFmL0Ya5q+vQ(wS;)9d#ZT82y6aO7g=9Z-4V zpU5|SXq?z}J79ctE7j=*ZG5blx$X#^*Z*3+DqTJ7V{3n>?xqH?>%lB>2&M%=+i{(7 zzdI6#;+A6cIkY(v!&2Mf7LS7p zx>``%bQ$M$SgY*Bw{;0ro`JI&98xFgxcG|kK+20G8sM%_9mh%jkw~Q=GLMr4#+mun z7j#jEVnd^gAGo4tOVUH@?7_g}_W0)$EFkK;m)6HDax`|a#@ z$}AZq#U*O2fBDv`N{ONU#d1?~{|<{BkJV!OZOjJGRg^vVe94eHkLW0$6o@ug{y~zj zy!XrinUUzE4w96kPcv)=G{N@ay^g=S3gOuln;tbDZchIV>ENxXdK^g{K`ck3&)r7>^AhR2*onI+@zIbzxA)JkVL2^EOsZ9HpV z1ML$M*vGq~7$|?AjtQ)I4)<(^RQs~VIx+61iJWCCTX1r$tHjza%i!L(4IJ^6>5dNGXlW;Bm?sPFWjEFqUXKImO+rEmXDgO14;Q9`lK$RQ=f!Y$ zd0m0nyMvW2&M;7EF(+;a33(?6Vi40P>tDx3C0n2nkxDSB6ha@@awiW3s^{)r0HT)a z+Q$0CixuR^Y$-tE+y9;LdJF*A>e-)+PGnT*92tNgM>X~96-nU?=k2u1xTqP2%bC$p zH7-Kb!24x3TPC!_!V`9=i0>qbEzR=zL%0j9EJWIJpa=9IvZQlEh;!df}neK-s0SY)ERwslscU+=!w->~S4;J|l)Q23PU0cJQ z71Fp+Et0W|uRY*eb5N zlS<4|un_~W$7ExXLG=(swIH5Ebl&~-F;oU4IT9(q!&O4wXWtyb@-H8yeU0GvvN2rI z`D|k~H9_DUoge*)_;gsv5RN&;hyvJO{)$yP>o#T}4DBzs+j+J*+e1*0lSK3( z;p+1xW!{|f(zw6wGH$f*)XChxeXPf7-vDuLfg8TZR2b2%B;kYA?66= zpZY&coHFFvL`0;)-BCsl0wd3J^gtm4@GPs!wDc^QO7&+l^%mM7Kpo%SsazE+< zFYwR|{0vln>zzAEPcqd_*jS)J5NgNH5ZZdBbQ1UtCCf=y-X9c;@s&slRt}5h8CX4_ zm{^*9aOD{oXAH@t*)lze@caxyoVpe#L(G5B9EljICc9t&ikxWQw=h-~r3-WQUu1le ziWtR}iqmC?w4mNCSg>Y5?N|%q>Ma1SztRd`{G9)aXEMpC z4*)74r}ykKf}pBNP~{E%WmOhl+*zR<1>TKaI1!g7ND7xX-9`z&c$WcQuK>&$?-I%Q zefkwiO}rC{b4f3Zz5v(YhpKdy#Hr$vNSDj}f{*esPP&N}Eh@8oJ)jkFflTTyj5G%* zl4s1`YY>zPir**!p58}g>0ma0F?+ftagdOa*VL1vSJPRL2WWvX3f4oJ239UG|6h* z)|dTcB;I06y(^i-!X#BlT|1C_7n13-z1SFDeq1Do{gAvJwZ%Hlp3IsN71NNV4Cpfx zbRFmHfwSV*-T+rWiKkKI#s8FM18cN(Zg5Ad;2|VR7l9X9?_Lb2>Zm&ST8yi5@vmgv zLqi;6w1O=F0}L@Ro0o@z|48H4CXRV5-T2tZ1g1)2-w|(3yGf9K?~Z8o%}ZO~S;P?N zqxYPg!}a{r_x4!Xk}IC}mB_Oa+DFl?-o5pj9ju0mcQtV%ILrF5Hi#lL@p+3(zp#QrLJV?T+FMw2XR^ozqwD zn2^eZ%8h0x5uyiu(gVvA5zW=i!b#)dET?~P5g1sf(2RV-f-?g+JC!tR*=KFre{Qs$he*W-o2_&Ke ztcb)rsIxQuS!)y@s5_}F?r8e2S=0fj1wQ{<{aD1q-RgDa)h$`n~^-H@w0Z%48lP9iTIPX?GE{o$h>Y&k}kLF zNb;(V8Iw^ZJEXr9+UH^q6n;q31j1uoI^cQ!@vMVYLZvAhl&4t7oTqi6q_Dd-v3Hh>^-B6VP&HglfKGwVxgWS3Ia!&gUFM6CIyYve72G_U1$WkD^b* z1(1_-WvRTNX+%jCWf&%aX+#8T*(f2hXT2qneweW~U?0J%Q=ep7$H zQlG`RfCq+j zzpHsCr1Rg*|9u(860HbO4*M1VCqJg5kFk52=oAmEVe-DHW&ly?gRsC_LEu|qaZ3F) zr6)N2b+R}l_*{@o(US70q;FHF!w@R_QT|n_&6;6M^sx=bc5XQ;HrzjS9bKrUf}IpR z<`^l!CsdSO8s;mz-JW>a^f+R}WWn(;Fx7J@J?g;ts`+Fso^poyIFg)gI4V~p^pT#7 zU5HP_=Imzz+~CJ@Q{nfHfcm=$xD8&PKFyAeEC;|ap`QO!_}U?kbpC32Hd@>BS? zU#Lj?gTZz9hwo0$PlY-@FW~x|Er`&Kwd}3kIqZ56Z@1lL z-qU?PTfjAb_OHE*;VUewJRknR0I8@@{KzbWgmsg=NHJEavJo|I_lN%7o0<;d&yAGa z-2mj1TtSV>Hq?TD78Ly4b}m!tiNo)CRz*Mc!6tz{>s}dxrJ~%Kc3o#fQWAgp=RMa8 zpVUsJC%A#{$}nC;zx2vE=~lL_xi5Lk$08I`s)SHlMD$D)0g}~l0gRcXp;gnwK&sQPeB&tTh%y1Ax7^Drsh69 z72gsElxVha9a0g*laO_FIV+7`X`*cPP5U?yhClM^+NQVi9b}M2d zcE_+}_`MC6W#*C`PW;F9DI$trzke9t#s8mtebBbFuMzSjWB63H(UyrsFa!%0X#ghp zX5*sIG>%~qCgAz1udvm7j|2L?9%B+mQg5vMdeZFvWdJ<$s9g+h84(}A&;zu)I#quU z$kb=kpgod9Di};*4$K`eP9P|v1UvhP;?#UU_DO#}{wT(F7WEw=RV_vhX=9;trzqiR ze#`!!eafh$Zr9ErzC@0)iTgUD-~*vC=DU~PajZPRWtQl(Um+jdV42Tg{kaaCz0B;Az5&?eN*cp~EHhBL*rI{QyZDAi;K zNSb~yc5L0p5sTv~eTT;1=F%{N;gJPMr!063!Oau*)8ZebX$leP3-j6a%p$|BTX|4Mldv9{eJN3Xpoa{azUZAcW}X(4j#_};%YTb`5w_+}KE*5MUJ)qwqM z2x2vmPM^-X&iNr%{cPUyhhH&~dY(-IIt8;;DmYs61Z#T)BX_6fBSVtG13~Hy>PJ?1 zh++(o*mr=7#^Pc1Vg29sjfNGkg9jv?=hyJ=vReLytWgs!^MmoM310W%Xm7+`E{hYU zA3s1JP)u=^B~x%YU{fj`G}QS9WcTzpVe^W<>*x>6~SGEiAN~4r-fhYno2Xt zO03Ip-__<|d`+h!;y<@KAn9P%>;6?wIn=&05hHxjJ;3x>^#y};3@hC;MwM%6o{EMl zTV*@O#m-S-suzox?zEI$(~Ivc`p=xT#Qh4()7u@? zokfGskJ~%L-+pL#Byx|Ob`VqD;!7YfWxw(xphB8Vdd}n&5fFe*GNDRLfG>r2c*o>4 zbnVfEWuzc|byk;tTiX1WddbP4*?{NF!ys{{4&>4_vH`~imaeAl=?RVM$Rf6MyQ->~ z3(GaKIzJ^?j5p(C_ZVe-`veM7Ucf`d_&`jlsDKC-_KF&~@$e_DV@ZRqJA&JIao6z0Q+4;SLh+l9hv zew7R$f(X8zNV_FJ#{~S+OPlAohfy82w@NJQRDHtI+5G75@PKjZY+% zl|2tlFK?8oZ}w@ap)0Cb0o16N7~t-Bk~dHRpc5EeTXW<#ee?3QW@|Bqu3Y199i}x{ zR?QFuC4x}>rtr{Vu{g0-nvO$y!ygwtuJ|F)p94r$xqL{CxT;55F1E-4Gd%K9g3akP z??uB1sJQAXW>k*ReH>;l=k26$(B*mVW8ARli#{ooNH#|Rsis;eEdng1EnoumL^lwfp2P&FG>ln(fsMd@>U3n6i!yrS zmnwCzW#+=S2j5-%mln*{!&^3ie=F!T{M}Fo@lolJg^RPdI;^T&yy3gS1p!8VsHrcI z^3Cqh;Jc5p0ir=((Etrp1RFlO6r;@2x@UluYeS(e80=-qBq5ubyP>_o@=Y!w!l=dS zh_t^9nW4_pnGTzY#3evTkS!wRZT!c~%cvD38P%VAUor!P;~kc3yx)$)qfKB}o}_EY zqvk$ZRxu2M@*L=+YYX>#_b|~;Uyh`A2I00?=zoGZLeu3!O1o;FhB9MYKSdDlAvzu4 z*@<5*I-L>+@RQsvAc_=`R(F(!jU!W1{KrEX0r zi(RrCcj1H72@fw<{ofaH=zsp{N1*C;OkEdIV<_Vhx}g5TLnCh zW>kZEaRzwNEWx7u;}^X`O!ny#RyBaZYACs*KY{P!dJ4x zYJUz9D&$GpyJQy;!nJPzbJWfGL!2c9+?y|fc}p-6DqUM<144;9KG`x2g2H!3TR`H7 z;T!^&WzozQE)9*6nm2_VrLhA+^uq=(wT3LN6qF$t`yxW`>54a}j6CJI4w?2P78L~> zn!b|sMUBy}CgytiATSDH`1K9qSe7|iH}&O8m41yaQy{T;8pMxUE;vx7QXnjz4^dl; zSR2e#a4n_btVn!CA9}JdE#c;e|9OK3IJb4TjY>Ikn90+20-v9R$8kIl=+N@R(J=7~f|3+UeK#ti{)md$QH5ZH zS4ZHxi>xyyqE8NZd4vJ>HT4^mTAxT0p^qHokOE6v$^|3p>)f}`1>h3;D|5ck0cv! zCWggma)zJ1aS6nK=!z>Q&^LEJ7=_$3R8z#7|-^&&ddDw&sUl0 z^elkmyAo&K*uHyeomi?_f)_Y<03Cy{9{gY=BuoslW-T(OCcyFOY4_+JHG=W&O*QmK zY#6r0F-?^3hwKUZ7*#bA-N#>T63ed%jfYf-c`zzRAo91Bve&W$67km&h31n5VY~4! z+x48<=0;Z*fOpx8%ffjT&8YaB=&2W{)1G3gOG!muK~d)NUjN2aMcKLZJBCznFvTwB z40ADonN-DbXK9Xx`pl|1m`-!VdN|iQh~uc?iN|NwQ!Rand2x^$5^@T3$+@H!L?XAA zac@kw8!vk&PErGz%V&rSU7$TWn8~H&`nEf{%nL1tsD?S}U{(4nnL^qW{kQRGqDflm z5BvFlJ1j1}S*}FS^?^zJZ#cd!vZ6liufXK80cqmTxZ>2^_q~I;jhi;zwm0dNONJ2m z)2o5jRat1jGdf4-PIc!FrBfN`2h&h4J>ey75e@beI8V z(k`+0*0nyKTFq5V>^=~kAagH5gB$Z780qQdP;RDVbDstmvT(f15_~JYSRw&+A;OYu zQpQpPHd>l5(|7lBh=DT1A+@&Ksyiv5e}uTtwpN#I2{j59rR3g-r9>vx6V7Q)5vA%o zELV)+p8DSc(}oE0%c#$v*Fj_))j|X(mQY6q@G-_PvjUCK__Z_377Q^yevUbQqee0% z%~a;I__03BleK%W+6%aHT_C~#4a9c&fnvx=^S^n-|0y6~3t0uw%t%}i@#MpwM8Q`h zj7jl{+~8A}_NVbf*=;9V@0wvXROp`n3#+NXau2((M0l^C4@?WVi>r#yHFQo(+UkA^ z{VNSxX#?TEi0#4mu%SV-ns>+x8RI7H1kL#70D54OrUv{Ego#+%0{sPcK!RZRe8~QC zInHXq%&gh%=!?}e;O0hm2F+&3pHc;Gngc2y@_u0(SpGgg3qtYM=(Hu^`+puaT#N?$Uv zF=Su(=`dZ9n;+e#dfr^yC49pP4_RaTC#CR>bZ_5^C$Qt`G&tk+@6pD+Yu9HKM8TEH zo{6P||0V$=p~ph3253Vh;Bs&CS|J^LZt~esyNTAZzdz;CV4GCWUBhtT^5KjvfIXBH z?q6kyQ2agaq$J0$*i|qdI&bhmf0V#zzFLn}X;_|RaCX?2bBN-md&}9PNp#6c=cB?| zNA0FdoEmV!B!&IQby&P(UytB=#9rBUxy|ylC;6ggTh{>Q5?Xe7mPnB7h6FMpcMyNo zfyXA@0pz!;o0pjk66V9cQHM`#@_xWy?do$Wrvuh7BEuYwa{lb)2m#+!S3hXJw&&~A zw#_61h7<7>QI`h#4BYnYLEKw1yyEj)MkA_0mCbu^1kubPm!e#1+rNCokSAN0#j+v9 zLWl%$pN1sYgit;wkdj8+{G*q2B!$q5gg~P4Ne06s+WaV71!+v&<;*Gc9TP zm~Ex>o&;ryq=7NNiwr7L4aypr=+Iv}z4mK0HB;MF&{0jQrM3O${m*FS5*me4U7kIH z6DIgw+wqUwsTbWY8)Xz0X<@&{=3xBczM9jIwaE+J2GzT~HZmtO{IRYVN-n>9dSp13 zOnjvdo8X5ZNMTi%J7nA7?xT%*B9MBI!VuPce;A9-^kaL|-qO|mk7w=5>B`Y8tO{A(&_8rXsL(9?=7-JL zz`f3mPh7D=0IBGUu0oLm?Jn(6;~GD%p`}k>ygXiMRw-_GU7d8+v+gC1X1LS3QQY@v zdnAfJwSNAA&s-h`0-xyCPTm_Ly=_$DM2>c6hE$ZlC&0-iTuSoe?oLZ$I;8cbcza08 z6zMX;wL%>=ym?fS^`T$D&LqSf`DgGgFkpO3y=xW_*hLcXlT)^Mp!}ivxoFt`LCm8; z!$FMstMX1|^KIhZ$EU)ZI(;s?Mf=q}f1xyv{^VuL+uYJH5J4Ut5qFB6?vVN$#G50f zV4R<;Qt^9KIa}zXMSMu;p;9JYhRof6qnwU~axk0PFW60CnA!;c{s=z%P}-Q#0IGT< zCtE+_0GL#vPi#QXx`Mu&fdJz9x+O^?Jr5DcyYh_oEsGZ_(H(YqW>Sgny7z{K)%FX&w8gsfeAz@#=ohHpAXmsj}fB8wfoZOIA+BB=MWm5t95KOXGJDCKYg-E zJdMcjs4(#7HZ5#_N-!>4&fuXPi=DHdN$UTvZ3}$<*w7dCNFG6aHiZ|V7jDSK$bH#M|14lkr zHHht5-+Fw7 z3f4U~dzZIeUbQ#DeB5zvHF!ioU%w!@D-`%mUdJ-YE8{SAeYoy32dP}Q;VW(eeI z>9RD>o$JS+wUB^s`+@S*dsE(GtnVw`y@8>QBs-G{*R1^YaVY8z4`e+e|()GS9eDYIrB2-nxSpd@O}PMfieRyb7;j7vF(?TsJkm= zw(BDaMb3Azdyn-=bv#n7&#f$pD*ogclbVY0gR6jtMKcHxUxs!RU#_b*HDZ6eOx$bv zFYFpxIU|Qa&{Dj7I)~ECPV;fd82&?5F1Ao+#N#oY7G-4OX&6ZS(`JV2H*ry7UpHPk zZQ$$*&T5W)uGG1_lnv)2C1ue}*7oMDQE));eHoIegVyNNJGEce{W$@}c3*-%j>?OuzGmSCUQ}v))rxLR~&^-Z?KOHq& z4ZU%AKt=6qC!;$*O^|pSIM!dZUBW=d`-Vzib&%YS$^Kcvcw1Cu%W+{&>T>vjUH*-d zKf?|iyn3F(x_wYolDyFTo8rG*6_kX;(X7(kYtcgv0SYMg7mx7Q7&#|5CLF-)cW*$c ziTKx`O*{g{)+bGjpGl@qR>HUg3SC-#_H~r!IWe;5BbpiKy=LSaQ$(#eP#TWFXXnwa zMMB}zi{jiLXm){ZeG#=(*s9&->Nc$UVu)>i-vW_V?5A>(b$FyrHuy+LgT}e&mImEn z6#!eLfq#o9N_3L5Q9NI)qx;WD)Q5A`kRzswj9N}LYJUWMWmYXEocI4iEWFUCk419- z{ZTqs|GQ+75kHfRu-)L6UXl`L6c>km8^*0Rjv1QOm`nIMJVcAut5oqLi%XMyND`P^ zZ1_YVKE?1GXfcZoGU9fSI4ehNeCOlzlw;fivTYJa8hbtL3Y*;hvM@8VqJGdS`Ydj> z5%=E<5LUSVB2W+l82OCH5{cO0K4skkhy^ii09{s=cG4M8q3dZTahW;|+bJ|$?GRMc z-~XJwa;AO_$!0a`CmIJ`GGqX-ei~`y6m~5zDOuK0ExzNFKuoy^1RLcC;dL_-rf>q+Hs`FP^Ji5Ndo#ywx-^j})# z+fpyJP*>l}sraWNZ2VLL-JkdWxiY%92|$Npo?o#YzLkDssE5p;F`*gi=_8c->S~Mc zIaa39>};Qo_Yyssn_ok3o~cO$==@oJaWPW*$9yJn_^&E{uAawPD#to=y>osOIE@*? zr~1Z`LWbzk>#*xyJUOC33tE?Aboq0=eLNHT$?GIfnPGwM&7Z3UOH|M=HbkI0aO+K) zxmm2RmOLsq7Tad_l*6rhyuh2jpzm4fy|Leac$2{ckWMKRZz47+{VUn!$*fre!2{Z~ zZ(aNRmMLEU+Lx_Kv~I}MNl_xNtt)RNxqRjsEv}q+AGhC?>`JF>DwR?rWUyo)DUcTf zAJQFn-DSk>UxfIKq_ns@6ff@H;_e!p7AWo%Z-Ju4-JRm@!5xA-dGq}5y6dj@ zQ$8n?nK`r1KEGY;Urh7Qs$nkMl2=jt9Cw}RK6ptQR9sl80na!csxL4OSR!#-CuRuZ zIlW8g8PMW?A+Ac2k|IY$X)HRrM-$z@87dqN4*@j7y7=MPAYWHQUb*JKM#q zhxwu9JFkS)>xUY6@rQ#4<0nIiKe%=1?Yt@!^rV5mO~#y|HRnpsv5s<077eRbyUaf^ z2-mSwW7<{aY<+FV6|^5qu6kXVNZye`58M$`_zqFZUU@5UGtzM0L@r?()X7Sg_!zq_ z)y~~7@)uXVPxx&o!=xsGqReYw*ZI!8s!6Ixv`V3vB>XLgN5gZ7{0UbkP3fm&UmA>1 zk*N=Sr&5l?l!f&N>;BUI)d5#1)k~w7#R4R1C^kpKlyRZN?78g@UrLAdEPllJX`Kh; zy2l!R?tI+X&GAN}geUBQ8f}dS2<`=_UZ_4gjsEZ8R=kelwqpWnbf$y9ETX7?tQ1a! zNDO=nz|69lV!w8Aw7|`NFJ`6(-%sY68O1qjMrtmtaZoEYyi zcBt9>!3N0pF*G}rJosE5(vS5$!)LOyl1?9BbVq%#L~Y(T-FRps+!Se^)y$96^s}Bs z?47M-4q<3|StvP61>}Z~$C&naaoYz@`g7 zE-0Xd7GA4ZDJ)F7E!$Kgtk)cOS+5;{0GRF1Vj3Z(`KreO z&`UKUgFlhY2a+rYy7NTn^A^FP6OWwigivHra$5qY6h3ioL7bjGDt{&)BWxd9dMEyB zl3g$+*|S~N{Mu6LonS!9AD4Ak6Agz6LFFh4=#au39Z*0LGdP?Dnh3>3RX*$k@M?AV zaT`RKhJN&Sf*3W|VXB5daGfTcH4y=>7gITL*rI+kyxQDQX*p128Yj$Mo%26SD^PYO z)hl{pU@0lTgN4c@21HZ(9pV}4cM*e!W26e4HvEa`Jc|XbP$l2@&O3@ zidd0z0Y3jJLnKmyKB(ExdVFD3pfMGGfE*uW)h#L?9I{>C#7OQi)y*G|@^nRBg?qo; zm)-DK_s2&sVec&DT~uvpvf=i<-ny2iO2<=Ftf|7w^I%oZSGIKDqZO4Va=Fy!KXvrt z$6Q;Da=x|4To<;__Oq_#JF+0?N)I)r@p-=Xqpg8Fsk;Vx|KV>orghtX)P?NqcavA3 zj%=>e$yKt>2Q+92^E@KMQyrn9m=e!3VQdS>U3?-ug{%F2l7ZBQDZ8X=JL@`cq}aB4 zd^ZA~^nEk)*|6Z$0hHy7;7E`Hp~k;3`0~$}!S7$KbwuT;QrQ1`!ROR*uN!zv=`K;2 zt*8dc0#$uGD+wS!-CRp*9wLb0>#BE-lcxJ5$!tjse-iQ>7k9GSOM;;|4nrO*6syxr zR&JAPzb>X%&2oNawA#)2#Wmz$*>Q2%RQ9=Y;_F*N$N|45bN^&Dvvh##3I<*DvNZMi zl=zkO{j5|Z%YX&Qm*g~vA^;k(aiw4^vW=wEw`*2x&iJ3il3aWaeLF-82t-*=M+3Og zmN%?}lu_3oJuu5heOpjkdc_Aj4qxhzY*VW~s)`)cA3jx_)d$>v(EAys%D-SF3d35C zL-fEbkLSOf($4A{zQF8bf25>#fS)gKmj=Vew_{~4nz+zTsz>*$&(Gg1vkl-rzH+l2 z)mT#b1o}c3zYJdbWL23|$ADjBuO?V<9jea0@*^U9>PEqE%EMo&;MMV|T@PEV*E=D& zG@=#oE*2>4ri9)S+Bd=__0#QL&l<0v83MBPxXRzzGuIFMmuTzMZ+J$M2tXM+@3`WS z0Q92xd5Im1#UKyZh?B(bn`P@XjG#QIxcOm#o#Bam(t!>8o6|p3vGYfHAUL^eQSUHw^j&3O z{Nx(>I`SZPe((a-8uAP%6YTY|zd%#v#$$4b<%J5{gDcXaYJuSc|8;!#^ z+kXcs6d{AeLS$d)_QaSQRYH>uK2(=KBu~)*rrot1O4~h&#@cq)yB`uyNhN^a+Xi4= zbrYRp00Yc&j!7nbK*yu;8g}JViE$(qARVxe`cdIyKyGZ=@4eEJ9WgLBtFh9!w)zMS ze8aYr4wLw3=;CV7?4+#yiuE0@Bi5(20}MT!HSAn?z%P~s zs30G*hwvdT5LF7_ztzjJN*mt>iq>Y=nI9B#bMOJ1o^U%R*>@nCq)i5lY@Z@z2QVP+ zbgvk~6G8^h!HAcUsDWpi^?sR>t0SNjU#~AE-jC-O@N8sf2%ZRU7J8_mM zmj`4ao+js%^`HfzJcyw=+UgXHM-!Gu+9FEkp0M>%i9A;)2Fv^Ap0gWw(tJNT*>1nPnq5Ro~eBPiR;RIqbqxCvatJW2N#1C3=oGycDQ?X+veR z7=bvvOsIzi|HAkZ{^y}S=f2~Q$h!~Sr8d5r7nOfRKa99;eAG_ zgzz~-Zy2M(cZ3fal2l@pJ#m=Jb&GWU<~lwH(?epIjmzjeiCh|{`?7_3@-8;6FuH{j zcx8&8uI-bL1o=l;i;0!^*wzG^znuIzVO-h1=<*(Zu3p(|#>wfMse^L_gw`&M(LEH@ zX#uLiB!ON?*WJX_&AXl8UkDCFj_HM5Yk1Otculh+(`FO&vo$n}UMZ#d|cq|AOiQxi`>r}2B z+F&>G#A*h_Bt4fM!Ez*BFao@zB?ab(wFn|9%FqcTX;`zv`{e(ESdY->`J5_4(){wr zmXpPp_sbY$s@On!^pRXUxfc^z;LS_qvY|a67Qil5uK%3jm4egns}6dGpuR~35JS*l zpV`HSDrSE(b83;ZskgwbJd7QFVD^U+hTTxFwVT?ZlZBxxfrJ&Zn^UPxDmGo}A268c z&n7B2;gnv?@%Q<79~2`NU-XOwTAz8;)EP6fnEw;~E|1pG2HIf?h(9T=i!1lc>I~vU z_^9dTPpFCxP^9|>48)Y09#{M5M>Vggmjmt|{E`@t_t~(qC)Itp{@hWw@|dGs<*3rm z_y$h1(_k^7SX^{=*PXvL{eh>pE#+yck;R@fSLU%|Hnv4&#G}xl?u}My!5+UtKshea zBX7`QHP;QIP#v40N%xlYlS39v60TRurP4{{MfhmP?GJxwfd=iEPFy3)E3It29+u{n864}tiM8r=GF_@#P5*Yn~oj@ zplkv*lO70YdG&~49iMVZr$IOO&7_=fRbzYZG(qK=%`!0*<&ahBXt)s-0l@-b`KOua zB;nffjdD!9C$BZE|jAoT%uwPaNi$e zqkUw&^v`{r3_O;>De#+55j=%;fS;0#RBLM=eK^)(m2O4ZgOQ${j`H za%{75$e5ja*SZ3E7YaZ1w!;`j1KO$YtwncQXbV8?mA?TMvM<>_Y-CpYpVv}WxSt1o zY)|x`=jG@GwDi_}2Xbx7Ae$9FZkkh@F+kbQu%e2kH2mOAAW^6u7j!S) z2{tX7JliHXoam9&8VtAy{Om+Ng07m49_cl#Y9yN6@hRsYl;%y|bjTPCZXlwDL@;wl+OP0% zxZ7;DN`^R=|MvGDHq5A(8#Y&_()<{%9AeD*^_d-Pu)J_~q zh+0aWGiJ^C;(OC573n@9!tZ};ZeuL!U% zp+y=-&8w+fjJRXaU&DDx6ta_dFMjNGOW2iaQH4{j%EZ?oPPtmNKRp+w^Xc1mi(ewJ zoLRZJXbE0DWCUH#GoT~~B)jjX4p|8WwfS;%#=xIsH)f*Rod;1>+m#|zaK!&xE3{8W z(gaLENU|-UYEQ5nfnBxfe)njw=8T9t)^kUwroLjWzXUPGrY=g80vDV01ekn(Nob(& za_8sX-b>qS-L!CPrDTm9D&5QD7GwdX}AYOm|IC$4CYsv}wW z=gSa=iO%a`x`Nv4e?g}BAXsudK#mG<1*p9x7i%6E_f>!|pZehxF#%Krc)~M(pE_b3 zNTSxT+I+pfYP#?=Cdhu0{dqliET{)L*7>%eddnoQ&$8AInmKGj`o!7u$PC~E@?a67 znbBdHaw1=c3Lj!#hwr%O(spVj`sJ>HQ1A*&+UZ}>99HdMINan6FYgC7D$xdSUS;iQ z(sX^1Wr6Ht#@4AR_i-r*HY}xLgl8@jp2v_mDdEpf26EHnYnTA`HK3V zX_im*x}w4*6A;;=T5mn?@q6S7e5AXpdmfF!iqrf8eq054VQLM0znxpvSH7`a+_Mrc zE$*38Q(0v*F`5okdFKAfcKJkiGocQzzL@W^1a83BoNa z@DJGEPKTM1iVVQvz6IyHOM&^S4g(q8f>SVQek2#tF0rqv-M@;i?KHEJ;kVZAj1Jf= z4!NaDz13O|PSIm7dLk6s9|3JwRDN%Hf0;eVQ@F1ABUExCGXqlLlV1*bzh3_29hD1R zPo*X6OY=$IKS_B=XdX#SZ*m<`g~Z)t$CAv3I7y%V#T9)iT>lg07rU1K^v8&m{eoK` z1NqZL%!wvmhsuLWvB3fhvyt94nBa__2qT}(qw7{Q{%croIHhUzXm z^wJRTbHV$6l|q1{2GPkxKtWv{!iph%qanHIZ;Ox`Z_2xSN^)rc%GoRxK%uN`gYohi zRpRFg62@a$E$p8r{#uV$WG<~mUv!hI^&&&_Go^R(q&_Qs{oGL3Zfm7*h{0=}f>YN% zvt1;m9_7~EK7S=Mq6(_eZ1^3hG?4Oi;Vu6%bd!3ZQW+E3oX$VDbTY}2+)*G}XT5&j z$Wgs6Adr2F+&M-pbTP-=sU6T`YjS(29K0>iDXW?iW}<`DWvY>ia2%#TQ+J>{1+XD7Nx2IcOPPZX^ajX%JMJamF=MXAXh7_8c&r`ZzaVW3;ED>~xk1JETL>g!p8w<(qN7;+_bq#nBji zt`98FG5*Y7vMgH*SNQ&uO2Ht>b+r&Vb$xEYEP-@0W|JGTh6JT(U)}@ciV~wk8@Ao3 zm2XZr=9@ak#+x42+xa85%=a#+Av`zHIp@pS0No6j1^Rgi-PMDcH3M*AHn5P%7|giP z`-W#VS6y|O=M;k`N-P)oJ(&iPiKg)RHk5l`TtP$&AFprrYwxd?pXIIidJ}-OSv>SS zUW!JiiRpITJbyh!^kgPG?Rc1MGxB! zeuZpFUKuXP)=$orWB(w{7KS0LlMlv0qvMakw1g%dXNMd24v3#ntl>|EeHX&2U;hwj z)zEG{aXT@$CMo&QC8M`>j0S-Mk7)ld|zEA$gp(Ns310`)> z6m(!-bMf?EnNA>PneW!H^TWnCxyd|Qlu6#u^(u{?;)D^fOBfj$$q^&ZG(csEZDc>U z0o=TOSG;)jE#Q!O6^-&2b6CKi7%q$4w?L5}?j7Z~SGWW1EruaZk{p;{6y%ZpUuuQG z4DYqsjJI>D&}i3EUkuQaRbPt>Fm%TbcNE*VggJjj?dM%VL(DGVq^&raXB8(g*tZ#m zpP^#Et3aH?ItE|ox6zdWtyE)KLj0isgT#1J^4A|ZtxP65H_3C6JGJvr0c|K%%)6pN zM^3|mW?T^ij{i1}8N7gld~lfj3L*|5yQ7pDScrJ@W7IlON#HqvO`3pD>znleZzUR6 z*GT3czyti-8(0GlvG?LG`VHuOj_lZkf=9e-s~F6uCZ>r#4E4D&}H5t7rY1(YuRhL&zHPbvB z16`c>9{UP8(S?#V571sLw3@fQ~u>quCZz*b{9VJq{|;)|1@ z{KalHFJIqZl)YRa9e1w^c{~dvFn-v9Rpog1;9*qV9l3j%{m&PS|EYyI7O@5ECLSgb zKFI$JYFY(BaY!c1z5u`3C`mL(D+(TgZv4rY{*FnaRmOW)xM+t+i2nRn&e!}ZfX!3K z#x)X}pI#VpoQ^<$at(;yWGnWRyl5L8o-43-_!o)T8UyA82Ov5_Xw+qSHX({zyb+mz zkK8`h6PX`uDrSM!$be7z$F`Qpu`I-~j3dZti5RIby>@~C#7?^RuCt8TDE<=PB1#!K zG}3z9fB0dn8AAw<$rlq@Rhk;P7jcy}R0b<1&~z{sNicx9B!OWwr8FYiMz>a3`9)tx z&YX|^Te?4lzOv%N4Z`SlwNi)^PoR43k?)mSt9-3gi50W(?Rs7wymm&FAG4(I=q+4{ zgpSKPNb*zaf}yjG1o|7k!wlV?59|>GmF?CE&U~IkJUJ;}T_WT|dM;H-UR3NdIiv$A zJL&7!bv6~U_qWqU+d$a<@9ah~EFQtK|6;c>=D`jRi4+h;rX=kaM4P2>#^K3_E!zmPYL3S`*M$q#nl{GO9?YY*U{A-rLDf| zyf=Qlt-5I(z`Y0jSo*yadF@F9_%R5f1;PXaVa8By(PtDs-$RDRapIS*pA`P6dxj=H z5&g+A+A29+USqAy|1AGfe!EwE@ox+{U6r41Hp6^DmK!z=Q(#*%T9HIM(_p8t_a!pb zV>28))8FAP(Ag!^jpcc9izaz5!&PA|WKGTXm~@l7AbZ2e*h6goE6UmaK>9|W!m%8u z)ToatU0D5f{X@8=;Bc6_ddjSQ{N&-=<;hrwX}bg5J?kPx1rN5qvfDSgM#BVvWo_tW ztS=X00~jN<2rhfm7WnSgj_t@p16rL@|wa>Pr;-=Fxt*J z6>S7~jvKJir*Vb9Q^_bS$*J4nGkT~$Q#r?@U?5BEq&4TEx!26Q9p!QxO)#DgS)<5@ zqh}2}zmfvB>91A2cN~+NW33|TtY&cF^U`+YRh54yPKd@4<+FyisVOejaz!(7j05!D z7_H)*)EYC!Hla_Clc1PNBSnrSN>IZSUJ+?-188&?Gno8g2-P29HqXIx+WJw{&E^;& z7|k?|D5*4C{UQW3{=2eYy~jdWTOUQ?v`?KKPtts@4-Eeh#=omwV%`iC_WhEEBE{J6 zx9Z~o*?sd$92JSxGfQiZ9X?lDM}a;@3EcUcRArXemYQ4@f2jPEN7BR|19mQKAB-%p zY%IAqV~*=uc8t|1PMvrm*pJMnk|kE(U+t6E%B@?}5O;msVR+NDe_0`9Et$N&54Va& z?tBfKh*30HQIKTR&tWrK>%aZn~Dlfo_x&9)7$~ z{Upj<)=3xOU16XRF$1#P+i#t@DGb@!}CiqE|L^`wE40SGjTVUX;)2ddw-LOv=Y#rcGd z{tdJf{_Stn^?F!hq8JKi+^Ham5b&wPZenY_#Oy*?WNT!;5^R@>R-OWn$?3m|KSm1; z!%~mqmP_^dqd}FL)rIrQ2oTFfrc+kQ_ia|RD3>M;*!I@&2K#7#e6wAyx|%o=qo{8` zBRYgbTxGui)e&tN^QR^Mac>mebn}KlEF~cLH{IXY)-3^+`bl4d8lMP#pnbFtxq+q5 zOH-IMNas)q<9W6pCEl#;SF0Bwu~bjT9in-+OqMtgvhC6}46*Cr6IR!^SLi2g*j4vg zv)76{7ENea_hbio(QFjBTfRvl__iK8pZSZrNiiri?JC~Dt&lHAQa4g~Uu+ki%En(y z00(_t?%dt~YdFmwT!`6zLPn&x1D`s_pXsuM5$QQME7qzDUdc2mff>a2wVDbo-7{yc zY6EOI>ofNgpNF9XDb`Bf1f&L&FlM{=_srIb_ziS+=8405dlx6ux=S$qJIA$tn>Zw1 zY3{=yQSJSYL$UT3R1qRD>P3PFw&w~hWeUSG)E(JH-{qtCbNTE{ffad_aM}JrajgH$)lz>fvB%0r+U>AI?{YgQ$%qbFn-YtmoC!GnhpY?5&$kB z6ymf%y_@`xdxzi|*vU3s3X7~WByGV107yqLU(B|wPcDm27IZO&?lFeMMV~9KC|x=Gej!l!a{lNm7~LiCa)=#y7Is$;qyyLGSskM zg%5b9O%BYCXcA5n4;Y^AwL*P{!s&$Nr9HwKhw(QZ_zrl)k59xC*Hbz4K|%EH1|*nrtl@5_7rDJr00B%tb#Oz}ZTEGp@jdg27~n-rgh zk8{)|TzA8f5}(WCNVWVu$$#ds`(gigi#85+mil5X)o}A)72>Ky|N73#*%UBc$w~r@ zAM5_Fal{)@d)w;^=7ByKLVfFwF94rPtV_Ih`*(4-<;i%S%wv0)G??KqRM}8E{$Z)o zPehLAMDeBdkH!5#*#J50Dl);13>jrr&bF_)MB2JWAV(359noAh_D*Hrl%G02t6=;* z{x{*T(0XrS%xeXg_=X%=x)Fn$p`hoJ6Sl_ddO@hI(WV5TUK&j-?us?q>G5}aMYPuOVRZ;=bh>He=J3g%~p?ri_vvFKr8X!)m9JSSN z*BL|80KB^s-EH%J=E@f{QmtXt`(GaYq4Y>sqB%;26h*o1RIu~s+Y^=$+NkVO$jHx! zh!kVUB^Jw7qWTQ&9EF%hN@lAX8u~<1$bPH$cj}!z6YAxd48%cC}?*!}f+Qo&2Mm%3J;l2<}?*3~%jUVhiw_!QT&;;nzRD{%Y9 zPz3Uyho1m`d=IOFpSVl101ln+JT3p%UoN>Yd7D55;6er18wSf=2yk?MKdwV2sy1q# z{C&MCQ?(MgM-CJ8Qp<+U(=ce{|ARk_q^s1;xn zfy&rX$94Oo>(90)Vwh=J8w7++orSNSWFb!sokELU&qW2Di(ZziL|oY6KswvboS$j* zX+J&{&R?HXVt>TyWUCf_4>xsl_5-~tXnNA`1-@P+t-!5gAcuLVow{6;*cbK3%pJs%E~wS>Rve?a^S1XrhPwS1w^Om;Cyp(qi^;c+^uN zwBF*qf1)N5x>F>@D|%emOpoNIR91Z%TEFhIp(Zy?#bfr2@T7MhxBG7VriNSd-D?{2 z#sTI={Oze@BeN``DY6?thWtfC}|_NvICa) zq);0ZapQ$vU|*zON+Eeiyni7i&i(lbs%WqO)|f(DAQs2$sSe@O4RJ$efzCqUtUG6Q z{sK9*nwt8KEU)Sm1vPNz9_9sKh=cO4Z7I(={n4;`7{Rl=DmAw-oR-0rxw)tj|ZiIOH+NcNV*>3^JU)=`+eDBibSDvJ7h-_!y@6LQ^*% z+f?x?77J%ghnG#Rg$K4Cuaz%}Igbhj{&id`!vC&*#|-=)!Mm#i6L@t!@vuo6>aODB zJ+tf>#=}_|{0Uw-FllCl*R|833CA$DxQG{*O{QA## zrciPMqn?rg+?b(3j$^>pAE5=pEyuqp=HH?VytBW?!@kmGC0RAm&vYuMCbp`EKxU+| z-Cox}5#t@c4pe}n^1oC&;Bi_ zhxAo983jz)BT21$kM`x<1c98lT+5iP3O31qHsc4ViKk4V;DY0r`*lT7%m_HS2BWoF z>pW~IcNCb*kg+b+kNAk?8^|Z)M`o_W$3L}gdL3;m&w>dOstMt(uV!KRnG-gLzhD-*ms<62ukEf& z4QzEpqwB&Ke@N7-uDU)B)o08R?}1^Rj0;{%A2w2q11O?bk~s^`<8W!(&B6my<`Hk? zca<%Y{!_z18X(FEp+laHWg%YMxK%C%kQ3ZZp4Q9b+sUUJuoaGKd~F&fw>)D};$MkP zliUHm_=qoo6k}TOJAzfm-27Vd(SYIuIP=V8Y$ef|{Ti-vye>oD_5U~5MC^3*+2Q@`fofoqq0R{(!Gw|fe6$PKU)Q!Ra#rF<$6kGP%1#Jk#tqxJ+-2ck z7@?{daWbzPdmJG#-m3@ng2JH936(PD0Vn+(bl4aF&k`S4J9(M&N_{==t_x|*i+@@O z(=T?8xZ=qjA-r=te3P6}T$uq8B#}0m#A;nTj*K-UTdx!E>RJ)q?x@x`9|d`LWmVWg zhc@eqXlaq}tsUFG;qQ=}co7kra(Ux~aaWvThhl0cR#nY~c4LA7zX=dbKh&{+JzMBM z&$cgU;5d%?jkEu^o%DKxYF_@jNL_GzoysP8{2s4wBp8Q2+@ z`+gGTV^zmKpT38u@IqjeMb$R5zKCXPwhu|NF+H;0v(>0~3}x3>n|JugI64q8NNWb!n~1Y#)hiW1(Ehgjc?CegGdLk$rCsjs z@=pRtS}GA4Up7xSCW?<{8r`%)JSaZl(Ck1$gE<5~HYRI{MYdgS>G@v3TirKJ;Klm2 z8Y$j9ANb;9uoU$--@GU+m~%5cQ8%i0vc}`B5eg5GP&+RvOAS3nwH2?sj908Cc_%O@ z55TBfKoHiPDwVEoI;jc}VJCL3MRO*d>$#sJYTO}N$H*LiDWfIKy}qDscl>WQDTB3H z1moQ&Kp94_+`-?qY-}fN(vz)J;Wjg(}$?|mAPK4<-3FTms6hwwH) zpeUE^czr*0PTsQ8lUsQTli^T=50eYS*#J=MfnYWzCrw6?O1r#{+` zyKT#}c1^0f(H3Bi-{}~z_;%xX4Xi`mbo6ZV?ZqqR!Y!K_6#e&EdRXKzAZf&;Ot|Rv z-Mi$t4x+R;UxgejQqd>!gZpZ;h#lgWQj7p@(kzBO`skSYKA1OO zV^yjheoKbA(L4`$Ia6nJ9Y-Y4dS%!xXA4H$rr(-RVX9>JvaZN#)H{8l7UJDK9~RRV z@#^1R)*KFn%(P3x(^A72qN_(;$4T4TIi;7G_ZK?2%D~qQ-^fZtak72;MPm{Zkm#uI zb*MyqI=Bv!9*Yn&0zz^EBs?W}CQ6I_=4pqA%_O%zi7m)2Mx=Kd61Khwa?$oFTz#y;Q9 z%(B+3pZOM^0Y|E{jqVS$jE+VEc5H_iQCvuL5nSQ(GQfp zjJaR3J~F31k9i&W7z8*7GJmW!+@Cy2x4+XA`|dpaG$U*>fnWOvO1LwxoZ@?w$2_4Y z8ns{x?$zx@y~zt9HEjSFwo{xxVn)9`teMD(9RJEA?Jf)X!oKzH3$geGW&h=S#bg- znIax9+q|1#mpPSow$ymF3eK8-6PwP@Vzk+tGH+UY@dWYkaxb&r>vbKkPwBsL(`!dm zO&($s@rnLcUud~C;=4~a#^imj6cr8yUD<)cxqkm|P@i;QZH-EiiQw*J{SoQzCC8T_ zOV}Ob=8JFZt#!dp;L!U0Y>YiBQAyv2GAV(cPdOK(O@8(7a(LL?PeXvS;1gPyDdH(TWK zQD68Okm%awhCopw)6>>K2NqRT6(%q#EkxZNi5(*A)!w>p6ikiZQGWHUxz{&2O^m23 zB^j?usB5(Omsk;zW2G<9>k&a&U`*@r^htZW{ZR*OFM|W3XE#(qixkL1jsqWJorTk# z3hb&*Y*wT*2l1WoxG>P{=ZrB|iDU*p&L8Yv>|+?Dt>NaRuOWG=z+b(X`gFnl;dfjH z!?-|iQ^RIiZbS9sqA}dg-+#9q6TID<@CgKNeTu(8b z7Ox&PafMQS_hxrbny;pNCy%Z`_Fp4#-5=3`c;M$dZ>^-109H&=9du0`BlPrmH4Eg< z4nM@g`eUqooj;0QHmHUPlozlO&OQWmbko0?Ixe14RTAqc8PBAv$xKV2SVz@~i)Wbm zrTB$_0i)aa;MwRKo&=z#X6AvA};Yeae45A#xk{n0HwENSqL| z$%VXUCqK+Qli?LA2GOXCZ&OvsSKb(D_sIjRpxNRB{ z1>e@0gkH4d5(tPE)7@)AN_a`RXwb|9@C};dzh-Jw#7U;!ef*+q-R-aMYRw!fuJ-|B zf>b^@d|q7Jp!Lic1>-u@v3qpQC5R+vPXL`J??f*TFw)48=)>Q*wLxn7d$ED0>aSzg z#WaJL-A`sJOjDgL_rUTA%#=_Zm7_NE4$-z8sfknNQw}96NNlA)hDfV7v!BH!d!y?K z0W7@^)5*FOTM$r{YL{6VPf7%}@q~K@wp3TZ6Zt#;E|QKJ2?Jvn+R(EbujRaklVy7d z8mNGKp5xt1cA(`gLnpRIswRv4KdZ$L(ieG_uHJtyKalPv$~+WrO?whZ@{grczA<=u zBgb<*-~I}?V;%8f+nsTIv@Ap?_BU#!qwkNr(b8=3B?AxJhczsjqa&x6lMeK0SrH!^ z;PC1yO#VO5YObXGykvX=fDfJh9W?j-MSz89bo*-=ijhMo=Yt2SJ=O(BQBV(QW{;IQ z{{gz^Upw&Y@g@Q{YHizR0|IcE45KU`RqV#_9I?>#H?P_`O&ctb+;X%l(EMCZ zrk(g#_R-Z*pliEcn8xDF{5i53HC-ZXsK3c!dvx9}j_LuK0F0UUKG8oQ=ubwj?gqnR|@m8p4!604=ORpN*uvl)3H`c8Gu{J9JW3V%@Pz!km1>>v~d&|&&ZS&%mKTbcYKZ)Acx{K7x3tQLQ1ju?8W zZMi6V4RrFh#mv6nX6`9%Kd`K-Yr{UZ5qmli%yEnWhdyu(?kZzG)f(c8LY?oqBRP(C zRoh(N0vK@^-b3L`jl7MgUOl)-Hm`Lg}@T>3(#@?TdFsKzLK)<&8m6 zYZ3H*gUuwUl_8Mr{55Lut~JYR3h#$9Z-w3qLz=ox`@9ez%$e|UvcFRDu~r|_vK;N} zQ7A6C7pUxeWO+qU0(~z0VQX7~;2eVv)Cy2%hU->N7-@j^_*RH<|tW>mD~69<43t9NV6`uu8yxhcUuS%e;3b$+aR%^R{#rg-F- z5g=8XWW28_1IUY5xp5avlXN3!{2yO)#mOpXp^GOze)U$FQT{`8kuO1bP_c8Wj=EeE z)p2yQI<3-UoaH6j`nc`8(UVCX_Ld%U&ENT8cO29lHGrEd5(+{A#IkL7(1-rm?fC|< zUdkqPY5>I*2T5Vv;UXq=KZcAH;EBNYSEpe_x1vbxhwWdx?Dctj#_m`R$m}C}gRS2a zI-q>x8wM<0FALd|tkK&~Df~cR`o=X-j7?0%mgVN8w^5%Z?$emS74EIkt?;MUK-_B3 zTaQ_zcphNf!nKbpIYse3F7^3-3@7Vxr=hEVI)^_LRQALOhgSS=Iqnerm{(-Qd71x* zy`%b(lPVTEoXqC+kaz~sZacQDYHKrO03M%%ii7u&&qivVtK#=mMSA0|!@&E?@2o0HNUZ?_p-=f8a>sJOJ2f+C5u2gvcy7`$Df`-$n%TN3f-0 z+$82{+k>j+Qn0F3L_eZ#k&s=9y?dSVQU7i_@hs?;`q9y8{OUXE)R8Z==1FiuH0q?_ z_lNyhDX}ZG&cbhAQ?aZCCwzM2 z7l{Z-c9r{B1j=!AMfft>zRsJQJ^jNbVy-1*+*t9)_&hpsk~&og*S~%y9~bJrRytci z%DK6{ciX!-OuC$ha67Y(BxU@`S)6VA+5Y;c9MOiE`X$UzU8h1SLE-x_1R{}dsq2;iSWR{ z#T-#f59X^p;poVLCx$FMv0>V1@s}Tf$L;qJ9F|Ez2qfT&lZNv{IC`iP=yuBm#~_T% zAOo2W8g?s!YIGBugS+RU((|17w!QMlwp^Q>1qFEe;!D)bw|`@_UiHdVqt2ziG{}PS zu8i1r%1}9#8zK%|28vHAz+IL2ldyvz&Ny;34m-tp6)#5Q{5O@QzarEcNW^LbsWF_+ zzuQgl8#)m)zXu&0q{?86rh(-B~$ZrM0xKfc!P*=VZ zVnjUH%RXgOfUK~;2nZumFE*SgWY&@{C;>j%fJNIVDZCviD~UTEILYeRG{mXp7f`M+_7&JW$OC8%HU#x(@j<~@g+%;K+o?T zO${4g*F}}#KoO^>O6a)q3%{S&Wsj4$(53UUEqfZZ3@r;J`=%)IId`4L^2e46^K(VV z46x=u4gqS zA{hv7&yR8KPP-61p5rTn{ramhyfwnGHcahj8GrDkrq>(j${6{)9p#?;rw8jgiX+*~ z^ZMDenvB9hn@ z^H7&)L*>E<1#ZeR5i`%@qrQ3}3!am;n;DuCj@K*XS!Wl3q3uc$0V3N1QvSYEw9Sj+X<~K!(iC{B z%Txcb66h81E8HFqH3jq}$8fR^ZY)li{< znJoI553&^02xuc!G*4{TeM1M*Z`{CMF%VWu+<))h+>-pG0z|!JK)uCzWj4|I-?=kl z1@Qs2a_s`D9Drzep6}D_g$tVDvRFUcB~7w#@^+K}Cut@l&e+CgDzXG z?1$7B6fHARX@SS-a_<;6 z#pnTpHamk2-e}m3fkGh^w>(hPvlo}U2h8#FY=Cb;8y@}yxjss(du0Oh(i=45Z5$gc z`y7yUM}}0l^z90|k1Q_u!4W&lJWZya!uG`K=gY{we7DWi)n*={qO#B1&HyybEch5E zbW@t|hCc_j;i;5Yu;+Rnj3XL?^6r0*mcKDlfTw}=u=$tZ7BKW<eHUo- zqf&OJT*Pv1Amf#rxlF*EnGq73gxHz&3t^X_zli@)C#ro8i!ekN_)H+KDzCMh3I}v4 z#{C?gC^Wio`w+4{!{!0!x2JD+-Ru_$Ay7kQ#F3*Wb&AzAeR8tWHyG7woY=cLNmR#K z!fY^?b__uN!`y4o{^fIDMih5_B_g=RRNr}XNLtk5+>`y~s0KUj-+q z@V*GPVIGK0GN5fL;5vUBc26o6IFU!KIi|avb9b5q;xY#g9=DWcX!7yJoEniuJ85y4d*`lUVERlUyH5w znfgAvp3I)5889{nz@pc6xA6yUkGk=G5`wltBwVsKQN+)lE3NM!ud8GaK6z5 z3{aPbg=h2XPSn4J%5lllh?g0gR(u0-)I0a*!u^2t`)5e~|E8^Rk?%eb!&opPf?z5kl#;r={1iO;1&|KifG^0Rf~N9{X(reQViKz-I- zGJTep2B(d=-xA$ZsJl5XPUC>4wtCDFuhV3x7r>JW8O6iQPndCh!mgNlcu%TWYKLU( zv2p4s7n{&9SP`&YHpaYt$#9|sKGov>aILHXRN6}6H?Nea$_DueaETn@Q1)0GZ3iX( zQn4N8x2Hv?M2)7JR4xf{t?f` zCSy`M{}jtX-6;LnvR>Lq_ntJ0rOADZqv?6``y?O})lD&QNu#|vh3oTpi5!n)DmdXN zzdwv=_>BBYU?o4VHM3Fvik`|AQt$htgu+-d@dfTFzT8U^L!~Xuv51jg8YYvdv$9sF z!{41mbQ<2XBzQ6&;O?sbZ5qAcc^`<@y`(PxC%PEF&{D_tRoB;JYWSWe&pZVJYL(UJ zY@v<)22i^IE-!@yde}=I8S|5cLCaOw+Zv*QHw8w>AN~|BVvGIfJmH`o)OF_eQIGT+ z!W~}-?tAkpyYuc%6Nlxq@JtLs@72Ms>btVd5y9I+H)m6yy^a)w(zBI57Tr%~k(mUdcg zKThLBQJ@0W)MseK-auQfM~rasIzo7At(+9WP)0QQkYCRPIHHMUm4atndtbvfs0LrC zWimS;t>1n^fQOm>^r>rP+@_eRi#31DCl<68TV_B&h0p z(Y$^?dYnDT7W%YvrL2y@l?3u41t56boR0zJqXDj<5=NICU^`Zam1LGDO zMJu4U{N(}lu0>;~H+#3trH{+oF1#1}SCof{Upv>Pi^MAPZF|;xd|*IoMjSp_8-CpI za(HaBdnhk(eQ8~)cS!G3&(?UK9+zTg@RDe>ipv#7BsBO?jw;)8Tgdv!}}! z2(Mf1`ari!@ZM#i*E(m{Lp6a;VgWyo>Owhf1LERTGiAZ0@sitA!J2zUCb&@Ytl2r~ zTkiZDeuZ5jS)y-q=aL8?$c!w5&oaz~7ge;q3JZ%)#y?K{O^h(f_2cxrp21#T{Xo|z z2zO^%l8!6AcX&tvxqLNlIoRq6h$2vox)1Jf*$>QC-Y#UCv^K^~pDi-OGV54xs7VVd zQzsTgaN}49|6J!ImBincF!6uA2??@2<3b|)O@NX1hi^`1E?h}KG@4J~M0O65u}FyH zC*$^8{LIR_D>_HvU4WaAvlYgpl^8KYgYhs^S>647H)GPwW63zAZGN(vwxx3HHYw;O zF`M6p>0<<1uotHNw|>?ip$XVCJ@eb#kK-c@QLoe>+|L4ZwRu3u%&aAgxn|?MWW<%v z!ItcWN@5@v9rS|dqAfwA+tC^Owa?vEOu@TN@y~hReXMObLW(ptwPSwG$_U8l=yFtB~Cp1<}1Y5^+*FS#Q~Zz5sAtIAjWTHTkD+%VloJ? zw~Q)%{gq(0b@&;Yv=#jG9_c&tb8m?TBJYfw55MZj`?Lk#i7)?0PPF4o9Ju|%dg2M# z8Kk-$#j%iXOXt?j(Ab=Dq_j*W`wHYjacmY`0*)!Vt9z+VeKLpk9knQ%+c!V673WcR z@NR!c<(R^id)Zo9al?b6tCpAz*04h|v_15+u7q$dV&vrp^yM|Qh6a$Q_cR4~GBT}g zunJpPOm3|M?&JMSxC4FTzH&rgDp?jndVpp|5;JS3qmf*km`F`HEVGe7D-s0WxdgG1 zC%8ZHyz_hQI*obg6>dXNb$5-w?0qFgc&PT@Q^8BJ@aEZQO}OL(8!kfjdEeY#BMDB{ zQ#laD`yh6b2_>*d=UY0CC>($Pzk&)^Hu<;PEtB1u=6E;SCge5QMJJF;-UVXOhyBjr zH4~%-AV{?*z=kRLwc_-3`TtzpU!TeYb?{W=BIKR-a0-8Faf5c2u=}_a$I{zWjf=YR zC*!vPG~+WGIZyBT#`97;zU5lnW%~07^X5Q*=y*`^^-dIZb6vqDy}#_j0pn{^qNS{)?*U%6l1;3j+C z$tT`{x+4lvicH4^=b;aAuc^B1;TjXXF(>syUB{KMI661!#|lq-CP=i`Y+6jILjuu~BnsxgWW&AnXH ze3qZv*=-!r2E6tm#Uhy{RYFoH=tvU2X8_fi!_4W*1D+^sHf{^bmedg6()OBY9)`oq z4u|BayY^rx7Oz8pjgx6e3xAMlnv$&;2aK>-oebiu@~Ib!)6P3A|I%-fZFYOUxbpga zu9=jbI<;#Jr_RgKADd>-DL`4Z#t-+iaEWgGUoh)?SRwc1WEsD-_mBzU^X8jW&5!im z;%?^D(*8LA5xC+8OjW?k`x#pNT5`ZPM}5QC%OC8(mk8^MEgj6a<vd zq@4V~^4(=%`;f_AnxTZgkEb}D1bBIfQu2(;6T#iFLuWS34x*1JUu&}5$Gk7_rw4(; ze+zmEyjK*tj^D-&d>rC%{cV$l0^!mVKf)O{L4Uco2=00(-9LA|G`RZtF5L8W`NxS# z>4RiJ_?YO9z@pg00)*b?F@TLxl7(jAUHc*%16I?PAA8R6wUK+le0C?2AEiPoIVtkn z;xys(2VVq-nBa9GMSpq&8A@t}y-fX>=DqhjyRg-fxPI$1CJ6s=r}+EXU;hMJt`%^2 z>1CQ!r27t&%Ws_r$bYGEo=vzyn^0%uo{1SSGD75sJQ9*?A<*5|@Kt35g$)T_YsUsY zMIwJBBW_~)u|**R8}~`)Xya3qnkVK;CZeokhC1M;G7Yh1;s>6GH4~6C$t7|mW2cCK zQtWxtWbXz&ROH_$QORKAj+%Fe@jG{={GS&a;K}_kB^C$3dy+pMr}C#um}8=&FP2JWz12Z*RY-rrvDoFWi;2D>ES9c zn8|ysGn{TDJE+W;dAI_CCk*FI^Tqcn&|kl*_zeq;4b>NVQ=mA3UyGSNY&ICh}^<iZ+*v`(r-Uj4ZjuwdnL7fN@oA^ zh)IsQ;-&Ft1#NY_De}T3ItK{%b@neCMi>=czy~fLoDjz2lzd}W^(IktSNchoO+rSzKROqxbnM^aLLFbJmwce}qOIR)1%?HTf4F;nuR_xIp7 z^3x!40yAet-iNm(kkc?u3}FpYj|5K(w|5(z5@e>9LJ{P(gkEzwOJI#A7b3=VI|VYR z8zAeYYm3|bY6~Jo*gsNn#A@AThZjp~bUs+ij+5#t@|gLbpHG9}m;7V>?TcTs8 z?QR2YZ=!1WPsLB(SK>B(j71~^SH^I_Nu`sAJUV=Uf8|6Uw8d%QzQ8I#WRhua`?n$=^cpgm`Fp z#C&oqf?WSCwN2phz3r->_VUE%nFgqK*L;#pYCF+t?efdN&)Fp|1RH{HMc+iA3&h)i z*z2A9fY)~>Zv)C4CsON+e+WP9i2wMCyvuk%{hP@zFBOSEQ|Bx^h3^M1fH~^;ey4w= zfJQU~-~gRR%=hgyIGqsN#;b$R5q97Uj1yD%9)%~<68}Gj>`5rYiGgwIN^d7~ew`_O%{!}Y+AD+IO% z5_i7-Mz%>Hw`;=UBri;5|0C(8Wh2VZ$as_&CQMwo-sAn)!lS@35kshpk!$!A2z|#H z0fO9i$7db^xS(T(1ZGf*6M{TYj#85*-7lqWpa7!x%07)lr`fQNlsm0ou*v<72bm?& z$)Iks5V|lnnO+ro4K-DjuC;f76=>7q$t?AxNN%4!c^oAY4Np?KT+b%Fcl#i>Eo}`$ z#oFTD>1<+H!A~S%2g2P|)`W#4=Z0^lsoP{N4uc+=8<7UOfqxm$flsrWo6Ohf^QTXg zMWez39}bp+nEb~LmP=|a5&nH$s>V$_9O(`;XIo7JVfJqq*|t<`99$n6C)30&Yx!aD zsPtkAw|rA_%A#~Qm14kpqQ#qa_tk%-v9}pUQEg>!T0y}!YT5L`np?-qcxEJxkwkW_ zjn@EmOo;i!5QY50GYXubNcb$Xt?_*?`Nr@?O6W+3r<>9)>U3o#vo^w|jI&RY= zw^ml$g|}8=CJ_}U?AOQz5+OLLzbwRp?63f*a(e9#I@YIx8F7>~(`>t@oT)myz%8UK0n&;$N^PO$E8A zhY8D88Y$nc;r^;J48NFTB;H!QJuTB%3>fUb=kcPDkN5uuYD(Vmy}NPr?BnGy zT@w*7U?P*%=Ofu%$I-N7mmG-_Vw-7l-w@ZtztIyn)_db%rWb&vmef-~wD>VK?~R|H zFGa`ab$Jw9N)hc4S=t$&JtzZL@vi8M5kH(#D|xA5b;soy8r>Q ztfYhjoadXWl|PdUBs7C&oKp1IR8(gpU$OdT$_CF+<7+P4mOlQ~*oGMNpN&)zHLD|P zuVkDyonJ4N9_lb#O6G3Z_SH8u;uO19Of>-rS8(%#Sn#CZ>e#A89LlDRIua${OypgT zyz)IJw)OlO5!XqMy6lly8Q=5lsvJ2bMm=d+UmdC0V3i>X&#YaIruL_-isw8;6W*|v z>pgzzFIO?g49oE|F^ZGmMCppKv-NbIxGi;9Kz%vZ3(kttcVqVW@MWPFtg@s!JIC4e z$?u0U^ZgIZQXIk%edaXx>s`Q!U4^p2F9 zR-mH4`2RA;gu#Fmu>NMTwa#{AV?+FLcj+P?`3OUyyZ2F7^p)@doPFYZ2^$+O1Ml!gr+&eD(@MRXfo5!0V|b zm^W}u4+H^Dyw~%DFnDsg!Tl8@9{fs0IM(U@~-)KMXYob}3sCo$asbZ1Rk~ zFi(F$Pf;bR{QgBR_7Nccy@I@bSBV9)nNmh3zp!c6G9;b!QXs@>N$ZmS2Xw>rO#jE* zcuBGPUHnkmKL+9NPvQ^R9B~j_Gf4-9@{%prjtDR5py=cgM1Nd6F>14isoN;)C}Mcc zD7}RTCB2fZvQ5Qur}th`4*UU1-{tADQNnpohG0D|`5;p*x)lwtO((|YO*jm-^ps9vGHG#O1*3HH)mBJ6Udv9!i|7`hoKR$A4{*bvp zT+a6UG)>x7}zF-1KHjw=jWB` zN;m_5{OE%z_sOJr;Exv`dN}}4uUpYRwrI$*u_tnrY2yVf5z|F5UVDNRPX-seSE`eZ zATmtEqTuT@+39>S6~1zHIqFf%Q0_*E*jdWxNHh9q57E$CCb~cjCUI2%@z`1*9#zTJ zH81+N@uzpG%MSal`(gAxHyhqRlB})Pc=K4lO`os1i_;NQE;v4YLjmibzcbbpMNL#$ z;jU+PWM438pj;n9GQsWe0-S3l6v1GMDWcBKi+(R!2$PIxa2O;-Qh3;7k2%wspCMCu zlQ}b7?swMeN5K2=6YXvI>4>~K3ca|2T$=Lv;`T}cW=5$VjKWI!{$$Q2Box3t;+!6b9y#9=ZEH-QcW>=PgTHdoWGv?5`BM`9?&=pd!X8Unr8D=WJlr2VAG(;Ikj-Wd6_DMUT+)e%%SzWQK zi%;?Ug0wgBAfVammL_IPOE=5jM=|hd*62UMfXO6w((iP2yVP|!kaLa^W4Y!fvTGI? z)1#=FWN1mXcDI&&Y@uWrNTLOHU?V+#kqb6QO6lr2NNk6o7<&GU{oARNPQ?FL1`Sk@ z#Aw4-DHr@h&XZ=!3%W9U?#743#`U$WptDA5pFD}{=I(D*#gD?fA9w{o)%7pSZd=%e zUz6u4oOmRm!32J3B^Zzs1D%${)luTfdR}BYcX=+mtKdVaIQRQZh7+Wmu*60XA3dWn@TN`)fGVbu15yc^MQ4n0*5)fg^44%h=#p$4{Ne60}R|#-y6Z)pVAAvCa(S z2Zuz3(!ZJGfY%Is+O_6v&_H0Za`w<=i8VF2D?ovi;O5tf0V3*i$1(2_s7oQA(FD{P49@o^&m5F!D>$dU3)9IZo1Jn0Ub* zvCkr}Us%tL5MAub67tG1tkf_7V#w$i(kehgXB{z@&^!TDWqep`pv5M~o`hB_SI3R) zgz6}rhPf-BoOk&NB{JJgmHry>4+DR>Jw;Hm$@~!6xcDH1djtGOYw!9-)!~Lt3x&r~RdgdF#IG*cIiE z(c6wjNBw4uw7l&M`)>U;1@YDtWS^G`gVibDWG=hq+F9-ogYPiH8!i&IxwG8Ex5K*r zM#|xjwkzvP%6*W+cQ*E~PclMA7i0%ujIk?2+D({nZ{UUe`(rANyCNqkqD!+{n?#J2 zxpKh`&lPW(g|`-7>QH0p6(KpICO zQlr(u*(z|(Q+!>4RD~Ob*T!--p$2#^ieRF!KxuE9V}4*3yAuDFGTBk|Kmza-UhW+v z=r~?AU-`{+_pzyP9xq`4U=L;2WokK^*0>yF9N*T;dRegAxtslHQE)riW)nkmD|BVY zyrHW6X*lE0S>|bi0&-FOYI-9*19V% zRaf!GQ`Raos3E*e|2y@@T=@!`vTw?xX!*NN<2lMpv%qg!HHPP$ZR09%I6%2wx256I zShyX}=%!mE7RutUY$0@GU{ma0$~^TWz+Kujr^ZAZ_QT{{Oz%etcc&7IYu1$ftg=NY z4;QN5lQ(JaGa7d|3S&>s&Z=$2G~L6K9B#ryME!CBHFe_P|HaObe|lZJEl4wy2A5`E zI8*CMvflsplgj?H60cQE(rrTVzoo>-1C3NE%^12a&wfM^`wTDl=vV?5ns3=I-V7@@ znqQf=*~Jw$T7}-?ey5~kP-vC~rz>GR$|rgC)f<= z-MU=V|3GrMcn#f;q30q&kCzgUaqkDLrM|-uP|0tbgS%jUqWdA5%QeN~mY-3LZgU#X z9&C`(nCKMwt);Wi@DzALF1YE?Nwe~z6tc%=2aO9P!pB+U#UT}A4o*@w+TaF-EHQ_>VAp2oXMl!LC(wr1tO)wCV*TNJ3=z42)(b@B61 z6(M=h`|-gvbmaZbXKd5^C{IMQ=!1C7)D#*`QG5d%c=(^1Lxf=?lKy+W_1ym0Eqhdu z4^2+fO{#wGlv7b-f`aHWmpKtP5HP!p;#noq#a_1Jd=5!nf$qi!o8cg8b92!vmQkQ? ztheyX>(U2p(7%;Ekx}AetDRF%3b7Q{CDwFxx_;fXw1|(W_=+!}I$Yr29UeFRpMx8t@AAxzA}#+D577yvnn*wZ zP~(75LLcupLs6%T;h@HIkkGRd#|0*W=5Vb304yeW7dG=u}GTE((hGsp&B6qVb3%~Azzts1ngLnX}$jy@lhI@JjNxF zcIACQ&w{-AHqMSOB|0%Kmu@nh7POzB!+CrN1|+ zn5Ux87FEJzlM+`1$F^0=gCMy1r2f{iqyjXy8dKJ2r!K`uG#6cP3CZAe=^NmLgE>IH z!hACK-xSEKgb{#U_WgIwHM$oWPsiUz(q=_-3rSUchDpgJoz$TN&g*ZHDDZ1*!%pMn6L z57{H0L`!u+`Ph*Q@tsUB7=lUj{RQh6>7lYyG9NCuqOi6o2ybmyszsJ_dEgKA;&U1b zXg4WwFRn$hS3dM>ih)YNX6^OJ7JqKl%{Yo?A0f5J6f6<{@NC{48Nd8)c&$(!`ryA~2i|2|K}Ad3Tud zj}fO;*Q{6T{~ZBd~Rs_u)|9fGeOVc#)=neB3Sb~+Uw&%6ZBJavxnKWt6VK~zT(`-RE{ zL-pRUL1H$@GYUDcPAN$={@l&m;9B)tjwhAmEpHLFxE&}+JaXs{vvuQ1Q`cjM$~qr7EBmg+L*E)y~kEnaxObJoHCxz6{o`j#`9B76ik zk}p1#pYyZy0z7;yrf?x6>r=q=(y_*`*G=C&NuOZ<@Gv)mvjv9#Xm?Gt$KiC{ zI`8d!Il}{)B>(487D%1P$mhddCMC7s-jp}SAlsChpm>wqsURw4@kO0o?1d7!(g6Ls z*Li~OhuT693)rne-23fmoGI8T1hK(wFon!ID>mY&F`$s{U&zj6 zn;Nag2ssAU#cRhm!2*&8qV zD8#Ge0rJ0niz66hc9Bw(8sQA1=O8dBgOjdL+?@93xnR= z=-WuCHTn;if9(9CyeaoafNWpB6c@9<8rirW-W?x%PAO@iXQULNiX437-{U@TOoDuS zx5_~Afjep4-2s~y=)5yuImyS$|53DPUMT_dUY7(-BjI`f`NQn85Kl^er9p}9^l=B8-vYmV`6ekm2h?j17BArljOs5 zZ;1v*x@lOC^kmVKKKKjET*0nAif1W7Nfw_!Xe&_*8*>fZ`OS2OmXMRw`>nWk!t=kZp?*l!d%T2LQVk*GQB`z#ullj+gh(_vu$Mod z+h8}dCn;}!CUO49R|HP63@2}Dp3n8tijs7Y_vWQik~hQ<*qK(x+V%nN0q9~vgm9s0 z-^r&RL<-=cF?bs4s8i85@4QY7*U5AC?Ymh4PJlN_ok#&ih>daEEsA_}|2NuXafcca z{l(h#*%ocOt!ZUYYdh8_2%HVn^C2*b#54i`&PZ5H;W=K|KcU)pqM_f4agK_1fM!XG7i=^u0%~b0{!4&A3@DQ~}S(mA^{axzQ?{hJMt2fjeQCp)QI=2ap?rvLt&Ui8f+ zUUul<&L9VGYtC_78meXg&kN9Ey{;$_*xvvS=bL)2pj41&K!9n&ADeDQfd`G?Za zBlb#R23Pt;+I{wk$^Po4Q2+f4A&L0 z3O0bB_m6~3Vc0%>mF|O91nsGOu#I7jEX{pyC>&Fd&Vmv*{l)G%w$OrZX|baZV1z16 z({kibXCj<2BQh+H|Ah;W{<5{nh(0rg-@V!Y-I8W3JX(GTLa=I;1dzncU&c%@ZT0~_ zx0TSZ-SfC|@Z7!h3Y&8=G{b-wO$QfKd~$n9v&xEeMCH1_mYUMci|k}}2De{P?1(FwOXiZ~HAD zJBA%#UHL`HDoSLd1BW$73;U=RovhQ0N&7;s|yHM6kgV@A!lJV3g>!d728c8g2idw6IaiT8SUYlOB z9Iig9me`XfBW4`PN+SC>un3VRfL;=l(22?BVzPRe8#`+)#%5JJw_eZ;B!_tPNl-f) zN<7N9ws@4m+O`+_O>}46w1uqe;TtN)ZR=EFdvnrrlwp{uSSv(rmxd}Z{GO)-!5t$^ z#m^0IvG%4i9kv;3a_60vb_|jZ2_UY#ID#A&DFi+5Gnn?he2bvIhE&Q@a$uww@|)p$ zxtXp&;4|{LUyB!Q*0~29?lQE%!@fyUja=_?l}O5KS1=Kn-_@m{OYv8R8T>XdiM>Jzcajg=K`A z>(m$~zH9J)j973}5d7~?5|{H|75o2h1o~BNA4JkH@+a3A3X*IRj>0(W%jVAc45_}) zpmYHx?=gGzk%QTA^Dwa029qdA^W2F)S|zHl#jRX-8rG;(f8EGGQX)6kc`R2@$RVU z`1tPUoIwClpVt^69~nVlw+ajW_y&17cvjg9d8As>eNbbNng@2zi_6C(#8`*!pcA~7 z6LGKSGSu>Z#2a1LfonJU_Cg05kxX-ZnV(lIjtb~W1QIq#Sg_ne3&=L>Aa&=WbIkWF zJ(1@WkoiPO0?%9Q0|#0XkEhXi|_bPukAtNPO7Y3c|Mn z@c@t$F^2@8HD-fw5INHuC=;?{^I|@$dv7Q1^VQ4cK)aK*x^eu&`xp00k$Y6j+wn23 zh`)g6^SninPkmvLyD>b_r3#xWu)Z)FzZGuiey;enwwvVu z{>47Y`;aKA3!-X-iKW7gcyioaJJ*8k9ngN6xi$XNjaoqAOFzTOJ9dDKI{b&D%pSq9 zVOFbJSqJCp{!;i`l0Zz}R zjV@TQSH2w&QEvYNuP6VzAeP)o2LDfN)k6@&+chEBT!zEGY-~1sFR~={B=54uj6D7| zrp`uQNs4ie+*vA5Tj>0IdZPJCsbyV%5&?J>t1di(gFN&XZE2V8xP&MZ;dQ z|7O>-Ct*fzpH5ew?U>W%8v^86Ousgim{RS%I^a=r zgNfLRUmaB8#%#QMQ7-*vcj<38JA>zIg)F;zZ(&;_XaCIDkB9BN5E(;(U(Y;f@esYm zaw^S061|b@v8%Q2cRWM&N0E;34e7YF%K9l~kqTQNFohs(GJ^}lJg`(;>w%K}x2ut% zU$TIK$RQ!(2i<7y(1r~&1$27L=&rZjpbI;sA;Gy_<9VM0eo?r9kcWgL`A2$p z9Qu2XyNU3c$D8pwB^*O+vd-#oZrnfhyp`9rh$e2nty`I9i}gYJFfqw9D?}{=o+ke1 z$8{mSo4r+uf?ef~O~cF2(4TNzJX7f~YUDxDH}`m540qHb-kDVDfA#Z^4H>XP0o+jB zF?<#J-Y{2jkUN)qh2u7L*olER(6aQqcB`zr1sbN8P!UpIivuTmS?9Is?tND-UAvbn zNp}D%d#X5t(Y37i1O`V^pX7wAJ$JSgV#gcz62i@Ba5m4BMhqfo8;@4#4+G-E{R?H0 zMwrn@hp#sELjO1n*^H0ykbR4l-2HwvP9twwdvCN$?7y@(-pAn!AQcBdfD+)X%SMtd zQ|8e9Pma3wjB|7?koc;V9l{fVGhqd=%FoKxOy2Gxdyi}RKD`&F|Gsq_^F5Oft=4V3Iox`x5~+&VR`Wa-fiJ%#nSa#nOaEv|bLKPto@i-_HwYJZXOZhmysUec`Qj9s@+jAY{yd!`-NoO&{k_qfmipR; zx3iadJHfkmokD?s>f!`#tE$><@8sJa`ZlLj!O+Tp*MvaeNxDqrjRNrJIb{$}n@61%5ET062WFy3pc#}BtlSM{Wl4#XkX>_W8qLy2-j@Bf3T8MBe zPptK?l-1<5ip^IBQ)g`_Bk`doL(3hE!TfQ&lkMxPOk{WVgpOZ|hL4IQv=?RjQ&CZ` z?v+%xr}Hl5`DG*zH3%vh`E6A@65@EWIjYYny&%o|u?P#_#v42H2@Y`eIxDETEIYFV{7%DFX1f*ReiEFyZBN%G^x^NFA`%tSoiAtsST6$|@d&3}i(;otrdrT!bX2HCb6-tszr zeht%(FJC)6;2Y-1)4f@?@-Ds%8LTR1@~$oPk)g4@=QGzl2yTd()#VF%LEqWnkivAq znI(?T=Je|evC|L7i(EKr%u6|owJ>KeMLS%oWvfGa;UNj+KQ489PMAitZA*N)GbH6N zUj3r3jzrai1e@d?GnDOV$7;J8CDrWfI&wboUs}H2o_#2G)}!SM z_-z24BzNX-Trg#E}HMS!so?;m|b0IbiNVgz1$hB zQ>|CZ=cYoJo?L*W zCmPzo3Laiwp(?+Z@;J%k9sBB;ttqUmf=ICb$QSJ zIy(aR)!s-<(TB*@7t-zdIONt}`a-#C!Cfw))qQxmaGN^aGSwBM82NrpPF9IfNQ(NC z<=ZbrkYzv3_BNBFD{7+JoM{mV5llI(u;GOw`e}V-f=36;Hlyzr?9R+oJh?# zM?DCK@_r~`SP6R#6G}xQSYq+?#ABJU^iJ0BCYTCw(iIx8lG;ChMieN)z*IhNI$K5d zL9lk_DA;MuyNIUZpWMtj~K2`sD8>jO@k4?^HX-A;i0hpkWCIrHy3mS)VlM%PgP0W7AZV5csgq&f{puWFFrZvx)fU zd$^#c`d9V!Cc8V|BLwX`FRjna+gdMcUs3$^`U?`?c4vRHJtwDcbvKlS*-_QPdr{)w zM@C*d#}_J>!SX}1sN()NjRo3?%1yt+$a6|3L2%2bw=;%OMg7$kGX17UA$6bpz7qJ#pJRG!yX3=n0RZPZ5 z@tWfe*Shw`Gh=B2ctP1(;AMkth1`oeMF3)}Od2@_*u5e2!4Wc=c`bO`T4`^n$e_O_ zv298`J${|if?0R!7M>Iv)@fCgM(Ir_851B%e0ofBaF>{nM7CM{a%p-EzW~h|EODFq z48y>q3LGvVAqiO%z4?KJlsX5S|59%mf%5fSM1%(5Pvn)4ro5_aKGqYjIdu?kJ0;RL zxt22&DXfPDqn1xt`>$@yZ`?lOIcU1bOa~tuG~^0?RKI2colh*CW~UUG(t+wcqdfD) z5(x|O@NdINoR(>C9zg{Q(>Vf+aR{tLlR%n{4X@WX^qAMfe!1BT#n9uFLI`UpS3U>T3{JpW5aPO*5zi{NmpR<7}Udq-{h|AN&QGX_M;4Fg^OOYAkG}?wpu9$H+IdP- z^}vfezE-Y!#UGU+X=MX1~9>?WJm$3T zAm8|3j+;eA>*ug=bwp3M#a!b`Bwwb5D=GCCwtC;$u01^(5?oncDL&V-kC~OO->6*B$m$^u)yNKHBGQ1opv4-&Wr_rj|ha~ zBo#dU(|_GrIEtq)@DcEwGo4IpS9IzolIf&EK7`1uaV+`*Vlsi8_C^@)&`R_jPX~ZjE#lMg+Ka=ZfBAj;@L> zzl4)4;RDjTci0nom3?-oxex;@`<;6?uZxPhhI6yoog?)0!5Rl&rpyDJg~d~JVFW6(rbm}`hJ&kxO{$8brN}W|;xT`XaJ2U}2>rx^*qJxM2^shg zZ=*=~dtUUm%9=VS%P20ltk#npZ8pfBtnhEq%ia5bNI@ zWf%iEKk?b*v{TQTtlj@Vn$E(l3I6Tk8;k})P`bMjq;rJQt#pYX-JKha(ozc2CDPpt zq($lO?q)O_@1Eavy??{D@9z7Y^EtQIXdz8t2PeJBuA~3Vb00aa=*oqKWd9ir@F~Z; zE3`A(XC+mmn0buo8l5|SJYlC^GSmdVUn2E`F--3+W_2K=D{#Z?jyBGq^xawBc1)B^ z7jYVZ+J*Q|}*Ku0}WJtabC}z&#?xQ8LM3`Nl zbjbtz<{csGA~qAj7-(J=?+TsbT2 zm6HwR^>OH^ZABIG!J?+Q* zk)gkP*U5X^)8`o_rOb?>BhcIj77tGh^~CblRH1Be$eTf3jIc}g;J&As87ac;iSR;Ss^dH1=tGkbqR5~)Mh^{ z=kQwE>t|&w{klMH%>&{X1g$CCT7HXh?qfxgkNf-tbb8KT=~YIZQ*PXc2|+ko?==0{ z?1IiedU|eGcm{4u)r&PtsdV!qCoEa-cKF@VYVT;$_*A!uH#rQZ=3k<^ z;sWp@yPY~elxhxBhK_os75-r!(wYo(uIES%u?qDGNr%;;V;OY5{=lg9xAjob$%;7N z?4HZGZDf&}bsKof9b{4Iku&VpP zZfdtCWQ)BrpkY;>ysM@=Jlp5?1N_waPF7rcZfp{OOCKQIrS}!Od}Q704_<0_R)YQx zNMm-TmC2ui*8x|~JJFA=^#j{Tt$wi$Z(oMKSkS$E0U%_S7swD^Q?pqX@=EafeOg)< z!0=*ij(_2yw`8p7X@GB39@wfD-FEic18t3WNsQfy3aGW1+vA=rEpY*CjLUAo;Vm6> zDc=4*!I~-KcRxDJ;*?ULW`LZ|4zXw;p@|5#F0bEdHx{9C8Y zCr?}KWv-V^8xYL{E}1e#0o`M?UoW$kvM2OUPbWR-niWRxcHXpJSjZUNmq?7XgM6 z>ie-fwY*)^{W;&DyrL(*Z%zULG=5qvgXO4tZ-B_TbU0Y&$n8jzul2U7e^rwJ26h-e zjqiedJWmd3DhMvOBx3s;{xQPc1Qs1Kf8&HNW0d+l+UU&g<5{woXqds_$q2ck^&qL| zAk|nkH=C?b3w;l5H-BJ!64uE6chE0;GYl28=tUXLFFzZ}le$xl$?ia3@oLUX6<-61 zR=vb<1;i3Pu$27rH$UMArvbNzu9&LHCJKrWMEPCvnvxtpo+Slx^xmi%7tv)vhDClh zQl@uals}Dq$y!ONoSRpy>bSQX@`f7JEq6fa6pV$U9oZqRx|#nLnyaCLFr|XA>)S15X*^+dQLo^AzvWw%$^Iof8{u;r z&k1(VLat_Nj@!!ByCi86MKj>?CMemNr0pmzw%cWv`HngZwI)Dh8f~s1i$Z)7Fgnvm z^jo?smy)X@!m*lESW&VnU2azQCuX8BHbs{VNzdNv%!)Skn3{jRRjWJVVo=^~+ylRp z_2<5n0*T$h{n7^`bdj`eeN5yUMImMF|4dw#5ig_d(PRwq1i1kfS9*l!h79bu1J^i1 z79^_-Vu@WoXd@;fJLuJBQnhS*6g?^k11Kt#;aZ|1{TnC?Fv=oB0f-h5EeP5YVS3F>6?bfpKtJR?mo58zF%=iSnCM*G&>|5ka&lG z&?a0TP*wHZ9yU|#se1f{(hwW-n(4#4=N6W)!)@azF24oF8B+KGR_ybEIoncp4>wCeU8A6 ziiIDkHuLf6#8WW-bU*gU6EEU)wsJwjW+9kcUlKrjcNR6_k&R?tP$~gjuIMa;(S}Js->AG5Ol1 z5^F#dh3<#v=p6d4R?M#3<%jO5IQY2|nzwG+I8Eoc?t!`->C?*$xUj9m`euFO1*ws7 zFj*Gm%8Q%1gN@M-K-|aXOQOH8wRBZ|Fn7K}FUHUe@B8=A6Orf$#eyA?HY z4Mifh#y;XLKja3mVuc$d35Swl!cEf_x0JB?9*}2gNCocZYvViO5c*dnArSt*-8bpi zggI!?GO1wTBN>W^Je5B8f=h@zzN)y<90!O9<2)93(?HHB*`!%5QqwioqL>C(OxdA? zRM7W^Upg=6-i`n1p|bctXG;Gt z>5HVPBp{>ePv~+*l2F%7ZQ!K$Z!e^nWmU6t~Jo9{|>V>q}c<9^i~ zEjPkut{d+)nvM-vXN;3q(MubV$+diG*I0gi5l?$NDYSuo(BL~aF8H?qzTUa}@ex6` zjn#_I;n7)aXzP`^rACWn1lSTWaM;SfO#=MA>u;ANvp@&)$~1c_O@`0#IgU9|ZGL<8 zY6}<4m^%e&JBHZ#*!@URidE==mvBy0N9|ex*TMoC2<$) z(sXcNz}*c>cYN%dnOeSSBO){jXgQLDOO{cPp#6EM!OqEAO*4HcGfGg-nS1Z?SxFh4 zG2K*&XpUJeW9GH9OXS>;0Xu3cJJ7u8eURvJunW4gM}5oioVbxB>XsvsXO$l3RX`bZ z_?fQ^=$>1#k%(+XTL8fGB%DM;OT|eGv}dSfzEDk;|2RF&drr*=&BnxyeHmSFHP2f0 z@h*r0$C)BVa?Xi{<2U?^(JbPWpBv~w2qtr2ls|Pp{ezxBpmtk=NPYx#NtyFJaQ{%U zlf;;4-hPF9&H`raBPO{BvfW|Ra;Fb)F02)k<&JzmH~soAUPa5J5aB3Dicc*Hp5b5) zHtz_+ak}vq%<1Po^ktec_x}vB7Y%_LH=EOw{gsFi%e1@7Fgep`vtA87MlBe75m5Jk zaI{3({jMNdQM+Q#F%SM1fMI?*2bK{3utdT&Y?}Mhr?_jGn}OXsz>#1JJ2C^c&%P1S z{(CD!jlXyEbV~qt;6uoxEKUZ>I7L6I$!GG%!dWO}ta9~Y!}73cJGjiGt!gW5M&EyC zcsxAo8UY8sQkM$Ji@p@384TTgVjhl>+uc zW&yr+_1-PMnHEA7)`E+x9cHlgGcSvWAgV(I{l)#b_M~O?y-Qs5+hh*Ja1{4AEC`NU z!Fyv{@6{LrQ=$g}jP8=BiV~q;ip+%v7kGKom4Vc-j=c*yWCeFP3FzK)d7S1xX7ooP z?D0ssb!Uz}o3fgc7+3ECRR7P`F_ljFlP3R1^7MZ7S%;#01g>s!5z-{cv1#aK0&r=L znH7+2@(&2rYQ%AvUY3cE-Fi7Dgv?P)jt>#X(dyJqVdI=%c54ALPj<3l9q0ufepqwp zBCTHuV}>GW?iAQ@5;Z_dVf{_-WfhEMFS>AG;d9>sj{!#;GKPh2Pp_PKf}=IckpvZd z2&rTE?1miagCk}9DIO6=_c-R6KDZRD)#cNQrsn{1+u-T)kXDpqE3ibHYQZ6T?E*#d z5mM2A>6e&Sb;o}3Bei_Shr3!H3ZCKm0S&DFxzcUFs(C!1=yoEbsipo!2!gGU$A*<2=+pAnt z))`C~W%P+KJ+Xdq~*6l&knRZQ2fK4Vf*!d)H7V6Jlj- z-nYp{P%fBJIC(Tm`O^hzpDK?NE%?(KaQV}AFzYZ~89K_e=i&>zz0kt+8BHN@@Ajt? zTRwJb-FU-SP=qCEON*P>NkcZA@IRY9<@~>iZo)d9I$s-KJnc&G`-_d0l+8)k?43`4 z5Y5;nyUbdzt+!H*cS>GBu{fCCIyWP`{?V&?YhSJLy!^5KHR7$6>TwKLw&S{%H#nEv zmGbbfT#usSl67WcufXDM@9que$m+9L1da?kH z>^B$$&0S%qs4Ay3|!=WDnxv`q##U@E!``Nt8OWA_)vwn5uYp7yTlN0q3CGaA-rK3=3` z{M-h2*HRWnj%ON8!M}4iY)6bKl_L?O5wFJ2Lf^Y>FdkX|eJRSud6P)^IYRL1RipGQ z@Z{0J+}>F%&w?ksyCKu~9cI;%>cMo^9S-N3B)Th-MKJ0qO8Ly>77&S7n9gb^$C=yo z0Wk9?o)-U}C-zEsA?Kcakxzku^+vw7E0NB=f^5DvxRxy<5n~J)NEsf_)>gcq-q|v9 z?uef=^beVW%IDC+zZ_=M4OL2(^s1PnRSSS)se*p>wWk~DxEJslbS`2?2qe+tw}oyk z%`n>0kM1fT&ct{V=Iil+<~0UFm?8C1!r0^P3uf3D8+qfi>$Y7LJe0xH9eaOde0i?) z<#lwazRpn7U2^%nBtV7O+Eg_ECKjjwUaUG8|FFgx;rhb$VWzF^J$YIbJulDKmS&xf zI%xrQA{SXjb-i##Gr|UOi9HOzx5*ca=rd1q;S-+$Xt8%}WwIebD|CXDd6koGr6cg^_%-Pt*wrZ)AXH)U z#PqE5SWb|8oV4d8>v3$)5D@W=gh6t^3pk%<+4*v5Q&MwNrb*d8RhYh-cmBsj(ArQl zNJ)=qfjTpC%Yafs&Xw`J^p7R`j{=@8#FTCgHhGKy(lJfqs(OfO@tqGK1Oap^cNt@n zC+^w~XN$E?qcr z@mMkW?8-Za_*O65Ez8AEN5}tEs^$+-K)dv+@q3pX zSc5?YNG0)V_Fex;{|JZ;eTJ0>SZ4+UqrkGX>EsCQcic;f;p{HhRHl+^&S1P(tQEAp z%GPhdTiA5J?)|babF|GHqo6MKPM{^${Ue#${6~n(C=BTbV2Gfa)&22CN@c>zeO6GMF zh(O~P40UV~ljN>P-OYh4nKST5f_)|CdTBaqHTUU-O z#~_t~TC)t0jk>u2ZS=-CR54+qvqU00oD%t*ssa>xGjYovmt#n!ysVHGpwDC6 zwvYIUj=r<)1UzUKQ@{U~Aa`V(B=1u_Cm#D>n70Bj*tfOyiYse~36j4l}7T4-ILiZmTU-0(CD<Uqc_J5QtdyR25>Vljrq z_wMz!bc}vX*%UKe-2v>?*xN`&(=z?gy{P~U`mv@J5~XLZG4=r=7dLvp0og%Y-Eh?= zlQ*d7`&4>w?stMZdO?SLj4gL3fQAI~BwpH?Ao?HrMT40SG<^OfqGP-?2{OrnGqr>y zQ8qEeg~i#|_mh@i{d*6(4-*c%-gWQ2S?iz<2Yq`FJ3jsP!qqA6dl_!j{e>0!uGFdHa#q0;i3L4{9K; zotg?9#3Pgb9$Du{mQO>3^ajMhY>AGPF33Wc#4AS9X`LO2x(N$a1GC#R9KZ#=^SdpV zAhV&+caUgAg81IJ{UFI0(|t};PQFN>ecZ}LU$W=$khU6mv*6wU0R>y)!Nuv)$nWKA z*z%Hh669ty1se($^+KOnV*^;EiGRvk{BChW=i z+Wsl7NwF;$#l2>nVG2yW(NenqTd|KZM)zrv@v1lg{iSBXdWo&Wol||og-E^BBe&*% zRiM$!{GV5VmLc!8{(xUDT_>mG_5d2-n;sODFGI2osyg0$8z}ZV-;JEV4aBJO)kK|L z4NfH*92ok53{hh)6jr3)?5;1jA#WT5RdKU4FHue8ro09$GW)9?i`@4sMA>({>~Oh$ zKclkxY@x`)Xju{e^K1osfo@~pVv)T^13Lx3eNucG+wRO?<#LD>Q=wJ?I{|(?x)qa% z+S>({ajzqF=m?l?7C$e&ntj{BVJcu$G;4aPTMucM+guSf?kFl5yFcwf&!~Lx`}#>U z)bIpRllr+{dyL0oDa^FtPx|)cy$vf!?M=Eez#QhY@3NwA`BE08lQS#1^Fo;eW+HA< z62C9DSN5Xq(Mgt|-W{$o&uuQ0#KLtVHi}jK!1LP=g38B#;Pgt!{kwNTPQ@Wh90yOj`6ysDfOMeNi$dNtI|F$iI?hlp2 zI2+B><`!`pnf#P|xF+D4)5V4cswn4$=a=+e4S4VI$#{e!FFYLJeFcl-R2HL_0R3*R z+}9+o@$dz+BS*rFiHElDLjP1_5@3882Q0{AX%`U>yty2&<1_`_F08hBF6QdKP9&qUD(<{eLNn66%HQ6>O*6N4I?WAi-@F=x5~Q+muqRkLJsDB znY>@B2uq#8q~gE_sJuOF3eNS$I{O#U>4;ZM@L{dXmG+HLBL3AD6AsOsFta=-8$`Qk zN$$>c^Xg^nE=3gGp?h=$8xYt3Q%iR{{W|`{DnS*&?Fa#;pp;u^8r?a2>tmA{^{m#x zYYy=zpV_G{RCK`MT2=m+8sKqOWUQ)R7qB3ON=JYCj}%z@VV8?%B7gS8m0*mzB;p_O zHj_kp<+PspP=|}uHIYY1X&8kvE%M|kNL;XnB@e?>fw>?x^He^sF*ZRO48dWBef)BeUNv|wMS zs!Nl;6bBt}*m5-Ra?g+c7vzBm7=#ra8EI`qAKg^QW!L2dI21N$tO%Ub+a+EV%^zv2 zAEpEyt46%0*oY?BbOPiN6KpVE$-3({>D^6v)4%w=(CdPY!vHBLdAn^D@>T;-YCK#E zHDxEoPIApA`EaI`A(?8bM3*SU=E9vQ(Vq;8#*q&eyrkWl#H`)L_Ae}y9s+@pqTl^U zbG|%(*R1uHGEM`|2LG0PQ%tseCb@8w^~dIUpP3y22GODSK`Al2Nae#sfft@|n4@dv zXcfJ3?$PI1v(2K!t$3b)(D`(nDK#dAZen5s?=(WM*SafbJkWc4|9{NTGD!OR3(hJ8 zySp@$$#c&JNpSp>xBE{X_{o`*_Xr*3K>a>|c^sptg^^zwGNQ9!8-nm~h+~ z4}zEjAWQgh*6!}oIaRuho8J3(tRI*I?^zN6adD}b)>PqcPqU5>Y3yrK3hPGiH28=g zyC-0xdy^Nxp~8)9@a#j4iJagA@}RR~0&$`2iO#;@zf7;b|9o@uxOZKtAL9>@EOf)C zcc_0{-FffuA-S4}8E{Lp%K)7T1|Yp=9|Rc2=o#!8uy)wH?^V)i>T#~kYwt2Ge zf1YqsJ_{4AwdwAI5K5f!1%|vGKF!UV7MZkN&#@zpjw8n*ia)^{1DZZ2UzvVgwo`Ip zL~myi0m}YmK7~4B773L>dbYC%{8I>)Pfd#{%l_=)lvTVgsp0q! z*!Hno_kc|ncPLHVcZIH1KP8J>s4qeE5n8owCf@z^Hh&(J_hg9{$F157z|xY7_=4xr zmNKW!*vH1FVEd_^{KEf)gv31|O;dc#%fvGV@#nBH7K>g)$3GeWI%#PL=3pC2fSS}f zgyIzdl}%{4=Qh4&npK@AZ2O(Y`eS-7nNmjz29F%UCyD6vkM_%Z<5KD&K4v8NrGPMI z;If)qDmDh7=WQo0c2zPtX(E|C5$Dxm-AhKMGBbJLR2f4BW{ab$n&*{(?cQP%sPzE6 zsv5d_9$guD&SiT}1@oiDb)8-OV)Ys2i<>rkXx0BGD|k+s{Ec~w&XA1A%GAA|B{VKJ z4}P@&3x^=PWWdQ^A0VG!3}$L#?M>GG@?{|U&9)@RwL+2tm6j_K(6;D7dl&hQM2ZD} zsqR(oxs?iCt6vyuJOAi;<;AvlTgrd@83+C0=(*OSh?yYME*$ip5E?aBm;GD=s6_}M z)Z}s%>Sj9Pnpa>m7~zHnukrWOtoDB-Sr>;bKkJe@a}FrUum%r?pyA{yQVEx0N~pAr zVY0L`zN3753}v@M1<8mBoJ-Cxux#sht8D9UgER1j$z?-gAfPyGz_=6*ARWv3@k<>u zTQ%=x(q{}m9n(E^#tz3CJ?PQ$aSJUdzgFER4S`QApLOz0#$)S;Pgv6(CJHptV0NXF z`u36$WF<7}g~QK&n9{&w6+!&!F1n+f&ShEOU4iF}*O8$jXTqGq2F>u-`Ms?eUzneB zZI?E`8W&FY)z5Bm5@ztu+=Ctd%g-3hmMP3*{O@cb437C4fWg2N<%-MKzCda!wY`#4 zUwj1ocvW!#^N=5(ekPi}87Pp9c_I7ySns<0T(1hPF|8!23Rhz@p@a*1V-zZT6Nt2L ztdBB2D!+;c*|O1;n<(F!MyT`(y)b<+MbujM5|z9lhRz6ar zjVwp&J?J$3{ONZ1Hk}gdo9FZ_-#AxJYx>&)j;pKYySn*X6+^TMujE0(1Fveovnyh4t(pmuiYJrC zP#27|>x>Zww>}~FmizV_uqfp#Xs7Et3xIo0^T(K#v;6{i8eir3KQpV@aHVtxL z+SX0UcON7H+2>ZcC4lg)FQl%A2yz)CK4L+=e$(*|uK@8L-Ap$z~*?yLEoRHQf zXPUI~RaOOnipGV_30k%deDY1`^{#IkYB=2)7q}`eQ|M67;0^x4S_V-qNNt+5T7O%~ z%L>Ocd_)KTh?WvCo^y(AXwVaZ24hZQZ)I2*hSb-6v4IY*ngx6T*H%8*Sj4~G4#gbt zWs|4WEvJet(G%JF(d$!8JDTDMrV`k?OR;nimJjA zl}h6X1ki@Ej(9>9u>Doe^4YDh4|LO2VY|PX47W=y9@rKRH1|78c=Pm4ANW4{lDCb> zV~G#T5SkGBVx+i_k_-EhyBm#f^FKp)t8#M?`Yf)-73JkhmG(GWP3~f0$LlOsI|r_Yts6aPQ?EWVSb818XoZJ_kKPHK zpAsv(i65s6s0MLdypSHLNIGtFf;cfwd72-rX2iE?)Gjy4Q1AP=2dJ$2?n9CHQP+Ki z-h$Grpaxo5J@-q#kf;6zg$E4!5+-&h+5A+x2biCvMdMz{KSl>hmk(XKfXcoToeTOK zTHlunKU0O9%X(;e@KO5sxs(6oZv3Qq;0b%s`W}bT-eoCT9~JHy z`(z$BxskYman>uSBQA|2Leju5*l+;-K#S#4Wl(vCfyJ>0ZXkbh2l_A(*~oO+`}rS; zcq@P}5F1k`-GlaKDY6h)ie8P2u4YQNak){NXkBXtdc?me2r^$_-cHqcOVl;IYR(b! zMs743Zo(>G9wW8+jdJd#L`Bm`jC318On-UCWTL|zies3nN`LB!`*V-?Z9od3eE zwYC_k&S0~i=lk}|e!R8jTA$=rNGVFNRRwh_2DmlEM*ln@Vd8KCiW` z5heAR44w*uua;jwG&)G21F)eUbEo5(IdwTFkUnUCN%6X|7wuDNU>x~_tYyZd(!XRFnIHZjCtJc(a^mGslC+Kw25f-%FxLl`u{O60fK4)K#z(m~-! z*rLcLX&@?y!y`@vbTZYVd}_YytuZ-xT3EVt*eZ8B7=YLkAAlbaq?R4n1_j2=OE%4$a)v(@Kh9JxzUuu3KNWm%#7VY z1GxxkaD00g%N!4L!MVuYCFV4Yy|wp39}r2^X< zTA9ANUkP&8;(+wx>pqqRQ2p1 ztO#s`@x%h#0RkM%+n=5PD3Xyb=E%!<`sgwTUywIg;qA(Io#TareB+fN9!3~2*4del zCw1MP;DM-*{s%Q!V^u4U+$emC8I8UHyZd#hadYRxtrvww%PwWESkQ-Ii&mi*s8A(K zb^I|$B5sYDTi@s#!>Y*2<9{71i3J^qmOy+Q+rd<|7cElC2G*>@0S9I|#RW8!M~jO3 zosV9E%}4Ier+3Sa4Njb$N2mW#@f#~_Y~Wd274qYKmVW06qZmdmh~B5{tWNuF#fXPt zqob;^wmO#e->+^nh(=ED2VUvb?PcE4`u(A>IvwLr(;rTw!aWs5*x=@!E_{fwCf-Tg zAi1#1V4XMvt8w>H|7v>EJawL-93$5YVOB$}=Rbc%=!97NuMpw2pU2M6y18urCT=F< zmd%s2+7}N1g+E~oKV|pH!n=G|3E1{VrGvg9AJo}w9bQC3gPA{}*;&}A+dcDJJ08Gn zlyM6E%Ou+PC@L}O7Ugxw+9C^Zbl%Yg7!;U>w8Y~D_aH=~YQcVEqj&p-zwLhnEABpA z6Gaj`Mu{+f0<7i8DHR%$7i-NBHA+3-n6%E{=e&~3uj$j&aT#5V)>~Dn_vJyqij0Av zNwZc`hE#Z@k6M_=x~>$N1Am1&U>uOoR;Ah2ECg8WFV}UaN*OzO1rN)H|5LliZhpD9 z{~T&7#b>yKH*_oTOiL5kjYY)p;s3J$&;Gr_X=ogFtRI+Qc)rdv!l>E!GHe&%4;;+U zV>I*(dN~Xk3z{MOO8%0MpjYCkr3()?S!PX$h)>{-{+se=a?s7*J>M~3lKMh$CE){> z6qc1KFA0uXw4gE*GxXv#QT2E1tOD8kO zOF#d*EH}aEl>x#g;@2(_I)qUW1qQ2{gvqVskT}%e{~VZ*3ci#X?Ew5q-VQbW4{}z! zYAZ9qhOP!&(M`JK@St^(G&1TO`+r0aDqi#U5pQ;oTE>*L3DKFa&rwBqC9SIoE)*de zY+3>3uahdu&4+Fw(+INtRLy0@f@6y)h8wGBdR_V-OAu_khF?qE%9(F}@NSc!Q4j7mX+LA%6g=6gr5JL(^ z5$g#HG4mpO_BQCs?-1(*Q|3X0S2vvzw);JT$5IL22RQ8R(=^&v7A zn`1Xf>&m${)VnVY7Fsp?A9!+}#x_xxApJG|N?rh53=rthtxhYz3nVKO4V#ANJl5fd z+N5D!{QWz^SH@>Fb-6Wp|3;ccTIb=-%+$FX3#T91TG!8Tv|hvAKgsWvD6WYbwPuDZ z<$%?+WSkKYXh1E>9itM%LE zUycM{(JaF@+gfT&5}5Jv*|6~arS7NJ)lq>5+F}^uw|49KmT(3B!#U&wI`jN&83*@- zS=7hD5468;hkD8doOQ6J>UArG{Y2Z&H*Tc*)>FPDF+-C8%?CLAI2I@}L)?b#1-R%H zU@7cjK^y9t70{t&7Bhe*v;0`a!G&sUYg{S?)Qrrf8^ot1nmo#L)(qjQ%j>|9Zj{`R z6wH8CHN-TGjgt0LBAvMHSK|8`d0(^}%LWb1T58P^bav-~@n3irqG@B%E@ap}Oc96I zdXVarwwp^*NksnKn_(jQUu(&z->$D)j6VIBq3eSW_{I31kf=g`f9VceBI5I+79Jl~ zzmh=)7595*cJNHFx(47 z=z+|_eZ;YD&ddXe|80NA9?R?#pC>0RR&=Z^OZ-a6{CC4g-6%mRj0?5lHPMmVZ$x8; z&2dyF)KYf8hkkzDJudubuAbFM#!GWssi79P@0e_$V3Attq zCdz6Rbv8bnB#&{rahE{5Y^GQBY?U5SX0Hk#azzSWKxke({&o-@@NKGtTf?zIz-E~b zHVf*$!~0%S9D3x-2PEW45J>_`-VuCjM);$Jz)oN0^3v#no^by5qGd3L&#;-_Irb>S z21%JwmTAVsr%EFeWJ$dM_1HwU=4_SIWTrGEeC4*K2j0SziK@thVd?L4U(#q$^mw}4=ef|s z(MAP%?A$(zMZOgrZmLidyz+|oh6%mH?pxl?L?QKNw#5jmQfFrT`qpjt@r`F@`0a@t zWB(4Bh=of*1|k@9pY~Fru<{s#oS;Zq@k1@ynuBLv+w#my=OJsr5k#)@qv%^T(^Mb9 zU9>8>dHHU(A8LmxtoeYF1Br_`;P1Xf!fPvxWc@C{4KYrl4Da`(?W+`=Gkvog5;%de zxol>KlM?2)PF$A6%ox>$;en|kTLss@AB%+E0hA-~t8EPbsVd0UA?Mn5_TE!7#}bW`4X?l+2(QCU^$%il*|qUEFqYh@N3{a=iO?;|+uz%?i=kJk3kDMjRXsAg z=+>Qx3m@2k;S(J)Ro;Ko<64p0&CCP2tx>;bwm1#CY{62YK*kAWDYBeWQ2UKFXA8&P zA<#Iq{+zk|iM%%vvpZx>{b_<`yNNEV7a2_P$L5ea>{rn`eKwKn?oRZI*FS!LM9FDx z!g43=X~Y@SLWocS5Fa!bYJF=$Nm$pPYg;Jn?BD=R!tljvUx_LC4-(Or%F`w8sszhO zlSODiDasE62~|}p9KC9qz&VRN)}pTF)z<+nn~N>-FYZU-TG4U1^uef?E^3NyMFp?y zw4g7@C9nHWRP_BKe+!%d(9Ee5ZL%Fc5It^(Bge@1J-UfbvFz{ zFd$UHe}II^_wR?F20et~*m13+9(1c%qfo(@2CFN0va!XD&Zv0vn$)hsc;m2<&2Kgx znS(i9t*nQxM4q~Zgj4ixtEEdGVArPHgAj6to>;Pmf!-WWaEukIC~uUq_mbv;MqUgb zO4iSCwZCT}HwwGdhSxR8B*hcRkwY)h5+m0%0h#T^ey=i--n=)s{mTe6Uv>Bo{I7z0 zPEr+>SIWE`QOW>CP74W(*b&b@8Jj$SonSir`<5!V!IYvi;cMdU;8$i=rH|{0{is+0 zlFpx(=Kee6H6cdVzd?-qAk?>xzVEcrh#3%kDChy`>a5oe+BZA)gLG7w%)d$pQ{!qc zxH64hQU${9Mjy=QvEid0?qp)-j^v~K{=^`s0o+fdDw7lA3Gj8YB>jd= zRK>b1?=wcr2S2Wl%kaL@AQ4goXi2^0B*QVfh^H8dsZD>8o*;+6W-su4!gVYG3JJ17 zp>%FM#6Y^{eNm?)wT-I?APJ_JWbRl&UB9B!$(Qg3FNc{QJy$BdIw&2uj|lqjU&Rkl zH4eNXA#;r8^kuec%#WJAOkF^4U*v!(85x9Dd)xm!K6QGin4h$mmZ(&q zxM4gm5`h%w;fBPfU3a9CUO&=YkvXf1G0N${J_N29#Xm1EYDwJv)~dN%A=P>q^U`RC z6TnHx`5hn3#i`HOsED`65-fAiGIw-`T_1-*TE1f2f_W3@pq_?k4t1#~r|SB0)f@T2 z0Mr`df!y-sWkNB zEq`KVOc_`OIIGQv5|~$gec7oZ*cHp~za~a?D3nXnN%^umOEHz3^RZhqhv*Eg>yYPS zn?`}lFyEaYXZ#4SYOy(g0qjh+96l^S;Q#m@m0f;`g0TVL^?oEIXI>u`1V z0;-SDzdzPTJ~#E=1H|=j5E}w0P4Px@xCn!SfVEwxzt-w*tGSt3qEGT34a=`3$%OFE z@|{*rHEQe=J1Mw)J!XUGA8aCmEl+ zuSUzZqZf}fDcPT6xbgsBj;DXV99{J;Etq<%8u$$qBB#^qp5cU>-a|~bw1vJ#D26WW zy{PPuuyHOatCCWQ67kvijHVhU9ROnDh^lZ^Tk~2$8ISkapKGbM1%l{@< z;k9D7k>~CDv+OyK<&N1y*cGNU0P@LYT}3AZO`0nyvCAfBlv!anuJ%4KiP4AiUZ>v# z5&^0d@B-kuQ4veYZa@&2mC!d;?gny5vo`E$rKtffwMvXtJOn06zZ)7=?Ucv{8JueF z`#AfP28}MD?S^x{ssxN2%KtFxA#(h^1xlQqCAUbBuJ33>|N?f>X zRPq%d?Fr7d^T+xb`|m={5r!}NIDck;=5GtUS(o4+F3W#gIJI&~*Kd|%Td$5lkGi$( zQsq_e*$j33(;C=#Iy3se=DL7P)CmOK*^H;e&wa|<6h_PN_1}N-TCg61Z7PhlI>JWl zQ^$qJRco7uOb^<5kNJk8=y=R|zY_k+Wc3AiqMG zQD*8s;eJ0jkRDQU+|~}UH5)d@`nFJ{0J#$mCp;5ke;p+N7(*e$va~Fm%|Ep3f5L4? zU5Px<0(S;kJI+0P#xh~PY|E}2FL(%bes@{mNfR(a2ei`pe-$`2ZH0(oR_hXjC9qnz zFfE*3MuWsC$D+^*75Nu~0~mkH%`3AAXNCkbxl$X*LHBdb5>Rx}UXBKt?W^|WG<_K* zu7{^IQvD~1E3C{K&64C-BYj4;%Qe#x#$BEy^h2V%A{}jYaY4H_cxE7r>5mphG8Evq zDXi!(hU+lgP2&BrUPV_JEOiU~6X49%SA{hHq!&obal3&Q$*p_ch0zBi0XNUi|ZK-lm!El`FScpdd1i(R0D9mU1B z|7S}DAlHP`sJobW6jD>{!0&HCzsCDC(i$N>dgxS&P(3cgz0;#1NnhbOudI^NAia=Mk~qpEI@ol z!{t@s`GVWRW!9>CI^vHD3c>VLbzAu_w6|-zU84VC9vxL?$!k2m@Y>f@*hgR=vC1yt ztl=PW(s=Wnp58WDToTYLuCPO_q%f~~YH`=Resg+v?T2U>igHSRGj=#K+p&02^o-C* zB}o4OYl`6jR=Bu)o!|nS4XN7n{KGdLas_9ne(-sm3J+d8B;8NQ?#y9byefYx5#eg< zeE;XH>caRetMUQ=-p`yq=z9}8emxCz*F`X1y8+;j233qDWKi1DaJT>`GI-~uB@L^Y z>M%Z=@CH*qOBN{l%iJ~`LL`SSXUOou0)PNM!nRd?Cd`9Rp56al_oD0#4+(3QjmaJN z94{B@9Yyz>PZ(Wj)iruukFSQtUIMsfhMI&vqSVpTbr0`<PsC3-#g>7J7nt>t z7;}D2i`9T22`~&Gon|L)m=_F5CQX--4Y=;eO1m*t@9E2AcJmbPsDQ0zglOZgz?kGn#9fww|pb3*~85TSTNCzt_z=q zWb;c7ol`K!xbMkRtK{;{CSW5Ylt_OU!dc!Cx-LQX4g^os?YwH0s#;DWJhw-H3wcR8 z=|iSD71!0yHfbuArDZ|rYsEF}4$qRAiE!z&ut&c?-1+}1i-4|OZ(Orj2j$i?pDW=o>@&ioSUji;2l_ID#+on;Apw~vnWa_`LxyHtx-@ZobrwgRq z?S~RYyRCZzO)x!;VL6lOKIi+wij{@tE~Kc)G2t2~^HTs~@_Kypq)DNmV#x0c;zot1 z<&Fuge`xfY87yltuCiKPei&K3ueVwcH$LO@5O%WJ_*d_j+8g9bz{p7j3gyusUJD|w zFa3lV?UDCPUQxcna;4jNBEt$=sC!k&lqmr}IZT+yf$X3V{>2$fC2%t>F=phh=Zg(r zxCj33XN=W_f}$JM%dv!zH9yN_fU~1>KQ+Kh#IFd=wWvWq(={hRecMHn;r^xs?cK-6 z!*!en34@ZsNxYG1ez&Pa*5nR1^Oj~`sfv>TzT^2DJtLz#gTEsbB=_o~sPhBIA~@?R z^tj!QXwKN5h^WK7QaauM*h3^*LnSe_#X{(f6keaD*-R|)^B-A;C^|U0S4FyS>QN1% z;SkgoMl~l~g4TVyI&pF}QLC*IW~`mrH=jZi;O3b~wx9(zR>Qprl;4+*-CzniY`9%R zaHdhd4E5XROH*wR(jp{Sdj>EJsWy6OVO9%_S|K?|wU89cH>nj)l|!r6oZ6H(Ju%(r zGcijBS8sy*)6-Yu(9b?;IRrDJi|q zI<-AA=Lc#3m85}*nqU>oZ?nNzbJnlU6qzA~mP1AVN7GqGMfts7|DK^kKu|)ukwyfh zyQJkyNec)_N;AOBPy*5+pmYgHxAY*=NK1Ej4mmRO@O#$!zq{YvZ_Zlhy3XGFvu#dR z{rx$ds^r*qRrA?-pi$T*j|m!lHg)j9PDHi#E$cP`dIAOl@Wt+5hxYedGNC@h<((Q_ za7S4v=~kqX>%|v=AQ8UBor4E#m#*^8-u1&|m;EH@``^rmkMHD}BPeq@+cbOa;^U<@ z&z2X%kSZV8bUg zIW9WBQZE@du2mF&n7n6xcx&lyBG6w2nG-DUd3SN0%S5D757gmi5!-}h5@FG;bOiKq$S{#q)w5Rt@PBQewVv?ZtBMsP&pD5a z?4=yOs$}f&T*x;57i@@I<0094!y}jKW-H7HgnkUqCEWHa_&n9ie-3(OGBrP%VN_AA zNtrGRsH$yh?xyY}Ff>?B2hIB?yaYQtnMwnC>^ea$>F|cU=Eo zq&MBF7r#sDdh-!7);lw%f_ZXDVM1wCi>z>C`6#+)V+*iNfGDu?wU za-V!55V&uXb^y{@ys>89KOn~(Gv1ydU-~6+s1Ck_6$Jf`TfM-$;1I4os?3SY_MU80 zqAGY(9#OADz*XFwRbEWk63jbrL~)MSPlP1tI>n*ZnWe%_x9KOaJ+=P@;=MHSeP&T) z2-_84%S<3146?a=B7$>}tHZ%{z$vZPOTt8>QydC;m(L3KJneezr+7nEEpaDEud8Y~Q=?@*n)0ZX|SRbN% zS3B1w7}X-ABF|4bW(wN;-|?7q&G0?^0@uIS(ELtcT=+Pk3a3;@9nNN^PQqp$ zV-(6x_?b;ki(i)OH_BNekLQ6_`S#~^&<-2oc0R!!lif!@=I=baRKd**{CW)UME!6l zUUl^Z7MEVWa2qwlvU@~~Qi>4!jyzath=A^o^Sqq@Nj43`(T-(<@ll@6VZ)tUiUOGk zl<0Bm{cC~E*BSQE3frwJ*|KVrHo8TWZsQrgL%sRIshmaQYZI9{o-H2^U->qYEe&$S zmo9i}L{Q7^=5xq`i0|0Lz;1Xqm6$A~Zre{m#zwk~wR2|)pbidbkw5#Sld{h@PYPmV zwKxE>6yLaRqxPr4P!W6pXdA!*?#I-~jO#8hO4c5XLP4-_j%2%MB-0Kj`+d7(T}=}m z?I*?IJ**#BXh{(Ngi|TU?CwphTPH<|USHKhoxoTLI!a2*460<4hXe=byW z?-R{UGspQE_wy+HFNL#5Lgg(Vj*#EIdPZa*N0*q-hJDd5qYemWdMV`k|IZ6RZd8N} zaQ;XTRuJA|Ej{D$+N!leikax#VK4`umJnVTy3GQ(TjJA`@x7C*3*5VHdqLywD&s>J z_`Z$sd~mZ3C}+sOYE8h4)rEtt69@MVIiPm_6;XKrr}o%jK}Xi{S%$zvLVO3p3f>xh z7~@M0g-bt3F-%$s=3-E!7Fq((rJON#Jkp6*>2KUuxUtKs@S}_{ZRJGhrTLYgRKv6% zcfyBJ+^LUp1P12W!Y`n*L5_zjMG_j9Of*M!>FgPY;X#s0(5le40eVfdoG#}+^lTl( z7>gt5g14h@g=OpD^wlD7bL({Q7UCq&NrfU`aVV(et{HDPNxD7#zw(#Pt%lx^(dUA_ z*722>e}->bN<~7KIq!y2bq)oW=$?&&zJ6}-L%*W3t&7Me&lj5e^xyhF&yB%RnzZ}* zC~!@UA@uLp{l|@h9J0X{Ml^a&);Sai>xav;4IJkZyL(b8s|h=6Hr-`(lT?vRb{wV{ z)1>x;NLvQ*Pyj1uYLIjNj(E{&F~rA|N3 zuagTAomBo7bFEt?mj7jIbTSVMrW$e88J(IY%cv;cmoz32CtP#Is!L&Yz!wumr#5YL zY(wh&mwfm-Ilvp2*n{y!S@+^v->>GA7guCl(aMXT7z9^$#vS=8*XJud?2RroU#7gM zDV8WxJvKosuxa?!_bdZE9UuKwiF{a*@_ZQlR2!aW10bS2yk!=W)GaQjb9`MfpCe%{ zAaeC@s1wt(7sgFW#7)w%02`C$E}Y03Av4fSC76;w2MssG&mQMdRwOv|)7t2HOy4ZE zfwM{w^+IT~oy-HJvCKY(aqK6mH*|}A8EKrhDwNb494ADx;<)&T!z0oDTM_O=RYETpGa(FaJp?#-n7Jv{4mgajRR+84i1z1A<){(v-bVyEQ*A_ z8N_KqEkN)pdn^Hq22q^)e)wHHZM`b@psoRFxTepO%i^O{Yknt<?o@!9-8Sa)sk z#dT=}7ZH6ZC+ev3p0B{*vIx5X)-76XblxKonJE5Ww(M&<0<~VH$Z7<@#Op@=5XE`u z1@O)J$#{PpJ6d#!HXPo1*AArk2=njs-c>~(Y0lgm@&s^mQjjhWl0>(#lCFOUv6@XL zYrgP&CUm7oG-UxNhg zUz?yeN(6e2s0O!AK8n>cQ&0m;|E1++#knUO$D4wPP8;~bTg)fq=NLYkJIuMw6#P|; zvRZZ87)XIg%ZM}e-$qH!>Ik#r=J9Ki(m_?8}v zwHK@=P@4T?-L*Gn_-2SVqyy-w zdBov*Hg@)rW5mL0=LUBb2-xi{7TDq9X4Y1 zN}ADd1qZzNvn2b6oNxr^eNA{Xl*#zny;~&TA>()sqrfYEzehjz78(T_*!fKT*oua2 zQur)1!iP@o9!)$obKYxkb2nc3^|?ln{z2A)F+cPlyqP~fVjJ?cpqqrB5nQ+Rh{NEu zPi0>}5BV;_qLq#Gc5Eu|8&6-JS;<=~H#dras6XFUb_;n{YBl8e_G(L!_vp#$kvcd) zHXeK^c&p$Gx~3+CxA2k)s%(Edrse-dM<^%*&E!XISr$vjV?c2oZM_g4E-a+iUx!2Sp|F@IX~uIyjitt(0`(V5LVw0~(ZA2i-{r6m%ww)>%kIkDr5RgdzWFq*@g-QCZ9t5m#BWWgyE~vdj7S(UHY_~#Bi=DHUKoZ7)VoUQ<4P@eG0UDRoDZ~ z>F^57T1JMO?*6fC3$HX^DD07sH;m{m5ySOu@ZX+^++HN;11jB|IY{9rMX}ymVCsvr zo=>K`qeX8CbN7}lp*rll^#N!-6CW0y(b>NUL zZkKF<7ByDEHwrAK?biQl?TmLi!l)e#0~&hk{-w)}HT8gOs%uTD0>xc*gRMUi;YLpX zA~bu4DA$Q?H#&{vKR3>fme&EhWBi6Bk>Vi%H|<~b<)@jDHeC?7XU>5{X(|dC zSIXEQS}*9~+!jMSUR4<(I*L8&N|PM&(#J-Z(M!mG-78uTNu5mMYJ6?>U`vLE zR_vRbr;0>Bi4|ryUdru%;YXb40U1efz`ialTb ziG-$|I}rt%-ZB8$+Di;$sd7I$KVACW2Aa0+s*0{LWI}Y07Me0@y@lTAGP!Vn`N(nI z+SrL)nUND_&k*WLBi)imb+RFeg_Uzs)`_PG<3<)+-5Rdq?^z-9~0 zm@EzHWztSn%>!-neg=S0vzGO2SSiR+hjwbyg zVgBQYTd59ZlZ%O#p5}D%Hbk~jM*X#Tps@OY{qIE29i zgr{*_4U89ZtJDK6MpZ-ep$)>+6x zTZ(ze7-M}l?UNYQ`%QBh_qg{~+nL{9Zfxp))=|X$*%1lbUZ{ z37m2x6@B9CebsT%4>Xky^_8=q3D|;ufb#?su^9+yag}<5`4>lH-O5Dt#(ca-mf^kh zL)+QL0O6xMQ!lI0;@!`oMw8Mc9chMXu5k_6R2aWN%lZ9_>?)E~WeTb7CxAM!WG9IQ zF!!y*Dt(B{*cFuf+`Si4P2BLSKyMffKIhfjaiZVXO=v!+Is%%pc;&Zw|42=!&JM;4^xL1e zZ+>Sih^Sggn-0jBicjHryZ7hE1lYsd$5c0lk<1u0A*ILD1UE!K&t>QmmOU8KH`$OB zSA@}EFWA2~y>jV=-Av;HIxQa>e(lUS2QBgbdkQWbQ>}@x|3t;4u03^d?N|#k#=KB?PYXY6+6ehXO&!Tgv$i?Xt-{x% z#iFo6ka{dHYmBCH2$0H)UxtLC7j|3=ub2EJD|*6$6}71Gr_<*p5aNL7h7i09>j5+! zSquQDMipNWZKOCr)-aL~k@UAG=ey!Ftt+L(Yu+WfG)6yJbN>O!J;c7O>Knezuk>2D zZFr=8o-|`|2t9!Aq1@!}{M)+N06*~ki@@?P=H}&>9RT}7;?!A6P^%0FZjrLIO|2U< z9=Wrf2Apz?Wp6uv0{Cs9pD6X*&SY2L@$nz4x$f@2Hyaz@{~+hP^saIZi#^x!oK8-| zL%tK|pfZ8K*^=;q7{wjKMhp?2LR2&y7rVOmjk1%OA(3((KU8MbI`^Oa)+LKwzgi=- z`r}4}rzL<(Unb>BUas_lNc|a;@#`a_)rt-Lb|T`v)PWTU1|?piE0745`|tVQ_Y2Kw z_dj^Sya3d9Q{vbW^5o4Yh7+A#lRk#mn6<{@$+xoMAiso1R!z^`vUAkm46OJq{ToTq zEOej3Q7Eaz9ZxV~G*Zd#(OoIm1#kuxIDY1o-g6rz|5PKY+lw(i;k|tmVAJY*7Fdw= zLe3N3+!Lg=SP@{7mD4Vg7*8I>vvF=KZJrmF5cmT#;Vb@Y1d2waC;zpksMSpU;F+Q zJcNZ5d5aZ~NE6HbcUwVs#wq#h9U54kc1MRItKQu=!P}QdB>ZN4{a||4t2@<1se>~1 z>ZH+syW4$O*x{>Cd=NE>4Hts*SW4UN-@jPo$boCof@q_I-K*R!dD;Eo3+xmO&cjM; z6Uso;2YhlmEd3Rl-f$9k*n*nUy&Qki$aYnz+S>y4qiljvDoxI@o~E{V-yzX!$k4%D`^UWDqMD4 zrv1hkgzEJP`)xul+3F6^ao9PDPgPcRyw@JkhL{oN({}s zm_?pY?B#(zsE#oAfO03_n|4TpAGHn=S_t8{EKBkY`BISGF95URyKw~_r53{D2HB#_ zC>SX|z8O2vs>1m)lK=>BI;2ecK@BT$9XkWNleGU=sV4dQp#u`c#zpP1_OsPZm_B5W;%dB(u{JO+4t=57Fq2CTu!~_n{grV0@nlv# zs)sGO;T5KSZPQl+8%QAZ?+j)GLvU76#@XSS(;1*+5f8?CKN;##PzhCkB)Rpw`Q0R< z_b&>5m^w6FhxOB5W8=e>5};B3;K+Az@FFz4(!{j5^L|1Lxh=4)WGMknqI@tSod!nX zM`9!HaHS0u08u7_T%;ujh&S9I`gX_SygoP_^N=ugJ_N5a#u}Bbtb7kG%PFdoLNYv@ zqU*8ctpiqq8exLoR8ZeR@2oFyD!wSCk0p;2mls(zeSxD7@mcF3UNi!3HE8eZ`-~@_&?s&OGZ1W zCC>ceQTyGePbp;|_)arTPlz5 zoG0S&yqb=1>0hzI9>kyWexZCM&-F{w_nsHlEY&d49VU{EE8@Xi;U#R2YQBHOQZx&t z!(A(q(b91lf2iTjs0{33)N}heM$LIaWUu-^R>$G)S!UY*G_U5*J5SBRp5Y_5rXqLC zNRfNyqmoKIfrNAZJ|Nvw(ww$1wz;f!>5ERB*%mR-Y4$Y_`Ww~`PBe7BmaTs60SF~n zTdm~;#d4R-jd=juzQAUhtH_Zx&sB~u68!uoq6|w6{BwEJAwFZ46`ZE$cQ1Tk5*VU_=fQO&+x9-YR1NEsV*?g` zcof411KWOKtR*BzR4m~|Mu)6SLY8|}_!;=nqSTP7Gav5hy@Nz|oKxk&R#z(RpIo!Q z>$Yx3AU|u#r0?0&XKsnRYUf`jQ5jdw%PdReT1jM;_Ju_Nh~|4WQIHUrja22UXAUqc z%JN~ekq}Y+3@6vKT%}y_XJ5zos4im?&gz;yIU6~fcI2(GQf*hCP!i}x@!T>P?mO!y zC_nb+n~vrV@*n3(!Aana+gQUutW3TRrM^iwO9?&BSC;=|g&zeQ-fGD=hS&%g$(_qZ z2A+PSx3q7SKL!zXUr^oz%Qu7<=&JiSd$U#S+~N+cZ}FTDaOIIC@9?l zo_QxKcF7wu?~7rgM@7cJWyE@w+kX2Ec*WFn>|5gmw(1mV;Xly{#TEUo6mMoeCf0WP zuTwh6jsX?aQjOHmP>ScXFc1&s7QTwdL28-XRiG7iPC5I ze}-Sk1sH_J8?5a+atrEcvi2!(UB?#~ptASKllD;PMtl6&_K0?ltd-3~_^`kow(2tw z89p#z=*VO#Q4wAE^lj{n*Etd`QM78n0sFkA?8*}y3nQh;dWXThAYo(A&+B8O1MqIDyrsl}8{!JDC!-6Bsr|O3)gO=^N62c~5kA6_a zI^p%SV&}_j=yVjMAzqTokfEPE`4{ zqEng4wIwksyDxMknom3d)-6=fzM=Q(YhZLAPD$(dUhHJ{`p8gulK`?uEEwMd-GBToXJEop>E*U zyA(iZBN)27HRfT^q4d(YBkVueIIgMmlCJ*k&sx67_rl*SAu%Jc@wAC%0rbSB5b)oq z4Ag=GK$Q%K@_rBp|s zfDBP(7cc0HI@Eab)L}yL%tZ~W>-(z1f;|s^K3(r{8b!0-Y>o20`s-VPRwA5jbV}ic zdjvm;bomUQJ>uh>=fRHzkZEK9(0gD;SYC#1nis?wKBJ`Vt013AGLE= zVDt>mxhQ#JH5~jil=W1zfJ!Pf;KH@V<-kzb{&Bn>Fws!!;zepF!2vfK&P#jJ_Tx{3 zKPWvfqJuC^70w-|;8|qBCgj(~`{MaF@!PO;#rDfAwJ@^9I_( zCCk3D!YlKgzf9u4@4lYgkWDOE_3=L@t|90)e*Pve<~NxM#n+8-k!KZTJ8~Q>39E|D z4VuiMziP5FbA?o&bAfR5$lp*clvhUhV~WenurVz@UV1>9z2{x6Dw?HzJ5i@-sE*Td zdW%&kAApzmO&i>8ne5yyS?i93x=>fYcat28A$0XdoWCpNB}x;=KQ;d@F$qIXlkPlq z7j#Wo8qB--QXYk^#{5qso<$918bqor9;u~iU;p!pjXvP`Vt3@^*4o6QPcZgGYC6Yo z2JsCrHaBWQ4txXy>BG2)4O8X%`0V!2lZfFT6JAk4nA&Dr4D|M}BH=gbBE) z-ruFt(;R0+YpI68x5`L-7tqZ9Zl$GFy5iS+yyPoF<%lz;3DRip5!Mf}rc$pc4uqdk?`5e#x;vRPGf{CRzmY&e9z3~U~1P+BUAje zX+QN~yu^ic2DBX=wwb?Zz@hcA~4lp*(LTV$UK<%|5hC%W6wrY37(g5dE^!mBA|%51hxxt!}We$KON*+j|pn30ggkqh?AcHyN_^{Bc3N z$t}3OG0|OR69ym3j7*k$UswX(ZA}OxiOPDhX$CMcMB@jsBnV_i_o0nEa0fntt=`F6 zVcwf^5mZjaPuP1BG&v?84=jSOPG(fLZJrUry=8)J2)29r5(3#R1OMHo>+5!u-jxR-))=2m)rXCfJgDK1()n+r?RW9hReBw0Uo^UG5+`V; z+fr>5who27gS2;YR3mdWU?c%wyVS5_KK<>cD*UfwBwb>B*4YN08Z}9$my1h?62UKJ zkV2lK8hNBRJABJiWAl{|6Uf>|3n= zP4;8>@y$wSmUhQ#_<;=|1HU?&{07X588A4@zXk&Dum1r-7@dDl%%xTy155R!$uXaE zE$0gN101ucUW0+?XTJkq`FFDke|qdqWn`_SZwktdJqvxBV}FbkXm>h@40O9omrVMfbpuoi`k;Abfr(w-JT9{^5~CCQ)jX-nz$m=zcGBy@p7q zc27M6q^H;B_mfzhyiU za^TKY`8Vbm0~}kUz-iteQQQfT!li1hq&+p!ZWB& zL<;d`$Ved{pl)0-8zu;V!XM~)&Aj=n)UpM8T-KL%jt}xH<)!k@Bb{?MH zRWO5HumYqk{y5Q*XV{(YG1wf(vpkV8Q%{vDUR#t3tga0Y~~x&9j_(Irq-YvC|C- zmz4+q<;XrXpA_N7lHrdtt65Bq^Vf8~{03eqYyEGLdh7o6q7JeT`0fcA*cdHv^BW!7 zJ)8S0LvA(PP3onZQ)DTj{rwtW6lFg?OIy_tx^Ge2Wp5*$XI3Npm@P#w>1oe}Xn?^H z&3Q*zx$*K~6PVX2o||<~*zZ!y`jK^A64XmR+hy$ExsBKH7fXem&P@>~FhYzQb>*l9 z9Adem;KaqC6ze=`5ASj?~d6)p_zf+V@mHZJ<`!5mSj(9WSp6%)xpX=Cd+=)Iljw5%yH#?L51%f>!lDzjpPS_CGLWf<-0>|_6c3zL!peo8aU?F}Ef(2hBewGPK5>)rofi#W3)7DC*@xA= zY4AUK3C~ZiTY=~Hr?+;U8NB)IXTMOC6_@U`>@U4|`NT`?%WD=^^U|wxY!c5F+gx<>1U0D_27fxEc7*`UKzO;708K*dJ>@Gp6^)WacQ- zd~T_^52_>Y(raMcq0HR)22E`VeP%18;>v7g{kL`L>`&2;OuLIWw2G3sjQ)9KJ5{Nc z!H&Btc~_=4LL+Nd8uv3i^_?){X80oI^GQL2hgxWu0kpP&e;6|ui_?DkRdvs@0~yG8 z2fI1=MRqe2$JaKnTBN8_`sZBMOeIDX@UYsI>a6JQNZh{q_ z>5KWtI0?#I_yi#Nju&}30sH0jES=6-RL2_n2FGh=Ykvr4Zm6oVYsJj zsS=#ninmz6pZ7$0PIQl901u&B#hOP8PVA)nML46ljS(z53MqVNYrF0EdfOp65(=f*$hfRm7=v(j`YB|JKs+ZyFRU@ztKaQoDI& zZG9Dg?-E2b{^YIievUgf7*RK`G9`f78esO%-gKLM$I#KeG#w`Dppf?7^RDF@irbE? z=|l_w-mVpDfKVXLb;n@6TxJ=)MC(bNW@ppH90TVN$2+<8AaH-Ly0S_VJD(ZlwgArp zVYYXw9z;$JKiYUqz5rsOA~k!r^z;Kv>4n0jm0kA_)4w_Se1p3Yxu)YVU#a8XNArGn z5o1eL7=mwf(k&by_sGNU9T8A9>1(MNmIUA?I43Za4y5)Npt)gZnjR`e889v!JM^p+ zmd9MX+*Q&I3^2#&OB<$L8bls-SmI<>k561b<+@rmzh0Lz73P}0A}JR)tZFn6%1T*t z9b-vnA&9Mg`FF7Hd+3j@*^()-pt(|MldD{PXPBA3dDRc$Cyl!p`GrrS4h!9HllQ+E z)O+LDnm>W}CU+IPEb>VtwlI#r@6utM#UOzGhrT(gHpU&;}aVl~Xe zY<}%_JQT$Jc%Cw-s)e+3HF&T)UOwT7@eTH(3)xpo1s|oodD$#8=k6y%@;WfBXXR>a zv)SAS<4pmHW6fx!CLa)_Kl3gFWIZL_7%hSGWlm`g)|)kK0R3+oHpp2~(%}4ElTX$k?~UeIRT%wpzM{ue;Po~EZ^lk+2 z+L%ND5u1wMmK-^Trf?Q}-dz~w$giMI%l2?x=JcLl$CZumS#4oNnLGit_^iGr%Fl27 zTf)`l9Vd~kmp^<|W~s$##^0YeeOcH)ldwSrDpC)`+U=YL5dBAP6&3hmpjF1m4{CDH z+_eg~C|^c@$Pm|xqc3QM-sDtJfY!4iIWkoQ@Sr!F@>CQo0QCHkSKcT{&;xrl7iV7M zR`Fr8fKvzGTKuLCve!&C;9k%&t#1=*M|{hDwT@WGIiPd&)fU!ckTfaEs_h8>ah`1@ ze=m+HdjL)MOU93N`rrD0i_IJ{E^0j&F7w66Vap{0ZrHn6Og7@kxkKqHxiTrsghy@K zD3#Yk75R8xt(&4na`RgZoH&3WX(An1(9hbwxxymhua=zGg0D(m^pEUq$r;clBjt(k z?%PWlrq+Yu%K_BY1GH=k#1?Y$l{*CNCC&V8gz9C6z-|;P=k9PF?91_8q!^jGUs8$2 zZd1c}z#Zo#$&ChAp|>8GXPOdEj!-g_D>Cobyf&U_ZLCOqUJBf387DX1LJ1GAt&jHR z&!A68vb_4y$753)rq^AOX^dU*VYE8wSHHa>Oo{V9xyXMor%RJP(Ttn@*n zWdj6bb@(7PDE}(Tuh;IJu=!Dl_I-0||MPi)!8MtZ?!%=xU{ORAbRTck!shuR0f`o? zCbr-HdYIz#wZpIu^F!)@m2E5}=)Wcjhx;F~bmMQDR*oiD+?;U_SJd&DGUaBLi%Uo~ z*|;Y8w&?`Hj|d*qQ+14R30YlH_14UEZJ5TH7`sNReW(_B{1j#h)IGLFJXV6#kbvi`gL$F}YRO{O$j@-M4V70*gLqIv3f$_;8SU6-kTXVR?{%dk^ILX0{`` z^#td*#+s+2k@ln5!?U`D>DYDK4RQNF8=b|LGR(8pSU4T%JxYIMbo4j9w|-|6E3sx?sxq#Jlv;kOV}5lq+bJ zI9}z3p~{Jq9H_x4C@C78jGYlX*c(xCMr4dR3l(SmNmlBs93QYpQ)UINR(Ktb$nI0z zkNaF}QsFQ?1`w0jRWf0VcewQJg}b*-qYF(l89k>L99_m$yx3dY*G+7pe}v#5rb>tr z@N&NhPj~NxAXgn5C!-SUu`D@&t$g(kOK3Ck@oOd9_=-=p=&KPVj>6zW9*@j%&~#sq*35 zxxc|dy@HJ}rGr`yNTkl>(tr08#)9R@D(<5ZjM@6dRU;`JyS*MLC8df%bs6M&zO^+C>}9NynWn-cEI!(P_z@QW}rb?H@|hZ zHJc_!j_S}&jL(c89>w!|6uDT*0a@jyEZI&xa4xOEF3loo7is=*``~34+mOXVHPlTF zYiSNcE2(sy`E|n7;$yHj{n4A8;@^Z3`7+n&JMvwoSAdO)C9QxLCI~m_%gp@aW$tWe z3)IJ$g%?9MvRFZ{$OSj|#F7wKgv9?DIR>y%!|8S@tgHeWHh^)%zQ??UIbyl<-ATETT-irJZJfdq)RL2KAkj2&RK;6y^kbr znL8Ej8T@#%eqPH`(6i@P94FF@YYG6-eD?#g5+YWp27W8{{xcQ~31V22X(b<8sHZ0P-lNPW9=3;$SHyi%U~7W;8>1lm5XS$uyg0n2`tda+Tn}(o(H-{vl2bWS5Q+i_}$cm=>wAu45t+IRk zre2Ej4mB&HP_SOdP=3&etPRPqwQifG@)~}fW59R5pNaE3d3T<|YkD)P$p|VI;B!b< z02mhBOP0WFYI6gcMkeDs9##r3p@Za7eynSHD<3D-f+(h`LM#DUNV^7UgNyhl>DkBC zsMmYFWo5}B`|KVAW(4g_e&vT= zt`rI9Ts9wM09XF7Ru16GBoEIZ^D(e;dym9g;sdrG1L+RnrqAWvAz0Zg3$WV&UaFjF z^)_zHl~Rs7W^(X~UcJzjGulP$B>s!#9{Yin$x=zN-N}AkYa;A#2W?${-isf;$T?mc z*B$Pw`hBjbbMIvg>4OHif-2NUw&+M`EzE33z^-H?d`Ck{q@CQDeU z+j*O2L%bx$!!tOp#T4zN0b{~heEl@cX!Lnx1eAo}ADvQb&*lcX&~HoC{Krpf=X`l+ znE31YR*SxvuY6X;Gp1@;Be@LkCm!ogaIJVuFl@9co5=s~q4uGAu2kn#tmt+-3cZM_ zk%Z-pNZp+5O$Bu-Cnt+VHnx_)Y4}=pep9zx*^(`%;pI(U@rGeva99ignBfx%{|)=X zuZXofZoz3tPCM^(W12xC7GeJWZ)b+=Op0I3G`Uuvyve0c+C;-`?NW1fow1HXNL~2k zzFvLZ|Ams<=Z^!ON-BzzP3T1`KpE=Fnq)2L`;TVoHXD2T+JIeg(sR$OOB$a&(@?91 z!0%NYg|c(XpOxHo>PL!{!<2wN!sEaDBDO18`L5kRlE0Zn>hTWC9^72w(_ybT?C=GL zyAb?V0WK7?Z>FGn4s_jbWmQ0as7DWG$2?Wu&q9wpSr*D0=~<&C_Q~3i%akKBRsXb# zl~)n{hda}HoFG0URqDXPhmFZBj!X{`ZOEk>{{lH*zp`NO6~6!5LYq2W&m>6W|Iu`o zVNrhH*S`k_7`g?d8);GL7#a}?6#=CiDW#jCK^jy*IuxY4V+57%Zct+A8fpfZ|9qe8 z`MsGJ^Y-4?K4!?$i(D}};y|74a_{~t z!l%guCR$imw0AH-_kkF~6#q4)Jv+Qi-s&0UmDMtK-?Y}{w`!|$okSR+*_q$?=XcRz z8i{4^f?31ECfdbJN2bZ`+91V_wK}qjfc5fD#@c~fm#!~qXfV1fr7MLT-5q?CV0<)0 z=Yc|eT0wmz_UO&Pe(nti<(n}@(!Q;@t~o|0aK$cm*ZYs8A~nJ|&5B^WQ`}Pdj1+ee z*iDL?J^|r--iY)U+KD{82?lPWE*Y0!fqSCk#7W-x-e*vCStS1ZFap>O%qc;IUFx*3 zTCyIO%r>sIK~ zK7exXZHO$P9vBc?=ON@#_h3xagUs2U3o5fdaZV7InDc(`RQO!iaL%mJVyEg0@UnoH zT9IsKev6HtBaWS$Sj`RR@$E?V#>I}cQ8WeAL))ZH!IkwMXw59{sTDLprmzsg@-!44X`U1rAh2@o%)Q@F!Q9Q-@s z5NzaA4`PwRv#?pE_vYiw?us{FzU{Xw+0K_atLhh%gERX#pk`&GXP}M{JLhd(NKhPy zE^KA^fw*7nh2yQ}3sgOa5|%aDdnzLBl;;e%r7Wzq~mnjxWEg)44byMkv(>7B*7Bv!<0UY zUKFzZ``<3)J>Z!mj>QyLkWU=Z;m&q!vK02!BfT4sqIHd*N3)Obs%}}V;__U>;BTKK z_|lPZF-UIGEuRUCJ!U^9V=*Z*_gDR00OLujiUv?z&Q+j$~EC}HGN zzt^bTjy@nA?J@)=AJ}syf*a68sc1Up63)GuQr-=2FpDPAV_dsoh9bZ~Wn(B97KTN` z$P+W_eWhgSl&1XYIT0D^{sVe6-*vdXMU(RlKX?)Y6f_H_jka8Bt$mOze=*+zInnyN zfAUcVFy=R7{lE24HQt?H4i}_#GE<#LB1#KC^o5DAHRHF~GRYJF8wch7vZzLvKT7|t z&|X|NCkc67Z4zpsi{vj-tfF@Ah1toaVsl_4Qi0Mk?t`BwKPtf1Ay+gI|MSr++r?K7 zNVt>Q*|Rjn{$M9exc$Pl0$Vu2;)o;(6u|io1H!R%omf5&s=KHO%j$>fy zC;zeyW1Hm1p8%7L)i$GR@3Xrg`N904g>EyyYdlmnb}9^TR5Z~BBCI~+zpmTU&pjnI zC4~*st+QWr=#l}>RhBQ*MbqWR8Dy2=rWp!APhZ>U3h^K{-fpeSM;6H@mr*0e0Qc>V zbw<i$KYO&1biSl^=e zz0hDbVm3?o#TOHzi2kfC8Fl*_-<0pYti>-cJFwM+d@X%}T-}?NS}l@)V?NGJ@)(}c zF#vfJ@Xvlx_PB3`edM8s0_=dIHb?ez+&*hbkGI zm#@!YanCUywc*6%igPK^ql%)5y>l4)4-`lcFw?Py3x+b_=^}%zM5M(7TtmHRD?udS zXUH!y>tV2*gVn?ew&Vuq)~jJ91Aq=QIm*%@Y4 zD6Z2#T)Syga?^=U@KNF2fe#59RC0su9_K%sl)62*>`L!EuIbW9v(=d>m%(47-ZQ87 zl^{!_#xu4f4%feTKyv6)Nb0QgXGDy2piWGgcJyL1ZtHQ9A&Kv7?rcA?m;3{1MepZ> zN+$-KVhbqCmWUiNP#!ho9bJO63KbN8&PnrZ5L;BwX2I}MndxQOCTuwsR=mC1KO}^` z<=d|;T_H7G(*LQG95s93Dgmn5lRmsGS>cWt0@WN$0_0fCcN`=OT{xn>nmK8 zS1`*i9*VKqq+eaL^ZNhrVDIwYh#~h?CElEqTzfZbMHyzw>PLI7WV+S+dBQdxuCV&B zZO9yLKnt%-BrmbYFM&RlCS%J1NTTU94PIX0uDO;Ilk5m3jUTI;`5v3-z!(ngIlVv; zDV#Z^>@XJh=U-PNE*R1ViE4WDxAT8204GDC^al}dMxF$fh!oI^$YJ)DE@TUc3S96} zuS?qnYf(h9CB5#tPkjb;WRO05A&ejK5+yuDzC=zm%(4F0D|s;Nj$Awi#acC07o?@~ z;%N?BUVLEpoEjmB+m%94#t25p)8kKnJ*L74goHNQNuS-hu~prM6*+ut@4tQ!x5WQP zA%#`kEKlyLUR zuo*#*Bs?d{`M3I<2B)#uzbY1JzL~=kczN-M|KsqUxdX>{?003IJ-lCTg^7m-HoIlH z9=TND2MQ0it!geh%atLlXc^KzyVrG9afHd&l~*n=^zeI%l)29z{YV&@4EA(XR6tJ< zBTq(UHDxlRo&&GevYTjH=?wcFO(=1QW^8cnlt;S1QL~W&`qtG?q;L%83u)Bx@Huem zPxnKPx<=XRan@>wchOCFWm4nWI&>?4fX;)E``C!Sa0zZ>J7vJbuRhRbf*iqw)m#Ee zC}mcC^MG*KF+$1(>(jE%ji%#{>qTVC`+g51I74z{#3d2?@BvA(9# z!%fj!i_5f{#M<{Is~=qITAi>f^40jy(}JG{XybmTku1>`6J_9GnbI~SKUwmXbYy}% z(mqZ4`BOmaPw-5LU8=nZe+VStvCQ9p+&jV`LYTt15PfS zYmSRbx2-;cFC)sW9VSm&M719Ku(~lwg`0q$=pZz|RvSn#%~y__hjC4j|dxxZ`32eivLMrpn9b=*myGRa!VHizU-x zHqF}J>UJHi8%(O3`16=#FCkpjOz>CGFBr#4)okT(H>-t*2RuYeC#DuQ(a`%Ouw|S4 z<17(4!3CdDzO@?rtYHu?gU!ekeR4ka=dG93<9OK{MNyi>?C%g=p{x4oE=S4<=4hWBQ2XTgDFi#6r9Epp0xanH5zl0(mJm*X2w@)o+vhm}#k zf=WD^wW>ZJUbNjlz1aDLh+!l~{UVpsR5-6CuAh_N{}l{r4>;?$+}gjWBq2yH$-~LJ zACm;@bcTqPuoC9a-YElJ0~wcN_r9dX-Um)PnDxQ1!LK1cw8<2^Nn#a*oUI3}&h1F; zKjDaIHEaTOXF-WOo})|X^3nYV6uG0;YCa=M7il|U3rRYPoPJS#qz#HI)(G-8G8IWZ9YdP10rf1bIt#r|u3j-v44qmI9PKPM zCl%gXAn4!Am7B!>0-7Udp+Z=J8)~fAY!a@$8T30uS1pt~v0ppbN3~>RU3&#_b#mR^$ClodtWn?a zO75Wf0`6@h27gv3)$z9mlhC;pnEO2KyUqn6j{gBEOy_LGv6leK|48_Lt1mk29@AsG z^Kv)gE`b@_&$^K#DA^w*xEQXzv1>ozT#mbVC9v$}Yr{wUdy1K9j1;E*#G5B){JB+Y z?eJfx->A?*4=f&LUx4A)2Eh#3{?lpYgurUn{$Mfo{x0%P_DadiYwL05^PQsr^yOIM z-PGt2$6QeJR1^Zb_Qf zD{GSK^1E`0DU~lRrF6Nytw=QXZod%p>xslbIJBBEwt~ALR!FO5l7|HH7uM0Ga2mUzP^m zygfSS9c|u`^9PEf&DYZ7?;Vcwe}$0suO(UY4VH4Ss}*t<90~-^LRUK)NFSOvR%LG9 z%j=n`=#XPWzE#bl2`54pyX2!PSx)b^vq*1!{*lgEq9uph!b6H#^HW*x_B>EOo5qr5 zSib*mAEX{zW*XzBN$+9s13$&1S2ADVqVUM;btCuPYRUB@w9qLvT?p9B3CrUr_Al>! zN|NK@#7M+4{66SDlpQCh^Ocq{eGYEQ&r_1C8vHMiko-RS?jFhwRdXgJPLAV~-)rZi z@c4}EjbwE>SjpolhvwAF#9OjF#=*xj&{~qJFC^ld`Dg2`ngKl2^w5%>cE$s< ze!M0FZu=Ww`>r^{#PattWUc&#z{fqjio^(fh!ur|O7jP})RKe=bAQVdqp|EoJ4Elw zwZeG+^~0}^OlY10rhLn5o0mT+{BVaU!$sonSC`;qY*Lksn0q_$b@n~<%Tkm96;L>- zIxKX=y*iF*S{+zM>H+C}&$kixO@qmdE>!IkP*T*;^Scfn zz~x0q)OjlvV1{;HRaLl~RFKgEu6$y{p*d!~V0H82QPPm@hq`e`uh+cvGbj*NXt!%C zcoWVND#)dW|DXzTZ)zuLsAZp>B73mAD{5mHX5y(&$q(tfoVY8k7+mNYicHDEx@CA~ zBJvmQkg-+G8YahX)4#%)!-y{6Jqvr&AbA(Pp^zj(7-(#eaSFU!0oM|IwWcs&$&;-S zt@&qd#}yJz;+B7_>}(Lq9ydPKnS~fBAOYx)#9Gb!p5?|sX5wA97BxajTvx(qR=G1; zT8^_0!^%5DO5*vYaXDB6{6d39ukl}WeQrgV9sze3db|)xU;M$YrvxcX()*mg>uz7HlU{wN1k?b$({Sgpjv;xv^HHk;83yuqVPw_> zs9Dr~D~*iHy^yIHN5DF5~A6u*T@TAjNKEGN*oc2@V&ZlXLuZ1lZ~1Heb=*N_+)u{Z&1K-vQ<)wyl~1Q{&x0VJnJ0{u zO0ACeOK+t?`2qYAjRXsc?q6bJPven6+;1DU@^?%B4u~fhnT!X&#_#JaH`ag3xhlM< zFtdeR{PPKcJG+H6$@ThTdUEogjHBZ0PTf^J%w?Ir=0a8Ro^5$x0jv%u4VAO_cGJdN zmu~P_?pNvRKmEw#g!3IDN)Sb;shUdBsO1wjg_c_h`%d2! z`$pB?qibgA^ChzaYF3n2L7{u6l)0T+><=|?-=7t@84fugiUz;lVoC2v-$ch;^|Bd8}DjjySF{?`R5jm7*^mMC(JD4qL#;=3*?IOI8 zPk)1Omtb){5U#G3M8e=hro3NqloJ2520TkC@4_|9HC7Dz<{*^gQ_y4^zGVs={kjeYCeseE2iK>qzOY3aj<$ z*O8Cvyp^je;N_il;CFWd4--zIEg9hZFV;Sqo|uxPhfjP633~PToTR_zW=^RJ#6}@F zBrnX-Uq{ARe`gYpljO@}qhb_dZP-C7oICjSJT(Sq&o}!qwzF~HIR7kLw=iMENsKgw^;?IELoh2u0#6~ed^_;2di?Rak&t~|!Ax_M3wR)t~RE-Sd z?Al9%a0_n)p~Q;(xVoC85PoCkLC)y;j`tf0t>_EEdI68@Mph6`J$p$VKj&-)U5yj( zM39pgLiv`xiPdW;UWdu1?>6!FlP7^vxcvNc^mxLThQK^$qB36Ug(ai0<>F%G7lRcw zyM&WS`_C^fT_67`zv?_pfDLX8AKCn9$KY66#^Sqy=cM=Yre|E5IfM_1C+au|_IUbN z>0SrZv**QpN&8<7{_K zY00ir#~;FyA0(Iv6j=S5biG}3uSj5*J zUax+-HpJ#PZXX#qV8XFW7*2z2q-U1$;V~h>Y5;Z1KdtB!dCl*fF#?_Ti{+ z5g2f#Mga>aSvt#B`wBWP@S8jrO$sU*K4qmsGW!Zvkm9Fzl*8K#BKF* zW-k6cLb=d8J4HTM}xom9B@|`1NK9KPCBQKfC%I z&G6!8jqG@`UeWlS7v^^N1I|J26!+migl}vb-bLd);WO07mpc(kXL+VUUq~~SJQ>J> zzt!l54?px5;CbNY|0aba1xVINikUUc_#8u%+`q`<_GBZpY1HEt1qHu)aPruU`18@+ z;x;qPsJ$PGWKYAD{`C0-YLewiHFg{>Y3>PfP6rHCQy#9Fc62Rnf0(F*Zj_3l!e=^b z3^HiA4%8|;W*TNMDf&qJTgvxqR?l1|Ue4&6y(Siy&+L!W@<7zJM;{1AYcpkxiFBQc zx1yK!XRI+z=Te~}YJzwrIzW2vzs`6-U)iB%)h4D2utSY}WR_DtWE8)v*Su1wpWM)P zCU;i3{+7+=Xpe=}T!X7h7gboITjI>->CP8b2o2={f-~C)ft*|KT2Nms2P@PH_Zjk6 zpfU3^s9f_f_@pm2*qAiQGl}6QjBe%1*Gnt#e zy2*o%M=qv$jvD;i=85Q$zC~(a1}z|6eGUTq+v!6bq1_Lt;#Uf&a)Zf#9J(^YODYMd zaU&c5K*BT(*${g3X~0`Xg87%brq;`xGVkxfX<;I2_R%9f4+$fRi55U>VV#`zgDNqK zBz?ZH@Zs7;M&m;sJvOztPq#<6ySLxGA`WPY$M^`-%ch0@qRn%|ju1{UZSx*5%<8Ckw`P?L-SLk{-@Ve;s_Z{H@hL zz5;IV6Mp;kv`cPG4(E`2p=00^kB4(>9zo^mQw8L)<0r2VRtGckpp(2#`Z$0zX1vL$nK)9db+E@jXXfmA|SBgm6p=bc>Fjtf-gNFSnumGeOX(+i9S zTK#;!Upjm2W)y5Bv=vS|K9?OQC%Bc%GajX41&lhD9%*0n4T^90k)(&znUg01fo zX`3rO+oQ=%x;NcvLI&-s2^U-p3HN{Hy1)44!pt|i=lJRug>TOpEBZsWuKV@xpiC{kFoSoJUC*FgH@tY6KP4Z{3tS`jopl13xA!Mp!C(C}q1mtr~m3=of`hBr;H#lFO$$8;h zh%shSgtVLxb~~{FN~_BPB+WrHzpI|SkG_w{`<-bTl+4n+2RU#Ovj3QV4Bym+a`#V` zcY;Lxx?mObl;(hNsPST9-X*>4cR?KBO02~gNpAp~o+SNkTv9AAa2UrJEky_+#u+uy}{@a@B+bE^M(DpZYk%d9uboC{x=*1^y^FjHlO2 z(qio*?q2GlVA}=_BwGHijWTM~R}3Dx{*fN=Qv*|sO4j3E(pR}j z?Gze(_+`+_xmBk&KJpY6IVte%k-1*S{ut)8U{ysoRX+WaZ3D^|gVVrTE!C_CRcvku z8I}0PcaPig#oOWUoawGq7e?rZwb0dMF=yEyNB1}-MNvv8{JW$B6wIjWfE|POWzHsr zIA!~;bA=RKzT5??xL^GSjQ0ugu5lvME)Y;e0?0ULWamsISTJxIEuSll)!PA+G-~7m z=}}B`A8qiZ@$;rduPt;=w`0rrC*q-YF9RfCCK>yimylx%9LlgqRkv)(>t(IJ=hN>4 z_4X9bZHyj=&Cn9YXwlEbz1gXDC@kk$_x~)S%1AFYsm6}1)h@1gzouHg&^LQ#Cx7NX zBLR?*C&*!Hu?JRpYfs>Z=Kxb>=WN6BebS-tDruD>2<}VP;@RLs--UTur{mGVA{T9A zVw@#Erxe*KWpgh?d8aRR1G2RfG5~|xGm`9r7aoxVJ<#~IMbMx=B*?mmKY=-7Gq0@kE(>x>^F3dPCeml)8eykGmx zgPORPi~lJ2VCNDMyEOVS^3jhm7NNdg((>UB59U{yr^BFFLS|oV#$yZthM5^%ZC1GR zLjbEKyxW4gaIUhF!_V&i`u{!~92b)I!AXpll+}`15aw)TwQ*wnY$;(4ojxEDGK#EyWpfCKo3gsTwQx? za3s?OUTccu8TrxlsHlI~C`jwydcq4fHmw;pC#P>HI6C@vs}X785i-+1E^3>r|5ux< zen5a*e{m(K>)piDLX<`bGDBl+uk@mQ#O|tTq=?$|qoau8A1LjvGXjP?F*T_(LNF`_`UDbhUToqe> z<(F1rf?yr9Q7(F=hu12nQVVpJX{05&D(<;oD{)21z4g7W!3bnlJJV1w@VTDcC1t~y zY-NnZL6=#g$9&FN{^<#;nAg&G@Jt7dxdv~5oe3Ie-ob)^)|BUM+E)ks3Df!Fj!dZ#PD*|8qRBWI? zfO|M}n|JW z7bp)*Jb_#LU+M|>6a#-`pJnxlD^MIzEu{4dEpQLnSk$=wRM5Fjl|NbiHz^f(ixf9@}GLL-D+{S^m zh-1yYJ#g;}*2RpNh`U*H2+mEr8~q_1pCSFa>`Nzi8+AvB<>c+=X9$5FMDx4?b`LQ8 z?l@_M91LgVB7o)=NG*XIx+X|I2PSzQx)RTPlZ$vf7J~ge#C|_J)OU(8DAhOnzo?N> z6Q7bdJmzti!Zs-=MYU;O<-9SY>usOJgM8LKB|^-yFH>we`xkgPvBR!57^({H`MkwT zNMh1V$a@X9SEhkfM<$^!_>{o4x8El@lu}nH_K6MG)uWJ%g;D{lpWAn0RP0mtzDe^d zalu@DI;Z>T0XMy@Pat(hJ z^R~G&jNu}>*MCpyGDJnlgOKJk31KfyZPY78E1Z`<(fezIkGi&pE-SP>`kF3B{a?j^ zq}ciE@q&ywF6bo2L0)VsdAIA)z=aUtrO>;YLS&j4_JTK&-(*wFr{{0rj->izLCa;Q zeC{DwNs$Sh`Cg}Eu^h*fx5=27FP4f{+7y^0Ft4;yR8yng8N==|gJ5F39QcXO2-L>T zzX?h(aCglEZomZHyhVcNeAt{k0=D#5cu>bZ)?)cPmP!U#wdK!gyGaGVOS;L1e$rhk z@Ttdm=*nGMvgpOQE*jjI%yTK`Y;mwxV~1RR6?r}}0S+Te_je|d??R&(8KOLR&vwO_ z2VNZr971+pG@9)ljbIdlGPHwtizg4v&|Q@s7+=Hv$yKaDOfHS@*zbBU%ow-ijPvl^ zoF%CvrV4VA)m~!WN${xkyo}ex#R^ps%-Z#AvsCaRj!5(sUHhs5FC91M+3c)CXzZ?g zOZAU~(WCyBl`*|*|5enRzqM7b9Hain0+bs#CI(br@Dl5uIH?H|z01+NC30|nt~q4q zBLV>{ku>;fjmQ~(;2abl&^Q|5VYN(YM}- zo{V@DsYpB#DIRghO3s(&`6{@6K}L^A)5-JX{2!hz@9O(XYfnFmhf)_;sl^6GDXy>`C<6z4M>{_3M(Qp>38m%j4{ z$*Jvsy$-P$?YP-T6~aJkiyMB0qkO}lum-m{rS0s^13OD%XPLAkb%>+_uG%xiQj{Li zJBhh1aelZrMRQBVMXcymyZUh9)1xOuAZYLrWcD-dU}~$ggs>5&M=zq_4%iK&Q+PhtT0rNApmo`6R;Sv( z=7X0%1AH0rVeb?M z)@9I58UmSB2DBR$S zVaQeRj+L_75jaWB{VNcB$Cy{5Q7Y%RwRRw59=3sH31D|f9C=G}2TIp`Yj{MMRkr{9 zg-nt^+e6D#bwGSc(XK4#wn-z->DTp=JhGSV110HbA>|7*{M&+&KU5EE$pRPp2|To45>#E$cGGFD%#Ghv?Ir0PHalK@p>cF_j6_|Qvlxkxb3U6+a3PG z`L96cv9Wx&DA3(nS207b5xLQad&tgB*!-A8%Alj)uu)Eu$LwED#coeY1bu#TNJ}dF zo%Y3zfvL+>GIk86Qi*Hd#mvooZP$fYZ4ES%*M>MhX?bA2GL95C67KlkB+UaY6UfD3T(p7U#f+Su8*ADiiqgoFK=le}Z_Z<~x|@BIj5> z7Y5!#QKNWwZ71w*a+zdiqA;T_ZQ19IWSCx|pPd2!jr|3G!bHG?FjoJbuJ5tRV7Qo` zhnXb!;~AP366apswJx)0Nc2T-cpFvUY1W@U$WrQ9erx4szRdTp^ni6jU_beJy;j_- z1G5)m(y&V|;ox79# ze4zJ>p@ZuXBoS_^Y!z6?QAGmG?q0W&5I_vQwB-?d_59Q}tSvQpn%WR9C1O8fogfC3 z$R8F6;brSdPBDiT73@pGyQ;P_x%FLAl~Xte6Oo&WqSmvqHRwj8^_tV_w!0oFGj@|N@;8dvw4XDao~Z$Chg{O7jKue!+nxp zAp;-P)$X7EurKtz^o*$(8L+$M5oz6_a5nzq@P0#@@?SX4mO4}nw+|EdybQY$1b6lQ#bl=VomuYCPG`m9j{3^^o3BRJNkSab_fdIzE^?xl z9U*;?#(wJB+p~gAN<2uyW`~7HQwq_K04_Qhh5b_?kldF3M;HG3UHd`erzyU^Bq+4x zyME4;cJ_%s6H5WOR@f7ax-64-J;>Fz585QkMitRbFb5xH&a7j?3fH!Rv3eqjsvhMB z7IrJ3f(L=;O*hJ@V$5#5$H|h^!u&b}_?>zsrGm60?1lN^=R{4kW|mnypwp5F8oh;( zp*D68%2sPvDxBsW-==^1I~=4`fw3@%JE4BI*(Xx$RS@@C*h16Q22U(6*kEAh2~$~P zS_75Tx3P$Q=R=C+5AQTx1E&u>4u5^Rca|xG8O^yip?7E49f5sbOze1^QeO!)m#r*z z@~>GtGJuLZ;B857#kXp*t-v@_W}$CB>5#7e+sdlmOb*Gip`7Qq;#6Zd z<*nSv#m1_%ks4G%PNNl83BJiY9A?2C8_Ds?pg!S=5v#(JPm5uy_r;W3<)0VDlFTVyVSvD?n0 zfKh>+YaMwF9j&VKYmhax?^AysmT<1OwHkSi-}0YP&eTNzNhpg|yJoh-IHftus%&KC5 z|3n~U&>sYPMN~i)`j2a2@W~&uyt8dC0=Te1-41oajonyUY`suDQ}67mfbQ3y`xIc@ zCHm5yBZ5JGn6%DE?MQ3gOq)ZNnNI}Dqkm=YfdpX8qE02V_i=v^r$708u5srGm3rV0 z!J-$hE)YXZ--L>*~82$rim)QwWDPY$I%YKVW7nKV1Fgd8{M_wxJf^?_viN)--lTcYnvaW z`76R^WJ2CbaM^zgKVDOWPX4`-l4s?Bd}4}$t$aLCpaEK)c!3XKRH;s#qYoJ~#dj+_ z`p+XoiQevyaY3~SvJYOK1OMO&-te{4T;f`dZu@048JL=axx7a|^~QqXse&qNbqOa| zB0@Ih39k8Sicn(_P3B!z2`5f#cnB}cw|I0=e1aU3_;K6WrQf8XlQy^0eHStWLAtS2 zRQ`d~<5k%tsl67L2g_mmv#HZ7R`?=BFKE);M6BNzniI?YOHPm0R=$9>k$x~y5=5{B z*`~k%1trwuS5c(%>ZS&{Yihkf1X5;a%*5fr!6!VXj%BYQ_@5YaCriC=qfIqL1vT=2`}1y+oC>Y<(Mxmk!{TtUz7ob2d-6_RcW9qNv_Y-5etx}Kf? z%9{#(84<6`D703jMB44|DHu<4>}C*yGiEne&JP`VK09w%<>w@OqG}zs_ifsw>b$bk zgyI3!$6safLLVvgEKtdxzn59N$J%-?DS8KE5+{(>bWgFTxS0iYTd_KJBF>rex)Nar z9gnVF5o6n@(ZUx9O!I3{H5stkp;={N;2($)U0po7xTLn?fDT;j*NsGZG?ss@xk&7g zL9Qq^bk+TE3G;ALLk?E8XCJmyE1n(Tc}n0gjVKT-Ex)+$@e;(i7zJodrG^m)m`R`+ z;J!U#F>qoOy*(|bhdLfM2~L)MNuK$cs5ybPDh~yH4$VP1ye-5793w2^3Qi$t2WRY> zOsve_Jq4J=zus8qH!W=em5=fgj|Ow(5VMzVV^L4(NPk*pvQ!f|zoC_*OrYfRooWI0^H-(1n-`^>(7Tqnk(f^uwG zPrbD1o1fOt?NH&1EhVt1pNnphTy9Mg$DB7;ag2S)D_GHSk(w<;xmA5HFkOd>({T}VviU3&v1*TWb2Am_~%&#rkm8x@7H zaU?Z2StF$q>GBFZHJwr*y4N~D<&C9^N|KsiDVX(`-k3mzFg%Q8L{TNWl3k>Rn|@~r;+W7};1?e1enrvjsT)@jpl z9}UWT(!2u*rvwib9IbLtOjJ^z?uUmLc2hr|<$4QBV|Ga%m|mE?oMHCx?cAw z*vu*)L&g!>)YVS_UxYpNeZ@9C?P$Tq3hmORgFg0+!-X({*4w3~PMHMF=bF8U{Jx~%c3%?M%105s_hV82)R@c2mVIH=HHd}kP1*Epv9(H;G4g2!hPat(d@hcS_%8+$RzKCUExfx0u2Pbt{JhglQwewMZEM_(D_sr9Relq8! za~Y3~{NQKmIp^X9|0GbdAZzT@K`*oM;mZLJ&QoM5xz92!n+?GDllpB#DlVZ9j_CF_ zFoic(8Lo;i&5~15%}YhO7TD{!VlGe5sR7i|JN)$bXQ_F@5YqJ^+JI_cbN28YRffQi zz?uH+`GOqgNv5dI@rN*r=>tWGJrO&3Ki4CMq!_~U#;|g%BxU6taWdUR_n%6*X@eq{ z)eB_!MOEr!Z8hZTw-1HAQtcfq>{NZ!NsoNCLY9bbg>y&}@gJH!RDl<1;F>n5X-G}y zt3Nt!4^NhQpvfLD!c$LzpIPDdP@HmrtD*?N7!(3`hiHXZQ@`tLj}{$#NhUnD6k7Aw zTv+Cwd_ov>KHycMf@Ao}A-E~@QLrp5Tk56r6N@`xIyEW!>?fKp%+Zh7yUzu79={@Wf*DNgBbl1MhuAms5 zJkEVrSApgi=Go9ZT%!i1|r#~$=JJ^dd`{c*Q^#5;$|sm zKDldfSvgjG?3F|l!r}L#ZdTgFU1IA;y{{subWRZ`eEI`!tXN4q;+|ONP}!j$69+IS zpOs|()@5;Y)F$!6Gct3CKXL*0n1G6hnj=35?xv}PSXyJFSLAwI&oUe>U9LeGuMC&} za$#j18S+p2{WNXL6MMUx>V=#Hg;*aTkJ&@oXT|JcR%j5{TWixgCcKF|sHCcrpb9S= z2%xp@1P;3o-EC|AldGqTnwGeirqIAS(x-8&6*# z2g^46tCS1tyj(WZ%uIc5`U49tOo)2{}7C?2tEj6MT&dy&G^(m{hoKC$6t!_=!1`y>2VZ z%w;*bTU2`jIt5IZm6_YOIR@VKVe z{Q=xS9qfT=)Op_xuRHo4PaaIn?7K$i;CEtDkMIc<>Zca9;QO2RLaug!^}$fk(>XaO z1!b}KRXrVFh(eg+P?V$>=62+dLLC-M8AHZr<$S*6I+&i_XH-n;sD}D*QRP$jCxU!d zRpERnYE{v`&ES8O_yzG)vhHn*0^9Y*yu4p2?wCA7*f)S~25N`J>|=;D^{w0Fv7mG! z?NrAe7poD#!HwW!1z9xPs>G^2PQS5|;$Nt{u}x!dRYgW^UwrElZG}JmnjSss>aV*- zE*52l$FN2|*Dz$`^e9GFQHjquqU4|cw)|L=hfV`YCEoae>(P&-L;U3U9Wh#*v&U!4b)@vO$yx9l zv9Zc7f1IBeB~XRXivSuZ@9`ANgQK0*GhB}ihEenH2-_Y-$A3n_0{?kZ=up0lRtN5c zt|?)D97x-pje^k~ThgwoPh+$gxGwc9iycscx=(Bkj;;1zf4;}M%hVUwRMKog3|kG(L$ zuh;9fspq1~;vX*G1_3z|(67%q6%YV3a%liW3K1yh*M$_)&!3t4E~}%3&Q-yajnk>w zXhXBD7B99|cH|=phz&+vW}KmmJACXrzYL}Sy|&kxVjH~O6Ne=L4=xPhb^YA_{C3i@HI)mKuiU>*8n83%iVjQLS zPLe)`%yZSIOl+w6Q<6qXzA`3xS2iFqoB%dIFjx3|AD&!T;RgxA8!_wh{<9A6 z6qth7t~HClNAQx6KxHGaNUWtEk(jZPZx9!tJV-0}2OJBifnc1XGY(p41r>2N^WQ^P z?5iPvq0<6XJnzEjV?e7sTwgs#+<}!9(?svu9>7qBR z)s649FK+nvNeO$9Aljr|i>!cxfKh#zXVFEJ@35_?j8CM0D8n#f!XwHwa0d_BsxBWZ zJe;#;1#p@#*^be|%X5b%@WoO<$}xx4cN1s@-JJGbYha9nbO-LY!xE!M^xghj%onb| zv7?S$q0ZviT2DWw1o%a;A^hb6_d_ zl3y4_DzkR!Po7l}KiY)S>4M_^|c*X{X z(ZgkSbwQf6-w(j504fGbk5_>IhB~@w_%$(3JPo3aBIE!9q@6C^k5rU%H6?ms!8N3? ztw+x-%)jOoYBTD!tFB7CBdJlD81mGHZmj89$K`1`=!X;Wxb_>x z>l4V29Pkc-ita684FUy_60&E*BS!D*70&0#w%2W59|MKESLBp|wN0^f`}TQIXu&Q9 zS+MpbmV%+M_Y0pI86~tR0Zjj3IS?To8O@51U$=N}rT`xMe_e@P4b2 z`Zw=wlGogE_jzdkKgYMST6W~rJiszY* zT7UKYPknLef}9vRBM$FUSQHJkL^0I;7=b4xdICQ-G$^nOw+EfUBNarcSwzKoMFRti zjn_H9*Ze7B>sGGEkfs=_ji$1(Rk86(tM!uVZAoMqg`Q247t&XkQ_>3pfH#Q`9d8tht!q4K@D{*?rCcYq&b6V$#9hVk4lm6=db@XN)(%Kbr3)xYqIgrsQX=<)Fy1 zhUnnS@YLVR(PF(;WELjsQkB( z^DPIz-L71v0uLWNWu;EkQBSJ2cTG~H0?@YE>Kgv#(+>B8Z|CefrLL?2CjL5~43l$~ zX*h7U_f@*zEgAjjw{-CSRU4?+czNAi6F6BFVQ=hb8S%xG~Xx+G$P4u)7oHzE*?)PzLT?Dl%S zH^y+^TdJWj*W$yb)>%W+voG&Y7ejVblbyk|uJcwxl}*dcjn8_TXaUIkqSn#XR){ zT$_O);r1p>**DN%JXx;QWs9_#^k#SR!OYR6uO)@AphlaQM%%oG-mDJ}K&-mwPM5lP zYRj#Exdj-u?fp`#%usP)#XYFKqU*z*eknEZ$Qu5yjX@I-9bx-8>FCq)nKuq0;|5Y7 z(Dk+W{+a=ykf1AK|B1|qH<#SO`eZ8q2wP8Qlpq0=I7I1m$WHTcvOTi6{J8k=1|EQC z%qif121&yI0axETHEhtWoLwFE&Y3{Q6+hz>gTQIEAVewI?)~i-YIJ- zZt5DH-+v43H@BR%!rVAAaV9+2dI>A*JtqaN+z|`hQ%kIvGVf=0o%^URTx7{RU(jgA zJ(cyT^Utc)!Gu5h(u)T%_>hi3w})}D7$g$_qvun z579sug{b&*a>C#)D~Y9T`v+lGdMbVD0@9-LHW7P~ z=qD?AknH^qj^jn0-Pi5;wP@%@=-slNpWnqXfjdXgB_7ZSX;r`N42v=3c9-DO_;(}i z-uA=m8a|QfF}ycFptFy7ZO$<>yyG0!q4o!@Ydn@E`FSe(^l`7mLD}J;5yo?MfT!J-)2+enJ|r%a4U@rDfZuB>lOW!-1Cf+FI>m9arM>n$u7rR0Z`?6G z5hE{;Fq@9I{+(yDwC~2i)MG)W8>f6xl$_e`)kk~R)pKw1vp{dr!t#KP@m(CAKt7{v z6+W23%KPdfhLilf8_`~xgEJs@`C1cO*jh6WbFt@o!<$NkY4fr)QL;Fe0x%HJD*Kr5 zJ^OtExEF}?+#L-kogeVgAF-&F7E7i59#xJo6Gfisl(e@x9^wC&1|Tf(ZrM;i1lq>l zg--+wEE!!b%^Z`$yAWrFlF*gnWA`#0uxa@)ZeMH3;#`d7;Wdq2@ov*Kon7F&;a>;? zR@pM0&0j4&$9$jS@5qqh*Z*g=c{wa48@MgMt%C?$pjI9}7$}{Lnt|hz>JIGx(F0K`*W%7BGGyCH5d#nAERCJtSVx&&4W2 zl&GRIa4nK{5_GqKoq&S`ka5<~uJ`nD-f`e{>iAWG7#*}yLwHpv+c66hfEI4Qa}~5% zbZtwq0}PY_VRk}tw?>un<1nInpTqPX$z-7?VWyj#+y33#rzLMNMG<`UtBL0H9J;xT zKJ>d4@v)IsZ@-$<%RECa^q?FQH-pq?tu1E+_IFN_C?@*f(R0;lzNa8~+kw>wnaMN?CFW8C`mEOh4=p)-bTUX!yLJ?1(!kgqLt=dmeXx#PV9=sBVD|@G;k=yH zNgh@FO>;;FA7=~L?j5n10UZY~jvNY9W@tb2fm5V>DLivs(6WGwrEPQYy8{W(R$&h& zzAJ$5Wd$8aIl16h{%i9RCNf8lQ7fo{Dqj%R^>Cry)J-_F_RR8}TFY0GyGEm*9MVN4 zuF$o-n}`k+M(nGSdz8&W!}pf1QH_)9#uIM(@IAwWzk-@{q@PTQ!q&i@M1j?A#!H`( zd8-?^Ed{}_pD4aWm`Pu+MnVoxNP`TFI$+=WvRYqNV=OBr-UX!ZY`oJ)4QMDbN^sT- zE2pV=2n8A4aJd1QH_*P9{IaOx$%3^8nY1nQQ{d+N$;YF>lTP*-{2t6-xCf&KWepa$ zG=68-K<1&Nlv!>CgCaA9Z9J@ku0&rW7hF0JfCF)H{AzS4&v0{7}r>6PBSC}z1?81?SlU<<8DlUt9#*pjBns} zGkkkS=aAfc;y?Wgbym5&%?n1p zApA+irX9!m9%OTK9l%PJovbR-;n9|_sA3gmZZ80G&f2(aU#t6K}@QCQlEuIn!&UVKM zzu`^Dj`tb062_u`hFfq;^wsc&>8tqm^2miqtCHK%Lng0N;{Z|ak_v_{K65d1@D7(} z8)YAeOzFA>ojHRE9lWL40jur7tr@)={NCexjJ5-!B$t@q^}py~iXVyP$4Cy~%sson zi9~mS^(2602nJ(A!|L8(fou~^83;1TgiB2|UZBJM#S-2hDiwToQ-Jn>*9Lz?J=SG_ z6Le^qsCZjJtZs4`?7{voG+8C+YfPRzBTUL1QDV5wTS(Iy&R5hd2-y`#q}SrkxhJw83WL3(HsxukDwop?XdwL)2 zr2wxqx+~;z=Us&VJNw)=N3xWfViC0=JxMlg1l=3MtqEeuyRhO%ui#J*9DBPyN#-4g z8UN^xp|XeU2yj7qI*`y&62kkKT9bfCSdLDpA-aONsll95Mt_SPam2uLiH8t}o`B?G z38ImDaO}9*K2&w+9!oX*!Ro8{iNe=~Q$>`dzTfn9EV?O0Q;?9j`j?eA(xk+YvXAIK z_SB$>zBa44kWZs0hHe)Kb{nmJ#w9KSV_Rqny6}T=FrDg2fL-&AC>gyWRZ~~9#gAvb znRIPGl7g_%UX;wY`3=1QprJ7#-)I7;W!kKG2*Z1;FRPa>D06te6&f?~=a#sk$P7>> zY76%Bv#egtv#>@H#jk%jpQ4ROk;^R2KaSP2Dp;8^2i4LYDlps3MAKt1HCMrqdS6Fno^>2U?Ino9%f zc;-hO2h+c=J7ws^H<#bvkvP9mFnC==@X)&M=#Q6mn?z3J{^_6f;%Uict3h56s*pHp zS|UVI4rX))U}#KD$m?<9(Ev?vkAJfI&us)v!!Ac_h^6KCt!w@l!4}G_Y8OT&Jk!6a zsw{@HvhHDZ$*wT7av-sv#Z)WF{POe`mQur;HA*{j2l$a_^@LlU6+id%1IDw?>xi zT@^PDT=#mkVtU~s9^}FR{wmkKdp!>*dQXde_dL3aP(4qwf7so!^RW~z={VY* zAoT5_+Zy7Xd-xm^d&%zy6TwMV9}6A@0diO{V|!)~r9Q|In(3oewOt&=Ri{ug>S!~M z9p6ZARY{Bp2;VtE%0+R@|6#?gp$1|1*Z7mpxbV?~HEVI_MX&J%u5Yd!Ul{Mxoo$4^ zOA|}&1hD&c-Ll`ADB^B>_B6ROX+1`z1W+_L+|G57DQ$afW#H3yz@qi$k1oaD5Q1{o z>7~HAi19eLn??-E6^Xjmw|B2i&YFupUlv(z3}er9rSLAx%6b(Y)(P>OBK7ZzEfRz5 zHE&1K7CHtj5A1a5NT$%{7$2TPI9Wgl3y(P^ zBR_tu-0DE|$&)60(HqBC;El8IhDJ}=9UGIZ`DwZ7PntGyDQ;-`LY$tMUU;YLDQxU< zRoynf8sg2nT~FOzBlpM3LN=v(eA0{jVRzyf?uR3ZvJ4vJUNmdnTE_x2Q7Kc$2j%G6 z4DYpzE5){vom;_=vBRxjjBhg6Yy8cntDAaEN{b}KSxDajEg(T~bg;EvD}=DTP3e)U zIr;J&zn5`j>#_-0ejvc22s>uaJ*Mg)Y##?cE^gFTRRlBrd~cdMjU>KF-fokf&n-&; zluY@08AQX55n`e4$WU*LU%Xy%^O9E??J}*(J=*g0s>EpoqCv~-cm45CA%bZRzkcVC z0S3!I{RI)Hl`AX$8;w7|Dw#9>{w_L2bWkHAT1CJ25jvAT zYIZI#YM;n@eNU!Ji?ID>-Sma5$HT@szfm#z9D0G|hh>N_qBLChKEjRjQ4m&)Y!zHK zHd&2A7?n&V9-*|RbhZ0Q0jmpruzZqiTR9;bx;f{D1QFd8bwAZS=^Rf8PJ5mr&IZ)! zsR&VDvKJGmTv{!FnMV@r`U#w7AOOf&=?1n>`iUek419!<@2mT?S+`}EKERn?@p!rx zz)ehjrHT9eeywtVXd~A)+QLO6%#veL-06J%A{9B{8Qdr{$s9c$DgDFd z2y6Tcq-6SQ+q?MLhdeWU%N8Y0{_g4MT)N01bNL%gbECk|Tq#i@6B`m~( z0=Uk3qD`Jt#Axr_$YMP0B?mP|*P9)5wpglK$`pW+m8}Iv&DYI1AG0;-h(`Kcdjc7J zPONRu4gwXU5_N1UPfP-!b7G&C%_QBo!w zV92_+35Mk}LRu75Lwh4v+O3GuWhZH(>qhDAO?JOFpM919ns|pRsh1q@b!w<$(;^WU z4R{IN8^Nlq{xl|3@A-m_|}5iX2QaVs?R*7QsLBl z`TQQsDmZ8&l_)d)-C6W*h7xk@!QnL zmkl@lt?k*!zNBl=eqfyz1hv~t`4xECZvbA&bp=Ye=MjCs(Ypn5`;Y-U{5)gZ{uj^Z zS_F?{q11D&lJ&-Ff}Lox%nS1Wh2y)laXj1iUphVcc~<7`hyd zZpFt_6(LTYIz=V2f@2``OaBa9gr|_4VVfY%g`+|{sJ$f;>SOSLPl%m|x}553n0P0K zL{K9X>*W1SF7Q<*?q**4C<|?l#*@t`%zkbub|Hlt{EP(ujwp&U3fgfq%!|hTX-^+b zHPPoJl`={=MWl@Ua2Ix%R;_NC8X6QeNFsNhFSm`_mu}ySk3L8#Mx7sTh6Wi>|Cl2I zrBM&@E#jDdlsWMCOBb5Hy1+zyt4j?kfg7Ra%ZEz)v*e|I*AVoYpelDkk1f$^5|UJq z-Z}TOgGC28(Q_M{wY@l+^cc%89v%J~HNhZaM&qA*XiiX_0D;AYX5gM^Lv!A`aaU@1 zr#m$@v8yo9&?r4@b}C3d4RUx(m`%b7_D(o^raRNp$L1jKWV{5;djl%Vz`k%Du8wGB zQen*NNWNGx>81(n?uAB*R!Ck4ysuo<1rgR#{|-9){f75esqN%ZdK)1hh@1Ng{U(|2 zM$6~YJVylu_`6uX{LW#6u|ijMTkaHNtPGGYY0vUu!=Eu{lq&4K?1#Wb>V$$H_!x3{ zzFzs+I6g`|G^<=lGnph5J6e-piR6qAk*JS2lp`3i?sBdbLHP4%0`?q%sH+J|IuQTp zhzYN@F1q{p;8+?&V1?)A`tbV@ZPD|6$kKg(;pi#elHc>&`Sn7G13bbz!bgFmiGep| zQ#44!B2uLmk${h5D;@&m(e(n(}sOY5pMyQz6(3x7{Lzx)Qd+2 z`#2jJ-N1q^N+wdwi@eYDN%-fQqf91_W1@)FJEammx=hC^m6B8Li1~%tGg-I_`G=ScxJ^@F%e$`1$yrsG`W9%y-POw{o5_ z=ZB!erB@U35b+a!IrTe#Wr~?EheNKD?W7pZxcg6fpZCg|mYTMp#h?#maGveR_Y?)o zk?CT3_tOE!6N2w^Wj;aI-k^h?)fxZ<`o&&*A2lo5#c?nvjIXHAIyYBpw}1g-Ej%bT z`1Zrjii`ByuVcUaGHGX;q8Bo~T9_~8tbUf`+Gv|wU7wBtIpT|D3l{rtB9t;yuK}JWoYlqTE12jeNZjewZ3cpez!oyfkMSEq%B-MJZAv{4qCro`=7|V zdC&vlZvky-jpBuK8y|~MJA?jT?&UrvB@9zE1JWTsQHQ*qB}XqKp>&PPi6CS@0!5K= zh}rDAJC$G^wmdV^9&AkxNF=AvyX^KoxrXzUs}`nJZ`U8)p0(n~SE z{N)n{ETl~sMDO6zNZoV|ql}V-By@cCa*-BXMi`C~KtAk*+GK;Z73>xFe;KtqkJ{se)W$2T-$~6?AFi99B@Mphl4qC3bAvIviawFbU2@c5U-!>tA+8 zYMDkbiiHxfnj)S0o-t)j?3SpYYwi!O`(Gg&_?`htoGq2E_nB-Q?TOr=Td=zt4 zFSxXkgJMUyI8qnfqE`#srR`E?@cEtdufU%#=;kHQjlibKSFu6+RA@`Z6TXcL)km$( z&(_lxI!@`XCd6F7iN&bx<~vHLduA`>cG*|P=9#2VQnqtdQ4mp65bj#?qzYE4;?#jpW5lbk{-u#ccg!){4 zpIJJ|AS!u9oyg&U&T^TC?7%Ng0KSF=UvS8F^N!K;hG)KtY+7Rq$91LhedQ}^YA4y4Rh^)~lMS}y4-=n7@BLufD90(+G9y=h8-y(j*jwsONjLAHS?#9GAA?Gz_ z5e4tn*KTV2wx}*W^&gQG_>T2_XX3aI%j?xEiLZh0iwLv_s92x>@+&CG%UnBa&7J+5 zx_jfAExV1pAQ;VSJT?vq^d?^lN1_(r^G+I7tP<=f0r-XU;=i6$s9>QSfnnBoW6R@{ zhwqLejmRk^dWlwBJSd|f&~9u_|QI@kwqPCc$EhgCn+h*;#^TEtiqWHSvkP{(QdYVv2%sN z)5QKt&CKwc_Re5s-@(bnZm#Pc_$l=uMtpSYI7v|<10ZVrocIHg66Ml}RVYCm@yTli zmziu~COKF{aHi#aY-q;Os<{|{c~W#7Tex!^h#A-LU!mMKl8dY5-d`Ya4#U>@EH8mK z&I5zdlp=$8@E^eEykAZz&DB2U7$B&-G5QN{%3RNUgVOq?jy+M#o~tQ4e3>?G>g8x6 z){NEiV@fndfKH*^g`v$}iS}-qvdQ!ZNFrSdf81T+eKYMJ5+F`Sj+F8W;WOFw2^mqE#=Q5W?5_TW3mI@8>NgvgeHMG0?Lr z*R`7D;yM6nBVouunrQmDs zpTMDwEXeKb>(+h~p(fK3lhL-|MnNzUS(8+eu$TE?ZQjb2LMxPQg8HC2% zIg|Z8fJ1e8_wOcx;-RKba4CU0NqtFXXq(D3;51dLu0S?1?Ms7JA<+0RULmmyYG>yM zs`RZeEKVQU^+OWlJ~3uay)@QhmEf-IMVDTy@V4a|)<+VrEQCHcowvIykP`oBvTl(!kI~oT_sCj(Z&fPGA5G^E!>izz`L54-;|+GUN%UcLa|? zIp66x+X>elhjm8w;B0o35U~u5t$O&B6ExfAryvhxo0c{+e^+xB)s86g_wR`vGIfu1+=% z-EGcx3$%677$wd3$Wm1W?1@Jq|D=7!au{#kg8N{}CPc0FI<5JQrR7Owr62Rk=5A)d zz3Z|SXdzD$eihD~Qetmg=W-016+>A;qd{h>FT-cri%K0BMRF=YX zQ068FVK%+QXaJ?QTP_cX9Lg3^t)9gllIX&VEyKT*4R5gEyCzKM{8jn##;~cURwsZV z1r2Y%!`e58U-@FroeAR2KhO}5plcmg+Ctv7BA^dnrnB;Ar!s%sW!=)@Z@pKJg{MJ_ zQd14F3B_T7+MW;Q7+^#1DM5uP8P~aH!kBiwP zY2mLYL+BM-Mo_Ng^^xteN$%UOgs)`$hmBW%w#@K^DB7hGVy_l1$I>3#;j@jA7)1D;6GU6@^*`nxg3JY2;ym$3R(6-axH*A_czS)Y| zsi=Hn*U~$5X|ic$_bqNvo~~bp;uBG(tPJ(5*7ZlCZK>GhINX!?-ArSzOWTWS$@6Es za~FuXf${O~v)~GHc^UMHx=B@RG+macR{d+SLt6!LP}f#<*N_dEfSkOn5)^D&JGIF|ut zayUbZ+w{F}FX`(`2qb(=-&JldW}<-y9&>hksx04F`g%ItV_bHI&agDzqJK!+l5-~tRPQ&+(cQ!}?s%(rlC=^AOj-WXHSx^{E&TgE!rE`%mA1iA`~kFCo)0 zBIdB);_VZ%0a>SP-PyM|p5R{JloMvoZ`SoZs&8+(LGBzO7o8l^=3UKqfyJ`v;=G~Y zKI}|o??nrD*~yj{KFZ`0f8dk6$epwMiF5r=LKS_7{Z``YxXA?3APE0tA&L@L?M78{h)J zyn>uEBt+<8nGO|W=t;2{-F!TxPC7<{z>p7vyNJ-$FE0fZuLK#a%s4~!b5eS^_AowG z#6%QOvd`TK8%UTzusqN)$nh@aSh9txFuyrawcn6QPK`Qt{6tKX!>b@(%cpS9D#sYs z(~i(BKyyj**h$-^NSEXziuMK1_c-la9t(VLy~B`FQeSMzC{wR3e%Y!i!o5Z7@d0$O z|3QQpJC|de_*-sL7&eE=*KO0~?GCg(6)ua(&po*PF?RLj;7~A7&zr7hd)3QS<9x=v zc$u{zNB1xz$tyld%~`b9Zd}AsE|7 zb$N7Jt2!2*JOAX`3zHX1yRYX8bBSulg{@|O7~g5&&sH}te=jwEFn-56>tA_z*1wRZ zwsZDdr1{=fahI-Xm;3ASf$N%-pJyRi2k?tJk`jdXeF3`RZk8_rH$eYpP_Ye5gKR5z z-!=4YKFC6SnO-c)Jo{-Tjr@GZ=T%opd;QI(q+KICKhcL9F@is}EUFE%(Pnys=0u6v zO)3NP2OL>iLH(?dr1qz!!I3^A3|VmT%8JmD+8@IsF{I)$T8rZqshmVs+xnA^v$=I= z*N0opcCR!FXhC{xs-ihgP2VL)wzXOvzDF59S-Y%`Uf^I69>&Kn>%SlZ7=+gXUO3ZFqs3L(|BGr1 zP~8UY(8MVt>H4O(p}3TLYof(37dKzUT^lhw@`nCJT0t=gp|B60C}bXR6csfQkGnjm&$3r=j*jo ztB#=iv2OD4&h62+H5Px_mj{A;il2+(;Qu*fL9t;!-Z8A9*Fs?u=qK=+z{P(C&w}1b z`%QrZP8SSung{`@>u#}K%VANq#_6m59VrMwX^tHFh(+SEevw36uQc00bxQ-2#y&7B z`5fLh*ROgGg1&m*weWHt7PP>k5m4csz zKRIy?jiBx3b48{;SjqlC(=6sE_u@$>M0@@`&f9clhW^ud%I@BqGuuA@9n4IxT+E6y z^9g0Bo;oCV6|=o7Y0LA$;H%WRf6ZMD0bv$`k;~(gO(838=06%Z`J#^QD(pLm{rHS) znnO$L8{GOk+&F~Yrk)^0o7wH@t6IR=nJLCHUrgKw8w!SCVol-D3yka)JN%3zFInB1 z9I;C*F<)hdln`*OFs-C*l3Qd1S*8hDH4CnV-+eIOZ1dSEUL>#HgX{vXW05$NiAvnF z?o3PvS<5?0@_i2FT z*=i=h&v1ClYB4}#g-g8LS5wENeqa+xf}lbvaqY- z?XrYgjc@3d^&`n(&LfAwUDfsIS`5;V7@gbW$7K?pq)1!a4?V^96pW*5LUrS2;E$1B zhcraQP_gFdJkijzwC+PDFnT6N3?6ijKwq<5S;2bxJcON}jP2UZHv zoSsseqGIRkV*Q~oZda{2F3lIO1c+&y%v^m-0%#z-OSKFXS20&-q_Y_%m8z^EHjBe^ zjCReJCKZ|WyX18#Utw{{!)_HRSkMGI$i#Dc;<$pJ-1^jf?~i8;dmQ{pWFqhI&G&j4 z#X;=|$M=8|FjMBFiwt)(7)N{fJ=~Q1*gIKXz>&!Hb?OA7eTUiY-i<*FXwz>$Hw6s) zIZBmFkY-`iOY`*a`Ua%5f3$Q_9Ybw>HCvN1YRpCA+ruALkyfcZggXV+p2!+G3*b?Y{(NmU6sktv$@@Cqus-zyxM7PbHjq{?ymys;Ss3 z2ieqjt?%OgLkH3)v%p&cR8;VpUcbpW(H@|)=2XO9Gbpf*7}lMoe0)>wJosauF@K5y zC?&(w-ETag!8>K}8TWIax<7U!Ih@pa`_I2U{JIns3bcQfTqo8&x}WgRdcx%JDLT8tAj zJ@Xhp>X#=3|3jny>`vA$IG_ZS#o^E@ohdQwnksgHW>+}ZO>b2}x$)7TgMesr|L{Wn zU}=3;i-PJo$c;rV54}*Rp@8DVZ7t=EybIU`TT6rERrq^RW=QTo)}Y*bG9?Hm-upAO z{ZQsFwpCrC+wg^*90B@sl26|m+T^Epo@&Ey8Gb2T#4ueziPguNnzVleEXHGfgtpN~ ztqQ;+P-wo(P>octyuX@JO-Z(ekc#BRGNz*KeKX4la#@$aRA#o= z$+piY3oa+wx;J*-O%+d@g`Z@l25;hS$g`Z!FYEZr6z8vzD%k=r!OOIsKbxwoA-(UYaZs?0EN&rvWg2 zy28yah|isowryMws89<#ecS-1QkWe%Zr;`s;}!7( zEW7);0J491v_Vr`l4vQ|4ReCR>e-$C0o0Duz>d2zapITy&%WN4&y;I6h@{4Y15bYG zCJ-&xI%UKA?ilys3sk)!j7gHtQ%phj8Frh$75TOvYMP%9EngYyc+#XcNqkZhA&BAi znDesr-F|{jFYj|s^)6nDK=IN!8_pnZ>byhrb@^KPtHl1IK;w3zTtC2LLZr=4Pps436P1z4;Rw?SaC5eFI_S;$|n^m z|C$1~&HMYgF`!*&$4~LjTUfmBJxnuZ{k&)Kmg2m?gq?#yMDCf)+26UNa3E-Z4aBfF zsb3#0@GUDWCm1&Thy8k!gkv)sc(yY!D_=Xqsw;}jXOh&<2C%VmUTWxema+6c(QwE zM>+o>*|o~kodH8DOS<@*;*P!AL8tFBUYDI633z9x zyn;dR!n~v>t;O1t9rmXN14x{&tEt*$iR9=i@O}cOa9bHjr7QinICBVGWKPRNe2SMW zpuokV??-r<2>i1s+k+9h$Le)70KfWBr_l3xe#Na&@yYI^+v}lh=2MtB)AEOhUE$9! zxJEZw&KygSS+zpUwBaA=KX@Y2cIo5Zw1 zfHX42aw04ujRZ74zBkhjZMf)Pi3ro*(Hb3l2gv8zZFV!=T6pQ3a>GgbU6l86o%lrB z^gpd)(N@%T%I-fk$$=B1+2tg$(pPqrcKhZ&X2aJ%n7F?uWG%;5Sk$`$@BULY+R7H2 zBa)zMJMZf&(C(~~iN%S_VA717FZhAO$+Zp?g>KNsQu~Clybqri46d&;lD z`NSWI{+5oK4X{KGFQjttmi~*b-ues|Ro?g7YeR2Mr6}3CrvTBnQS?LP+K$(qfua;M z`?p&Pbkbg<&QX)ypN98x^t3HW3Jk&#x*pAFKiKQ$6c?Xi!Nwk9G1R-fC@nr}^YhRx zy~Ud**3jX;rN7Gl@#~Nk9+1>V{A2-A`hzUBe#D25 zvOG?L$F8-7{pv5tw1#W@#;|7m5t2NuXa z)VMqDrH)su7}*y;dC&N`fu`rnRMBnWi+Rrw+U6Y+89?#EB9AV})a@js~Q*`*4VGD`c1ZB5gG9##RZr}P*n_hl1b~gP=Si0z{@lPu}miU;d6i%QZX|rcf{yUCh0mBEz#5%<)~c+g-3T z8F&U7+!mmIsC)GFQ#M5b$>UB1z$-mF5V~70Rj{rQVHv^Y7Y}|!*#6ca+i`AkkVSV@ z?Q&7c$BTN)!@dBM@7&0;n#Gi)>F4IIAv@=&R2E3!&69^sou$gyDI~3pJTMkafB{eO zf~G0b+9L*V8h@8qPX#W$ovE%0+8mVkSTsq*D>(A|y)@&wV{Mh(6A*4bNDY){7*l;H zE+qKnw@7I-5zr$kCX)8L$|9(F9HO}&h=w6ZnlMgFydrrK?>pYGEcVKkIqoZB&Uw1h z9)22W(8M8xgN5mXY1!lK&<*<(Os3^~_A@4GDYjvr0gdlgONPZ~yk_aC3~H~!*}pva zMbt`yL#n}SziB@n{PHLL9~HJD*Nt34IVJSf8o5fe7WLJR6(S8{bXunW){Q@rud552 zay)V#QLwM|>(4p&alCPt@g(SHU%_pE@a>`f9-VXzM@+g=rX&(*UT7!Q;~B|waPP36 zHtq_%__TvtVH!O;m}pHHh^@QyI4YI!nd1I7k^V>y!s)=2#4KDUx$xt}*xTvt_Bal? z=-$N$UYPIiK=gnxH5*sl3i~w97gdT|RfHlxFiOK{P#;*!eJNrgnHvhCtvc1zlDIOj5+3m>nu3rzL5eg9T z;uCk%p@Z9j-mt)!?3%FaYFRn01{O-|2;p<$0y!l}09Tvk5!6ijH4ex@JM9u*(kC1l zZw)WZ=e4;o1`@2-TJ`+T_F{(D-QyFQlEMYKi)FJvU9VkQ9>y>0P_q2AzHR<1w)WSt z9;V*x6uBi96(qM**c$M|CB*~(qqwkHdRj^^x;uSagsLL2M~bP3(X&=J`alB1rc(to2`~Yt=^hcEYvZe6Nzj0 z?i()VZ`R3>8nupy!2offw|js=_fyxma{pxaIp6=G=`7=#eB7wNQ3GjELb_2JrACb| z1q`|yr8`E925CgPLr_|}hmz9W-Hh&V?Ah=CJkQ&`-Tk@m>pIst-@}XBwk7p9BfNxR zO@CX3JU46Ha4ounW5z!RWID}fz5c{rV4bmFXNJIiGxg>hl0rig-bOl2!<>}?V-4kC z{mj)RKG>MP>Zf2l@_{*=Ei~b2`>X^}6Ty%L3EoR4CyAiR@FrZ+;0Gy#W|vojp+E`I z?8!w2UsB{LrLh{ZqhWsR9pqQkh6=UwVGa-AO4#x4RN9}$a;;+tbGyd;BXGgvQdpPm zsi~7OAWBD0E34~Q=iGe$-4()p$BM7`;-)kGdtV${;Cp9AUxMzcg#)Q|H30w68DY7vIZow54yC2%oBvc}Orq#D!vg%AZ+z`&fbyN118X`2-T{SCkDs}F+3|G+P zpb=mFvnLn>kp@WmR8#Jrz?wm@bnKN#{CzbHeMQ;l9~E2nM{vIaq@qbkuuc*=6I=%1 z8Go01M?!W4G_6zGabo1MnP2rfpm_)Vl=mZxgPdOTi`Zhp(}^dzNN0H3)erCVy!rK9({yQZ7(l6+-<} z4-FR5fVFJcnLq~);0H%b%K}o1P|Bx~5lUFH+CFt@21!!Deu-9nO&((`T@HFP&NY4( z5<#87+T&WO;4tz2KUw*;hmVRI+O|4>m|mW`hep>YZnNBARqJE3GS2n~|7(&q-Ypnx zBf*&=^3unG*#tUfEB(}ZITim$>3KdQOc{vXrKMXbhjjPV*nmZd6_pm>O z-uda&J+!kN zB^k{TJV`OMlhQ3vEo^Oei0O}G8i|pTfB^(pk1lHWhKzhOEVMHiGcP-u{7bTxPCMKS zbp5O@v-V(=u97Xm;);+CHfqFEtfc2wKqu_abPbhX(&xL#=@f zHD6MH3l@}G?1PM`8UEzt+`48P2Tc?AF{-Vl>5om;7*P-<4s{wcc(jqpn*X?3Jy4N* z6U?iOa#$rCO@*$D|CH4VeSOVfjF$l|H!NFIjel##Y+6GVw*h`9HF|PSx8TzYwY?5C zDZCr!DKIMNImY>OxLyRTcefCfau+A&ecZza=yo@cE3HL0V}d)Q+S1ahYeWi|@#IRG zO{-&Sc4~%k67CYyyb}=Io%z@uD%!RPf$$GhKk{*mufo#ACtkHwXJYH)24&HjnjW6x zm(miBx!#6@HUaC5KvOC^S|w4fE^;xd1yWpwF+5i90*T=d%t?aEQUkY`>4mIszjaPU zzS5{1mi0rc8lTJ3Xqa?>`)6ewkOa9JA3D*d?Zrv-6mq?}0G3L1g7I<`c)HXI} z&;ir*cet8#R0)j!1wl$x2b~HM(*m%N$FW9FiE-o6R+-W^_Iv0SDfp~ycRZ{yK!gq) z7?8b3JhQ_W7XQCZ+7$uR{0g8?xUR~)yG@4oTFRzYjqbB)NXnixqhwu>oj&X4s_R~% zHfuOy0l?IBwN8-0wh(?^eL%$UrYi!4+w>GrxU4&!L?sjM&QM;~0-~<0-?nB2Nl!>G z5Ykd3j^_bhauQgBbr@{z;~`g6g;fs5Oc$N@5)N`YnIL~sJS#JdkznGZi!B1Qw)NYX z-J&RZHe0L9Rgolrq_~+yw(|k08(Zyc%w}{NZ>gk+VTKaVr_26jfg2PT#DbQf&JUyl zg~}dRaHEvpD5PtZ_da;(Nkdpgd3polX-$iM=FF+fCMz*#2`&XVXFLP*WzTjC7O46!AW$1#AwBc zSzJjrk92lv$m+;K@HdU`_Vd-vc4A9NVlw;;F)*Tc&jNgpzvhQ!dNtGW_kJf{_>%Wz z&jT)4C)L1vq4r<`s%EnFJ}dUY9viU(z}iTqmKf@8YY$ItWmrRFB=~=0Mm=Q$c)PN6 zS$Tv${_=S+|NmY97J#q_49w*~NBtYXt6_8bg=_SC=RMqD|F3)WceW86Yk{T%On+j= z#pEAtJGEHB`4UV(RUejo!0`={iY9i&SFKgkbV}G%qX19T<98Yw_v5RoLbobkwn1x$ zt5k>X)S>?o83{b(00bofbWPp=J{z1tu`k9$&@{%*j&%wD!P@xA*EU;*p#f>kf-xfu0r2cLW}+E$aei_XG|Uz+W35Ms6>{w5N_k?O z6{z@RPs-}`Av4HihxvGW@FB&UC*084;p@_oO+jd6$q0%mCzxH>1!Y z$^&DzLeFqv0(D1r&^UjZN)icS!35*QnUTb2SH+t@#^Xd{^1p%(TsY+O$DvSbiS(je z7UhJ2fBU0)X-fT39~E#TWT^OFbk6>KTe*UVH2|pjM9FN*FEA4Kttcp(92Mr7drACh z_lLB`h4>@OW>oeIOjH*Xz%%%nlsLt~1BK&E0MP?h9pJQ7v2b2&-(xxN2##FLbN`MN zGVBxV{)%|@?2Fdv(7j`d8CRz&VH-g3+^i`V=(rNtL1YZ_p30{GKzZPl#Gy)KFpA?c za{QHJ?~^HoJ;~?mQZ`NXid>9Xc zd*p*)cT`f0{(@9FNgVYA*k`I8g9qFwSFt|?Vhy5iq#yRB2;F7eSlQ^mpsP`}P6wnHjn>=h;S7H@T>-E^I*)BMep^b3oV+8%H}t5|)j zl>7LaYj;P_v7mZ()#<~3Bk7{MsX4l_5;;l`2T|N=RzC|8&dEv0SS0%ROZfx$k6hLrK)*g-ouODe1(-8d#_@7eM}HVF4XZ?`BcgA<=; z_s9?4p`1R_Ri7&==!UF1r}w%#86~DLiI>M~AunNrdZ$Vo8l&0N{g=;qDamTetfpGS zEnWTuT9rsQh6X8hDAztn$%<=0iHAo}Y8k|%M+A-b$CNlKFHZdol(>Z;^S9aX` z^W-=7_0CXO?5(Qu7vBBxM(BQ5*i1$|<78u(MDuNqK%OzhQ=GCVm*gk7 zJ@)Mu|9%Eo^4`n^3=3$^hO?u|meRhGX;(%ntV)rLpDf&FBe7Y9e{uk5$zLEovheBF z8dN>8D-3^~M?f0{RHB*jkmMOhj5T za5S|?&{bTXvu{BW_|8W}ZFX*B3MP@k`s$;fS8E?ZI)VObdC?lv6HGIK9qn>;wQp@q z?#WsZZ&%ePS(Ik_9+ns09yplXqivr@5s@@a{|yZge>@?#>dQbgJN3W|t=&EGj_WBY zdSt{J7!bO0=GEep02FjyhDl(Hke2#D94LkWu;-d{prt2*g4GSIDvUQp^}Zre7}&T% zm78V3%kBLHiEkC3Qjzex7C1@_b`ZQH!7#&`!`*X;(TAjyu&AM9-Z(kU-p@!yhq`aex=DpmPaMEm~raqzKZtqOzBq5{~*`-zK7ygF`HDad3l zzs*&#v5m`RmYar~nAu7s1Gq_1-H-D^7YF#1Ss9XNO`%6@nQhnHl%X1a^IrGOWw`d# zeXGNZgK#rj6MRKWi*Sc2JAkF2=1-$H@iar0#}$P{+_O@5c2F;5s_1Al2Lq522LqGQDqZ9B_VrptI|3bX*dj*`^u%w@R#|E5%FwBH;?pU4TT$xG%UZ zvzf&dxcfr-Lte3(;OxjYdp9$_!I)$t!>BzkXCftT>C!#lJpdd;`zCE+2FZVfLY?tSv~3+uu+l4=2lPw^!LL6UEJs6 zu_lJ7_HS+dzq3ODmS3)a_g9^ zuoWC95@*0;BBu}vhzgA-X@K}OHCV4Rqp?L1Y*?^Gri+vZHY6d|v%mE2gDcn`0b%Vu z)hoy9k&inz9l1Gf$RF+=uCd=sizLj>^g`GUs z2Id5y6FGUcCF+%KXr-(MDk1FVrg{mV7DE=NW)(|cB)xjDL+a?9%-k~oOFI6&8Z1A` z8P+nc2=5|WhEdg+UUAO|9{6*)Z}S_f11r9#+T^uevWzD-yX&yN&oDc#5c>phNIvJH zdhc`aor^_B@HE{}jU1#MU5;r|FZuje;y}}?^kRwD1oPXB2sr%~-sYkEkAp8XxwurX z{K+M6empn`a-VC;;o^=jf%@vI~1Z6uz%5Jpoy~3E|WxbDs-&bcwMRx(AO0orr z{4w8nODrSxm(-$(9@r6DQkWl2OUKWMoNqqqNSrph&;vRE3~*+C!Y887J2cqL)?5ni zm@={-iR&J=ObW6&9?6C^0>W*zHHbL9ZKbftk~xN37NYOMS+K$~ocl1pOs|@c@x6H^sNh?1?TuWH}i zR$R5Ht{60Mt=N;}{WQ{ZBdaE;Xb)D>`<~O}><})2BTOVa}Yw&*6MbQ?CAp zfU;0CIOK+6nflOa0EeedN2z2mMnOZB6d{7h*pE~hjAnovlK>`UGhbUG{Yx`|95c%& zzuQK!01crLFGxFJfuvTl|h`hL=3}L zCLmWsjGP$$%U34`kiwOsZThUJH?~uKc|)Pi?V7~;rdIm{WP2V<9TqVi!CVSDz}uIu ze}h`l@ zpA}>v==0I6M;;l@$OLf#*_Flswx$R=OCS?{_by{7Zt~yU$>gzrr>eL{0N?P)W9O38 z8>+}W3YiA;XxtY5TIf!WQ_9&tduT1Ah#9U&#l;o`<0&?`n+A`t$trf&Z$4!3DIxF@ zC#{SF;v$_iYM97MV>w#1r@)AXJB6ox!4Ay7PLwvCXe;wv5<`>HnZ-%cAR{WR+Pp)3Pm z*#G3wq#VF4ha!M+<_4i})O27hxrz0+9xF%a#u}gFiMp_PJy0**wKZiqC~Wvd1XF6J z*2sfA%xZ)E<2XJbi!kdN8tsGd(U2!A*v*3Eoa zrxsOQX$^vkxK>>Mv8L%SQLpFjKp!b|9^MyxSjKX%q*I=Npj`a**0=DaR~puLX7W=b zQ*#+Ib4ETQfs&pG6EdA#z5C&XoMDc!w~VTPr@0%Iu#cN_LZ!YK?}aAcyCwY|Vs_5u zJ=3BX!w$nfTM$G9{xiyqJzf+FDVS%Su7zZNRG?IenyB0uH6+J5&dKXpkFdR&=(Bwo zIQc$rgv$C-<7NZLrFk|bnCzyv1pIduRJ}$3v{W?Xw;hDn!Nxy(9Cv%GDc8YSAktip@ z+3`&4g|2p*9TUvv5(8fTFLqGUuCcx|V75AJkB&)DBv>go-V!@JanDIv3ZS@fm~tb-&eW!kDSX z4}firSJkISEuTM3AAv<*Q^(!324vedsCl&=7P~(ZRuvDd|3SzHp6lzO7M1@y>W~!k zW2`Cd{mH$33wFC_9kz}Su>d&Xbgae;v{@a61~WL&ks=BWeLJl@dhG^k|FrJPIW;Sf zHo2$mEJl+#da?vU5x;_lvH-XMk0C-)-Qzntd`JTZ4)o?ZjJJ8#XddZFjdqB;GM3^3 z0uVjg;v`sW?AYPw(3C_v#2?{lo3RXHK@z|xgYU|Wp^plAb1b%G6WxPtpr=+6^4EGu z#*)`2!LdJpTM}6^!qva#e}BM!<7J5>@7&zxpf3gZ7>=_9T538-5nMtjQ@tU2u{jr0 zF&n1LXvwOnXsi41X2qJPPksf_=ke@lc%}rqL?uc#Va&OQF*kKHi zSC@$bXkvQ}r#$H>WCF+@1~x0XllfyLeZVmlb(1xhd7V9OUZe6giT@Nle0=Y)5b(G4 z)6GWJ=xcL-Hjr9achJYIIyINPC4z2SG-y6jbyM=IFkNjYEf zH<6Tv>AvDq4A{T^wy-+Ln9G?th=z~Hx>$L>v2I{b-PQ7BDv{a9|9#bi72Qr3Zn%3d zY%7~u;^sRrZ8NSBa17l)u#El=7Q^Hu)()5sAkTW|(PM5>EGP?5u8Mx8Ng$~#OZrju zicKK>P4qNs%*bn^M!yp9WF!dlw`gV5V@OP|&& zk?-Wb&UT7bsK4ZwUaQ_Y*0a}x_p6xmP0=G88CDk3M896ySwSm0JfUlhfY6-jxkxDH zm$v+iNVwii{^4H6$S6BRES4?epkmePArH=81QKZgQ{Phf)K@~{sXhxB}?kqp-ZpVJ z$_kzm$cI!q0Xg^FtYt*dceTaD)&&iw&|*RkI6JT!ed?A<9RK0Q*%?s!RZ2Eu!d{1C zy++2nl2V)Qg~k>R6#iWu7+Ntao;qC|t8D)JboqW*&`CZxneXW9iB@g`6-?lBFTOH2 zJyUv<=&OjbugtA-?vVjIlP3inhgl^@1L`g%RV?^);f-CTTAiOE{_Ck3soub+v3AOU zJ?;b`o3aDAxaT;=+CXRj62Id+3^iP9GaL1Iy507;^zDDo!rygSfE0SR0Tm$;)>2Ze z{JQs5$J~3VC!LVM+fPcLJRNE@j!}{raIz0#V{`SEUJzYO^K)0FF6d{o^2|6~)^GBA z1`*##5@Mr|F=3+pfX|>)?_eOJM;PaH8t{HjULl?EMjMjiV|#077~j*y(i}`9RQ)}b z@S{1~i22T>=)FDn7^_{Tb?x_?zCP=^n>tfEy=S4?VyrnN9a&=yXtOMGZ(m(aH8#vc znKL}vP^$W%u2a@uBS9y^DtW7Et6N*(;$ZZ>_qdbOo?RtdnlV(&XcqK;@ zw#u&r`zoUjhf;M}gct4m+@ zI2AWaUp`UCX|mx1q@9h_GaP6v{iD7`|G{kl`-^u)!r6|=1Sqr|viA>R{MM`OvS%s1 zetOk-7@<)}hOQC-$;+7I!uLOLd}{J64E)#9RMV++aTq)8eUa-cak;*uX4TfsG=f^U z!}lHk5H~g&*w-`O9VpW2pf}{Zd<7mEu>9}0BaMtKG573sbFBAf zi!_G(x=(E=2ZYMHE{s~&fF*Bg2g4Zz>Shgu;0Vf4w9EUjxbCtrC3EaV6 zUs!RwaqP8ar3}Vwuh!r2Zn-v>Zd-{ADAu;!mu6O)u9rszL&dAC@bL1*C_*K09S%bK zi`IEw026vfXV3Isrmm#10IfKE#PUP2-g7-D9{;G^=?=>kc}CJOsdeF@JPUkMB=GP~ z-ah>%vU-!TKrW8212!JqMF>gj6_p{}q_ygiEO8WOG*NooBb zcf8TNBEJA~7-OoAV%AUmxQH($sF|6Vj(UYg47Q4=H}uw`!oWJGPhn5U{88^bm7n)t zC`&P}2j}l9-cG&z+<}`@SbS20`q2pV^-A+tjf4k9^7~ZhRG+KSfvrpNA-pv)-I=nH zu$8#(nX9htKs&(_8;8}m(#UkLXMxz?!aXt!m{%1H)}Sf;)o&cCSM1_{yD49I4*qyc zZ}=E5y}9px-b{Jd! zvcx(~L%q0=@_otJR_QB&0BE;GJD9f>ytIUqJ{|O{2HPmggHo>clBbog5_6}gz-@52 zzP)KdM`)l8^NXG2!L4Q$Trz;D?AdbYepx8yPu1(zWQ%(vv$sF!VJ>%4yQi=_vy8)> zyu0Nufs@cwM8FxtcILQWf)sA5f+Tp%7*uk2e zo%y7F5DFMOXVFwR9kdVo#sKF@$v?aA;KixJy~Gy9Pf);+?WO+Bb&C7@Nold9qW0y* z^E85xS((!&TRGvM9bsUe-cHJRK6*UP2;3R2AI14kgV1s9uTh25azt6P5f{mk#*{4! zQ#VC^5wiiv?f<3l=T8bL4Iczs1m|Do09QOrDD&>WZ`BCFi1}6>I{<0p1^>2dxNQI? zd`{cz-kpd=`3vgohwrHqceDQlGVC2I$&q(vhJgm}FwiyB3Nn&2jY9vWF`TI*ldY_& z=TaR=c)!l=YUMDrG6dGfM#lI5dfVJdWbk^KN+F5KWW<3*XeL0fvR2SyIh{T)26}Eh z6L?2wHTl)X@0-~J^vIwdr6(R?({qL5;-xJxfg+acsPWs1$-9M5*%^jDchG9qY_K|Z zd(@Ij0$NfQ2$I-Lf`iB=|JTcO7vl0Fi9UEuz;Gtu43H*9X$(&JEim#3@k5a{WI z*#Z9LAW7lMUwY4QP9pDYcflHbnUo&$yTyV3>@U!dq6?!0+X@Xq(O;4Hs$x{@OaP&I1IU48ZXbao=%e?6c@qFHt_iD=9K znQCxnS2ez&8Gp*fLo};pad0(b0J9a$PL>ah7|kt-cDfe^-)WiXF<6Y|U#;q6K>r(1 z=E}P13m>3ZuvMu$N?)wHUnDGJy}PeYpI0&6%D*!;nwM;nSXX2Xeq3>ScSx-VyD_CP zUxpq9Hq`ZcULT|&ERYN|saEGBz0xUOukNLF?;{FPk9|5hg^dBoPYdlW@iT$Tho&0b z?DI?gde2F{5p$LUf5}X3kEimbY|hF(o|!#7d=$wj^7W`Q)j5WI!!+@B!n(_D1JxJq zyiG{JtoGShj5Q=}r|!f-#u<~7-U?}3m+{fC%-=Gdc6ZFt4obfMm|}FdU|-jg_f{~a z1+wLu`d)iD(g{p`@Uj6)T;yPMM$d%o@R2;T5H(g*+lbsvxrwB{-|zqSQ-mi{F^Le~ zApCbng}#7Hdx4uziH^lEa9EsUWL6;cu}M%)G=!akz)t1!0K*Ng?b0c>bLye_l5vjk zKO@uPH*AL->U_;BZtkkcYqvHsNg#_8RUcq$(*p%c^ zwU3$GoyjLTW=Gst)5M==iF_z%WrC0XR0r#4tdu;1I1I zcyzJ9(b$!9kyD!1xT3|sa{6R>IA|zmBYsw) zQ9Et>&!n9Mw|yp!$w!mD192W}`6;BGdV5JcsIOj+=e{=lPMCjYCnKyFl+f=^*hibe z+$c;BB!$IGeF(B_?lTay@;+@3{ZqL1`J3=Fe93DeGJrqXpJ{12S0bKBYxM5K@>=>4 zTIWCd@aaM3JG=P*_X4b#)GbF!b)W5e!!GzGDpM`x)Km2_4Xdn?5xPP&#Ue$aq+alo~`u!W$GaWw)KORsTcEjPIhe0Cg z)WrbC`g}4$DKAQ=(bo5n)5s55wtpz%CyDQny{BO^h#(f>{V!a-`D<~OqfZvF;mW?) zl<7h@_yzVHn{hF^rubQ7>fmwoZ=-rYAIIqjr>9GW+{~tNu3a71$8=vEk`2nmX?Amn zx_!0xJ9SZkhdnlXqwW9VkhPD0;zZ)N6$hwJV^fB6lps*|Mdial>M2$;L-G@D8XjW9 z+ia)G?hy|LSOiAJVMbj({>%{bZ!qWDD}bcdSlm$W!A$$xy#7eJMId zEVzZ8j2CmB$n|>S_n<+*H;dV6UH7{%|vOz74cQ)Bjg}!N{x8_OA`>i#@JYPV9PP z4rWy~58QHoAqs<18bWE)bbNsC_PekD^Ef?PjpnI21+dUH?>a&~kItQpbwB!K%IrbkHfgQdTYhUhn)LrWm-3T(Jsw#vI^4$tM$Bh}vZTebs-XGaRD5__=FJi)vpz3~H)yCs~r)7m0=3o`*Hwt0F`(Yk1~ z6qJ^y2`uI%R2d0ovhc-#Su#@jBI10r*#01-WW8zFq zE2(3+m|vc=Vq>ZJOi}geYj9Xm`gGq4!R_)kr4VoROT#CReT+e0s=mgZaEv&kEX}q#%2Nh zG_027HIY@mokMiu50_$n1V1Xu_qQDP0|{hoU2#4lwSQr-e=pqJ;`#F{do1~J#3mpu zr^SufDA%5t!}?ww5nWYTTJINqlA4-_ix(taJ{N?QX8*=Ez@e3(KYR5b85s3bXUx*V z;^>?hv3|cf{GY_vB<9+-U0jC+ANYm`n||I%5L~IPdcx~DecZLEq|?B?+uu%P=!BE=4)YWG_)A;RzTT%q6a2t*~Jqr|S3txR) zqvL)A{f63foH<7noV?T(AY@cfD*y%2Yg$4$FDR6&!mii%8sYk0Lo2_J;NveL?pvJl z_1Ck*nLq#b?_-RCIHDJL(a8~ISAtDf(-bo7da2BkaWD~#%Ui8~W+ z`s%DLAjX*Tx29tja|XExWs5xfJh#2G;tUgs6Kn* zPkfYx!W|Z=noLkM6V4_ECN-oRkN)oqDo|(gEY!7D6E;f z0S%-T$4uyMjkfU5)jN!4n*)TkHTW*@cHA7|x71PG-Q<9Fu8*F3SDJsw4An95wfKMO zQ$}3=!EqRqo22$%4dE;ClDh2M@7jO1kiZWxIy_kVBn#Bl{q<6b5AaRZA@Gwwwc-lL zvD7&(UGoEm(f#%8WE3AtkoD|?BuWYs;{s227;Z`LPdUm!@R-rf z3TS4FChJkOsOP-b3RgY-`z!3(DcJT^wabZKIljxw&e;X&wm9*0tDBWWGeo_{y;%67 z1w)G2znX^l`ptLOdf~38fZH*H7?IgYq6+fa;>A_8|lk3j#BPUP81PFF- zJ?HQL>Zj!z7Qu|!&RkpMuQ%jJ#WJf7;uP;r7^=YitHtE9(Xc|g%o-#(q8S%3bWbX) z;ca~UPD<}qwXlrdzk$O2vOh7bRgQoy2ScAeCVH0c^6>Q6m75#h*r;+)vAm3;l$N9c zcJikCaj<7@MP?7|_}0}jAFl3Qyo$R{lKY}mJ~BtOFTxXQYJwQOXQ=uC9(fWal%C5O z3O}4WnYdExL}O&7As2-v+<2IS1Hdrb>aUnDDJHUj z@l`9J+f0~?lLDnF?+W0$a&05j5He<`eYH*`#mrx`#kJ1nczY!USkB4fFR1PpkbEZd z8<3O$P!e>8a)$yp?&;=)FWx2+=Sbw){gD3D<)edjg8Q{%3KS~$%9Gh{Ds_)%Z6H;c zKz=8gj(D$8DPR+9`PeC(KKa4(xS$g~d{e(S)uuJksfj~8PUxZv0Oe-N1aBgIn#Bv} zuk75f5jh%Oo$J$eE*sqo2Pn>)YD8b(e|!d_$nfO3f`QPEqipFnoo5E981-CT>i)y{16n*o)h+>3O>MG!)~(v?Rkvdw+{-!k(9| z#2cX@RoI$g_9W?BSa97=XR&X-wRTqjKL3XI98$gAK}+z5(&N|5CnZO%fkD6})TuZj+Lvztv>|Uh4mKBcT!JE~{EWm&ai8dd@B^Pn`ONv|;PJeJ)u8CT1utuF z2m^Q3uODwOGKm7Ol7YG7iq-n3nLrr7XNFs!&t z9tSF8iqfV$8#)zwfX8P2?` z>yg94LJ=t;qh<~mA0B>gJWWNF8PvJ_6E*T%F55etZHJv0aNJK`xgytR{<~f-J#SvQ zeE$t_+rdzu1 zz{Ez7SsBbXpwk9Yif#JKs`{e$vdA34$#!Nf;0&QxWdXUH$kK@ zH0;DcJY1`xKHg(=DZHx)3zk2LoA!Y(EV+E30=0am9Y)PiHT2cysyQoGsX+S2m2`M6 z%uk^NoVT(gOEf0QS^4)Fv&)=#pMh3vYAt1D^0qRNm9=SJF*N8V2y=hfKgkY;`0Y#o z)~LzSRm|;pcLVpWb>F9eTi}8i%B&?|U$PE*u+!&Yq#)efK`?G(-Ms@7l_x>>=8367dKM?ql(2FErg1df zbaJ&^BGnDDgc3+Dcv(V1D2pzEe$ER!yY%>n$V=T#wuL_~dQd+Dm1k3820Kmio#`jl ze1`e>+hi^kv0sR4W>ZiGI(Vf>Lw2xpp08<;?49(o*U5Znk;oor2;B0y+{_KcH_DNa z4Y7ep_nL$fzp6^uVKhWagbu&?V$h$kcd}KRBa`~rjo9n&!9RTYzG(@vL7!Ez*-{LW zN2`eFAncTdD*CKVU6JW?!OGv3S&4oI!ZajgJ1>{LCe6K0A%NnbX;ekpSIFPR5xrV0B%s8O=EcRS@X_emd6@2)Koj z5^H+V$&^SMRNmi!8Na{j%Jyiy(wEqF=3nQB zudCDd3K%4nrEPj8D{Zy(ExForjcPEX{A=I1?ctG7CimXdAt&?O!!avBYv?O z3T6g6A8g@0KgakVWaAkbRoo-g8SUgnUG44*jY|H%*q|gOo$T4fP>m!231EgjIViV%EcQmTRmre z+Xjoitsf*o|LD%sm)1wx^zk>`u$*3^KR?ucrI?{gmUek{Y^j!WAK>%d8#<i)wg7!d*Vavu!=VBIT>$C05M7PP}3bnQOj*T z68de}YtgTVEC5|U8r?6RhgxFB;r0H+)Sd43Hjla<6?Zn??F^ON)6I|WY>THA8G9x%d%sY> zQirk=(oBv76UO0nQ_DzpX7}@oV|NBamV$Gcq{ zeD?GknPxhGX&y>+z}8l$Thg{?6Gir)Ep@G@>46%O2|qIVlUu)Sa$nSEyDaG*A2rQ; zUskLaKV&pZ0)(izUaSV`ajcq>X_XAEr5a^q7@+l_OPduIUB`)4dZsCC6nk`WGlu&G z9c~&Y1MX43GEm6<#rt7NhF@Yowu9(J+fTNOY!`^!Dw&LfZ&ZyUdXCaM<3^#<*S%t& zYsFscp|-%X5$zb4;2yv9iR)+O2p;(LKjy}9NJgYgC1;w+lcfhSLdAz%?|0tuunhhL zVcVPT&cY-7PWjnU)RW=30DAOl;@W3{<=@ssB7yyJ=1MWh;s6_fMbI)TjxOuC#JAmf zITj}UK*W1P!mC-nGY$Rusmgah^c0ZlVFKf}-tY7Fr;p{n&XEzRC0@DcT5#He>0NHt zge?SCDE*CDAyI_*@L>S&_DC7AK*Y?0z2`X2i$3Q4PKNActo3vwZdeQ9NSzaam4$;j zqGX*BjwdOYJ$X?jnI3T9CJmYW)YsI)#FvU|U=U6?J+Jj#GF#oYqg5>$vFP46E1k>R zajjM7a=5o}kgFWv|0(c4($2NgNPJ6*5b(5Ap^4X20goW%kq-^u4Mg$m?$NC}TQkHz z4wh$nZ07W^onUwGp`l&CezFA9wU1acAfjLEL$ycJ`smf*4y^$lMyAkzvvqZ^JTDYL zS~xz=5j`GmhhwP#a$B1JAbzc@-Rs=%b(zdJSVdi8@V`F&WYN!TM;eb=TyyxjM<$ph zI#&cf&mo+Z)7*hgR-y*5v?Si+v^RaVHUiZa%XqzatBNOx@b=n`V~|^90Ri~5;zQ%o zW{HyZ$ojC&Ul1Dfy9vAhd6ST*4C_!ADqRRNu{S?5P-eF#iajArC4kN!G6<=B_}IR%`E7oaz8Pb!A?GDsIZQu>L>^K{r>19iKN{Fo_lNq z*J*@^%Fcm_2&lMZ5^uQDaeQZB)BVQISjEQVRKl1koGRA;RrOx98Pbd=i1|My74j6_ z3rCv}1YR4aHP#ltz|NMgk?DMlgYFI0yx_5Zc#1Od%*J)JpWTO3^BGOQ9}6Coh!bsE zD6xVpcioTD2oWR4C=>R?hO3VWDr^k_S`kv=9trc?3S0*sSL(j%6wr1jqhW=lyTM%|Wg9bH? zu&lAsKi>vm*LJ_*awbJbZVdSd%2hS-l7NWRoaGGuYBXV!T%Xop<|A#D9LL^BZn>cZXy>H&sX~ zyl;woa!cpjpc#>+eCDsn+~q?yb*)Q--owrR)GN(AXVS5B%=1_!fBKT)5I;0jH@Hjm zkttMmR#fBAdHCIGSSGwI8$MdDnCKhqxn%KFc^i7A&(E3iC)&iG;5m3FJ;5 z9%zA)&qj;XoNhPWZ~tXytp}z(*rf*QUd=Gv82ooP?qmxHxk86FXr3l>0pVe%OF@>Oc$m9126fOKk^O}pugU9lbZ)#Q@BG5NcI%8X?FB&V@&iy@_s3lZGFLAO zLB(16I=KFxgCEFfNv?l{xGy$)_SRmiUUam1W%8gX)U9{Nh*a7ErPY9_V{xy@oz`goQ_aDoje*wJ!I>=&L*)UIYNnfyCWD7T;&VRu zx);2gxR(whhsR+?Edlnf|A(ftV2iTty6`W$Wl8e|R;)WNV5) z1qy&)lpLyHY1$mHKg?v{oN3Hdgk zs4F)rRdi0s%Yt&$1D{QvsHuyw3#uX0k=sf!`n#2$&285C!kR~)EuUFAISjs?zjiyy z#M_-;E^s7AQ&hl;k6Q`4y!MuodlKjBy2r)#V5@aet$iR|Z zHjxO-#RF>gxHs_5XV`nG9HNqZ^T%WL=;vzw{`RWs?AU`i>mp z9?B7B;)N&d*r-3cY;b-m_Lg6UYs7Lym|E#2f@x(1bb+m}V20avyBWAM4|5c}9E(DW z$Vp=|(*XT|H}W&6S(CPmCQ;|WeprQFfCk7LgIHJ>9#cC&C-62xhkmLwTPBjN3G;`F zvwTtX$=gpY?^=z9QZF#uh4dX1$JeDY?sz(|o(}xJJt4H$LmMYmE2R?XbEIUaJiNXq z0mJ|Pi1E@Qe7?mm2M8~pIad~MX!sR8@)*XV8b1cz{zl$a4=0fG~QH*$kN zyYz7aR&`x*pA}L@Xo4PRq-|qv;~I57n4LsSIJQqW4|>i^*1V}|*cMzv=&M%-n+^7P zy|qNHUJA1BR(4)Wj5KW3NwZH{GnOCa)pEg)zB@CrVSuT4jj{nap89(Q{2vfm3gWji zpgCgR{m_iq)n71NnDP6cV^A}l)plR{p72jLogaG?v^bM^@<Q>P&;_lI6{D>M)D4|=%JSCo&F>5xB))nU;oD+cZVs-a6!18?O8XlPKTC9CM$F&QUvD>i%+ znwtouQ(Z?}Wh~)A$d|^j06H#2=YTL9``7Tu3!h;sARqTp8B#Z`D(?Hi&5YNNFI2(CO^`Dgm*bey83iI5aJi>28&b|QH2Q!n+(rdyqJJt-u93xD%q320}iV6?-+ zlpy-?!-t)vS;^d^;0xiYCw=zm>SD(AU{-Jmiu!c>i-&nw?Q2a1a$_+U;`YI0;vb0S zeVd}}x23qJB+4BiHA9gb=WJ#ZtGg?m@^Nvx`m*e;Pn+$xMBW(B}3{?|j=H0Xa=NOCytUoXF$+4>2HB_oW%R1ecTm-TQszWJMa0&A0jUok} zV>MfHU-~kP9*6Tfw7q~olEN)96~O2F;r3nM3EItd=8Z5>D3+@q_o3TIND13^)9+Sr z!1G%~D|$`D$+r_DNd`zRjI#i%MmT76>nh4rgy&QHenP6~u+i)l>rYeojipm?cE_ZS ziD*`R>pOPMS%_RLvcDMK=DOzuxdr^)<6<5tgHydnb^HGPyUhX*-9C37EkjC8Gy1E( zdoI6_R`++0-m-+EE{&n^&ppRT{JoTk^i{&ntqGzq+X|Uf-u(JE?5PtRNIg~g*vri2 z6$=W%A%NYf#pBl%dT;5*{(lx=F+{?Wz=JHROCQqQ));s%)zHN?y}oR21DquKvAvqP zpkxMCDCQK^wkSLpu1P3>r6+f!)y16dB|;}h8QpHZO9dJp9>yWeVp*n^ZZRm^x#kOu z1qn&FsGr-$jNVQ;Kl^XxG{RHr*2Ky`!``<_ip>khc5}tgtRFwL^+$1ZEn<7)A~{68~;0tJC`1OD867gx3S{? zpN_3FCLx0y%H$b8xV6kJM-KF`h(P{eHPzv@J)x%gY?7ZESpYdIMp}C>J#@D`l-+&p z<byjg(V3J`QncYRT5xy(Gp`GbJxqRn549N$_-lx?84g^k zcdFQL|Cdig-jUs%>0NdEz zVP>#O*!R9`#H~9^dRi{kydzBv-3{cuA+FWC_1H_1P?hC^UDjfMA zVzh0neG92G0*Szrdij)#9DZ6{&SVy(?zCDy%#KZ6{t4+i%)o;?eoP>I2QxDn?e-xU z9x*b?d4WucOv4C$dM~r|^!W16#`*r>C($&akj64_!$PSJ;&#vWW8(d(byw)nRc#c{vX+amB%jzv2@=;1x1&QmB^V_MZu~H&n2^`!jS3~5 zr|DjZebd*UMzd{7kzy0MWy<>@u5$ka5d7B{kFIJ_|LLMZt}V(u{xj|i%NbK{ zwD9tWq4l}aykASyv+vO^+|0H!4Bn@H)I|ly^X`O3YT>vr^ent1F`un{WMQ%liW3Es zzC&Xyn$9^{X4jS!TgN{3GlP*-NW^qVB5wNxiFgav6 z`!sg?wnuL+C}+PWU``GOX)?If>P@|@=axI_D6TNg3^>G@3F*A|RQf`TIw$;`9qdHT zY|huNzE~2RS+RWKSr>G>@hW#T9aG=wo$zBQZdEnKQ0suICt6Yee!h4MfS^ov?V!&% zuP~f-S9>KCTtu&=GTr;(x+HEj^+Cfqx3-w(m@M7q&$abSo_`dfa=x<8QAA`rqKOxs z@%6rFP2th>RV?|I4-vHA&#>Kzuo4# zzoc@dBjIsodqqL~Rbkhj8S#U&%XoyVKVLSokK%Oc$rq_r@Oap5_%Hj|upH+a4YE#?XaACqykW`~EqL+!4323StWM*dR-=eKp+6f%aR-Ip4% z_)ZAR%R1W*=l(V-YpL=%nxJs~VPq;|fb@UpzfHX`^RbY5A6Mpp!g}Bx%j)*9T_rWo zvWvq##83ygbc|Wahsh%yx$)06pfvqfO$^=Gw_a!m|i`LUhlpQuatn z%oeT0ad#y>s`UYappTCVX4^GBYM2yg8&=lCm`tI{-(B7sL+HikhP-UhH(b z?<1SM+np0I=B^DH&X_edqlSv~o;`geqa>I_G^<4;D(c5hWMQ&j>N&kbJ&aRQ8NUdg zma*YVXzk5Qo`j6oi-+V?WIr&D2vr{c0iEIDGbJJ&Xd9dEO3`SSu2qrKpW}6p*INFW z!tYVY@ma^B0%_;-;Bykha|Ny*#O8y}$w~ag1yJv5OF0^aV(!iSR04}S21xKyAkc-{(>Byys*j1oK3}OXw3vxIGC(3Mw?Ie4r0rdc=p+f?0^4WMjT6-26co$!zk!a?TIaF4d$1BQX>ddsEIO)v5yJ;59N1u z7NgS>b+2Y3gsoTbZ4fBd3L0&y|DKzIFIRMv)xK(|U^3b##wnm36NYVNV)sB)yr2}_ zPn5eS)?04SvY-FMY}P_aA@V-H5+j^hd8jOHcfxAESURSjAVmfAhG=tp#%P7pHXI60 z5c|-5#&2yGF}jy5^#WGTvPe%w;j;&V2#?uVE59tP6JSynv+X`ZC&)R>qCg;Upfyy5 zN%H>nK?R%Y1jZaFgRF#PV!*^+{KF5c1wwqq77=(m1bDGwo-8_e8A5o%FG~Y>I6vCbYr!tq!Mu)t;8(embr%&qkBnsE|8fLo|PicY@6Io01i+ z5OfQ?4RZ=6B;1WB=ejx^4HV@}{I(2A#S_8IOHvwk&eIk?H@~>dL5uz*yERC^eqEib z!oS1%_02;X8m&ywH|J%|bVL7rJ5ummZzCOXg9X27!@qQb5(ZR`G76zu1 zh{3$XpX$0?_xq{vP&)Grz~_v(%l*&5M_`XG3O(o#*D}iuiZ1$Mg>e$iDA8A&EOGwM zu(o{(&gWw5alh&Nf@7UgcvK^IjY!zuO5_@r22+bM_9|48reR$3q2&Kyp0^Epzk|wk zL{87hi%YI7IkZ$^1En=%c+=MYi&EsOv^0c{C9`7iYkyTlvGF^-s`J*%`M1uMH{8)- zha}4ts1V_PP^ZFe(3XSyLYZ8kOCB}1`sNEH9J zfCObeu%y*{PKU}FQ~t-UQGhfeFoIHl%o&tiJ`|_mlID=Mr)c9SlTqd#?!BF(;4`v zV2$Z#YPj^gT z-boGi4PE&pQtd0Tp|lC3gp@{T>F+?mD|_g}LMC(S1bNwtT;8%}?%-zeOo3Qm9;^h1 z^lb_6_{x?2^{;7O%3hV3#X$jidxgFz`xsDF5bzBJhB2Z^m}cDlllOxePW+1>6(G6q zTc`XVg+yfYE?o_f8v?3rC~lNx4;RT&j#&8a)bnDQJ&zkTJJPjqGrU1Idf zC)4F^t0cKa&7`KN1sagL$TP{yysjl8X0%O_ta3V&bP0%?uir&>BDD3)cGcD_PQ-sxOkh zcIUr6y_Tb%NL=MMM~D|F=BS&09c2pBL1`G!?9e&8de@B;s)NOh5k6}&?DW5)GvA;g zMgqgXitilE5Fo(7(*dyB`*Ozplh25Di0hL~SxB~$aL%FoO5Y|8+VrM+Pd=S(? zHINR%#9#o~jp_fE!&&p0ukw4hzsH*&w~H?6<(z8zaIQCrJpUE)4imb&nPRJ!^4IV1%r1fjKfLw9slGZ~u$is+I37~{h};p*%I~}`_c_u&2tnh- zpPm;)Q+WOc)JWxa0gGJS@2W`)LmDzP;SCmt_d8QHZEowKxwQnjIS)a24B?)HDIo9p zO!+Fu#S|_z#?(;N@F>_YXlGoAs#N|`nYrl_1RYy%4v0n<;8*B+&1jd!eSryumK6kE z6=*vjNCVf?UkN(IUXoSv< z|IFRY)t4NVzEZJoi%2o(bhM(~p{p;@d+}?9WqgV@mY?_KEdOzCUzZZGY*`Awi^934 z@Q;j{PW~i|V~o@}7g5gqP_G(&WGHIaPeN&wdyl6)`Z_DSO5|)0a8%OEVt&0SdKoe? zLno~vcj(G|M?XRt;xWeOA;(?0swy|7bT|U9QWT{mcCKmGy7hejkdZqreK&ZMD zmdmO5=23*I{;-4P4AoqfUFb!ToxzU{U2%Gk_o$V_yJ?hmHe4S~S`gF8v#e0=g?$2y z*Z<4;#LmdDJS7D6UF`*2HHe{RUUG|(sdHQC6a6c}1d_Y=Oc@BS!K8-9OCFTM0E{2s zDfosuQO<$zFRbq9)ObbpM~j$nU|3}KAs$0U&JA9nJ9y!|S+32>yKsFbHGbtS4;+As z4|4Jox*j^KXWqXCr#_SM$_r#op-+lP{>bjV-XzK#iDuV>3k<(%4dk^En%N7-U2mQM zTjB58k+AHgG4DAJ6!M}&%kC-PTh$L|vJrzzchk-VlG&4){{%whDZQH?XLcB!HtW^t z`m386FA@?-WvW(#{)lD4a~6=qFBSce*L!SjJ*|eHz@s9Y>Ii((bruVrjlA>A;RKj_vl{G z%alR)DB1GisiMRClEVJ#n1PpwcY7Q_$(dM>1+?Lk``?GZfhAZt6u^1a1(|3|P;s^e zfz%++^h)m;mFCf5@%%6Eup>()+p5=x3_;|6Y5h*U7^*>d(p6G1O4tCB+JSN35>W); z5D_;2_1627rjGQ+tFS6M>}^m4%iM#^hhgha@b_O_b)w5f7!4nG*-h_B&L76^D(!tb zj4H~X`fx;)|95L{T^9@ZvOmmpUek&Bt&5Dr@Mtf)HKFb6dZ{$v4G|X9ZR^%qm~0g$ zL*qsgZ;&xYp0`Ka*+sej4`>Ny)t!kqMc8a6+XIrePoPCWSM zDbr{|LwowgD*La4;cBsl>fGk<(gY@U?X;HCTJ)ua#(wKvLir5)pIig#qxiQXXzAY; zGjxh{x(+=zX6P?mtVYyf{1fofuNXR1ffU$eZZI*iq^byKrHfM#hKw@D#& zBEEN&e*!Vi#lx2rtB1|}Ng1m|>ISj^x=c0G@(TtVo$aOuZz)^Is7TJ4o`32cw#=iyYzI|Js*QRLwZ=70olA2Fb-@a$6P%hB}mRNc&iz6a87pQOow>) zrBm38u*c{=Z1-2;Af9QpQb!RoSi-ya1K%p7w+Q_z6aF}aWDys*m80nV{5U~_)Vblt zGJg=*cXh;C>iF-c(1@68N~uSM@Sx^(gny$vW#W4HTOO(PfM&8;|*c1v>>-CDhpdyy5e zZ+FBp7-l%IFFrl^m?8yvk1hhvxbXZ4ip_Mo?b&H@^(MC~63uVTjgFc7imk6!lBwU) zyG}}s^JHK@H25CrD%EV87+I|nz5Jo+8CK{{MF5!G5e{z7b2IwmMpFNe!bP}Es7#igfJQv(eA&^6d7-8{9>;s6+^h85qMP%lFzKnV-TrXE4$>Ck z4D}xQbqc!(+FnAs*xA$U7K6ypzRTNu{gH%)i7!KXzcx#*DMMsy*{k-9FFBps``6pO zcVGC^`BHSV-o0XD#sr6H1&dSmCu&6zqN+t%Llk!+ADgeT0uZ9BS8v(_4(n%YXG{Y+ z5u#UFQ;iqxW>GMWVg*Zctmf@!q`epUs{7jQvoo)7vZG==vf8+#skMFKyUYfxr2M7YZQM!jh$3Mb^*_o zrb#q}vtg;V5;A=`>tLp1rqv{PnlBuzGhD}j45VS*Pqx~)I>!qMV2NNb>OuV6@G;b6 zB3-4Rz85Sg?+rU@Z!5NyJfeL#^^pwI)ze@;m!3)%H2c11zJ@GgMTLGpY4_K9PIz!9 zbi1}V(<-tT6n9!sstm%&L&Yodt7D|vPi6?0Vnko|SC>Zz=DPKCb(S^&~#2)wO*+6sZ5c)NNwg@X(H$^yCW=zF*hmnm* zSUHwDECqwAQItGcssv+pk`R@6)XnGUYZ6`cb@s70cEyuQb3na}Sk2I&0r{spC^lIwrE1>X#F zy~pEFr>Hv)#%T{(5~P zw+4xJCq~N?!ue)leYO(|(zf5W1aKcu15DerlF&EFnZ5dD#f@p~4Y8Ghcg!zTguP-=75?kZAv z%HfVaIEWXu=mOLS5Tu+%hQXKo7eo=*p&w8yp4lkL?o46w516oTZ6)5 z%t-b?Pfk0EDc|Mz97#--q2tV-^)BWzpvXlR0mp1{ocJ|Y`WrwoE@nIT8=wH=y7u$V zh*FOA#%E4l$`amz_+-6SJ{P>F69hd+|LX{&ol$WqSpv}@ZS_dlSx*BmQqU9bzYcQ( znsVM3%Ej;j%6QN>kJym#!v0IfzP zfK07$*(r4qm+L%fZ)E$B5ItDWL-e=fxtpd00Exf|9{7|}A`MP`38OMqDkp~cZo1)(;31P=9fPeZteHvAV%`J{8F31ZnL$XN-LGf&or$Wu@)q9K%hw)NK{0)nv z?DWsI;BH>j$V^$=A@jHNQQF*1`-UA}8mDIwra|9D!9y7&307xb0}}mx=!=nc+6gui z-tsV^3cD8=!4GsxcMP!t>&Ur`Y}&1a2$p<=?lk)N@3&i9x-kZAa&SN9%Z!nw+v3|N6cV_-?hD!-m8P$T)?;1s z)*$B2!|k%7>AR<3$!6E%`6DF~X?`b3)K#-Al}pC_?T*yf&B|yfW8;rM6DI!AtSXBO zcf?<61Tlxs^=h&(W^0ui|6BQ}3Cfk=lxxNQ@+{n>iw}`0#FZTyLTAZymqm?NVC;jN zoEoI>x$~E?tL7auPED2hQ3u+dL!e+T6W0Vvy}Mi-$qJBXZpOlHP0(+9wh&gUgNHS& zm*g%QQlu-p9zf1SSNPymKrLgCQyLP3=giZvE0B7%OVP1Qu`5o0MQm8ES0_k;QsyUU z*32#XG^Z+t?0-gM;{n~a0FkP{F=s%^fmzMQ&AzD|M?khCp&+xSwVE2UX}?9?$~W6Y zp+KSEgXMfzITwh5v{r<1_jPT-TYdO8b?iz0CK%*E-k%2`;$Xb3Iny^n(QRe9rh z%;Th-`p#5{R3mNF;6f#pr469zTz2Wi3;|&CVDJxTcK)^!a@bP?wq32-dlrmN>X;Im z`bEXoUdK&@c#q}R+o;aGW1GRz|IY%<;jg?n-oz1sujywTv`0xFI<^&YxgH1}YRK@6 z?-r1RcgT_q{~S~`huD3$_!=~Ns@TZMBdxnziZf+nz{f90yf*R10$8zpuDt0thP$Jv zHgGSr$F{e&}Ux;b-K5q`fAq)bQtCP41?kJ1&rp1dxe=IJ+ofzursEjbP|E$8My$ zx16L1efM-?vFnsbxH8=6?Kq~=_&DJZ*5`i+zhOCq>)SjUIa@#CH&!0F?wm<)leTZ% zxfIrxgrN@0jXK^5{{8iw;Qi(UPYxcMl~{DCCVimX&!-BVgzt znDLNbEkpsj=&HQ}$wS!r2H62_0Qn-?#5)Z;h}re%kCK%TET$5MopTE~feT_V8Nbrs zlVpVFOms|Rp>eY|Je~2HSgz5zNIH6B{1+*18)X0Avljm&pPfm$;E0Nx2a^U6Zlmy` zC^HaR5f*^nEr>~H7cKF?q2Y}$L5*D#_|+}sE?xXQ#faKTMi$f?o#QdeIMQbC<^|aj zq<+rdB}N_>NQo-*g^h~<(vV*fHkr6Ma2G~yIqf~rhr_3`$btQ4T7M@M_0+#k5KFU` z2<>gxc6QVM!MuFl{rwsIadwZ5kfX-#gFZqQ?y62uuUl4_A|T^ML00 zF+S{I?KgKii5YEIcJcwnh>#{bZlB$rw1UKd0or_2V3h$;0d>#oRm)fQUb#91`{|sw zc-91@5#w4|@vPlLJsE07L9cEUqZ%l1TJ|SDKduNcD25!;JYnJ6k?ZbN8OudgTOI@5 ztv812=@?98%3~0i1O*E3nw0~I6sP`HgAFi{Q8Ng&izy>bAERj~z_#Yv(vFmPe;#Hi zy6=m$Xv3^<%P-)~c$ zMYA$D6;+Jp0$=5B*(*YXIgejGts2B2HQI8%DJMOy%aX8RAEDl$&Q{SwT;c#AEseLcY7whiYjNEroK z@alBaV!*$iJrjv_h1?p|>6_(^SSsj^-GdI7t{qwekDTikTE2AfrghLJO2AR>Hx*|I zi*s%%7)>N5EKxNx_*>B2g_KkiXFlm+Pt=@P%5wSz!K#fi;!3yD80We5Ur2w-27&~uvBAO)jPC0#5 zjt|!Q;cB?W7*wpA%dT^ZwG}aQ$#XD<*;Gl zAN62fg|37j?wkxQ6K*BgM9 zruPsx54THehBdBHkL=n0XAdHD`gaP99D;v=H9WvHpjak};12SpkqFJA04yPCh475> z!rl88vbP)$v%(u@P6%EdN}zXrl_78ONM!Qdwf@t5i+^`D+u{6B4oCGI!;30=1a6^x zpD&w=kJ1O)mn($Cf7;V?ae3$FMPs|3S7?c$(4bkrpv-Zli99z*G*L4eaMtN7<|rsK z%$)JvDkRD9)dqa42mJFGLPI1iB;9c1P?G`Ca~97{kB0zzv?*LF zzXUHhMQ_cBTp0!|Y+Mik#xnbo&@oe~@ceKJjAPMl{#46A8(lU_vbCvrXu=OqkDb}f zGdgH_C44O~2}|0i7z(m+Dhrp-Tnio!$F`!NYufI(jMe6J+JXw?|6%>@OGqKrG{2R}K@D)zO*rI?Gzp;}_goNXDP7^50AmrG5Q zSLHp>$djpiS&`us4IP#6`SdF&57z(DB^wNvTT7BhZL&E^37Y!B`#$Q}3UtEdX}d6x zw@RT-`x@)S`7b}CN0Ra5Q;EkA8j7@f>-N(sx^1QrQ1a-gcsUe^uaXU*Re8im+slU9 z3(3BNNPl{YD#=JAN>+lR*$Cy(74bRdY^P9-!iRh(Jk4ZJ>j^DxZy}&DE-bbpX)X>9 z+j{wzO_op_4Ub-2iy+l-aOIT~AqP74TC^KqE>1DNYz|MJ$+gamBqgeR*=7?l`pZ<+ z7B6pw%DcRdmAvW$x7IaTiRBWHwE{199GVVDhO;$p@3~6-Yp!~*RjW&~|4qejGj!fxWCZ7<;EG3)2d$gSGhX_~i`jB){Roj%eC{)E@Z zS^4=Bkr=CJnBj_{1DfV|T;DS}mKkw{T|pc0sgRGS&V6i*O3r{3AAHu_!w z+N(3FL@_25S!X5H8Tmj1&lEnj<{Geg$jIc$9hQ>=A>^|n34QNQ{L%XU<`qJ69WmUD zp9dFcgf8C9cIAU4fVHGo{$wt1#)$~PAA|-q_kc8{Eq*O^XM1k@$>;3=blX#so(+I9@sDot*qsN z;#$zE_-CQ2?d}HQ?Ay*L4g3}4*g@HDYmOJn!|<7Of^O=p680n2a6v|tVbfC+D8-&uRA{ikrKtdPrcbgoXmd$Je#wrqB)U{6ctjEr z>gsBxIHP`|Lm4VcJ{$0zXnOB!D&f^djU)H)w@`0{tl*hkVMTy|ViaohCjWZCD&bD7 zN<96=Qs`RO$5)8m!?316hj70~{wvJqOz+yz9K6%}^Mt{_|7BoQQMr9j{BXQ5Xh9L? z^_@sz9=eP_#th>+)ZYjnmg(5Lj=yX-&9n)T3QuBeH|k>+i!6SIiTRb~y>G#vGThCbP6fcLcJ4mVDo;M(`nr_J0EHl+7^kcCKu66Je`+kb_|?ZOT_TX5@miV2^0Va(xMf=W^Du<*6y#YnzTCKXYvP%LWI2 zX)aP>`rS5P?kkMyA zb|T^=-%94Q1j7`+W{6FFzHsM( z?(Hug5!KQc?C^&%F6+;GnwA}hP_1|7l>slo;3H}W@11IHYI-Q@G-+sutYfg!cR86R zm9L%erMuWa6OW~RGdqu`eth?^<)g|PabNCXb+eC4I{Lyi;wH80`X+jdt7$9Q=1iaD zO$6dT=Lj2v!$pq`rl3VBm9M+9B`_+dUq-m|vc{<|ZzHNtcYz^^&XJMm-`~NSIHF&(d-ue1=3^YQt;Y;WX8r?ka20cT(SAfUvQ8y9 z)CP63g$|s*PLf?_-+u>nbGe@(R}hE=9q-Yl#XJ$e&Lue?nEhRb)2z3w(`G?%9{cKM zrXV`_-tN`J=#AdgQF`NqLvtq4HTg8Ad4&$7WiXe5@Fh${HCw_~_O|!zrNzli6XRXd z)?G|~=y&N8oSB~wvH5nIcCq7LDDwiZX6Yp_u?OlIl3cIIP?3p@i`Sd!NI<2mAkYGQ5Jyweep_P`bQ-?@jBz#71(7Z4|J&%MqV1D(lz<;# z#?O!k7XoV~sIY@}PR!$rMf)*d^Y~!KY6Y6fzlF$9dgB78bN$I^}oTeXAKH zhe3|jd3P0MyEF^|@um-xi+1geDH;0x95)!a7TivrIl&m@I$>M`@e;vHo9APo6spkU zAe{S%0S+GjG>mh@?G6FFU1_aq5m(^P80X7oXu6;95DlP`0><%WF@Im7U~aI0b)BOe z6U+Cl#Eev>m3y)^S`!DCN^`GXH#jIgfY*OI@>#0O+CqvSFR*aDG_eH@{w!jrpcVcT zJMN@mk{Y`55pZN+5|A1-chS@ha4g}~h$WpQrzWlnN;5JuV^Zsd`I9a*2ut+riyNMC z8%wyRv$1x77`;1*s>C#jLJx;=t|MJQwGm{5{CEd49~OL2i@>=r7^4HeJj4gJE{lkP z`W@o%mijqFxFx3l+Qf0egqdg-<8UcJxu5ya7orL<(l)Y{e zPU}v2kXPHD=y1m#&(%vt!@F}WG<}%nJ_hSTH({{fsv`H-N2b#%Xvi>fEK`v$E3XT{ z&8Gxe6kvy0Mss}dvpJ^aj%pxRZ9xov&bi#FESU{nL=5Mml%+qZP^163B^9+;R4nX5 zz`)Osu*pW!&@)a%oYu61&q4dlLyC;-x9kPERT_*i>vx`g3z@RWG>i2rtJoEq{TEw{ z=0MZcU`Fsigc_8bhW^P8M2ULOE zt%4|T@1X$%bND&o$)zI+tWX6ozxn-g%?GZK3x+?3JCx51I>g| zh7HGMjB~h0SE1SAH8^<+&PHgemu!6L z!?a1ZRL=Fpkb>HucJRlUe*3GX%-zX(v%>a6>9ty~+F>HV@oHZB;&zCBqz^cX>k>p% z2ezXX*ZaDhr_1D<#NiW)ncWURI)KAAdO~F@e*hZHCB|`{Du&+d>B^DVK3qggY9(*V zqk|Vpy9>SNizJQXPkc}>ij>YDI4efpuYQ!VdF>js7O<=861?t&3aF1rS4!g#qeJ&y z)muo(P+-lT_MV}X3-`?|yw3gy`$iR4+<*Dq5**vvKl_vMGaMw(^;z5<)-%y7YN(c0 zI;Q(fZKZXA#rGs}u8POG#^mcZ9p#cbRi8*c-x_oB8{6)M7CKV+G=duu58Eu$R6qfu zOc=ZVuNi?ko*?+gh8fDh!b^_V{yhO2A%|ud-sfSYwZZ`rS@Vdj= z1gK9KaRPdNSQIkWl(fr#hNXX5Ad-Xa8uK34dff~!?o7_lPJYuysQQRLH_x2et?Z(N zY5^^soE)Q9ID~z+)2zR>083VQm%V=5=+6VkKn=O8sk35&Zze$)`zQk~Gs+aGr*A3b z+pe5=&)4q1G(b5C(pnjU$TbU|S;x++fwf53@bW0Noz@hVXT7uWUpn(>OqjF=oU@pn zHSQvUmo-N0ffTJXvstSD_6u(zCaXuk|^M{6C!v%hWrTRl4iqtI0JdOM+*NAyYp3|$F7_SHkaoBZcHf~AV#Zq`eWF0Dc z*ov`JWY()Emu~jFcGVwe`9zRFGE@U;;9OIAzWw@7Mka_PFP1k%-k>V)-O*o#9x+q- z?Vl8nR-u*cvRXv9?!+%O)k^M2jw0vt&V<3vJ@^tA{>`^?Q|FT%wznwHqP>qWu}U`xHn_AqBd7Z-R~Xi0Dacg`JIOW@uz*1hNRNg-!+_HlE}#C0eZrxkts z=56f}-}q;pI~z0QSBx3Is>-`p;t}gDwfd<)R!mGJ_G-;%PyBtI0k# zxsL5IcjnWIh>mf}3#N@zB>1u@Cc`hdJT z%(4cN4A&zQeeftm?q9C%P-(?Kx62l=KfSnJ!9SB7IR!R2k3+R~EOxqWq#~u9^q5)y zuL&{`qtM151wy)P8(*y`3FsWhSm$veC3ekks$qqsX~z3btaCa<(C5fHgIL99CZRVh zDush)bZ;ydI6Q{++Ko?ZDp}s_DN`G>aHMv%#1yzt*X{d<-6)whGE?v~$Te3ob5T2X z&@6wX@u7?6+j|(R-0)J(?*vn3k3{zBOO{A@`)ev2F&{N$Yn%>zdWbfzVs#UT$ib;9 zB_3SLnrLxG&|#KllOd0;N2_2bNfK7E{4#Bd;*d0sa65^1zMZU79)bS!cXW+8t~nI# zSGaFL*ZnpoLm#ODYbhM*A!9sOJb3VHoEE0I^}WjwWg?Jx$)n&Qk58RCB=QI4Z6(?1 zMb>N6^BRgl1fxz~V4uck)cck)z@G~=!>o5^97f%};|Gz@1i>lO~CX4b9OdA!L+ zJKy8SdOkg3W6yS4{Fz??^5*1%2|rnmp?c?~)eU{Is8(>3oy^Yy55D+fkK~dB)7K9M z{Cx;Jsq7?+fa<)~50|?kcL!E^ z{}d^p-;EUo%6gvEfcKrf3IA32#(|&CNNW|OZJPp8^)Jp>*4pWKl_Mgrw>B!Fs~+>Z zCiq!j(vwzU|Kj}POu4Fs$|j1#nl4K&wrojp+I0r{%v?A>T3z*5|A1q4blyxTVOPd*gZdw$M=W(f4HvmJkIAU zj!@t`To6ZXFksVfK`nVPFeju4Xb#v8qOXl&2MTETh^JRiMJQUe39I554_TghC4n&9 zB+e=_l^dg+?yDOiY&7M_F~UQKQF&6XUN^EVJ|vrVt$mHI-4 z4U4c!;EuUC>tSF>Arg2VgQG3s5foXeroGfN_)&m|f)+48Th+AwSva$-dWU?~(?8$z zvUD63E!uUoYc`mi`=E~L@`O^|B{u%4?wT(@a8Rz2LB1v2?lG+|J04X)0u-_H3UuxE zDe23m{HL}~DH#H`^2PEt_?FCE_Ze+}?Ele|{_NsEDB^q_eL73`Ubd`b9{!|4XF8FA zfO6Td1x@J57UbUW45R6CpW5=pF6mH=+C(K{%A%?YoFAGlaOd(O-ukRMEog*kLybD`L449ddKy<3gZ66l5%SU`NV zX#@gigmZM|0%#9S(RB-jxmA)!>|hB}pUR#|{b)FZcZJ+D!+dSx{a) z(js@J1!_;Po&~j^^`3J;+iB$(FBfMb6#WN4g;->C3PQZCiog87Y_wN|7n*Ii&?dnq z_nK_Nb4Z`JX(RfoS#OrN>-TI21}*yYlJA8=;DP6qJ;r=)cjZl<2+@P7gZZi1>ZJFW z=+;T@=dyE6A&p^UVTIU04LN&>h1J~m{elI&Hcx}vJ2F%ma7pWNg zS!u?LT_29@DCUQb(q#NXDWH6j>hqI=V`Pw{nEu$*PtdEuXt$L|O=tRi$4BX3Jw)%G zWEvaGy05*7O1p1Xj5MM5`NE}x_iKZKKNytPlE7)Yu@COlblQ~Abtb^A!G8P430pT{ z;VQW~j=AEPhMXY*2H2b;vM;dN@#AJ#Jy5GEdcN)W+nqA)M_exSwbwNh6Lo&cN@C#9 zLOO_gdd#hLPxekbwi|ezW*l=kS#YUfF0VX@BnY!Gkyxi!7R2&gDYa?)TOYlhrNdJY z%d|lvGrDZ*{kXMiR2#a##ZYv3uhwK@7l~PM9$k8 z9^OGi=C*WP2Eiu!J$f15DaW8erEdSi0SyBxeO{wd2p5itgdfsGGQdpeC}883Ct+?2 z=daCl?L?IE`J>DgAyfdS2(&*67$xqKiFS@t%fdE;FU6g{Snr^eK5eKaR#><<6W3()z6CpUxu6-^9I zSXy+Nj(TrcTjbXtydkBkCacv=v{;La`CZSX7zYEs=Waa?KCJ~Y7Bm#!-BKaOp>#|X z>r$`!;~P%p{*$#a@Ro6X=~OkbH!B-{Y=8N&N?0UsFpo)S*ha|7@p|dnBklyYS{{0P znO~M3dS{j&2jr;SHJc~--$W`zsjKQgr*4`T2lz<^&^67=zr*VbYw6xhxl~FL(>UrR zATYxa>k74OLjvt$IuO-vJ8bw1K#r}08)(0yRo{qfXN(fC8bTWorX+sBR&+g#ClXUc zBPH7+OAq9_Mu*H4e3g)q!uW(Wfm$F1l)`&xWMsfg3g5<|k(6EB3_Ktj5EP*xsc^ke z(9Yf998>})4rVZWNTx}IcR=pG7pW~ZOru?;F#eZXUe_O18d>f(x_ol`cejpyidX)Z zK(VZf|8hJIThwHiaqN`YO4(w2jLVEEj<3O17`DND==)1fhB5OgYJkH8k8I(jCg8uv zH}wFCn~Z%6Wq$xI@Nl>J8FUWBTC^AJ*FLeGMsi*z;OAHHw`Jy`rD;+a*DHZkZb`2A z^gAzo+r~iob)?I%JUHKf9K^Z1z&~2o@k$X&&i$qb%IRf&5B55n=`LD54ppe^izR=i z=E@QghGsB|Ob5#`pW8>eJWun9X^Ig0NRa4HaV2JuhI33{L|^0(d7K<_3UNTUMDNKK@3Jbx}|%% zn}%HZ$E&3V%dTr-XPuYW^ta|uxDatuN#>Njer@y1oVArQ0j>b88d~dY-Gj{Y6^nzc zDG_PUphtWZ!9J-vh%^g9e|(6eldBwYG^7jMHei;aZgR#zPqF0%Q*Embu2qL8*b>>8oH_xbm-A*D50@79zIbOQ`utW}C!faHm4w<5xs zOS4%5s0z3q1dh>;t0nNO(PE>tDp(|$F^UR0Fv`i1oXX)9gJ%0}Nrwsyv7cn?*=EvQ zbflYa^WjTiRu9mucSX%c5c|aYJG4wfkMo4lh`)L|>`WBaBS(LUsy}>O^>1v-9!x-w zToMcu7Hf=r{2X7-GpkPJ53A>v%Iv&ZyH3xv!hgQ0M*oeI&$=pM;+8Jho`&vHl;CAR z8hcU^KW&Kym{TP|ywF|#NF?T4Np@E9fTJH%pprEPTBf+7P^2o!h5lT~Nn3XR#DZJ}YyiCdRbv zVlwaMFps_Op?l{*W(C}C!UPLqdk7A}Kgs73Wy(|$nafIrurmZ+s?%4e0y|ua0I6y6 za^kE}Ug{#@C)F{q^YFFeTEE*b_MHZk9yOQ+;i#Jp`VU6Z!P!BqvWW{<0UB$!o2P|D zLAT6aPb&t7BbpQFQ@3Zi0Ta)Gm~$tvhX}$TShd@u6TXEg?1<-6Yqi zwu)2OZ5&5=*6YUh`mTNyrX*UkaKyiE+gL$WmK2J1qVTnP5?JLvT~r8!d1>YtGO)uJ z@&W%cJ`%su_$sVY7l97Rvf+*;kAYgh7f{?K5W|97$R+`={Hw_%`aKG)oh=5X%{YU_ zCM@q0i`Wi{AITYbqsWMC)WuqB6m%M9DmV7*@U5_4EXGCuc4#B<+jbvKiLuak(5L(6S05hzu-n}?oQzFx`;ize0O(FSpAo#p`a{aCIr(&6j5-yl_owD+r(x&gZ>Ao? z)5LvcR4~b8bMD^yPYHXO{X(s|phawgNgS|K8$m||Rt90I0IrGh1E6>SARhM8X${@I zWEK!8?%2r4SZ-m*uP824G!Eb(U=p>@f$sqLc`B?7X#E4*G<=UwCW%6?h$fR{cIe0Z z03(b-$X3(;QOfkp{fQn9K{I~i@ zTGJpgL$DU+EXn>=*jrcu1FNIr*PWh)+iH#pIvJTg;6-!%S_K2ZL;D4ZOG|$e2XIQ@ z;~nq*6Q?60Y9Oz8zU%J~GDK*Hjx7$F?8VShULr2^&79#w=QmS>$?b=W6yEC>7+Y(oe0#2{M>i zM*sA!J0`t)Fh=KT?!2^eNQGrv`1$soC;=eGp@B=7`JFke(m%{cpMCf6EF8qVc+g^z zqAv5$E!y%^dUGXo+co$6gEh^3=hrVbC9k#*cxBiAIDe*IH#k@x)kzCh)Cg1QKr!DLKF8X)0ZZU^OLgj3@VkDrfl+%imnjltkah~lT1=|4 z3hJDxO36R?lp7gl#stvKi1l`WppOOU+u>nxGR3*aYb9~ZwWXXRGA?BJWW@A49hNQ~ z04w+uHFNm=Z*!ZMDs*)@`llgz;H}bmE%I|y4tvZ8%hoa-oX!srCd%9gB$xg-jfYji zcxYo$$i6(~ZC6QUrSNLVmheJr?-GSyXxX8EXv7%*@k`PVm(5-7b<94QJ_Ep68#_UH8ow zNshjAnZK@?*XGxzsuM058c+JZf3B`VA}3DQ|BrW{$3E@ExsDrng0Ek%Su#VKL z6+miK(zU?}-U}jB4avOXV(Q{+csSIQ?Dv&`8Jl}?s7X;k@W4c=2`ta!* zE&0W~isx8+Q`WB=bDrU!qMjGtL#BL_cTJKX~2;^h& z3K@3x=rcD=pkMpF^|IcB_P?B!hgW>=RA$z6N|n!)l!6gT?)*d}{ry85$UMp5qLXwF^_UcMPowWpfSL9I;NZ(AqF|I+?jW$XN5)^y<*u7Z z^H-Co^8H3a$J6{zo+YfV=|CT<%*XPY=EsP5B{uOv$vcCABjCfSvB@+ky?a#1(LJ5o za4gDdaH%bsmrqzl5H(G60QPI$d^ZB2 z*Iy3-@vK-eFTu?PhHztM6*KUL@U4PEY>WY?Av7?^u~-%?Q)M;l`PM}J7tU$9Z&4F= zB`s^0rU2X7N)rM3AaxMAT{>;GV~~fXtWZ>kol^ex8Qj>s=~dacs;d<(zH9r7gd?(f zoKcDi)6(ZS6}?YJ&73c(AM0FMD;tynuN$1sy5FldbHIh|MCBCG?7s|J6TkW4H0ay! zOoRUioNT%;BrBl%zKbXMhJq9?`5?KvP;d?hs{C*eVR|*gF?M2Do=lr4_&{(mgSj#p z6y>>sz7)1mX5qyb6n=L+l$KK25%it^s}_r)KNSJe6-4t;_p&Ja2mNpxgj}U$L1oWd|s7XPO{K4lW<4eE2+E@>WBbXz> z9SnVTkse%|D7Jq6doXipoB-}hzX-6$XdNIkwA}G^Y*V^1Hl1t!89skLwNbWpb*A6qfuN5paZEOjs~R$SYclVROUR7ISCl|%OfLO&xz3qt+q{x zirEW6#@|c_$%{#pbi!`y$4!^P{u%O?t`7-66ehqTPgUlsUzSHOSH10(WG+^W_N_zj zJncelcRY|uh7*`hV)FR1p0dxH$uJ;pw{wh7q&;ca=4HI4lgbPXVtwwv{qMey-WA!9 zkDhy?CdX7ia;b_dSy5yzu1_W^|!$D}gxSg}jQa8*g9E^3>lhmJ-DN;V82wuAm z!%lK8>g^7~$BZZ%W@7X@^k6F@%ne3MF1N`6wC5$-yJ@{V; zp09)tCL_H}HYfFW{74$50*uSRi2hFG?SYKJ1yA=Aqz%c?(Er|XKWDfd!|kiDafLgE zT_C!mbiB$=sUu`4+81-_PGUTNE`a#iv_Daint3Hp$JxAcEY4<{ND!498>qDaWu*9A z!}G+v+WjEBWY86PO@71jk_YufS?|^Btt{!VD-F4XtH?@>KMM-iX2#~j1&mvYY^%ng zI)dy&fAI=NuJu%)y*8@r*0ll#AVwBBE@Sx>WX=YhRSxh{lmtF9gm@_7tVdlXt{>Hu@uywJ9Csn2vKq$f{!j4_RF(3}u*3x9ddEZzGcLq2*uRDVCnYUIl`(P9V7#8S}8f zw{JW9_2y|oFk6zwEHBK5S7$OR<6-e=KvkmiHcLJNyrB!QvqQp<5yU3~F6$6C(xHeX zsU;C-z;5W9iXgHc=+?Q%)bQh8Y3zx3rYoo9PMZ*qB1a0apo?5Z+{$n)Ahz}eg5b7R zCt@emHdtknDuFGm?1rAiXE)Al?S=WpI*!3X8U-D2DRFl-4!g~%rK@@+-F`r8?JbcH zKNlQr-JaNB2yDeK{{vJ&pD;d2b5A zPL>}`oww|7ZZ>;mN~Z=>jRb{wxXV!s)ZV9r?{W3Y^>kgn##$8M!Ca}~kp*tafx1R! z2bv4)0U)(x>lO~S3`daFhgZY0=hru|y|4nO1E-5x#%<&jEcPjTJdmmL4)ata^x~QH zzfS@BKNwN>kUt|y&+G*7>xaJpr~xZt0lT}gXar+PCKorwd#pNvkZ3ES%XZf5V30(A zKgWO9h&FPnvZUG#Blwy=_`Fz2b>)qq#TtvhFJ8SlrBG+AVj&r+TT9Goi%Fk(J;FvT zWmMZ|&O_BUiHQSEr7rjz_TD}^zhTX8J}8vA2oO%tdKsJ;WwS>L0|Bt8Fl2Y{GJTDZF^woU%{Y*(Y$tEMz zK*(VQ$BIOk{Nn(kY)6o1(M~#W{;=MzUh!G2E%We1EGl2|Z&CEz>=-$J{ zA{=LzopTEqnjOD^0wxIC=&^5B(&L!e`hl36_zS@x8Q2ZuqW~_qRfcbv@tXN>eP9%R z<>M|X*Vl9XiIb-+*kyJ&k1+hKA1ZVvNL?<sKG%hWr0*)`_%_;du)qIx^ z=d0_B3C=cTO2@iX{{gYC=kdV2ugVF^nc|Zlvs-MCL$b$&c54Y(i3n{Zz=x{Zb3OyK z@#O;z(jK|Odz#Ny0`PAK%|#~7!;bxM!?-;tCIvLj$V_=J?InJLv}G1D?jY2qW2269 zu}lEdShiSO5!K}kD}5AZ?WdEHR}}HfvpTsIO-djNx;F-2|HAW?+W6tPU1u>$6{>9R zLT>nF=@%w?G%q@r{cX^yv`HB<`oydwif3}!UP*6Jv32pY1d>>Q*Z-V^SP#IvC6pJi zuw3MZ4BNbvk?trI2?JrLo?aUS*TTCIZ)8_RZZMs*d_D5N0vSeZ(!#<}HATpikwDaz z`r8o1Q2FD76TrMa9|?y3i@XrQFN8#F5vAd=vt7kAHz}k8QT284`WU3{Wy~|c_;&1HaGi~?B>l_U;n@jgM=81( ztQaO7U<(O}16-Ej%N6-mk*#Y0Ft%WaQaZfMAa6`It@Fm5t+)P=CB3*Ng(&*3EnX<0&5@O}iY*%4?z7y@5_8QZ*T2e?%?sgfySw$Q#8W6P=41 zeOFd%Dk_!U@$j|AuIAG5+2bBlu_hNUcLc^@W)h*?qF_h-TzC`d5YZZ^r@^(%&Co9f ze5yVkwAXC7m^iJEZ}KMZi0k23fTg~rLtp=Lrxt=6^-{l86YTdC0XyfZXMqLF{2^}l z`q2=irT{w0^A|+afVYE}LH+(`%fI(QU^Wq*=N>$RAaino$H|;P@lRG5ed1Zms&gL$ z)zUmSl$rDUCjP@hOAUKy?_H+LHK7_QXVG~n99cv+5-Xz61MRze3xeZ5l9-gtPbvin z9y^~5Nlu&NfN)`F>ED`6A$dv)?dRPRhEViN2_{I~=>bf;Wd|C-Pab1cDahEFjm^>B z*13Va55OSbr=a}aMaIx*#AzPa7k>Tz|BvLF8VrAlKe=xq!Yh?!c*;zRUFdyiMW>Rjd#~Kg7HUal!1QY2 zBTaoZ%OtqnF#wjnD%%auZJ9!x2e9o2#v={sWWcHFCmzxjN?Tte+T%lT*YP_q+jwUN z<=rsGDGiHm&J1Q+4<7=*+#fGF2OyPrabnvGh0I~T?^qd<=Hc$^7MM>43hz_Ul9yhO zub*9X=bb^Nna9h!w5gS}x>GFay%})>Pg>Zl(rgG+uq8A?tN91=P{-7d*Jq7@@5OaK z7xrCgI?#k>mM?DL&*gI~b1;wXPl8^4FicOno8x%2$slX??{7jp@>7fh=mywXcnEAU zh>KM>ua{~mAZy8|e(lsS2P?+|LAbVOfE*T;X-Qm52Iax>r~awGj`&iVaK2@Jcdc2m z+!SE?>ujPwPq6u{ zx?5Q>73tPoZI^@)#l7tB#|ep6c>08;0Hs_q3X11(35nowG1b!B-x6msAF@EOCqRxc z@Ll0l^J@6if0CGDb}lXiYQjCF0=B~FWxi|a5)gFd z!){Yf&-3U9+~dvqn^p6kayx}P&#aS5YVV6Kntpba?bK5On9T7#y#a1R*lpNf+;>9X z)6hW94EHlcVRoN&G{QhJ@6Ed>2rfUiA(@DpeH*EH%u@f8G@WVn|LPM@b{F!FT?Rp8 z4h-=QEz7?&{`-xfcS9GqVxr>0cxluqfn0FZa`<99zY-r{BYn4u18C`39h>lAWi4VT zaxY<<(}9Hu=2~&Rm7bSKZo_=q*Q|Vay8|<8j3T|y8IH%kDa!FQYaq{wXH}TS5Vt4G zurfsm)2mpP>P_>1trkMq-S-~8vW^>LE;FF8wfHu5Kc3p2K@kZlL3 zsXb?ZFgXoC<&Z8Yxe-AY?AuW|0-5~(UuyM ztXTNhqbCWpZ}T-G02+Hzq<;DSXXqo<&yG8D5odzEK5Zd4Karbk@Gg_%2aM!L+gn~0 zLI%I|K@4g}^EJ7xa5C1Jq7X#Y95J!9awIk>?Hx7b6es*!u>9y>cwqt^ZT{07nh${L#r{8V8PTcqbd zRLm61L25_#%^&Z49UoOOq--W>N0~V+vcDv}1r>KIXkm*^lQH%2`bu;W%|QMm={-(x zSzi;d!C=fcq%Qwu^1${kr-*7Be_%jCLaV0iZ^8DO=M=AAeflEUsKj&%O-8H55x+IW zsPo|9RUEJ@1e3?DpiyDQx8$8NiS z3z2-g{R?A1sQrh_6&=@qq}_e+GcvKP^!(a@8@dE za>q`~KmhXm;}1gG(CWO<#qkny2ykV{2zoIzKZ#CHjV<&YP=qmYtIdQ4{}OVRj_s#6 zu?gw5yZi!?-jR*NlVGXO8C8>@DBq{$4Uq}n&`8Qo0FV^#I#LSN5bM zIG}yZkV*|~uX#DiRho}OGfP5X{kJW&7teFd(ZfZm-_yZAn`4_A_Pw0xRUiC{_;0(I zinoyRJ0d8^*xW@SnJ!eR=i)!KFDVv%${(#57E}2}YyKnUhJG^FO*{PU2nqkbpGHzp z%d}cS5AgWAhuV-Xw_D@{)=^YUtF!}WII@kfxyAqpe~|yeA>g0*Z}aPbeMKT2>8CF( znp?DKEV1pC-BH{3--J&GotLBj&kG<9pzJJnVyDpyu-XINub70?Oo)Stf<^=5|9<)M zQYZ$hQ_4WP&$~6V%6C4zWiRqoq`0*8A*fsN=*+HA*?EAP*U!yV6Tf;=3USa%YLD;AIoXU2+lES4u{vI|Fzz;(E@tM(F zf%E6<&}1ewo|-ky%CVgeNmmxj=%@S~z5PsE=|wgG|DQ!(z~Eh8-gwpCnx#GF1fAbm zrAPx{_ER0Qvu*n9X@EIVR%o8oKfl4y#RC*fn7ym5 zXcZco-?b6RTS|m1Pnv3Lv{moMS&(q&lla}FaI0cj^3bO?_+g}vDe_5X zBW?O}PER^klQB(I!oOH{z))RD!v7*fobO(M zu??Gm<)pxxQ8^FU{*HI`2|X?{Eapt>Tn}=Kz4aRWPHdqc?%a>Qy^yD$@s25yG8Cp! zY)c?eka#KBV*Mv~vi5W1pN?UK?6!7TP;Qb<02Y-#8bJ8PLPEs5GmZe+Klj&f;lhNQ zIoRz0?$<&Y$(3|r`b2Eye&L?Oi3ytK0A|aYDbkm_Pk3B4VI9@z1&=YE`rwXtLXr1Y zAF`;`B*P_eyT*PJWpf=6&@hfoq_5O&^A3SSbmVu2CIHK7ZFHdA!?OV|tvzpA?*hvN zX0{S%G2C-D{^1X7AhDqL*Ju>@D?u42W=IJW_`si?^s&ky=5?+LK}l3nvo?Y;f;0HP&VM9FCQ$L>L)S>qO!OvtqbA_ z#>FBa(}=?dLn<}zEXr9 z_1J1U)Jx;AMD3g`i!w|^h@Dnz90Q=3=#Xa?zUCp{qfKt2L+t$gYt9L`1h!M$*VbvT zu^`Yd^guln4MK0Rp7$JTZ|o>%rzWgR{DFRPus7r^qgtHnTEKGoxfsYkjh)Y`*tKQ6 z@q={OgCTTMGEK#wNWhW+7kO9h8-63Rt>wTv|E$2%m@ip6B_427iyArSkizrN^liXl zM;H}*(zP;{G%>x8y2y8`-O6OC;_CO-y{+2um_3h0taYO^W`u)|%%|{dv|&NiZ94=1gon;1D)K{v zt}d(4)2n1E(oCR6n}ld;7_C6Dv_16GGW%%|VMqVFz*J7zwC2uXwh4~&c{rQaH)iQ$2LZ*F zq;r_}&&T6a#{JYWG)y5<;HmjWLR{jYzo@tPm-@e=F78(wH_X~Z zFUUqx*$PGreucUI#S4qHC^edLCT4|(nVIX5YiFL`J(YuGUQVoF?Z|#f!cjeD8vCP3 zp+tG>2+OeO`?x32`d&%b$}qO$WtaZacb_u&AJ+k0CRVJ?*UM7T`;RJZ0v@CZG8x_c zr`_r52{XyT%p>pEv~bOXJEPx|#f^Shwh?~w`;r+GU8}qTGP~>YvnhajwLO5_#I#%H zxZKIjY$_n;*0PWEUYlriVd6m zZ*WdoL-Rt}_{qtnugn?C<@ES1i=p%TMxgA1F!Dz6pC3gtbv62kO9+r7u5jd2HMt>? zwf6HwJh=lY$%!z_d*VZ+*aq_)n;Ft~-_Q%;6S;jzDtz~ML?=VYTZm1O&RB>^KY6Do zkl=@GCKf^6jAn|pv=ueGKz~X_;~0vy^4CJ$ z3+uAZc6@_GDyHsD7B@o%a4(`S{-GUMW ztEB)LaJ!S>UYVGy!zrR<_;!L%)vhuOyPYgH&a)N|r(tt^`Ec;IGUt&eIIni<0#4c9 z5VzbBV4QcC5;I;$D+*!icDl0YaoCuvg8S6Zw)$B?<=ZbAQRLK#?Qb`(zph$%R- zs{~kq#|TWuX+Vk5vr+pi*Ca!Ieg1pZ8c7ja@Xw3#PfcN~*J>2u8mzvbPi-{P2OZ3#1MRg6U3lk;m zL56~A_NWJnE+gB{XMY})C^Y4Ly&Jz_tv*eY`}V?O(s0dX zHLDp_&XR=M3ID;X`i2SX@*5w8aXGI$J6iJ#2G@=Vw|6ZZIMzw{l&#LKJ_G%+T3HQL=w^3L!W+d8|mND^^L(eJ!v0)CW#dPH_2WgZpTx)0@T2pxo`ls#Bj^USU<*VdSxXOCm1SSZ@$)*g!R@5Hc5kN;jfcO1b5OUa)Xe zh`RMauR;s%uT9k{ZbGxKMce%c{&~&%AyQE^zj!;e;oQv_zmqdM5ua<`oFn>{$G?D^ zMo6$bw3N-I!CxffL(~oB0!mUE3OJQ^^kacfqk^G{T}RJqbK-zY$5u~AAOESjnlY2} zQ)4thH|~}Di;I)>$L>H|rP_zA=&3biqDs!|lTh&o8h`+#&q0gCt^&{mYy>l86FFil z?m*u$;K}}xIx50nr=$N%NJgcB4{;+Y)@%ZRSPTpAl~M$QwYV_SCnK>xW!{Z7g(khU;XT*ySN*DFW^OL=JE4* zCX&{~{k}b+7tAu5kgg{|9USZYZ+|OsK}O@RH*B$2vfTZ)6sD-+D;8_th7Cir9BkqD-VEqu0bkNmlR(^j2 zQw%(6pPKuPV!YB=#bIx6=7~dv31u&pF#7{_0OnkS3H25XA*^R~rxvf|%82sW%gYpN zx9yVuIWpQCips$V_RA&Lz#CM(mHytv_?Q17cyUwupRA4mKQ#_Czw1$hC8t(D-28VQ z9}ZqT?zFqC)oTrVPF{2dPQ9w6FLBq_y(>_;ow7*_{v6S;DO%p*zyD4Aj3QdQt)+9* zC*ardo}IV%Bh(K$B1h?`ggzdKSNAdhJ{AgeP%)STi)o|xf-2^lKP&@?YAz5|(6nh# z{HCG*D04A7n|Z70gMQ`7X6u$D|u|>AI&Am5RZjs0)!qQjA~?OH53wjPff88ZBIWN(4|f;l{(F5M8 z2T7k-VAhSyrpbSLFM?C}QE8==Xf8~edcXm5!o*X<*C^Cu=1SD_#EOqb{T;oW136AY z=Dsq=Gnbpru3~ZFWgjf)6@qlVtE_L6hMU~h-u1EE3kl0ceFF27UuW~(av^=%JAMo8 ztz{69s5+JGORoa{$J&%H<2PiNjU)wqRdq#J0{g^qyf!4DZ4tUkd1)I!bsz6tfo7E|8XS6pS)PW zUzH^>9MPrIf|O!ZIn`6d1Z3P$x}srnt9UQ>N3ytJo(7w7yjXFM&qDt4k$#h(Vm=~AIF7jbIAb|UJeAq_pW%ht^ zRy9O;B6V5Ez!@SnO`pjAr=Gm;W2w5_<}K^ol6lrLO^5K4tT>H zbic?^<<>&akbXg;7h|-xN+iFGggR-{{l!%7P4}QKV#AJaJ|~R%bN9;7TV`z;wBJJz zLWb)n+ok&y%IZhCy~@uATr%%pX6k)Yr~ecoAX5r4!kB_nZ6{UIm~Vh>;=QQDXhX|D z6JVB5)P@4N(3J9-Zi-Tc_yifW%mknRlE7!hPW0%v-+PhnS@O&nG4wo27$)X7uXT0D z!{9>{&C`@;r%e&cHm*)<(}Z>O4_a7u{b%m$DdJ|#7#k2 zUrx$Gng(okSyE}S;f_zTlVbP&Z>I} z9l&Lo#PwEsjoq@c6ulXYCO?FJQb z*G{&@tqi=*g6$1V1uN@Wepb^WSbJ1QOS}4tFNnz8&Bb~8gFm{nNV>f(@@5}ai6U*C zzS9n1MLhgdX9Y#b_QmohM84${4qIN}r~f7@K8G3gS0}2t(8tg}_z$$Oj%(c2PE4SmPIZ;{qWYnT1|p9YPXp)_w_GQW?yrNJpStGGio*q( znw#|}P#Xd-)9F1EVVCF2_KgisZN(Ha1dREnP%c1!!f80`lNhi4=St$=80#|hU*2`` zIVf55NMIUxXbYZxD3;X`DeM*tl~%`{aD@?oe=XDoo9)pHOSl4Jem!_*M9>Vt+3 zRW+28d7!^Fp?sUHxmXP;GY1RHU@v?*v*8XO1o_BXzUEVbRR~kTq=66F)rEfVz}jEh zreDNxaWPC>?X?ucX^l4xT(~QC;vUJ+(oau1da+6Z^8At)FP}z=&`v}5zqZH{r@1~0 z>$g6%?-i`sOb33Gevp|??|sq<3d+{)k#Vum?*Kb24+9(lOiFL;w3yWxgysHFS`LEp zJr#@=PaVYVYe@>~OJ3EHvj&Lt|P`7U-w1;?mr*l`~t7uzLutX{X4we#BM)<{$9bf z>2`f!%|WurWR~^GyLyX$X~FP$u#c|>@&U7DswH$*A*t<_-XW3aK?corhvBVxIu@)7 zKX3SHMwSn)`j;H`^Q_qi)KvLd%b4x5+u+a{$R!tAO=JS{ok7 z>nS1!A|DJ?nnqaJY*q6*WcP#5QVS{sRf%W+$SW!{X1ukDGCrVJJB{DfN!57-w%PsGd^zm+ zTP%pSpKQoZ=%#^iWscbm?D>}IHRQ*uklWsnxSfBBFV>SSs(FNZziY%5W!%;}s~|U8 z1VDEI;`AdAyIQy&S+avbRf*5iDG1KoCob+LoY=s+WnIQYzCevrW{_R|kY53GB~$+b!S+VnFD{T~cVtIHEx zuHqW5p*zL9reDNya*!;px^55&jl)I)Pm0o3(UEs#~bvgqM8pE z|KOULaO`w}$^j*G3TsAV{POy&ocaknM<-ywt|G{)&W^Gtrz-RA&#Rxx(?Wh&QZ>8n z@zazX#NXnpX>XkV+d9i?2_BZ7tYY7oyY=IjH!1cR(}c$?b91K5#epX7DgWEiB~asm z013-IoQvG$#;Jh|qr%T9S{ScxGg~!P+p#-UjYkhC7Ba+0rRfCKsH_Kgg#mD?STMjFq)RKn(J!Z^ z@m7HQ(f`Q*cAAjj`u9oh{@DP%kjLA{SJS=a$p0Z(ICskgk<^vVQg_oU5}?Xz5@G4a z>gbXt2+fr&L?_;XfxSOcjEJ^rk>fk$)W8i8g*78GEekMDLthTdc-iU$yF*(rX;%XN zMPL6uK?~t~=hbOTlfd?i!Zw-M8rO5cGvZh(bjSUzemz@rl;}eO0p4H7r~b&xWZUcI^#rb=U|?uf47Je&5B=%a=M){TT*kC2G$%wueOyRF0-t1C$df&&(qrl z;Zm(i+;N>IHgLedL-RK9a$k6Xo~T2$<$1|3Ts?M-HYH-sfJM)8gbIU<{xPF z-mlJBsvU>5pH;p8~w7InFtaz31q71|uWlG~FxJ7+-l5R^45G-Ow8xu)2Wj8mK z+XvIocIC#1+J`iVdHGXJjwfzi64`3o5BsicKzK7iMBp`2SnWV`i&7chn zt(>G{coSwY2`WnV209Ybyo0dNHv#1R52KF4-cxm$4aUAfGp6PzPC(ele`_ko4WIfn zudVJqP6fapw|3UD)$wDN!jQm@oIb0yF8hCl42)P?nPAFUFtg}wKO8~zN|K3kAe#U>l#1z0jME?D~FN$M|GiS zwj|v-(P4iY>o?RF0>=ih3vDjhmSyb##jr4^-=rPLQrZovs8lWVuWqRsS;cHvD?bB} zsHmbc4$+U7lq7z0%*1*%r9PQ5IB8(<9e9SBVrOY^IJ_Q#p~5S?fvW#O+}rOz;3sZFz@NlmTjh` zAN(k3`7t6r9sJlD(spIMqJ|0Y zqz8w9-V4~5eN{8b|8aVoe+Po44O~Zv7Yo9vS39Mfe`9j_|Co9UuPEO)+WQ%XZWQT8 z0Yy?ehZbo;l)!jbuRR&- z7o`pep+jr@(OhCgJ=X39W={fN!#f?xxS2cN!M|IX7#$$=*xRT0OI9q!0b zniJU4vZ4{^JxYt~YmRo`eV){3oyT)>N!*~83mG-(!>2K_5kyd4aYpLTUH3KVZ(T!t zKFf{lcK!w7pqYlj_p29CVFO|Ij-!dk@@Kv&4U8h^(AN=|sf0b{dEdTYf4OKI3+X=` z0&4M7(6QYt){3l_WL!y`(mJ8<8KCPo~Hn<_3@qu>Dw8$_Pz@`{Ml9;a#!Z z)5{}F#;aOxW$pCrq~zCchg@2Y<(|FnP8V+<36DouK3pxwCt}uzQd`^B@$1)XPFE{! zslQ1%7#1r)Zm)yw$;>#!+M`YdRZmUp_ zG_~2seG8L(Si$i!bB6=P$#XGBBH7&69QFYfQ5+-Z1R@J*Bu>1^PC{N`QS9dInU~as zS-mSq@SI<)5rPxRpLVZtUMvsBH{O5R_&^IX&?LUL~hRaT)Sqs#$cG5bqak?Q@iV=+=YJHDQrFg2WT`5$2fwnjK6|vEykTm6W zqZ|ybk#0j6`6N^aNlq~WkEGhYCuGF2E)1U`I>|K=E0#xRe~2ALFzvjQVGT4MMX@!U z=QUvf2WgTMLyM`Id@~Xqy@0FoM&9NBumBs9oAC~w@CSk6+UWM4y zO}GR%&AYO@iC#0WZKRV;DP&rx(pc;8A_MX$Hq!0rBPp$$}T$$%k#Brqtx*Bt;QUo5@vr4m|#{67n?&(`ytLNoo@+5E&JHm(<%QXN4@n-;}58d&r7D}y)7`H_Da!n_>eBqbYM{ z1IzuZL2yJ>6ggtD2Ezv6w%kDCAx{Eb2KL9zsvA5s{+G*fH=pPWwX?7Fz4kukU|aj9?RUIrW;DSi(-tS0RX@RR>RGrE$e z$ar$l_^!nwd4Si|Q*|N?&Gi)4dO1Ljj5OWcCrWN!bm>c0LS~bv6@1Ht_%W=~4eW9% z`knEC^eA@;3@Agb4OYawbklBAwoSGiLbPaaQ5>e&1GQO6jNVdM5Z@4<{QRC(eHLIy z{J!j?p<+11l3bIO7u5gk7h@Mk?KI`jUd#X9WEw1K3VuwhxLLj_$j zLFu^;5ltkGliHKDxnL)LB*c>wK>HFHstXGzx`-FC0shLQIelNz ztMc7NLsKHbeGyV#upwLJBxlxK^g2DZbWTVXTlyVl?wPwVx}sJhnJLSz>gzYTKglq4 zyTyr(03@5L+U;a1_dwD`o;fxZ8O>gwB+fq<6w z@XDirsKeqbcf>YrdZeT{&Mnc)E(%DlkX)6LEn zO~7L1vNHe++%lFRt*V5(arnIyJydXbQhtMdkj2Fy>D=Ghc`H)X0kIwEndKVumoIb~ z1P(<~uJ@NtXe?vo^^S;;B-?Jv+_ zY433h*~*w!F%knO8Bxb(FUn+zEO6smQ1MnMJj)foUOZzeMxDP&GNbs-d$Qu|KTDaG z*m!%x1rD-{{hl)Qd03X(=%ZyifZ+PgbYBJ@ks_O-^}Zl5jB=VJDs8i81Fs#D(H;DW zh1Nz;c;BM6p9VERLIjsPwn3_BF?Is(OFD;;W{h&8-OCMHPz#A?L_Dg?qR9rT|C=$} zZagV@%#Vj$3=F!bBoe&iq!cd3DqR?9CX*!e00Gn%ja$z(h$3OnIGhL~qZMQ~K9ss_ zju+tV%j>M&DV=NJ1A!;Ir`t&L4WbhsQ+!9s5AqWG6&Tt1>mg(XX{k>!$>k<)|Db7O zDK3M8i}1Z&34r9Z9H&^-P$CU-3#s9R@>Pf zUAZOl7>R#=&C?N zRH1?X(gjZvOsB2`t{)MrX`D8^bS0OhKFCj{;nDad@+ma}Y#kq1F@e;efQiSi4xuS) z9n|5@oG`(0`K-TCHKMR*nQAOiJdTUC@vnwAxcSrgty2g6{<<*>YsxZgXc8|>hLNz? zlou__TIX)-R22soK(8+hw{*PhE1P=mrD$RUy*Hgq*Ea69ZD>K&G@nksXP_+F2!GUf z#^&w75J3s6tskBZ%G368BdUUjG= z8h>aY^!=HfU&l>m9v6F0Xt3`&DTmAa-i37VDgVWgo|wl(fNxTc1v}>CCt7|V7{0=J zMtY8CR@}E#dghSFB?%6Kn({=MX^k*O=K01|vivofS)1aC!J7nd7PZ9^#e7b4ri;by zy7tqTPedPWb7aN)b8uYOAIW$DHPl+tl*=*|lp$lf|BRvHr!zUZ$Vd5rZ8}!leiuyny1|X-7 zj(G&{iYvC8f6*+05Rv=rs~U*r;H1qh9fGR7Zv(VOXL6px38D1LlCrS4+3>s(OJgqM zqiMm+XlMWXJ~xz}P>rloW$mtB6-kE63;Jlh<+OEkcYi4GSr>S#{$IzV$y)W9E;{|T zy2!j)Bq?E5bQ0PhRGLdh%z+pfqdo8MV`K3bhJ%v*+(vjn#|g5iru+QeS{EpjVdE2W zviR~Xfzv_JC>QuM_?{6X)xp^E8O^T}-iki|F{j*xia?@;@NJHE5;B85L8#={J{TAs z`WiixYh~-O!hvOShN#F=X?W}B?&C85H1qMJaQ(tT;TiPxE8tQG9Z3N)v_UOtx9&sQ zioD-(@1!ljEb;Ch$3=$5A+w%X$E}|^?;X6leWq7k=pp%GqXAU(|AbT`?(^pK^#3&m z?f+QrzUyjh`p(gM!_%?xDYmF?Nnx>^-+6_WtELR}X{9@KU~2nj>3Pf6R$qJb?YRpZ z1&|xaM?X-2LDK-DLvJA>tbl5v1%0Vo6Jt;bz(Z*D9mm>{1qZ}1r~V|mFvv8?8kArY znkFrzAYOF2TU4RuUH8(u-nhd)+h5}5e3rG&yInn)b5S{WKPi~vqrO>?Or3hHi7IE> z&UH_OMr}rCQzC#7U42bBBgOM|#DE2i*MhUvLoT{^c_t9F({qc>o&9qD%}*nv32A0B zIxw2}WCF7bMRgrO)rY+vt6`OLncsNWmKOGeMZ}t;j&VL20?cNLeYOStuevl=NH8J$&hljAEX1wL_`Rc#B)bSpPXz`P$oh~^7v@-U@@LN~VmeW(o zrOEr#dMgfz_(AER@PhCx`h+XVhfT6YKk8ZvHb52JlHY_W-f%~qU z+;NB$<31Kr0|cJz#Fm9R%6EUOPh0(Ap41_i_o@CJfv+-~C|)G;ML;g5 zINM3eTnf*^&;eA05b`Z&$Bv=>h`?9UN!=~`^cNl0`r0|aA}cL!-;i_W z#C%+^^k0XKem1o%jY4^S_)A0TL_fI!id?&y<=9+iI?ZS$! zUo*zF`Ml9t5Jw8VksQ^`Y!L&?GE)L(W@OVP?QWh$&H&nJ*^XbCd|Wc&;7uwQJeQkA z7Kj;<=zubU*pKc>9^l2uB8Y#olROAwnB&HvjHe2s@HgoI?&Kjn_?8+V7XxOq_wWAb zRKVAF9KJshkes5MBRiX@U*)x#%3E)CMgqnDUGc#X$=)Y`=6dK#bT`o-cNZ`>7V$W%PAe;ZnK`bof;=n~O(hsXtl*I6E_s!nSGHia908I-2YWWQ zoYhmsXgl}kmbwrERiK`#nhQcv&!Sd6X`^RP>1}u6u7}Yr5Xls@@sUpbl#3P3dl}`6 zOUJNWlQzS#OB$^&A1l9J90_R0(s$85kyf9a$1r=*1tfyLeSx#ArGHWEu?xx2mC5ixRX6IOyEzNs9-)c+4&a5U4_@nbI$b$BMn~iQa?3+zYJd=HmNjo`+ z$~~cb?r$*{u_!=WR3wcmmT4#P#OEXumhWMcj2CbI5pGwPuG4+Yc3t|hD$^@T(3PYhMaw7KHevo4WscRyRLaMr3Q+b_OJl+ol$@Bt zk>OUv;m6FgnNK3k=#Pn42gOW3y{ty`I3i$n=Py>vCzUP@zdPhKHj1SjbzE2NCU`su zu#xCUm$Der=#-ujgvM}ULk$QwAmJ#3!HC0OTtP@`F|6x_CpV?|UzD z(|z>c#WFY2v*7@f2EssVt}y^KcmV8OfSbfzRTRGXa(rX;QPlVI5mrne^71y>hlF7d z^mj?EL0Qta;i5!EoW@09=X1@|LcZe8eXGnNA20jrt;|L3iL8Id%69T}{R(9}a)3@J zd;)eype?rrBIp^tfz}zudo{fGK1me0oH0XHjc#QA=K4^M_?DhBWV2a%F$aj;X8O{i zAJ+>%zKh6%RG5`v`0mXC&EuQ}tK_<jU3c^e@|P)_CIFR2?+rIWR-jOWiaZ2cSCL#*POIdyAJ5ZO_8SgknVI zRdE=msq%7b^sGm6h^1(njhAt{*CWknu9G8*KqM5z_bVQA#Uy?84gHo@x5vZxW(Dyg zyLo@sTvH&f>MZBxK&nbqAj5bU`by0bu~E{qmOoC&oqr-i8b^KFjPi$j+#lB;Kf?rd z-0ek7^qRNGgXH#T6k3zX_XZcdakn&=Valv~c1zn6aMOE6otUEcD?K*PO;d0llJGeB zC_KH=4e$3R-*tu_EJ}68dS9`&;G3HC(BwT>7JQASlA!op|ED$W==NZ>xJ~>{#j52` z(WrT$3~;{QG`6EaO$~F-R|C?~pJIA^d;P*6r1#Llu#V4{aIJrpqgy-ec8^qa2Yu(` z`Tt`wh4g836jp!}W?Cd_mZ+Z)DV5slhuL6HFvY3)PeDV;Qi&YyAryT4ocR4ITAI8m z4E_C%B@@FR(X*+sKKI!J0{)^w#pG`rlcjSOw5B}*E~MZ{-}Q?6Me4F?niK(eDFxUM z1OE)d?By?|;(mZeOt198a+VXD08GG`(?(Kl{pregm!#?knvDo5t)#wBpjN;4u`DEO z_4Qd=Pj8xL(!$^!!GkpYO5d~tD6^33{AGxPTz#9W%}5nIYx<>>zKtJ$-p3+2OX(cog?`Y18x)WI1~W6sh9-(Yq@w+FF$iQD$ozbqr|HlY z@*;=>mjDC8hy8~cB@-*R4dpXDYjB$OTkR8=A1uo%eg5~bxgTeg89D=wd*@zDgug}f|#h0)bJJ_;+G1NaM|NjuU^-Fl1T;y zqSl47ecoz4gJC!^ey1OS39Wm(^h{E;ZTDBCIwFP?N&hbgnG+2R6h1?u*?T-iWHFfx z@PHHrY}mrI#Z)NW9WUz-%k?H&g6==eRJ8#L#3o-kNPHO~o5gPD7F*MlMAr^qrJQA! zdRWUmbvDrZF;Bj@MU@Fz9>2FTg%ExsKgM$Ky#Esvr2>QQ!0JzbXv4+#xz~zV?6%(ONeFRg&l^Xb^~I<@ zva$mCSwK&q9o%84gJt*uawpV-l3W6YoAPuP^mbv=hFKnFa_%9e=+K{JftgW)TGn1j znY6@*44IxGE4K>In!Ss%c&4H8?HxLz@SRb5a%@BY538J{vp6XRA|-QG*mGld?l%Wo z+29-`er_Jx*xQm5uUW^-SC@L zKh4xjrz)H$sOM^OIS`(Zc#|LD8fsJ+as1>`sSIn4FAiV)xHswjmZM{#=;%T(*QJ&6 zm}Z%?JI3|(aOWcZQa}qMH|MFZGt}RrdU~mOr12rxJKd1 zk6g&M7(qI8Xr!<(B5|iP?h&D$lQ&(gT!;w>x(qO>89F(n)>f%lDd30cx0zghP1X#+ z-Z~6iU&<0{ETQ$Ci|y(oXcyz_Uzra*>amcX?~7l4YB)vJeq;_0j-*B#ksLaxlHN&7ER zTVAU5N`t@ZzMqm8T4VSsliR{l=SXIjOiHB}Hpa3ifE|OI$G9h{s;U~7K^If^rPVzA z-I}xgf67x=k8O8dUB`E$LZ5u*{4Xnu_cE`#+yd6`ZQKHmeO6q`+z&f`Sd04{FBSvI zfbudgV79)ZKjc+N3xrc9P{x8P&b)u}N$OFn;n4yGeY>KE`CqJt$2_jGxQN?-6zVZI zo>b`b(@bAA4g}e<(60FuE?;19V?TIxzGKGf{77UYpIceq$K0_3#hu)%{E=5qN$*ln zZEbZ4VgB)bDO65Vjkz=-SZLmJeYEi;XGCw}rUzTR>8ydJ&AK4eM|gnfGUibZ146^R zy8#^J3Q!Z4ptl{GhWPbC*(W!+GcE+F8*MOVY+Vi1p4%-A#*Sa_wjSz=`B5?E0()3& z&6{`wT@yyec9!+(p1r1o$@_!i8u?x;F9ehB?rkV^S{&(?p$~rVi0)kp2I%6%{O@6z zD7yZldS+vpX05n9Ej1*f2%&HX1)w9g^dU=;RF^LzI9Bd<*-z+VQ$LAqOgx}#nJ9U5 ziWZ(S*}ctC7%70#p55a+CPY5>UpU5+tEZ=rP{6uDF@~6Xmu1xqm@ueV^KF^Ko&E2N z_HAu<%}!Eo(9HHduUFD72|RTwg=4TSn%It zxdQF$hsR2^ZOZrn)%-J6ygL|Yv{j7J9ineB-KP^{Q_gj?q2=pmg-oy<#uFPIxrAU0 zSgS;AkGHegjj2>UN(KAI_$UNiCBIYQr2&EJ@&uYOj~l*ab$WEEt%x(J@?SHl0|O>E z6kL+p*~idY+ZIq*AHu^6U!sYeZZ^A($$)GypUAPsStL3{-(z(0uh~o$s>53IN!qEW zYA=!JMyvptxQ-mIi?gfe0ffT?;QS^t7ao{Q7ww;)>4)e#YqgScqk*ta7H?x>o-K`n zCuC-LoqM|xXm^)mm`OGypB(NNX4zm%l<9>PeXSI+^@E#s@18-%Gr@EMSfnU(B8`im zfwd8G)s)w03XVton`MV!cd`i*jK1wgB}(v zAgp$$(zn$ol#6vy#ohks@h5uhLqvP$14FQn-$lW0u1}H6y{S`Uzq5tRiv6b2r|t5e z{6xL^nA2CoaLY1`lieJpSkcF(Gd1XPBDHEHsAtjj?ONv|u+@|<`Xv(OgLrlJVhylW zuN9N*&fN;%NSuDyw6)>aykaC=#wP0C&srb-67i2>2kBdHJDEyi(LB)FRGDO^V>T9-Bn@iNK>4(9z+pnYKiAdjKBI)pG?RZ>~Ru5H%A3birSvJOGV_ z--%}u2=TWrxR^l{>2)2HSf8~II@-NMWTx$CaB@hXj!kdhVPKTx3evku#NTXYGC$O< z5w*9~BS7=kU2oX_4+{{oH7p$ONIpGY%Wh=dWDiR>`Uu~J25d6MzOjk)zjNNm7`Bhg zf9n?gtG<4LT~ym~zh2bgUPx{=dCo6?c_l$NHbZtG;mCxgj-_jCam`Yd@0q^afW|S} zZ3g4(ZmsY8*WFIuYxxFA?7H_|XBnU(Mik7&Vv>s8903X*8_X=Uzb_|hQe9xQa85RP z_{@4KVm@RTZX>q;W9q6Ceq3;P>h1>61<#Iu?)&#Cd^%|3BKV8}TX2C#A6v!(x*Tt| zfuoDPtA(kf3mRnU&tSxSi{0(xRr}lJD>RRy(H3|VDaG_$#BVNo<}jHnpL1omrS zHhXEOfu(1D_Y{;Y!I`Z zUm`lSe-VwuOxKE@M8V;2^}fh0pxx59>Hs2NzGK_lqzBxLPO@F49P$V`Ek{8u}V0ZD_lMMYx!PDW@DaqFGR(0;)v)dw4uFYhdn#J7I@7v5uPmW*;x8mTi_2pT6#56_ zP5VhYJObrCV4SPD?Cx}fsj!qJs^0I=K*)1WrzvC*hc+o7uxR1CwZ;v-d9o*x+{TWlh|BR9S_^VeIoGI>i8 z#x!Zt^oPyOp9n))N@d^|oyLn5yeZ^D>ff^w&k2zM*Tm=qA;v|97)A3!8rb=CFoD00 z_gQH`JbvtE1>o#bi3>P@CKh$QR}5mcm6|eKV72HIYII9^E+~pssJ;jHOb@YL$h#>F zv5lWLTo99~TlkxQr(F48AphS}K@3=QuF>JfqO)PdqqwtsLg(GE*z1RG{w@}~G5wug zB#_Aon*y*FvUFg1vf^adpeWL@Q z{@%PL!RbGp(gR11%D};E&WGF<(4g{0QIf4o5oYC#FM2rwbn~eZ5=%u>=a0RX1acC! zSSy+Q78~mM0ec*S?An*V#qqNja4{Rh}s7l4(%@N5rpwHg8!JzzX$0$be?GV6vkmVS75u zINdQRtO#6Wg7{kcq3>T@y&c`%RTX7eAk4B9E=$U1oKl!-Ah$ffujSbr`b3KQ^oG5Y1Uou4(IfH3(gGwYx?idF%Rwgg*TwJ^q zD_-n&^PI5{N#b6FOkUfAetrm!_RwnV$<&>ZWM1o{u&S_s;uXM43X$X_%BM}XxZ{s? zBQN5}(QdoRrz`X(CxUryZ}ePWY)zA#nb0Lr{>}SM_E4;*>)x;ZRDPzL3KkZMr+Oc4 zvCsCP#VO%mCNFwy&OPW0%#Oy=W~;febp)wk2HKDZDCFoGNM`I0q*#=~{LtyBMafhCHZ3PMySFQPPCaEalgMXS-YFWNs+56%^+&6M}1#qj8 zg>-E(@uOAiHcUyMC05N5kwVKsQE6Z(&GDDq;zOb|Rr`|f9zxiT7T%ld#~)(FPp@1g zAO<*V0*5_Zz;e@`R=~R1FBDY^K^jsBrQ|C^2leGc{HgKw)V zzv;L<;g?mn4PF-C!|+h-ZHyWd%v*Y~`OY%c;FSKjZ*7vR*!3sY;9qi?P{F^$_ofnCxttE2)y+mn$b9X-hp+#Og z?yevHdFS@rJPx{K9L6!9_HQOqh3<164mcu(p+v#BiSBAO;PEV3Vsm1m;1!+Xx=w= ze@vrWH~8|$yESMjsT|Q9fXRJ3#CvAld}7_3T2jZ-N&{?LMrHD$Ew2m6cqX7w@`JrA zktI5H_+;xMz=OAIo>=6*$D|%UC6`_$D>yTE8PaXiyh=5 z%nSDaQ>J5TD^zUFhWktCSL19hiR_G%qtKe>nkOyQbKlB-6N!V8aMPn1epD9mtt$4) z=~Voj1ilpU|7flU@1`%Web?Ivc^{lYfc2&3=Z5nI*cWlXg)j(M z_b3p*4Oy+D0f*sgcNq|2Xkx%#)ExGUZBhZ7yioq+^yhyv4&%pn>54{;y}>XJ-wjdt zryN)HMZN5gt(6$^S;XEo)fAjM{AGYCYN{-BIIqHvs2T;zy$|JJIt{JY6K@ZBIq0xc@)T!!E8`jI+_c)j)1vd>di#2E;UTYdqg zjOpHiP&Z~u)=>0+PCe4(?@!5@tJC`S)woNH0te?7)n`IcbJQ|)zb~lcINtegS{nNH zylO1O1N@&ST{_#uD2)IfiKQ;s2cyDjvKV_xItR8LLK-O71Ft?XwtYpHbC^)?mm0Eh zmp1HA3$+{A>fG~w=difRwT*CuBc9APabnv@f2_8j&(!^@q(PJQsF)l@kpJvYk zU!d8=9>rBO*SqM}0yO@+!qaK*kSx?R*&o#X0OS*yzyZ0Mmi&o8-siMqSDR`sa6xqw zB=_8Rxlkg5UeHt1P!-@T>3RP?KUNm^SVgAU3$<#w=j>zn9*AdWH*tru3)* zX2^RS`OLC=aex1(j+e!q;^VgCEh``Y6nqC)ggA=O!iO1c8q$(T!+5~6DVY0&9SB3^mPof~7#_B?2nd&_sP91E@Q449oxR(8!9HBeS&DyS z*}9j4@`1qF$mV1L>D#3$sW9$iSMB!2|XyB}Iih4O^JgzJSh;-a!Hx(Y9GEq}!)>6*?(^oeC=_?6F z<|0hYn}Gc z)rE(;5h&%*c|wPaQJsSL#rL-9Yke{#Fu13GKAzGI0HD2F1+UMTg(joAYY7P7 zHhH4}{<|-G?!u1BL`a0KPb{T>Sj_yIA@`ZA%DsCT`AQ`a#*CyAKFfDum<6f%R|`I) zCUwG3wO!D``LhUveMar89T_j$+_CRI@mQ5FysIu?*>_AUoihg!7R~MpnGX;h`1)ui zsVP<(=xUpR92idCjS+niFNqbmF(@$_S9Rpw_-eHno*N0e@{VOgWwKEq7_g%s3OP+! zIliXDWl}?MyMK8uZC!zne*6CJ^Cqm@idp0wpX&I}P<=!v!NoiWqf#Pu(?a!IpAI|) zzZk;}UmWK-+ZfHO(NOi}Zhx(0P-Bf{z}kQG$T-1z1APtV;YJioGIz;SpNuh8nyHUM9763NVAJDnkZ=mnP(LqycKxpv?n zUnsz=b}@Ek?#g;|<>GD( zLl>bQCz^rT2fR^{LNc+bsB%(iA(P(=ToNn*Pf;1b5YiGA2{Q3-@7^YJ!FH|0I>dc) zvfE;&`*+~I{@4bI;vvWD>AoKIbAr#dM}N;$8crh9N@&3vX5_DBxnvP1K4_iQA{;0d zQZP2Z(3#s>-Pc#re1zd<(4-)8LIuCP7hoP4T!?{Jg3G_`%NaewUXNbYe~ z`1I;t)URi?P6J_qv2L_{$3T@`io?0_4 zH3GNeH86u z+z{lKcyV!Y?JUDvoLjO_5i&?F;}sAWP1rB~u)pcSu)Jej%k_`HQIaG!#j-1^)@W~W z=-#kLeu$+UUQ2t}W%XXqdg)!kMm2Y0WR>n)kRoHy&Gqs2bz6hW1TIh#Qt%2LA zii#E1_KS1fuQF7jPLUa%cmB}W7CYfqb*eA{^lZ_ukK01E!Rl5`KHPo?{?!D&r2+MH zz*B6%LGAHFq&a%Ym^?M`t{2hKcM?2hS`kUAtK}8?1 z6LoZPMgMPt!%`*O2AbgrxbAyYZ|YJuz=8vhgFDRm2fZfY9p z!i^0$8B_qUE})%$WCQ1(6$oK`<#Q#OVKge!*gDq0Sc5ApA<#IG?TC%zTmHXP&9lh` z$1V?-;luxARTR@7_)z8?e<8nhoz*NFuY*(9TSRF;$Wip0vuO9lMZw*|)a5^Mb@xeJ z?eyq)>vyql5XZMuIrjrOu{@qXYaje$Z5D7|*~aoz|M4u^-qnQXI9-lagDw6KAJ}km z$iiwl-xj@|c>ZP!?%@Oh*e)od=C0Kl_M;5?h|fee?u{n@7|W>BB)mZ`GlG)L$H#qJ z$YY~u%j<5PF#jW;Aia%`J+;Kr4iv&7%?{w$R@E`dD)n7VI4Nm#IKQdYd5-P?hF}yp2sPCZ6H|7X zw7Q3Hf8w<;R3+PiI380(8*YplJ>1n@0Gu!Lc7bsk0K@Iacdw&nX_6$AOvxr0fsXlz z$^l7dca?%JS;$rmkzMn_7m^_Mw*DWFT26&}kGB;gzm~4dY8P`nLL8bul}2q`hU~=- zXKav9;d=kt+(*ob?${_@cSg`9Cbzg(z3iijUECb+$%Ct8xi#1m&AtXYbU4QsP7}

C9LQ-l#(-gOI<(^!zeVC?Y#?5HWGW-iGLGb%R$Nm_ulF~ae(Oc%JPd@% z719MY^@9u{O>7XRPm5NFF0|WI+dA+kK>AQ9d~XW(8>TVC*`P49ZP^~~E|eBMqWeSc z#j`K{U%yzJiM&&e=qG0!Csc)R5oYptJ4eG0`%c24>?g(KT}`%n8F3A21L3pWU-p$N zB`tntETDw;#{YE1aIKwKV!&SYSPyUGn*g^FYzb8fU!Y^EP zInf`emrCdUUbih^@g<~}U6Av9Dw!?w>n4k2oQvnvt-BkeLKDI?M{8%NFekQI~RgUHePA!Y%)_*AQdeO_00Wu+bR>oK6V%OU0el zcf;?WL_=v%z&3`)_btvNcez11Xihzsz%A>f)$a4xCAW4ZA-^|=jj_eC58EA9ZK5W9 zFQvF3h0`(fO8Wxl&R=c=I-7nms7Al*QK8QM5S!VR*?D;0$?G>qi9Fi_2%Rbd{;zWy zZUYCJ^?Q+;9&e-i#buDRf^G81LmmD9Iv?0L2_;ko(y=WY`hmx|0L(-;~bF9pF>(~E6NRgG{=AWsD{z0c!jP_mcR3sZSx97 zIirq~9H*afnsCgGC1Db9o~YH-Y;!bdnWuSf2tNegVhyt69s2aXJP8C0-OB^bTvInJ zUfOV@mr(|{^!C3_uE5wYTEYt}h9I%2NDYlR1eHIs^e_Z3GRIR^P*0HP+5+t0IwHVJ zJfTc^xiNjv-Cy5qVC?$#y*AjAjLtDM;I00*5M-{MXMzH0wRec?!wALhVLucq~_|Wd0`#%d5MPuWo0H6#|<`6wAe&ywpIb}C4 zsMvL{bXAZdf^-#H(XHA;VoUy%oro+Sj+qfcd%xHQJ_ZW3hFSr76Hi)CO#oF>_JM4d zT{+n>KF+ccWCV`KwUhxCpgbw$>w!+EA$2K{ItVKb)S|eIPcQ;3CwzjFFvr_t?NVF}YU$S2+vn%X63_5NDnxns@&8 zVO5aQ`&_~Fx3Ky13&?>?ibM~U!Up4OKGORdaB2n-+?87=wHlv_QUS_Y^3-G1SBK=bIHx^+8u+o6 z>Y0Vw8EfHkm0GL+y%F01cFjfR?SqC*Q#|mSZJdbShNg4NB$+8Yv`Ex==z@^zT(|* zaVZJxIcX!@A)rtEy~D$d&zvzdUSj`BhUyD75IK(+S(y<4vOO8H#N1450FgQ>ftHT_ zg{NiX4_R=Zm5Z}_(I%yDb6fu(TW|dqRpW*I?qPtTLplaf6h!G}s6k4lL+Majq`Lt?LqEXp87tpDB;fHGX}qJoa0|7ycj6tRti)PoB==#F1uAr=F*C6b_?t`oba)^9-e4I!h*)aVmw`7i?4s#JGEDFKz|1nrV_~OYn6dK4 zLTVlZPTx2@@CA5{U8b8}VS!4!94Hz>#^j=@`j8%IUs~@EoDh!_y3aPukf87xFy849 z1zP*73|C^GJC1^WhPYxmr7?#C_0K%o6vXMqpBD3E4F0Yr{8zsyQ<#sZ?A?>4p)Uk? ze0mNKf;UQcrizB6*J=MYndvb69Fd98PuVT1jFQBE*JoupY7XWpU7TR#vQoec77;%r-yy<2lv zuBBd8a|N1CCtAdA+v4tHszjG_{siuR)b7Bmeo)5N|EG*SYTl#)J`N-!GSVy`5t?Q{ z+&{5bL03KW%&u{{L1<5cfy{c_N*A@y;LwJbJcvoe+L~yacz&r*4>M>u5rTMCqJ;>g z%#veX^QGJ3cO|;BnIJM@1t$Gr7$H^LN!A*c&Q+$HuERc+1&2yTCh2o9OLzWcGljA$ zj}q{$=6!EoETi$eZ%whs!Qj=BKiPSr)IsmR@Qdw$l*v;Pl}j9!%hm~;O6Q~%VnlP? z4IrU7YQaCvc{`IuS0Q8nmj%$%@z&o5lTKbzkM?GK@l@LM;X-qXxd2b7=be~JqLkqf zFN(CXME~l7LM?Ku?vG#gb&g9ko2JgV%-JxDfu@OVB_;0VbOGMi7dtxV%r8S2!o{bO6eKm=KBeGk21Z9cOXV@%rOqKsO@K;cR2$B z9MC-Uc$RF4hiZJJLYEp0(cSRJuub^48SHzeafABj^9SNPd2Af-kua; ztK4e8avy!ru%9}GYy6_dSGV`SjwniPHzk zYVIZZY&R)5UWtj>Q)92s?61G?q|iOF%a|$-6Mh|;wvv0%x_OS0^qDev5Jq7_uHyW- z9D_CLz8{i=W(DZU{AmhDS8~npXQxvdh^)xl32sd=DU0&ozSMp1Y3KB`4QwQ-941$@ z+M0B?GWw(`uw3wlKEShz`-`12gSKefM4x)+zAj))*jgpb+@G3!+^y*zf4{ePUr~vY znu%xb(m4@~c!+w-RqtU-7fT+4=B+%DE@91A5X9E~<8K-o7HQ?4I!cf0*&hdQ;zi@YnB!-`kAN1=Ps9Ssc-dF%?3{y5}$`D(jEu9RpLAC@6a?V@)5W*x8H` zKF7CS?w*2ShDBYm{YJFT#V{re(JeJ5nVMc9B!J8o91#YqE}}&^kI!JyX>Aq*Mc+!# z@kjIpaJq76=rQV=oO^zD&A^=Xd1(i9jfZLFU>hhJrgpAfI^J;<1e7F&$g^AGTmAFtL-*^4gN}ZOH&IJpd-eQ_PymT;<2G#QH&CT;*d%SssS4Z>~sL-etd6ZjcE5 za_WgqTBoTx3%sq$*Osc9UQ_Svi6j?3dIS1>Tbngu0`$`jBx@qFC|WFrEx`e z1?b8DJABtyBhM@hyiomO^{pU+rMV`|rm* zaES_A-hcv!6*IslRm9F$eA`!IFZFCFLvw$oWPXO3HIEj&Z|EPeV}FMz=9-NyajZAm z11FQ>v_LH~jEeWdMD3t2#r~mV%IhjSJP#3+8}bu*Gox(L{5nX z`zcuOZj>4=e|P=-w8;*nVtP6&tn1YGtjR^-*_*iHp4X#vK_cgx$CHqK+tW6bt!Xf_}B@1?)_m*f@N&_u|X5sb$ZB}&W$o`rwk>JQ81e?)%kvq zXu~6$p=4=6Gb& zX!BSU!97QMYbTqgP0NoZPNW?t@yV-f9WRuYw*L%Br0DG0dRb=j@J%`AHjGoJ>V~Y= zo*~LcpOtG!s{>=F;x&3GmH+E+@ zBK{~0fI=KL)6d>Va-%8~2Y;L7#thSBHU1C>&>L5-MNMBjp6fl`CLfDijH6>c4|ft7 zq!Nmhnz8hM#d9Dnxuhj{zWFB%3=ay0`%%^W%0fA6_otYiOu))rUZwbAt&YRTxvrw|Od zSNbOZM=r;2jq&+HBryl(lcYix6L)4`yrudGWf}ISX-e6 zVLQsA;ArmSAoH$UA(^S$=%0TWv$=YDkkQa_G~d@=LVETHM&qkkni)2@FgX>UhrzT-5m}ln6H{&GP&Oj9#BEYNd$<}I;Rv(2@M*5S0HJa7bV8b*vupVZ%ttx88 zQtHSI0m&mh@Md0Rhz;E!^)T=?J$_jQ8tKr(jf$IR{gqH7#M*f2#Vi{Be&Lh)CMflm zYG{s(i8qBBnHI4m$?81SvOecmDVrI?Zrg4k@zp^s?^{e-ZAsBl?twjQ#GbK2i0$%f zog+CvFnr>ZV}3Bl0upz7L0_;#b!8n!fhrdI`c}cmw*cv^2YZlF#+ey!rR%ZVG!sh$ zJeAHM=|&4o@9y(f7GDDE7wZ+^zfGyU0H=)`a%05H3Vn6$%PfAFyRdQlw71vV#Hys& z`|p1cO)`)$Yyosi$wYZ$U?X4ibMMz?1+C}L;eX0s|50BSURbAObbF!IdSZqQwDVpg z47rPOI$&RF0v#Ho@4hu%MVWmoToDjt%w%pq%b~n`@-7#K&Ii87R-+`h}z13U( z2p`Q3?d?8uF?=ODoC$4iAt6nO@WMoi2k^rtl5;3r_2aY^emAJJB~qSWsN?`GRo8`e7o9h+KEXVBUurfnMv<3Qu(P4iTF0N z&Li=ok$E~{s#v7*y7bGjrTo0%IZWBzOj~_0q>CF(EN1FNAy9IpL(k@a;hgH0Y@(d% z&t)r$h*C-V2DaFB+Kf1GgSm;PIP~c?ar!^d7V!`7vcf}yxRMjhcsbu-=mCKz#59z~ zjU0QoN@l93QlSg^u7vlK4btDYIWMCA{}$&(T|l_>ZXUz`HYlUvw+}>wu`2VU-}iq_ zg#}pXovoab4exFykVd`}FDLeeRIEOb%0+HyG&NbqR$RaNRJA-!Zg>jxf%Gxp{Vd3K zJ1dx6?KSD`+qalM>efFc!QJ-deFyvk1V*Q6s6lP3K=1xsH6_Ul9&jolJe5WOyOX(f zAiho`J-GolA{gciMj!Z`VH}CShK#DvOa5ZPMgd`rfSEj&$B{$rKc^y4;K>}tQY^2kv{j6c%HiEuj-rjjgd0s*!!!^Dn-Re3aj zsXWK`<5Kbx4pV3^jAKtc8s;TWq0#APVnWn`VI zcw@yrEJqt9N$#beUFfT?n3}amxwtYwbNyUny=_Y7UX4ml?U?5_>boI6#XQdDyjcXK zn{Jc+`@mQy#n4-O6OC{;>*$@L+T6|UcekH5o%u@h8kB|dhH~zIGN6a9Pojb9B)k;b zF)CJI8k6Mj?qcyA#^w#@NRpxP0_jrfj%hLUPDOV5Rn&e#7LI~(o`5yfDqfZ8= z?1>Ouep;TWZ>-~1THsYog1@ym-!xG7(Ul7*R=@ku2{-RK)|{a^D;_x#&tmFwN%%(T zB^|{9{%Qh(x>}HK^a~{x#v~5s3(*kiKu{b@C_?0M@0E292+j3}C;fn6ONzg4aSJ3U zDI^X?S!s~CgY&}a@=z2Uh4KAqHtJ5obt9&GFS;?9nwS46{G(NIu_)z| zNt7o_VY)~!U7Q0uk}n;3s>xASK3^2)pJLdiX8;{rODmrc%!!7l;+-c=wTJb5VYOZ6 zF<8@~(&sw!u;*2U=Qok<%R2Ddp?Nc1*#(yu z?s$#YXlf9BIP_yST#W39aofD1L2y{wXK0H#qCMn^Icac4ov!Al2&_Q()i|y{P-V652N{gPUi~X=?<}% zcMKNOZ~%;J?GoO{A0zN#UcvNsMqCRYbazhG1iKBrxDR$lAF+%r?tXClRJyyOg83ib zK*l8N0*KvpCZJ@Ejt@|m34F-O9l(A1jjgV2Z4cX$=fnr}hMljOClZ=BVe6D!B7S!$ zdq0=p!aYs}@DZ^83pD}Uk%4*Yd$u*xDLS*z_nh7>T+g?NzV|f6sDN~Cu zYTDIjRf(je{%A2qC$dhFA8YntoaUPWnN_%*c1U#igve`vJ~Ub6h>%IplmZ1eO;UTZ zZfyLnJLDS=>1ZKe-uP=NuEF3{#Ig6mAdmBR=M*?YH42qeR^?(z4gs>rwrYn(1h7chc{PZBrDx*z2zQ^@J< z3~B*~aub2i9wfb5T?b90)t3Q9|HURh%Nnmk@y;hFEBa_IvPeh2TPaUhM&ML=uppdd zI(_3#V(62O){Dg1hDqO_e?i4!RiPMBBymrKB!Vr_?xluJ~HVXjTR#7`Z263dl{ zRFR%{>-iv&e)dh&z^is=wHcD+nJ_nLpHu75@NZ`LSP~x^p$QqOlOqb28O%v1#NWe_)A;QXNkPC)~gCGW~ZR7cK7>c%(GwLXlYoe#IC)+v2IhV1; za93vowJUiG9YDY|0TI?;4xqO@s%V^mA1Ok|%^WLKs_CK;fUk zdiSFww);7AhMRZJ>8Fv8ZoCl6A}Vm(7rC?L^g6kAtfxE>5QyP4`aM<_0W~k2&mKMq zyfih!YBZ%k<=>4)DDSxZlsX!CzdVB2$U)BAbVA$XvL}VoAGy=PSJctwQu)?G2yIAR zP%M$8{fh|oZU+T=8FkZ)a%f74ZGVHLl2E}*TYkmKTujIh5@b>ROA-WmwQ?s+w_2$0LrNvh}!B7{7H7rPIs0}ygJ+x4Tw zRNI{BEN!|PqSW!s$7-Q2;cPxWEAUq0V(<}d+#;XQT_67S>597w8l~cEJ8ziSRJQ*9 zovQ+x^Ww|t+JO|2y299=AV(?&-=pR!PWNAao6nhM==aU&DdPPpHcAt!HA??P3})W* zWG33Bb^oL(PRA<-I}V+#ad67ZJ1M3Ux|Cg8(POSLSZmYF+dXDHx2x-}gJcRU& zI9rbj>9&3p7pRKHa8~Zt7PDt0(f#$PtsdjkYF3s7bu%$+ZX0pKVA>fO1qj=2l5u7C z@e0$5lO}4*wg1)PJdgO;=3bb}FUHan6pNUP@7T-JJm~7}AU{3v*!dWjhTH9fRli0c z1cJhvJpQ!qJ#`(X9$q!<(}twu=oo^exH=3Mc)|s_^&{Yu=hT-*t4wXk!AsK_rj3UF zNAv*ZY&e|1pK%|Y(_^qOFqd7y{(a8~I;XNOFToq!duMe>S&WU$ajfd%HGSLY-lHf7 zNs@n6c&6kXNlwj=GoZpZIUX3js?)Ej7Z z`84BbEgVvawUVc+Y~7>@*D4*NlNTn4Z~e8h_k)lWJX)h`d(3=ntydm^Y+%SUFbd)^ z0Rq5dGNtr3Qf;4bxJSv@11OsUc{(FrV8RIKGh};*?@nnU+pnem1Q^Xoz^bum8BQ(j zxz1pfEOI)~)&Q~5w;jC~e-*=sM&h%c_Ky3j%R6l)`}NZ1HysEk&hms5jRzJHOezdS zJU>Ms-}n)Xu~B5+Pm!D|L()JPr;+B01Unj&xYbYJQ;PW7p5IhGlM0Gt-_0P3`f)qYz$lSIRDD1jyDAXeOcNvS~bJ4MGwpnjl zFYyw6hxLri20i?hKv-H%0=1IG>VOg1-Av^S-ripRt7zHZ-3R|3bq~>FXg!h7^~27d z;(2=d+kpY`nI;kz@sdc!X5FxJMpuz5z@&6tS;YcH9oIKT|G}mPM#x+2y=|H{xJ|`r zEe$2?lXD^PZG7MtzxyifVkG2!NtLV3;zUe@jcwat{yKZO=^$d|bR{&w{4goW7J{N6 z95ld{iPd9K-Y&M1ADnQb%OzpFGQuQ{oz595LnY*$1()5#t|2-R6?1W?r_-*2@VA*T zzeN%J*K>)16<2B%4gg`ttqy*B)dfKXz2WF+Lzn%cmsRf=O7_>p>!g`^1$zo*bU}d? zTW31w$vB!rOD?tzY25m-gE4Hez6OBU(euAxCovO zZPd!zt|!it%x!2KYKHK4HGFb+WtZI@b(;3rPn^YEmRf&uo9T|x`(2~$RQw`xnn3(| zL<4D)7$b<}0j+t-8_~i>zfWfDk%5RuYAtTMwhK=uoYVW8$FQbSBG$wDl91nzSa-Cd zRUpY^J*uVDnNSdqfnDfLhIBw5wsX^#jJ0WJs7gh^PqhnG!shEs^70F=Eqbc3 z-08>0YSx2-G&`bBa+qyR*>-t!;nI&&3#n*k6tULh>Bz>M_VUJhSazj0-EhBpHOA79 z*}IT!@*2UEgIo!@bL=)jzspq*Qt5#ar6h&cXojm^t*KgI_R1&puLT1?5$M~ILhuA* zA=*g&c8Vw6+O8#Uo4Id}xXnE?RLMI$CFXz5MV;lb1j`BM%c;!RYxX)Rs^F&(WwdiqJ146b|GqY*Y`ph|kAcBpAL=@xtXD}ex z1N)6%L$9D3Y-jIY??Jlv&D@sbTk6Qs9Ip_QQznA~HGSn=jGIg8#ehpSSgldM=cyw0 zcGH9h-*jueNC1C>$l74Tf1I;`0qnjIIRCwEj?`QD-Cbztsl4TehBajsILFdqz1RfZp(_ynVF#l zCayPJ5*~5W`q%1)5LHd*47t9?T=}RfYyrRV;TieY{{5HPI-kO%1#4$h?jrG?cRf@; z!upTdZr-}qVTEmjgNW7!)Vsa@tx1Ue2vx>J%M;;O_I5B6{<&K*Sut(7 za{J`4;?ezU*BjS1{KI|5g(Gi`^xzCp;Csl^FSS7y46+=B5i2ToT$TNL7HXwp_ICbk zbXt$=l{g}K*>>oQq2L5MG;9`%@3#+{%tD$f=6De!6tEQ85O1 z0_k5a0cCp2Y)TA!NtFQ>4BM1z|H$GDF)|&j6V_|* zY<;#JaJBVVqvV{cIXV1Urt?m|A!%zS;ye_~spdLr*18`d%@D0wkUG8@?FqKB*ExOKV8ndW*#@`5{u=JyTkc#Co=PR|FCWf5m6K<+tX?F0ON?tHwblGh`F z-e-dFGEsL(5rzvQYoJl+Z@Y=aJ(N zEqX+i_;PMmU{>tjS${uh?SE}a!KfP$f2WPF?#PYfyMQXof#~AKsw%0+Rei zg8uc)9w@tfBJ3q466-Ox%98g= zrHXlDtPV;p3fHX0aqrG!*CIm$dARWrWE{Z-D*L-TCpMPH4FQ4B9yr&01SKw3CI|PY z2;s>g2?j39Vu_e05k>uW8GH4gUNqe2!0ahTCGlQM&5zbN!)O2r3QYHGBYHx@$mU;S-*+;1 zco6)UN+ciNn+Uhy*|1%Hgm+#LrYo4yR}fdYKY=q@_0^G89CL*S#JM6kw}7%&u66um z4hxfE@Mut9U6D&$4kI({En?>-kU@B;dMnCQf^}r}N5fG2>o*LW0sS)eujN@7NXG$* zQPW$F0Sumopwz_XjML;vl{TajS`O;cM891uVUqaOMeyT!_>qfLgZRic+m7~D@jS~r zg+BTL7`3Uc3zao#yp2u;)=jeQG#>Ujgb1i5z-TsuKLnRJ%fq}jgU1tsTUenXU<3s) zNt4MW5eXPwYhC#N0Hxm~Zr3tn-HuG$qFE4!J`+aDj5m9B1}m5(8`Zvc!vAx=s#iS# zwW6gUQ{y{4?AJ;4^EIXC9?+irl$WYhx!4|%ONJGQEtYlUo2mF=VBvk@|;$uKB~PTI-ge1GQCu37DW zHFQH}zh`d5T$TsHOp)m-AbRl%sVCL-B0pM&=<)Y#aK%**p#dM62)Bh0vI7W9q(-PD2J^*zgM3b7}@-Gu3+1 zKhrmH((q7^<4Rs&no-22@5xktRxgQRoZu?W%?}n+!eUZNLL7>01%PQk%4yNK_=^JJ zIF8gy$Mf{t29Jtu+^Gwb@XLM^f!1nNNl zR>7Q32D;A?(!LaU{L<~y!4Y8NZ@ygVPSXxgbwUpm8`ld*mE7N-d3Qfv?WUb~sK^&1 zcQ&ki+TX$ReUzDFy@>LcbWGK>@Eq;3+7eAEg{GVN^b0y41rw8J=8(1&(+}NN=R){&eLi)zZ|d+r#-K^-SOT|oE4a$|A1}Jg>^_4Rr1^Kc3o;-~?eUyF z1~TvG)DtW*6FZJE`I>y3lg$+EgK?aGR83nt(>h1S3;T>G{OeiO_>0@NFWV}sGXK*Z zWAPy-;C9M{+?PFsy$X6ohkCTJk9vXc5A9*|Od7e9cGcKl5#hv08O(T^BIxAn5RQn@ zV27QQkP7pvW_X)*{rjAv)v_&OR*O%XNPYP+{kJ|D1TIbljJ-{e2`c5;@X?ucBO-S& zUXTm0z-K19RGkUHGWO#XH~i4DWFh-#PW#1*g>#;lQMyN}!~{_(q*G0bUQE%@DFWZ8 zTjWiHvo(rw8a{MQ9ro)O5~qoMF0UMOE|gV>dbVmbQWWOz1>1UEq2$U|9_}F`5#eWIb~SvN|W_JEqIeG zr1R13O19+r^#f6RTFlj~{J_lKiI?rj^Z4|0As~qb#l{K1n;-p(Mp|30Q|C?gq#`QJ zrE)k<JaiOW~-j4Q$dC9AhteEK8}{`3T7H zQ1llTQIqZmgC9FeeIQQmes0e109c)Ss$4?E9r!L!R<4iwTvBo+DRiE3rpr~L!`oh& z$=Upkkr!~uvm!NuobyX{QS*npl#z@spuZHX%>$tF%!Tc&j<*X;a-<8*Z4u=af`;fG zr&*qNH+Wf7OD%)(ZW^t0?cq0Nl2M#S zmww}@cK1-?7ZB_mk576xs|mJXn+t@<4_jR+OS%5HurH5rv4vQT2Z1*PS?vSMHj zRK3zG{Xe2_`lCYd>Ktpf71taz50$I|LB_RiyhD~yE&p_li*!;#Sl%)T+r8=gS<3DMJrR@I2W$C4ny)0;sv!ELN zX#2-xkogDRSB+3jVh%Ez zkRaz|kHZuU2V5)5P^$?AKo!g&?!)pDTnhX8L?MHxN;<2Y^u`4tuoD9A;r98whf!gX zIb}PC6p@eJF{Za3rAP(~#zwVBoDN1oQcRfiS2o8F>!o{~CkLpm^Z5;#eRUV2Xh$Mm zqy9}MGCd>_U@7M}sMZscrP;o19FiY5Vf3(rNwjP;U#rfR?rbu3tsImKj7>eFZh2HD z=Hu-C;^EagU06wIxmWP}>;l-g7i;;0AL@mf|K~xOKgYFthzIesO=`KrKTz~T`nWBv z_TN+A&Mb`tzokBMnc-FF+uix(S#DPwkW#ijF*EN`xGXOYC`Pnwp$cJr?B74+%~Y?;!~6fAtrztbD+_5rg!9wx_I(f!g6t<>oI8 zBOfU{P6n|#;|9}K#h#0u4nK9{xDgbU&%QNZCg8Jbqb(+a`V9rzBm@^kf;iYEy2-X2 zBhQslE0^D@`p1|)w5b~pArOu4^D<IWJ-T-untJbX3DUw9_if!IubquX6*JsD@B4X@Xq zExp2288md%R-9jN=YPO0u3fOg0dNg}dnH6(5y-Y>Dz}}q>vewFQAB1rF1atvfctzf zO`+JXXFrALbJ#HracBY6h(i_OyuS-5*9cDYlvd+kOqM z{lpV>++C9Q`YW{QR<~kHty;`Xl73O+YOuE|`it4Mt>}Sq;+8Yk3EKj-vKA~vs9m#e z?9Td0X5ZOLW&D4IoMTJg=gdjF?)DnDX#N{p)m+s`H0D>COex5*I4T1xs z=wpMM0l#_Hy_p*)T?(Y5;c89Cb@@O)9E|nN2IqHG5Bx(F?W;r&C8%ARn@vYucX~^h zWMx~>!B5GSO?WxJ+zf&fO)K|pwDAtD9O13EUE0&ANH=6`8M;Ksd@D23l!`bgGhu0F zr>OdO6oRujQi1m|dxZ*3KG?uEaV6Tb>JT6yCK;u)F=jg<Iu4(5sf;6zm2uw|i@T#b9U}%zVGo_Q4%3 ztam=s=^>b@eN8s;tnup{^l?_!$>>DsOUz$ZjY04H&R@%?lze%6y%`T!%yaP{VVWGP%sr6Elob4|IoX^J&-&io->pcufnlR-Y zV0E(jE_k)&b}t1K^!0=i(GLl^*)lq{bC0afkO!8ptn3sdk2WM*U_fe0*_w{*)WfUz z{#zhf77-$t!Gpi~&gM}0A@>}m3tpx9xiGdS<@xtUu=$kFPQh<0&@FGjYZr89Gm{9p z$59v8Hc!u{r67tjK?{K5_+1*nJEhx(*vb?@z?mxZ(OO~v_iP`2d{w!-MCQ9uQCDN2 zm?;9zti7_#))v~L!&A44^B6AdR{Q+ckHw~?W+>%(k)SjeVapU}g zUm2zNZI#al5obh?oCMR8f2Kz85xVcq+jKt};VTnQdau%rMDZS};e3EinQHHy#(p>4 zd^IzcUHa=8X_K-{lX5*4(5%RY^}}Y;0rS%bSi{p)9Ht`sRDWzkMLUw)o}2*LZc#P0 zSf28gc)TADVt96JaEVpNS|dsQmgfT}G?C!+z|0~79&l#=lgq+H*DSC@e!F3@>YJG3XVgd8v%Wmk9A~DJaP;H-%RVoA_}#aqhs@z0X^S ze0`1IG)TKOR2tr`h7a;2DRuo0BV0VK7XQnR$oxYlC;XEcIJ|`(zV*B1xVIO_;HF$f z?3fpY8hgq=dtX`?g$RABw|z2|DSEjKB3t!yv# zL#&+zm=lYCEPqFAbV~sAIr%VOal=DmaZNGv zfBgXtmM!kZn`J3IhyEe`%P{5znbU_#p4&`4hImk#{JBJ4Vzzo*eetO4iT5JPk8f!W zGo(+F$CQ=V^c_?SB%&`>F+)Q&C1|z1Y0(EZgo43MAhE0QVqD_6FivQR@X#p zOw{~_h{^=?7Dme%p!7LCoS(-blDov&t(Uosx==tcXGR4|R7CSI#|RXFSJ#Pig<5KY?R=L-mL9jE|5L=rrmkCCXD9=M>Crvf%l3`r)tZ*!nDzaab-qLk zV`8B<;*yGmD-h%9$h!sejp39TLR>%{?8!_mppafhKtqGDWN-&8Twi3&)lC3ngNePI_;ti8Y~ntDv-%nycc_p;|0!cgwy*0F zNSH!SD{#nTKH(ku1fMt#BKny@N4Kr~hp&qFTPi_{)!5v`Z!UPUN$eEpRs*s?aLCIX zeLtH@8)4`cFG1l^mIV>Z%09%Yx}yn^aj5*cu*QdX308eM65>Ey$P6~kdTpJuHTHNE z(Qi+*5Sq?HEE4tT;Db*W@-?z_pFCYVm2?!x%Z(<_lvM`($8o^jx2q`>;RrhHMjk#y z<3v|_QO3>c_`V!@E1i?_ghxLY<)qbv?GthN{g!>;vMpawa=cu#HrU6_`Om48d#)45 z%rnGQEz@qm6#ceH4|THv&D2%8W0jUt?6cvASP43VD|wN^J5uq$XV1NphwoRSImNEr zCMJmQ6Mhm!s>5Qp{itS5C=piqP@3moJNaYPncGk>qZp~3Ek`4d1qAFSl1Mp6e#A(5 z<=n%M`?gQ$E~jeA#_>bje~k41C7(*bZ6vAmK4g}2O+AKUXASDSsz$}a{BAqLOatM$ zUw(LftV$IjIVn&?h%JX+tX`UqafymLkZ&J~vj(jHN*C-2_ViYD$2IwK9YLh-d2=Ip zJ9=#ty?lG+W|;TaZ_z};lzRN}^LrjOu2r>$++n;y8^P}A3NALg@A8TVb#8fDY zI~mvLa@%%qGm3gh_VWGphl$r|Mcz`#pCc_?3G;f#EiQeYq9ZlP&L!Ws8ke>Ne%@sM z{Wi%zyy73zv-KI4zmGO9MMlYT?e>MEzaAGpN@9DwOFOaAFx;jtk(mc&^zwRT^#P}X z2A%@?8&<2#6)YC}E&lolHROL?C(iB&xJ>`tMjhGUZtdP|jNOZfciYVM@ecDX9&T?v zNa%F};%BS3$iH(&R#4!P>cr&zpjJ8AJgVb02@Yp8|-`7>0b>X zRho|*#~IO0lBo~7dRR3KFweQtyZZW{e>_{#8Q?cIdU?3|hzgqq3S9D8tAt15^mc`y zzBy$0eJ<_OEE|${l|wDm&H=xa?PZ+#uFSD;;wgh$*xZSrJLs*3Z4eQ;$vZL_)o@UjrVoi*SQ5SIxX zFv0%2=w@LxC<+YrzPPRYX!$nofjg?r>#6EXW@#qC$1A{KT$uE+tXrKBj|30=6e`VeFCrM~;|B%cvUGzxXe8y)!c;nLx|!L)Ekk+AvbQV7$R zuX0*RwbuEk-A_bZO>=*RI|1KJT$0BnrMLc8-v7-GX)?eVHy@tGR(qfCe_EQ%BZT-e zf$-ict=iW2>xJcIYacT3=#5=|m ziZ)54=Wo6aB%STMq7>Eb(kT`8nx07Ie-@qkNfS^(D*DJRdQ}J8Nf#-zIT)5#gSeX} zD<@J|aDx8E6nU@Wi}JCmZH)J0H(vfs7`j(T_=ME!SEOjGUF&ZXrdnJg51Uf`;Oq9s z-0yDuZaP?Qt5@!YCaxuJrLD06s#Q;l|08|E*!zKy^=;5qASjQNlt1dV;Af_sb{uZ- zb|@tbbdO!2jaml3ffUh=exrmZ`P!&mf`kj!k5}tv4tdj!E~{`xd7`wk`t!*=7dwm! z<)jXz8|bAQ9jbI{;58^# z&tMaQHMPD-GW;a3lJdU=yt~-`N(BQpY9+p?G%E0CcIpySWfM&uc zA_Dm~!3)KW@O=|cb}6Bp-+i(~5i}4iIa`zw zHKVOb+d6oA&eDyVm&XDI%(M?u^xr8<8CD%Wi(qz3Wl-OBI2akS>Hz<8AU2iv)I(3v zs9_MrW~@d6qaIvDp=)E0R*%JwW<<)Bf`i?1Pc81`VAtD5>{IuVwPwYyj2Q|qLYq}L zo->|!7mW1LV(n`ub+xB^6-wlJ?VxUt9ulNntde|DFa21Wk4uC~1%S^qdXg^xr`(_P zogi)1*gYe13=!S#SnHC)!ms0MdyulV3^_(u(^`3=w&HJlM0K?o;NvY}$I~^|&5-R* z-te|Cd7i04dfJ~Bu{>$OAK5FiVp`TESH~64f?d!J78w zWhNwwd9g09`pK7ES$^q@g^GWp0lCA4PbyOsdim@qgkHj3@Y)vb9R=pX zp;UtJ51tz$J~e_ zzA0*0eZVa%xNt!*W|_?jDj_#fA6EYOQYOzZfG#M#aeGeU+aJexux=NC(=}~>)iw|g zn|&j2=}Bh3bwDVJ-l(c)F3w$UxsSNW!;`o5b=+;^H)me$GuNN0&pI59v!&jCsae_`E(^MdPne z@m-qLuI%}4IWUkk76uAMQ{C+r-r1*guVW+#)f*y~B>POsQ#_{nkv!M zWn~xf0*w*gPt9vz{tpK$l>uHQRK-*c{hBT!p%^9rHCDc@JJNye`7l;2JXOW{CW5d?_%J_RV7IKuE_;1N~v}S6(zR9#g)u%fFp@ zRg+H*amVuX1lcRTU&Gxnu8y%(8H?W!wS`or3qbiC0&*HY`#Pnsn3t9v$9brRz3$+1 z=9hz0g1&8oLn{cweIX$vmKaQ|Uq!wa*r>>tky6x_5v?O4V#4Yag)`NBzm z`&F-LAQ6%NT`!-ap;^nH@~~uQi!*@!b*ID75$Ij%f?QPV^fya?R)w<*to4o`{wv9Yr0LTx)7Ab+REDL2J)lY}DonvwZ#EN4dD60DT4?n0=LEdwld$>HsYh)a|&GD%Xv zr$5y0ZR0=>GST8*dUD?GZPzN-xuDY{_ag)R{q_|Jzd@6gjgsE|3(b51S zyJz#%%(M8@*>pmC=OyzZ*<4_yh)AVuY7Q1(^7gU#h5hVEq39KfOLm)Xf}r;JK$qEY z`Yqg?{d8}!A>~L_`JW}9d9D%^uL*#PGJ|$D&_EF)V;A3&0meQ%o4Fn&LNcnJJP^Ts zB~Xgm%MA4&u9Pr5cLC0aXm z*7LN+W_xEmiPxhsMxvIYp~iype;o(5b7n9&OxcQ!D+d$Y$^C^_(i?mm2~m8Q9hC1t z7xumcn+7=oqN&xtf-LU;++>^_nwHKIn5VrxZmlHtk#2B4_v7fgA1_JjNV}pM-qrip zLK3y5N?12*;o59f`WZIIBBm&h9L!PjoLFjQy**m+ldyQEsP+HFXFVT)E*Q05`?Mo- z<^FF0_07~kaebRFXrT(?>vA(Mni%GAG;{%JAWf@dr^R=NOujeS5=8Dw++!h27cai= z>Rz?7BM0EH(2tuA=i^aJoZ_!fSz~49tmCEOp%RoOhfd2)y)KyEpkCpOZk$c}_!!?d z^r_D&EuM``OH*nd@QRE1{iWTuexGqX!gLiEy!5L_F*YKN{%PEnxa9n{A8;>oAQqnN zZ;)$||KKb=PDT!(Vs$f0%l!s5ZNB?K;7NOL3>rBE=nomtw`CxVu|%4=qrN zyIb)VcZvpR@nXR#R@~hQ`Sb4m?J@qdoa7*RM%G%n*LBVL5x-riGPLN@_E$W_yWZ9+ z)Ux>Njn5v*w>l2oZnALy1-5aKRHEPZ5}y)!g>73}BS6^p8KmC|CVj z&Q^6R^FZFItKbCE4^-eO7yY7Ukdf00Kc~rYV>?TP3&s{?f*~~Zc4#wc^Y6soWQ2 z78frZIYdWu3gEQ@_)KT--6-73qeE%UTJ;$}IW^^z$v@ostLe8YE5181IeRN8U z`)xhn?5{oZ10`RUrP0pVCfdlHgw1(UujiT*odZMbSAOtwycnh70ho_A_VrWE&e?ZN zEx?qex{Ln4-1tttpYa{qNN+5*1MTHeWh$U_R~dwAoo!NNUxm)F1jeXSw$kckj zV*ZdbZ$`E{OMHD`xf~~buJrUc>XcovRHNQv8>daDn;^=+u@gp6uIgiLjn<|YNP&}`1w^DMf<0I2c2TfM@;=7WT(FOH~JnOE%I*E{j} zFL7&QTKw(t_}XC4kc`y_%GUB8#J7%Gg1ghwv{7#YS2Wgbc+g9Ix=+MtBbI_!MW53g zaI1#pBgsAmouJl{X(AN|oa>l-GCaGEFVO-9j`eVgPyJq+r{{fkYvcz2Z2fu8FySe0 zV|Su@AIBhdLNbycS}CA%I4>Mv-A}QOI1~-LybtOFWEx*#o}i{m!99KuG}+4YmzeOR zXr$HGq({-8O~0u-YVmM!^pni?j7}^JC|Yd2x6lDSKU=kjQfRSQ@n2E+qIu9J_Cd-! zbeA=G!UMjWq_f<4YHeYcmmZiz&7)qQ7Fe9w({g|b{OLmHA%sTrI{HIaZLQ&A$nSn8|rRBu+RF^Io1^y zIR2yN6hm>FDNomtxYb3(<=F^iNq8dCVv)t7V>h0L(d;-Gk!KyK4PJfP^}4aTMbhMA zl3NG_tOV$BfQ*T?YV0BtYvOj4$nzrXRPcb?6GlqKA>f>l0P|E|Ne;Q`Msg)Pu1Oa~ zJ?hAiO%`vs_aA@=7I>Wo;1-BnGoF!aMlN>ZuTYz?$?Js{<(0%UY--U*lR&m=-LY8< zW5jLC@oJZ6@RiNZV8U&4XLiQZyFrru&;#Toc>#2)=Js&34BPp8@<4(}q5Zq}i#A2U zVWsO8yp8Gw(ESaj1<^pmeg(##cUS#gwrvLC9n(Wg&U_>kJWLQaNX|3}%R@(2OU87W zEJkJ72~%ftbnV37v8nrj<0U%rN-)%_>0_lKK6djpgue+HZi#3RzwG^ktF;W>jlCZ1 zr6mKP1cHG<7Y%X%rFad%si1Ik9#eHZDcPdP!QJM<3XNQ4#u=H{%rGMu0H)-M)3&IS z6c|JcCkP2ChT-~8k}cANwLRrF(`KOe{%PFi)9ehR488fAT-TcoAN{Xtn>ux`=jZ&QXwYJT_lrHo zhi7Qnz#K*EuAo`j9wKl5-)5+gNzlJ9THsv9Lu8W4aEQBg-*gQm-MwV{Y#4%8@O4dA zrnjLli#GXjLNET}5V7h$PhLvzzgt`N8!^yl9E$$GBWGGFkhCNU59u8bf%QuK#gc=0 z7uU>vJ6FS*-s$`sPr={%Kj4nDX3Y=ZjhS-T3yx#oybJEn=Q?*`F?aLR?{!yvP!(U__FRv%#cNNfJKNfN9 zWd$_P%{iV~j)iXrm~h8lLfZRj#Ogz&6Lp9wg-wjS#`Diq@Wc%X-Sq)lc!ysjA*tof z$;b-Pj(85vrWJ}aEoMOE98b)ZS;}5dxbcQgkbi={78dP;f{r6@pgW)mBShp7mxEQl z$pzeLVsuTpM*&v(XohVMSon~ZO1BF+jHOwiuz&DOk&GY5%Jd!oX8Ck@@xya&F|VExF+{v)a-YBoD+b5JPYLMA|g!S@&cbB<}6 zaJyH7vLgaH&Rl!3o9Am$6E@K^(skB4se4;N0cSCKn*MokN4a1BedS)@`r{kg(|pBU zsSvj%mx(A7Ci8B+;U3_njq`TTKc(+Z>a`RMO<}e0(ao^k>nB8kUw^^-wTE`8mV87( z+J1oS%EcTNU?RlEo))nC5f6ghWI*|)UE{C@N4kUS6L!!>{*%#NJV&1ot=|_Dag>mz zp&!I=k@xVQENNa6-X?pT{;nLb3TBQbau|XyQ?|WeecOF@UI13vPbzQS=MLT;g$V}@ z$JF*$A`D}OaMAuz*h|+R+r~`bfX<8;${5BNZ^!MU$5DdiqpEX!LWVvc^*sPOiHzrY zH^!ZB#X>5g>m_Yr!%C^X?$0X;-zRr!re*?~w1Va^{s`<@lUf_t66mkLHuPph)p22w zO=OD;k5OTCGs%>T#m&*R5vK%f2I(63IN&gRpr#o~6n(k0$tARd-mdLU>bGPJ^Wj-W zwN}4c;FEn4iAjnKKo#iA)&H*+nI6ZS_`Hvn9p($rIlF@fwXX=?pO@*m{jvaTxe zhZw_@MH!ImYsK!EOr2ucOcokg5!&pOafavJH6Hgf7LaTKD>w815&?iWZ&0o8h)IU~a;3X_?|Mvw8ySvY<*hh(dWtmNU;n(N`}# zV(FHmh1upzOZ~jngV}ZOdBRToerPs#1I;<+ujj>Wki4EoGa{dk7je%B$qVTRlhZa~ zlTJTm=vM(?=|%VD1<*BayFKqhzChIoKyjZ?Jv?mq zk#-cwWJ^9Q3?@u5$b%hak;Vmi3plke33s^5tXBQJ7Pb-!nnWF5df*g^5D+;@tJlqUO{( zCz_3xZWwg#fOG!$E5!)`)i)6?-@IWL4IU+4GR8RJje0#lI8aZgi1>bF(@WQ#ToJ-+ zJ1w$M2puhUN1K0+i0!(wqWs?db;BCZ|4yN}nCK_*%*LH_j?K5r$>GUiqwV<(f|83t zp@FH;TJm@)zz%QpL>`!j1*1J>kdg`BRh3by^K23tvf2P9)hRmwyyvun;7&D{I9Q#a(IM_VDsYh&|Nd3lsz3ts!~>}Anf$@Ag41pXXL zfYRrADE!f^4$LMe>xI2UJM`7kaY#M0lc?2P`VN#p!dDYCL-kM z(98}Udf^WE;cCnoryAb=6*o{Bi+z>+a`dzPW(LS>^H7@nTeIF)?OjY?X=Dr$z9%+`e26lo4tIP_J(S+t;E|atIa9I z8^ol1Fxee{XeaX(J;y3iG`x!8KYSUtiY{Q&Ag4jGGdwMyVYDBR-78Nhz zXPYxFo(BdYkinmnqCnP^1Xv0ggNdWWC=Zg(d^-z2XEXh5@8mylQ+R;3FUb@wu}Px2 zUKAk`Du- z)C{$V3)~}iSa+b8zA56?E@Zwy*$k15c;n)m6XfzA`wQ#8Vb=>TiT>MnAn0%9jQ2#+ zWcNNz<1ddYDfEQ|Hj_h1k4-WZ>;|On?q`rg&a1yd@b$;j_owhaa-?WZHM?fN=Ofvj znf1%=>s;XC24mKKG-Wf>P|vnc>M0W<&(9Q)h%c_#GGe4hMP3&f{&M@cH%ncJ4*|I2X}sjNp&4h?*@U9rf^C~VBof4%t>2luIb$}Q zUJf4?s`d);zigGsRk{CI*R~w{BGGPhoi~f5nB|k%40@bq z-<>UCVAIZ;;!ah9fXiOpgnInxs?^*tQm^V=R@T@ELD_utl|?byK;}N<_SoGgn?Gay zF1?|zVU_Pt*CTH?I>zO`ZB}C3(y;?SiO^EC+1GvrnhlzYZaKaDpp*E=fq4`ns>JHz zjf?r@yf$e?mFaCW4`BG`!|3vltHMzz&;?ldPE?P0y-hQ`Bz(K2g_bNe<^iD^wOC?c~ zBRb0IH&muRypPO2uH+~sopxROu3GI~ z_dF>G%})9mFCJF_N%EjyHhC{x5a9Z-e+v%?SiGD&KwR>foJ#)>AE1yx?*!n70rI5{ zCYTTW0P7avtZM!1mH9l|ntFv;u?63_a|YfdwYRhRU`Kx^9h?5il(-kxESY${UPy&1 z(EJxwrliANAuJ0b`m=Y6wpX#4A~ybG4{z@0&?di0tmzIq|9CYL6iLwjvFgrSmg!my zAlPMcfAZbtO53af&(JTxXmwqHJ5AAT8D>V;)YmJ5UK@8l!MT#McPGVmj*CD7s`9yt z@jhu`D3+%Gz+D9f&#>#hjgMb}y}M=D>g6UTXJ{2N=*6F*Fo>2Z-mrqFkXVChOoG9i zXydrIQZj%Ns-j|GER$G}U0UE*4IF&dai*a3fe17;3Ve#QvLW&z>-B~T#t?d^E7Y(_ zg-z(C)VMUiXu2y9ar`^|63TP%E>&h%1iG5!oGtjzTlrpp;3VwOICMs78W^vl-l_J+ ze71BhUqb`gAs}3aNzHy2rFDjS>K|NQo~uZv(c5q35{EZawr0kA#!fl8ZAh=i6(MBC zdzI>ED5rAN9evJvmc88Fjh0OBGWMtY*OKEevjWf`QMLjN0N3qwq;+uhJhZj|Vt z@C81G7Z=U{3%EQ=!QuPd-TPmVXN4Smf*SSj2fVd&X6kSLXPlk*_p28Y>(PCqC@!wD z{~ZZq3;fS#K_3cdhn-yDp&-n+bc&c%#9!Uf4?cAs3wYVJZia?>^t=?4G z;(U5z#VwHYZOwLjvK2gLU3k`tcGgnwR?223t8B2#tOT!Z<*QSL(ytOQ4w)EbxQ`6 zKA{FJ3Lg;x{&d72kN-Te+gD5K0dOQ;i+KMb@PF~hsg~Y=zxpp#zdtABu9QgKr)QK& zUCd>ajoJl2AZE4s0V2s%wo`6E@$rvQ($gG~oq|6(P|$vw8+eUh=G3d$;|hpCU-r~H z)u^^A2ZG)%>SDEdoOVZ`Boh+($m4Xm7IpyNldLyXcrVt)d>x}<+sKxcv$9h$y6+i! zAEuQ8OwO96BpzZb>z=>)@RbPj1uc8}oj!B=buSbAr@@b&qJ((U&d7ij+cA;dd4`T} zvr5UwQ=T6=VxJX6rgdPCg5}2CRUlGq8WQLE)wd{+A>i^sH%j8hy z!nTaCtI?H7I&j92r}4a_%Lq$f!_J;cDl0gi4VY5{)Oj3jiKTfMe~kKEF21{cc=hgT z%?t<2KslGGO|HM;`>__~2u(|%KCg6f?6|`U!48c|V|_4mn=B}Q_D-Vdm_#}O;0R@_d zf}f=qzQM_;MlO-t_bQWk)3=sG5ejKwsX5KB@*g4dpnbT zmOSO^%px7iJng1{liB^v+omVb6i9&Dl`>XIj8+BKBP8-Fp^*3`IxSYCapdgx88S_A zR}l;dzTTWEXtH=^nJNi6jwfcdElnja_*IoN?u0s}G~-c35|bDf8bVUM?V%pp{D%QC zwk&i_Uw-Et?T_XoicKl=MSbuTB{)k6f4%O;=7mqMa>#%*$IX|~cSnO^$3l-o7@xB4k8R^tN8YcSJP)2<}3T?56XVv;x-DG7WO^+UZGx#T38n|3%85 z15fXM{%5C17JQH*+k0^O^1q?~NMFF;94^!L+{ziLL#MMmz^eRz^mAmDw|V}J7CSUw?hZcDiQ4lr!E7=ihl4C_5;#XdsH$hM?$_2 z84tHpuMq;2rJW0aqy#=Hs5Jr)X5|}%;bh+&*X9oQva_!*j&KXRK^x59eIT0H5FrYPE#pBg7r$B-3Ph zdlT<7au{5K=%`mnRQ39ONp0;=b#A~=kGI1U)3hBp$;%vN7@xaNW7pZwi;m32I3=1k zoIoeGtk|A1ga}GT3A(sx>CJ`?XU@+*zu2j90{-5*E6k0-O}H`cAHyj<;0wvv08T&}VwN^7Gi_q5c|>Yn>S{PwB^5zKLC%Cl ztHsIcYbQ&+VxflXPWN@1^G8zx2IYuNLKUGQf%%C}Y9BWXae0cp((*zP?&4G360r=Z z`1Q)(x=1PYY$(~f?gdj~RW(O+iMI5dM3IKH<=3l><}P&cL`FK`0B&O6{&LGQYF2h1 z{vgQ#`+>ed9;#fc*U_eZ)OhX&1?NilnVO%X^hy=K<>cwd)vsucov3##;#JDuILtlU zi^Xl)oedrFX?*|0sb6sc4t{P@No1YO`%-dnC4uQ~TW#er>8T%fcc1_IQS>-mnK9nB z&_p<+d0_nFNd5U7c;U9iKKkz?yHCpW2F~;evL&+263RonTNL1PBPE9V&YrS!exle4 zus!f%IKw|O3$@on!}$f;v-TfAAXjO>mw(1|Lf7G1$wbi9p)LqQ#e2Bc^*=Ia+r_w0 zs3>4n(gwg0j8{1MBPU?7V=Q250+*V*e>Bo!0b9&bs)6^0yx=$QUZVsRk=tITNBMEH zMAzQ-I)en#HQ4>&jVxax4mw~vPeQb+=;pP%C~l|(%Yy7n%wqFovnbw%*Z77)Y5q3>ssym0-(_W>w411ocveH;Zw?k z%hOH|?*?W^6~9Mj$5VSDTcZgI(*Ha%d``w}Qkoik4KQGL5e{R$gZyD_+TRbGJTFCZ zHS=RC_;|3tPx^DDRkxfC%%9DcQj}XnL5dKcKIil)>nP+brwdgL2%*+_0<4Ij`vB*^ zj15o*8}@Rq_BXT5m}ZT0JgZHHhB?Xzy?MU|TSO>~yG?&FyTsd9@7$=ZT@U4$ zyq>50-#=K4V?cM2v>#vi0Ytk2my*uoCtf$txYAN6GuX+T0f?J_#3E!*)xIlZsLLXO zn+3(LU_)L~z*7x(9yXwx1L=ll2LS+Mn9qli(oih+XoaFV+q2CfQu8H2-BMcE3^q#d zBOLz-qC|%Ise>?oXzV6p12^~c><)xkeDm+qQheUAu0i@A!C1|Q@z7t4f=pwdNAy{1 zCAIFMG0Qd(%uVjA^~c4qGSk$b_O4%pj(wTn>3+Ks_7#EF6a|c)~<#t5kLzw^2`&J0OG2MGYECv!6 znxs^US0NBRz9)Q-MN>yPwlMpWDL$r`;qsq|Dgo8@p!(?7=3#l}9sfS6K(T(|Q*>Bv zhLhBMSk(EDr13T5hX|V(vk#Qw(`Ov=0U*nV^yt^$Y<6l8x51$Y$0x8|(ei?m)*i&_ zPv4MzIGey)a7KuFfoJ=p_-?MGOCHL(Av&|@P(?3NRu%m)>E(tq_aQRQ5JSg0LsXIj zAk4NtaL*ImFUl0{{V9_CQ~7tn1*yF1=(oPyBhJ#v=`hnMzr5;#xr$O zf0l(Z#}wtfPq=c?qo}7f@Nof)E((MOw|r!kyZJVr;=<3_?OTi(;%;I*?7F#JHvgOX z!Krz5$&%)+KiNow#pbdt?G>$Jgqqh|?>v-Ax=&w-3e|Dl(6E(`OV0|p-p`r_35$0U znMD%BJW)=@OgW}i=L_Z=xw^hoY>?J@)0Eh)t9fSdkV^mKu~Q@0pxv&hvC#2{Q%M5H zLv`ouUe9)`7Gn^vxJB^5>ne}XDBp7vlSRQmRO{u~=Stw#zC7h)_Xme9JZTdzG21qV zxS!9FjxUB4Jw^rhXhz`4MTjW}*0N7!S%XUqSuHyvha&BhGv=L#Qxp7{*hz-ezY2t= z+?~JW>t5n}Nb*S7Q>Ds-3YV7(G?&*2R;PIY;vry!w2wE(t2;B zWDTI{Z53Rurtke6^rUv0pB+k}?0PD0ezm9dE%*X{rG{plh`)^3M}zEgyY{W!pMd(8 zP5n1*mQbM5Jg6_H^%mbip;+%#q|aI$pj|@K5>}}wne2t7C=v?_4r@iYIDQwtDmvb! z$_c+Ouk;6HlldRjTI4rT;}aIalag-=tpIvt>)rg!qbQtSO6R^;-+4E;(`iMsc1rNp z_W(^=sCHgaTel0B?wr@7VDMH>NNpfhVCD*>;i3`K+9`EoG*4Zg1-EM&1gH*A_)%${ z13XKZ3$VAIzN&mO3Xw~uTsa4ipm_%knEB@hv$AFosa*S1SKBb5Dw=O5V5r?l72292$q&#HRP6iShYaL#Cqg={y@8pVZ26SOfxb`Hi8*H zWxZm+R8*JR2GM&7-k2T2k)In8E+Tiup;9OC0-sfuFIDdjy}M%P$B07M;-nZg6psY#ReZt3{SXdGB=?A03s-FrbhvQ zjw(K8ama1|vI$_J8jtd_YqDwW5r|clm+HrGO@Inh>I^j>R%pGVA z;qG@GT;Biik6A@6O!-k+_Y+XeHSHEME_n$2p#cNdlrWfY{88}|IfhL@sjAGktI=yB z=ANX{oeha5ka-UuIjJ{PGB7;%MqQad3MlS&2|+1tFDrbZ6Gl-wOCm!9xP*Wv75pgh z1*Tv-bb2|A?2%(elNI}j!~VrLwW(JH!4^#Ce1X4CDNP}lQNwl3I>j*1W%X*?a8pyu zcLukCL;IOcX*;#Q5jtXrP0|Ih-Z>Foe9CuQRtDJDy7?u8O7exiEe?&Yg%87SByR2L zfLG(q!#6fWuDsF2^Cg^@T=>UTd2IXQPp)^yN}!d5er%3HxK(NPHCSl|;b-0Zf&X zxLTc|Ste0^#or=a-8x=|x`E^KY=VQD7!khY5f7XovdKVgIyGW?Ru zv*>73{Zx@T(wAh5NxL!l^#qn)9bP@9udh#k7Tnu!eLyeVEMUSd(ka1=K4W*E6}s)V z0WEbm_W&4U-_}fa`~%#<5tju0ji(RN3u|ix+ISGZn~%>{=Crlnu%c6r{%#Ec+7LS} z6YGx+ZODcoT(Q-zOME%Nt^@(&&-qNKMzf5Ce7^(@o!{bc-Qt>J-B#kXEo8z_Nzgp9 z!sp4+v6#ME&Z;JfXr1##tj6aE=cYG^&!_YgUO$aTuAE(znwAs`%&*E1cO>1<6L9g(1@DG|lJ)CMrtW(PsnSI=7)(_Qvz8J}D?k&fDAs;&XS>3thhwqFO z%931Db-1S!nAEN^IuEVWtQpetVOW%5M&Q)cQxP&w&Qga4yn#7yMRhV317&*U)U^X2 z)fie9A9=Uf`#Fzfo=D};qOnV1DoyyvUJHyE- z_p}P-`5>MQs?&$BFs5Se;jWD*o_H7Fd^bgobVPf>wNJFbYd&e~JIKN2_S}}qNJ-*7 zBTD@*aCRKTal2=wc!FDa+k|p8yt{9+Lc2YCl*ejK+oUykuM3v$Mz^L)MY>?HtCFR` zHp%TYnw}+?Z&#JKd^k zw<}sbT%Ovv!{Jqt1)R{soY2>u4>oRwT8PI!;}h53u;0Ks^v0UG;+#LsD9>w;pF@dP zZ%zmzuLEd(e1mN+cn=5}`X8LgLOM;!XzW9lg&JIlj1s@5H1p1V?w5GI<}{VRyHsaL zA5N`)9AO6EV%oT&j{oxKV11ev;Eyg_EL+8?4dF5(nQa~uG0V$Gl1H+AOz}KHi@dh0 z^%{G*x-MunZ7-Aq``UgaVE;>g^0(IapT-!ptYAj}v>UL7Q90YGj*ZV8=UCFJkX4A` ztF?&b=X`ScrTf)+KqVXO)d9ZC;4_6m-2;&z;8t<`vUC#I$sbS2f}pD4Uj^t3VHUS? zes+#y?a#M?EgYd$a_^~V(>3z8{PDZsJ6nq-`Y2?I>t^AsrIE0R{L1-=u{yY{eu-+? zPNyXa+!#q!0)!6~z5@D)=`vAx>>gqD%J99O3dwd-YeT@9dQoE62OFu9cKB^jm2@6N zMltXNSr)l!bi|}BdLOB-8|)r1=FWI1bA3D$z2~eW41`B-SIA{xt?yTEtqheh1W9)k zDhghFxDuJ@Ij5Rj^?0RWVhfhZ)pKxC&34~%O5-oOqPIV?wkr(bHM#vLpHBjZlq&v& z7^#j}YUZ+tpkv*+#$S%vO39HfiT4zicUSvUn48M=6=ni|V@>6J5=6=Wda@n+$HPE`Q_7To2D}8{n z;DVNvCKkBSxU08O-gB_q-$4(rsavh;TRlmBkm81Ie;pSPR}z_LU<1HMb336Dnpo&Ra*Zp=~ybu#hcd{ z`T>6UxigS>Qg*8;q?T8s{NCxY8HxNt*9S$0z*w5_X(@VHy}mv$Rn!)r2nj^QqbcdI zFmQ7Xh;vfMnQe7}AlFH8G*1~fiX(wb!frP6Tik{c1}xYKlxumVb5#*g#)EAmMQYSs zrS#!8Ag#J%PlXEKYuqtlOn zlbHCPx6JdQ{eI4vT10NxHyB};i)r(hde!lYbXOq`vV)Y836h2$yMNv!l`DTdUY{`s z=5Cvi{=Qj@GhYxj>dbv(jx8`9f{ho)0ZQ};dt!P}&LvEP*+Cr``jxL@&J}AdN`v!d zdar-cYVQTN6LRpb)q3IWT5s7U2T)m!^U8aL&WH}|0{47n*1&jl4#H<%#@++LWE2)?7Bo#npXUaF$!Y+&NtQ9Hz}6Im>r=-z}%bH{(BZKg%5Q(h(cHNME^poa2()_hq6EB5Ko zL$(z%S&VpRW*GnJUG3~_P+V-UB3YLjjq(+1{}?eN(@0=Yyc~i$Qi%WJkDyiJJo@&* z2x8M=$n$YN#r`E;$5ogOnxA>Hts{2)F^Mr=)~Xl{iN%zRNgk^rD>EE-`zYP1<|y7tHto+&M4He3KXUTYdyj zp;Xf0`Zn6sO%z#fXV;dRJNS(Hbj?~100p_2sL>N3i(HL9%lNRKGCHv@F@aohH$Qv6 z->s=y?0SYWhx7N);QPQt@KPh7kF>~SxgK<*9hXac{$07=5RLkDHZ8VW<1kPl|< z^~os7t}f>yg&&%qm&glFbyF1(gjbw$y{e{J8iWc2R8c^f)@M=bnm)dX0ywpSwbW1R zo32qJX(rhsjbZm;@P9-))L3(SJwS_l*{VKpLPY};$-N-kNb%Gbr5&zet)`6FGDrmj zQ=d;`yX%c>#`1?1OMDg?{9ur}V?UT>dapuzagh!_)ooB{Dkl2^5BJIL8SkJ~-=&J7 z6h&xT)w_E8%z_03p=l4wEbWroseT?2Ioc-yKX^rQU{r%#J&}wQ#gDojPAmsV^tcPO zA#@zl_Ef@s1%gOjD{2)I&9rw56%9s`QGC4cAnWDosa2ywXrxiNOhKVkim{6&A%(X|DZ3yFG#oZffP9i?$Z zYk6tWpcp9c8(Un{rpxo=?|A9hh0WD|jdbc8-$CGs6+D}Zh=}AYO~^g-Q1ssHj(YNO zXPg_f0Jl=eFWTK4QAq6E$N3jFF^eq_BEkLFtV1W^$P)MEeu86C(D(iXW3PW-;w7k@%K~wLL&{eXF{CS^+9uFefbse!<+(@@kCH_9UUk$|1w6EO5|7g&RS{c0t@G_q0RPc_K}2mL9e zPJog19Zo9bH*OR5ih*=BC$SP;ROG>4=%_zK`}S>Mil~z_{TDor^#z>l7Ux;9tk@5R1}TbBPCQSJnWWlj6VZyKlLKGC}jJ zC+BD1Js&ry&CXRfxI<<`^KKv2&5YkXPmFr{TN?fM&|O4*Nb_rH0kA1A=e3`xeZ$xa zm8D42>vRtbd%CZ5!*0OS;>~@vzr;UGdtq=_mn7!Kl9Nxr)crSwzLi#E;Oa(da|%{Et|GuK(&3hj?@6kw}7KfhvCU(D?A^sDzSLS>)R-=0o~}K4~&J zYX-2#R`AKf*92Lql-w%MPc}nCUOU?wUacyEaecW0f7W0%UNaAy{5sapje+(#tId#y?{t7wzGL+tq;H|U6g&O&y=Xgm0XN@4$;9fx^mv!c}W z7>$n9(`*!{$7c#5Q89SAK!B( zAMEv13A^@$I*`+#5JT6n0ykj0H~pZ3JN>)**5|hi%c5khOh-o?$bEfu`@}-@zkPl5 zC;MH#2cYxtjWxiSs-~s)mTZmdn!5O6FUZbNz1#|L>+|-mAml9RY$30_FCg?|ORNyr zgg1np4WHTV+|mOriV$MRrO9)+Y+g8P5Pe>N`uq2pXv)Az_hQ`huA9x_FhqOLDL4-R zPM748W*1|}?lBn&Y&R`Ouurz86M5FW>+cew(=soXxn!A@7p0W652b}81Bt$M5`VlEPyP-tDtZA@tG?Y z!40z-mhr@M6N^r?XX<{_F}jbX4Ol~XnRWo(E0ep~3a5>*XGm^V{J6q;3eN$_<3Fm# z=Sx|U_P+u}SuAP<8VSj2QBUTs*IB%EsR3E*#+p;oxfDfTo+T)`pL(>n5jlM)i-ynEVDLuLdo-X zDY}rYEJTwtqv7=Pqn7cBDHVte1T8&3jqVJdr3L7fWa#ZN@yACF*L2i@F+&lLrbPMv zzr8JjU)mT!vIL|nN)Sf<##Nm7)(3WM_6%vqIe`sRZ)4~Sx~G;7-L$)L_Nt}zPeMqq zZ`i@|2qlcSWh~XDxC?U?zonL=pDQ&h85TO(*KX!4S}qnFLvkSU_r&VgwZz|q)Dqho zxv44U43cBiCq9Dz@zKKfR{w5F7_M>$We??XwwwuFyp&?@-~O)r-`KE? z9!M_oG1RE1LkRcnMIQ+*=4=#U@rX?we5X_lN#M zIt^4?X$tsDX)&B^mzR5D*qOvw)hhX%mV?Eyuglp7c+nPVOf(9vi^pQdrzRJ6YZczT z!Be%vjZJuU0xq_8f+$ZznU02uPOp@Jidpigk&pXJ6_TpEvZw;-Cn#;Y_ersUxs0D) z=lD=fpU+l(Km5bKbX~b{3*qD@zsmaSzQ@Yy)r^_TFRk_4prlQ)A>hlq2M^YI$wr~} zk4@rLd>r64Z3&7+*Y%*3&xJz6>V;l(zq%!Rb2ooqhm8COx$Da} zPBefcL!q-x!#=GspyJ4<;y?tYNL~7uwN_JHr#iJoLBW)u)HRfbnP7su3&A=8T!x&V z|eW0XhY`stW@<#2quCRPyWE^&Q5zF{LTO(i|WGa6jzvXK=1| zc`;ynqKC=XmZ`8GetYG>jDkOZwer+M>#h&vt^BbR4N?oSx2wmjOMHzY3X=?$*-`@V zp`J9VQ8t7w_iXxWwWaY9dx=~5DE;fAOr|ZYdNFib?*{%^nIHhu0P4O~SMTNoso9DD zYpS@A&2MPO@>6Z%c1hzsW^<}MrZcsppzS(;^~BX~M(RNlV2#IMq#mU`;HEBfPaa^c z?@9|_3$OEhL-Tm#8<~gGL)w@6Oiv3?a_7re2?zz=8r1&n4he&&xajW5+5tLyT06+O zs&qHTLNHH*?%*2=Ygc-GoaHA&>xfJ>q0!n1RBohp3HhwY)<>tFkUwi0V;Uz}8bCqN zjm?JCJ+#Zh+cpOm;&WvN3`ezSGL%f;Z3S2&_N+b^rUZ=M%02yNU0m^d=SPF)jcwp53!Wa!F>+{^(EiuFnm$5ZimQ zl&hZQV}uv&`1*rYBhdl;2t|X&3AgZrgge6=EShn`U%fq}yR0;*!$A3)tlRCfzfk_L zE!3idj}X9YHIg@)pOgQ`d=Ieh4~=R_v3i9QJ_aK^Bfg8j$n`u^MC4q!?0@$pEOB6; z%I%hFftM^r^FaA8)pqX*^uOpEgpat~<#zyNAFL6Hy$nTRd zgR68o1uGLIW#92Z5=I<|w|~5c1*1j!7{}(P_K2O54=FeBz?2Nyh1)8PftT<*bW@7* zv3H)wC7JWcQ2UfwV(X;q*0A+UFF_>y8@~X3c@=60_%5E(g8s`2q4C#XEy5b3yG&v0 zfhMZ5sF8>x%D=kt;wpEF7$treimEK@3L^*6EeapEF!HcqZ+Md5-7B`AwV@8ZNLWsC z^2jfA)Xef1F`w7`hoix@+i|m;3*`=UmtM48PcGuf5h@Z%rt4`Z{rhlyQnv zB!)Q$*mV6xM;BhF2@VWKg38QP6<3knGrv2}S)=8ch*iTwMqh1r{9_LCuH6I1E`}4} zEx&V%^2r?^oLCdNmwfT11^)P3FH{R00~RW1t10b-o1 zgJxxNL1#^rz;;#wNe1#_TJVa)=veS+^CE^g^+p+6E9d!oB$#4EGTxAp;tYmt-^?gx*bkS_P7DCyr3T5R4V#ZS`pkM{;Sz*y;9T+NK<0~~B zOpw*W_RNj<0pPI_0(`HYnP{g1@%-m_O1O7isS5mORpVx^$>^$6QBY$FUq1xMRn>oa`{^ee6~A!9r5c^El!W* zV~|2m`Kc(?9En0a(s%Jb7=Je<=x7l~s%)wj$o=Q@p~F3Yu*>fFshrPbbd=3MVA%vR zfM1WGf*MIlQjK;6Ngvdxn+wals{+&_eZ*o{GHMU|nvGWL4VV08WYeD=F&LlBPV`7 zzOg4gXRwKsG5Au|)f7O?x4msEP%b6+mAL&lAFeR||w#&5T~h z>QK4+H~J6r^`HNAe-};B2;}RhM45@R!`_x%w*GSX$Aa#AEY4z?4CUKVe9NLhk@0#) z23gvT~Ga5fB~d^Z)&DU2>6fRwVej=?X zvY&gY!^05o<6}o08t>UI3Yf>5FTJ^DD?)X_ApiLiNiW;$h1L)P7@DCP#qYeFy(r?~ zre}HWGS5#oQpr11+{Q=2LSGuMpP~NCBun0Hc3`gOX-!AKyQP)v)KVOX<#rxEX|ko zlex=e2ZT5G&BCd{*y9-K!C6*{eszAI2V&!-ep<$e)L990329rJ#*!e|YU3{UhL{u% z;$n3q&vmo5*VVKpCd*ah@5v8j9oB;G&1PoeQ>k{1Bdb-Qa?)_hHxmg{mf7)8v(*C< zrShL+%kpT-fi~hj=aWnC)fLVUOWNb@w+$}EU0Qntt^jV!ePU~sVz@eFREw#WgnGX? zfUykad_2}c!f6Z?w56|%E%Rgd+qBPNAjK?HY-X3IZ|gPK(NjJ%Xy2P}T47C9K0fF^ z6#Lf(c=I186a4V*-Wpy>SrI(CL%gt2iAwfKBc9*@?Oj;kO&agW1g563(ps{zbmw;( zy5)*f-s2vZ?*mcWt|`SJk)?5RVDTcBBt5@tng~_m_`PMnt{+Jg-5^D)eb37-z|_kp zU9^Z%C0QJv<9dXshL6~h-Lt*W-6C#yNC?S=b9GB{AfufRYo)KiLBbP!Jc#gE4cOvZ z?*3AWH(ED+H}*xl1lk`aPjQU`pn?|uzcP_ExvduuCAq4%quBl%usXeM_KRAp!2#o@ z^1iR^x_K>G+iocIGUz$Z&WScx;M`MTxTD1Q3!MkuInje%u24IEu1mY7uLJ zMEY2ar9K|2Kl2Lj zc1Gwm&$}JEzVXFwD}f7VmZz5KC%(Y?f6bdRge_-7`+o)S&xYi$eXN&N;Put-e2yCm z6RfZ?Xev{Bf)QyRQPXiD2(!DdA}Vxe*J&C98aoLozJkBI++}mgnulI}x%4keT#XDm zeG-409?7Qq;skm(9s#yM(O*6Hn4Jtn9?ktIh3uy1M7nr%Crd?MYCgFN8$zxh+c+)6 zKkjB%&C@MZO|Y8@fSNJl87?+%Jd6XmM~dlEO6ADr(;UMF;s!+~f487Frok>{_ab0$ z$KQH@dJDw`UF3uCYud~PJ&&59CbR*HWs8mNN)hmYOIb?reohY0$BbIVTdW`KjF*(d zVs)nf!pOcB<&-A9$6fx{qAnt^>bzAqQOZYzU+OqeY8xBOJu>pNC#J^5Xc@(6M@dEN3_CY@ocskeF5Cmz3Z# z)~TE&lce4aTiC_ldi5IE0@)!WQbqx+bq^#olB(V^uY5yX(5*`quF%O*Q*zH&=2YhM zYLa@x?})_za&le-Gd~ro3}4fZ`}M4riS{;Hf2|x|rv1TcbS6GWqQlr(;i<}-xGUpM z2Y6xP|2pQinLMQi;{)=&rCZ{jQu%GR_EWZJC4tCUt3MjYY|FpEzhq$|Dm*B^Vu$%} z{PNg%&509*0TDL65KSK6OC@utGqsvGPhD=X>;onGx6=6!k7PohA!_;2I!xPbom{-u z^Fl<#RNvPB>W@U0EDKBx6u<(=i;Sm$xo>Lpf*4(F-j8OgJEcT;5E*2-&v=~~4O#zX zNx35f=Ej>%ild6f#B2G#wSLNu0PtnxrZ%#*{(^lo?PZW_pj3_6&~aQ=1g@L5PZ!H) z>1wHk!wJnk+{wa2_jUDSI_lmm>u9#+3!e)%^yvz$!TSx-(JXuSq5ia?SH++HbnWj+ zYD=2gE3I9GJ@FWM6m31NHe=r$*$`X{zFF}(=qEi6JPAq&Ek1orclvhvc`7Yr->N(0 z?Z36xBQ8&6uK#t@?)akw>cRk**$FF;J)b29MJ-?&-#Jz07nA(#-@}3Pfu@Z?{h)%U zMH`^g+P5{QdB!v6fQDCyr<+bXzeW1_h}k=pg$(`n=Y(Jb{XELSf}{_I$Q78#+GbQ= z*IwJ3ls?p!u|{%*_b9`S;@)+A;^oFv4en_YaapQB!&^IFB%0euK6)cdio>q-HKI|Y z@U)@4X#}t@L=j5Oi0P_=MsXc@6xpR*d{&H(vVRexLkZFwEVOB=l3FVhhh*|sj)pHy zZt>8Aarmut=Hf(%M_teTXE4`QY!Mx-J_e_BmmMyYd_1qdY0A>1Z)Tiz%!bcqk>maO zZgiw;UNdkvB)2xAjy8eck>>|>CUNuH6pxwgq|a3%sbED(2a4`maZ9|t5G#@UUUJqm z5v-Xg#EexkQttt1OzYP*e*I-i4a#s2-T$J1?7p<+@yNFYoE4*a9W61Nm5M*jK(UIOaB#z@{ok8RXZwGtGMXcwZ6i0lje)tZs z;BX~P#ys6T>h))8+|2aEEOHq>Vl+jKoFJN)k#>>Ov>=>kP@WB ze01}@QBoy_WUk!Ffv9Jw1CP4vSY_k*CQ!()Py95B=xN*0zb4xE(bF^>a?#|p{(Iy< z>^Aj7C9gsV5a3w`Ol$;f+5b`i-_y%~Gc+!;2joc9VFcb&d4|&XLRU=?qpZsFM>H{J z%U@xq^!{1dgIUD5<8ul}LkVh8cm>pYC)G zzSMl6RQSFFyOjb=I5Xa<cFE4(I(O2%0@Z}9Z?_b zsS1^~I?Ym%=&^GPfe~8hXa_rJ;B>Gq3UZ-3K2vgi7eXLM40x|bF&YxZwS>2ju4h$$2%bn4ytZ|41N^kzxZ^IX=Muk5|G(dpuY^-q-&EuvPF01T?h?XRXA#r_nzaGSPXmg9 z19AI}}CtF*{ z>FPg(R@I4R!Sdo}Uu;T6;|v2$v&2}w$YTk(B#8pHQxd{Ajh(F9*fG+y+zjB|gG>*l zn{-wei?td$fJJZ}xV{@8qvN!CgSS%HX->Y?1dOoSA9AIJ%s%x0UbWD5e?}!&`eF|( zj619N=d0VdsduTG!rsJeIpR8^bZpD#C+66i8{eP=uE8~s@0HEsv}w-kv(lh&@5HlA zl+V*R(R_}XRWd5b8`MMvjZl&eGn)9X_c6p|2XAY~)%8E$>7t1MDDhE6D8JF$*)hSq zcQuZ=HK=(zmyelMze+*B-y~vd26>Ql)8G90U|WL;8H|-*7bIj+Bb>Sxr;=FhjAm5D z|2w0LTR8pw+iPJO6jQ~nPMPbeUPH^dwF(~dA!fgmv%F#bLC3G)*x^2+DpMEF>4uFc zb&5IxTO`BW2j@*qLFUeyPW z#xYl~h<~-q=5S`hS*z-vC2XB9W}>8?03&NXK?QyP0WajOK1x8V7*!AsrN-+Q#bi%f%;A}*K1b;&SiZ+}gu(8+kI=1Z?ukR8gFWFf_CB_=tuMvlu!mOo&lB^>pa(ZkDl zg*r1P19$!Q90>rsNkCj=(%T18Z$2qyW%~~s#FhZ^JAlhYTRvZ9)ls-yT9W92G4B&$eA)bQjba(CVlfuZB zLD&KAMTmdGMfv2sMsb1_-)5eCxyIU;gq=+7isPMzaZ>(GCg}7LAr5cn#mrRO4zIiH z+K$)!oS+l8+$zbPg|}y|`(MJ6y-NQRvdsBQg^lAH6=5Pr5hB&V4c{JE*cv*TCTpPB zy;@z}C)`icT`YZwN7m3y-#21@mxn!HS01|0h;BN$R=ihxw_{RVja`zsSe|>|Yg7A0 zxx7^1c3G4VvzOTBuL_DUGxoajvnH!jQ(=A~?AH!eX*)U_Y9w503dcaq@-JQ>r*ilEe-Vljv)1pSd%hAN1# zksm$Znte87iUy7QKpRc(aZ@d)@i%LchMBE}0mGnt-$ul~M>Q(a=Ff{?j<&|}NPw!< z(QuFSE|i@E?h@}`1)r>jEbk@-^cy1s!?SX^F{a)*c6uP!YNG7a5?-(nbBlk1AkiHl zMbZ&-AE35TJog`;kdODRmcOg%2^mJm^II#cF{4A# zS1diz)%-*aX%@Z8J5{b13f5)`|Bk@18-q;aEmB7JtoXm|JS)%h<0|mDUXZ%zF|bRz z2lJ4T7D}pQ0mzM8Gwxb&&_w6MOdqh827VGf$)B-a?RCLFF5+aPg*0>P~vLRe;mLU6Zy)kgAvWS$-Z!MUF9TofXTRy zIv^fc>JFFW*y|YN1G+<)bob%`+G@BXgvL6Ax?3uhfA(9DdSW0x15LAF?d>_#%F=xw zwZ@Bz0XS2WB+iKP!S z96PLwcMccSEz_AX29bGq_o9H?&CK;yPm{qd;v${SNAxN@JDJb2u|Mzn!E7;n=xfms7CH@9doiP@o#XzSy5ZOx_b>t{`(qO+JmZQee_3S|5g$>9o6(a z=1B2a9|kyTRA!1Lxkxgf-Jkc0yXrh`I!y3QM`xT^Hn9lD0c+Gyp|~i^<5)0Xo?&AL zVr=y15%Sd?UVD(0EMjPJV~p9q zfEs)P@-VJOI1OOG5XVo0rW@fY zz9z${N{mW6rEyn+XsC?p$*@x(GG^jy$dQ4ju2&&Uhf7;}4eVnptfk?Gk@!nS_92|* zh~M!{7&BilDFC&n?BJ=gk~Ctt5I~MS=qL(kmAh1=xc2@j-Ln;ma6L>6kii`{6I`W;((R9^z?J&Lw8TQd`i&q-Ha{g{R>@nOFWzlK8AW#Y8ec#NUJgTpv8JYlvI)>!Gke2! z7U7#?Yf1eq)c)Q%{V=%VZ{g$Kz|F4WYdaBfOs!t5G{7T2>C?Udj{JGM} zgQ~}dpvS)05xd&=PB65(`LtkTwkhz-i`XXH$ED_jFpW|)0?+ejM5oYyI@)+Jr7fCN zqs>L9hz_}pqq*)n92apMsFWcz%Q{-QEZsjOxU=+k_wh!Xf@5PadG=sAQeE8fjLP?P z!2$iH7r_ZU&p5>iJhDB-dfRQSvK5q;&_m-)08o z{k^H(^Ss&Ktg}gDtQiKA4Sx?Yr@fiaf%hqqHFN#39eRVxC_HIle7D+^Bu|o;_W>@+} zy?b~Q7S)2c8u6M|opfDl!9(==vW1M{tN2Tal-ffL++oKtcPDa1TgN$Mx zR}M)XBPCp=gAS2(a^Lf(iNZ;n*VYaW3V7NYG;QIteHtoCG&~$-Aj-rmDn?T0AN;vM zmqBj6HTlIzGYh)L{Ew?1v_xXV!=hLpE&8sD8Qr|U4zV8&D@w)~M*8u}e9IscOZIr2 zAm6|R?c1ltbQU0}t&s0W3DK1xzxOE?znQrG6=kHA=@uL4$As#0Z}!EJZF%~}wrDaA zYhi5vjm#Xah3qk(ps+KSmS-WtQf+3+vTS>Nt`MQN&wCj?4ZflqlX^MWjx9GbFAU)t zk%Nf0rMJIpO|%|TD(ka7jMKBz>%Y<%54NiWt%FRMeDsgMdm^_^(w@7(H(lx{b@V+v zdYj%B?bSaMDR@LF4+<2UOJ~#WEX%po6QLuwPeu3^A&O=GI|7hXPkoSH@_t!w-b8RD3ShmaP}2 zOD;HhC`e&Jyo8O1Pt2p%sXgZObNfbolr!WN0CxDHK4TH~#BqhZbbe9ir;eFqK4z5) z8cvm4Ui2zU5S>~4JG4go+WB_l7gX#L8Wqsg_zmKvWST09awJ5JJV*WgD>$i1GvBu| zGCR6ZITK6jY>*}x-3T&XG(`g8Q4|E3sc>)JPoRpI#-MzH{x(bi_-DSSh$qNw3$T)7-T3@afGR!36kDXwXPe7@}z}?R1^GD zeCsM&7j}~raMS6{smtwG-9Zi(-$er@xKzdmr%MRf(kFhtuKf{w7JNRSPe9+Vqxn`7 z5-f=`>_x%e<3LH?(rZ_)C@I=oKszgVD#qH+f5@93^=)+4&$xAZrg%m5%VDmIqH{6v z0f3{Pgec*-d*CpZMoX})k`Bktaz&FWC`~HKl^P#bh~?1!#W*t!dX zN3C@|Ey!Dt1GBl`dIc^wr=m!-dy$Op?C|PS9X6Nx3AU8Z@zXV}i>#;e@~@r~DbtWs z#k%wIM!FL-kr{7>eJLY^uFp4jaW!$eAJ=oKsAm>W-+U_8&Nr3k#vipDGkFzsEGy=G zlHETB6B>F3VS3oD>vz}Tenea_EK_vZ*?m{@3{c)uZF@>MJqhi)hnM#l6j%Mwpuj2G zeDJvbXpTs_PPHV)@q70lokt1Wj|QzbJ@=i4?!s>JoE{gPx%=X!&^pl-zlWK7S~5Kr z(D<>71f(Pk3;puU62iXA`M2W;Js6y3^uY_Vg z!QwJW0l&T71?x%K+ER#dp_5ilCo({YlqSmh#PA?g=j@e#l84mzx#glu-o+u3<+}4R zA*oyIXE6R<6|r8HT=(?9*_I6e=VpR#>0*tGPlmOOlo~HT7ENB<<2N}`SFA6yH`1s? z3l{hfy1SLIJ<{|08JADoXfm#TO^r9PXa2TQbJcs<5-jp!2mWOz28yf>K^~KBMJ7fK zAxYYI;rf>-10ucyb@vo!1Kg(8p`nMPLM=Ze>5nWe7QGk6o2W zj?=Z()e83JY`T&_r`)ZR+07!&*Jme_nS;8Auf$))s&2=m941qF8sQ=zD^%H<^r(Cz zB@j{UIF}}K%W%p!fJf10z7hqX7zp)%4{T-lEiSmXJQ<7N~!;oq2tZ~jRh+Wb-5-^28fJXIS~%xru8h6r)!t?H3I&)y3j&* z4UQ>|)`{Rg&jrt?C*=_sO!anU@r&0kfnb=?{jk;f|C1xV1=xWtN_cHU~WVM&h*LB|!LHWMF7?30dc=y9q zj^i~~)2;AD`-SFhuofW_qcq?|cjmPm8vAw!(NpzZA>ZG;oqc#4+qv{niOktpgipF;fKG-Q%qRh z0=^On<=QK(*_=D`;+*1gT^e*n^;;}xAO5a${l1uuP;s2>&cSEW%_mcoyaAOJf?c{F zu!Q6ksTq(hf1#iC6Bi@YJ?KL(2A~kF>S(!8MP>j!X9wUkDvz(zEk@8y=P9qPb8zt? zOVss|6aL0AS7jzSUvv{?lNGR>yOL%)*{4mdbHf!A=`9?*PN{HAO$ zAN?sOl%4YS|M@TF(I8(tUMk}4`R2PVc7n?FW8$hW1rqLarWWCzyG!5fG^dAPeWy)J zt6EsigZfnhVc}YmMGw>X;2hyL48%4X4f@*s+|P)Z7kKk$)gX==Af%z%TlST;!IwL< zLosQI;VS^Gk4EO&NV6E+WPvPHt0-ADD(=Jx4cgeZGFx8(R4bPQqX8vHby$0Xbc=Bn zn8cMTU<`$j;Ng!B$U?SBlA`%Gmg5juV9nCUHPOd)9pXkV_ndc2$7YtdfJa6h7V6Q) z{U1l@)ZQdam@#k)1x4V7haVr69ef6KGBYXg-BbRic4c-bcXLRRWH}l%H)&1D=gMy-2_^! z)#AJ&X4xs-bbX1j^2S~yTzYJ}ybNpd>gp0~8-lhxEw3`W(}#7IFj0SvBm>f!+r~n^ z3sStwc8d?ubi(;!tamwIs#x4)7VbII_!I=LM@&t)(w=iJB zCSmj+pn*O0OL_pd6PY%f(1Nyz1PTo$8uO5H8#w{slSLzTE-*joyqoY=Ya7nmGlJP2 z7sS8MtE;n&vEzKu8$rR&V{-~iwb~Al%b7NS>ZAO_TGRFESvSN_fFatPw|0?^S$~82Z2ZJ{8ap^91 zJiPFlcnb@7AV!I?N9rsVD}(LoyXGWleyz(9ZZR3IZQ-@S+`qS zkpB)IFCV#1uH6sMB%@pX%@Xmpc$;LMl$;-$f6b>qZ?X(c?FA%N>NWZF7&b3~Z|Hj&7Xva5bsph0n)**IrXcmm4x%jr81{E5q4fVu z`d&UYBk+`iffR7EBI38*J0F2Kd7#EbJN)d`l8M2sJs2MnqDoj(QE1cg7Ayr(JX|E! zPGMAWgB8``64j=hq?q;Z?zL2e%_`=hIAl#vjzs$wP zXfqMQD5Q9k9u`w+WZ0buJ=vO!dZ7t|7Ii6)4dZN!t6#DmDL|D30aO=li~dYrrl|w4 zL`paMss1-*l-dZYw_cp>RfR-&*R|w%8N!^84dcIq3B;^KUQ7z;HhEEGk(lL4bLBF~Frw9pfmsd|mx`CM znT4o9Mi(=7W(_i!AR$B9ZBcL0_bw3(aP#Ru=9IlYd`Qi^5IYdk?&51&>vGyo&@`_DISDjc#YRrGEFGm4Ui?`J-|m)$fzfo7%bUmBp9~|JD;2MlVID zEaRCOXj@RKEE^rY2)YkaN3bH-CN$r1ds-^yh>S-!1|%%|YThGAu5Wq+&$diAj=8!x zDsXW=`WKyRr81G=_*wral%@oF8%1G(=^^1iDWiNv-}s$8iaf^QcG3CmjX2_xWE+K< z`Vy==u|1zj7^A}9#ECEEV^DJp0*Zt67A>b+fq!Rnu>i$Dzo8{g+8V_HBu>=Vv!sqf z%C9-2{FHYDK_gaIUgW&?sJG^B6BU$eLW0@#lNBuM9A#x5Qp>#JNNL>x5%=#S#}IrMn2-m-OEYJNsK!(qioQniSg#0`N!vE zt3?cIPk8(57F{sl@R3u2_bW=7uoc3e;?q$Ys{tCOMz!=z0VDyUk_ItG$)UXv)`E!r z+`ZTiCcj`ff{qQi!dt9f<2(fEswn4pva=|JJgztG*TbCG$jQ0Pl*`?gyI#^e!mdPC ze2+`dzUBm|2z9pE>etiTv;Pk~Z2hm<`eZ~2;8J;`V!L6HLVVKoB%Tn#6$tNl+87lf z#!wEVCj-vu;KhMVZ+TzjZH8#-Ba+?2)tWa{;NVlDi4Q@Ex>0=mU9#P?t#!wFDM*u; zLkzTHkql`wF55ruRQ(b~nd-48S%C4hyzdhA-7*wv4Oo4MrAGF9Zp%^46pX|qqv=8H z)>_+vF?)e|A+wITJ{IP`U?fz|<2bz3zn;g=UV7!3Ip6?&{Q?`)+#WIh#JNn|yhe+8 z!22GwfOz0*)Oz;8_Y+9~$Bk9^GQ}zP+pEr>5eDDupR&i--uu3o%jQD@jj?Tuy^pz% zCp>c%DPzwlr-#+Ie1viZx^$-R2z$@opxrWp4ga#gp0%|oe+Cc@Cwx7``)_69X zHVJDt`2s}`utn(qPJ(VIHciHX6X{%TJ64Ej1%$y0Zs5_Mh#ue54%evX1aAA6Pfdz) z{HHJPUC-Recup8VoTW9IxCI!^7)u5UE9{c96yM%!Uz_cu3rpOxr)UITc)B5kCuQH8E`D5Zoq{)21C$XqjZvW<^Wj08O zqGQQF1`}yUw$IIe>tAJy0Y8+?y%O=j`aNW2wHG0D9D8E18?x;=mBkS8=e*LC43E=4 z$Bt>A^bmb2HYjI6DB1EYt~7a zJ$NW2A&P@@WYiWX7TiSX2|MCGPHEH)*Cr3MmN%lJ49%z#rhdM(%*L3fk<_ZfrEnez z!9?qn{Xd+xZi@za*Mu-CdVdtFT*)(CXXys~XLn{_Q;ak1po5yL{>zbK+1}q&)hoi^ zvC3d%m*pR#CrssK9Sp(7X}~F4FQ_RCV;5SjsRDM8^QbY#7<2+h0mZwb`vd3Y{?6>Z zKl}?+u7@-M>MD>#MJQ&@tdH=yQVFWvpqV%r>x52M32k+c=GKdsUzRrX{&~F%f;-?Lg8KxMZXW3$A z461Bt&Z-3#AgXYrvMps-v41(+`)YYfg|GFtl#yuAfav1Hcc>=fSk+nQrU&^%W2kB= z9z~BbxRrgM_Cwsn-*ZiJ=1zWz&H?6)or{s3!TnO0lEi`6zzu6bHh2H+L1T;-b zt9kYz=H7*hT$DO&o|(mS?m*I94{kST)HLygEqp%a^~l2|FY1oLwLV#>r<7e94i|ov zOkQ?$#C{bY7qtgSr~Q8kF8Jjq52Zjy17H5ZUCtLrzNDMu?RdnAUOvLqPA^+mD6t~t zEol9B^(#{xz&;vMKzsN!$GzH=ncM-;vY1+{O+ zX@qqRM68{b)N&1%5S_{f;?v|+BZd16)t5jsnXLRA`7{w>tF2DGW@zyx$V$#QtI1){ zZrX_uLX33D?i~8N)VzXPDqR@;bc*Mf0&+Edt`3m6dEk6M3ym*7hl+0<>m$u~#jfAo z35zd8DrT-!3erGft$%V0Ax%cNU2m(&x)WSqn zv$f-&DYlj`7J7n&j$Y8Bk`ewA4xo!+Sb4ZgNir=2uyz>qO-EN(6;P~ko#Rtp5&h^d|5490e{ySv4 zCRvL12jQMo#-WrCR&^A~#4hSzmrx`c%;y>-*&<$vvgFA`%u>xT$r|yJ0Y`_BAqJK} zS+x_r3Heo~nTAnE5Pb4mg{}CgW1LAVmJ1d8zYj6~;|?Q#OfyNo^PvZQ=4bjbe$1BT z3E)fj+^TTIS-{#ON`lmjw&KEqEuX#uN2zaP%|x&Oc^)GE2cam|QRLH1sDqE(?tc3q z$*Z4@1&FlxUl=C>)MlO90cBERx#d>YvJqrs&1ED0<+yi7&m|puX2AhHHT?F;`wpsW zg$DXP(Kzp(M(g+POZD*6#RxqF?$^8KkERn<3+OqQ=>n8ZyDsSU98Znyr;bp@us-sv zxh!LDP%UQ%o2uRJbBO=47>4erSwC!lzC%#FEUJ8r8Zi0;4(u=4Ug`YQUznFbu_;*6*YsVg4p-x66i1V9We#YBx$617mnnj}131mPUN`4-ciDWkGqBu9Z zn(?@TH`7K2Dc9}vp+YF>5A_)@DJ-k*yePV~EeE2FY27yC*Ok1C@;A@Cs~~S-!@q&G zw(^Z-n4Qu7MxOP7PyWwy|Kwyk3YWh=oGxqlAulhbx_=^FO?&Q+2iawwB+{8RXj(l~ zl2V~T+}NgmFt)rg-i>`O8P=pf_esap6g_K;m8N(Xnr9jAiCgoITvHUu$<+uw7e>C$ z*Nt^hc1oai7x8Wb^O--BojF2yed$mwLv0_kETfiRnwN>RIemK-kNZlG`qPf$EVRcn z(HaC9zF>bkMzYRCx`_F_o8kWNM2Mb{bs`#qEMK9Xt8BGLV*3FO(_$1-%MN*_li!tspTKDL8*7P+~mD~je3 zbL~<6_QdiO<6(Esq(d0Jt*woyCOX;8xSTzo_n~^E=fhf)RwPjTtespgn)&|A(QI_& zsTS>MQTlS90q|1>wJi(x1Y244{&Ez~^iO7B=PDg`4&@cHQVVKrOP~h){a4g&*c3|| zO6(~=23tL`{7l#x-u`4OCWG$S{mPHx$*j@3(YB?iX{U8C_2*oPCP#SgwmJ{A?6S9T ze?I<40vmscrj%pVi0w~1}0Gsv(Ra(r1`>kBwCt;13tHWw8FI0v$Z9 zP1Y`c|AR5dhyHlpe)oYo@|9cI)7XbEZk!zTj;|DTjQ06)#a2A+-gQ*Cl07a^bpKk2Xr5?T7#0Uln8h6T zG7_GlmN~|Z!K167D}q|Fg@y8M&~=y+ZALb~2z8d@x@^!<{O4OPXK>t~o#xAW9tCaQ za+DpE%|U52f}hsS?aRGE6L~d0g^Mxp8D;yFi`yeM&2c% zLV)K*NyPD4{Efd*rjWA=YV0~7@duF6a`mU=!caZV9*ym^Bz-wQDcWz~^${(Lp7`A3 zIH|E?K>0yn<0Do=pG$3f264&Q4z0th+fJ@7-p7G8xVn)4n4yqwfCwWVwA3E(xvA@y zT&fD3n!^Y=|3x%*UW0<3&^`wBEXcFz@Tsxx#3Xiajh~b4$Y~=IucQJLkP%2(dfJ8t zKh}D#$a&H~!Mthx%WgU6aD{Mv(nzb0XN(TPkqLZ|-yH)n$Toyj+q7_DHh~+qD`9ac ze8}+Y^XMEI7%N6coJe$}OkI;3gw^>YC|<=FQ6Rr`EIDdzBvw{gl2 zb!wU6g2~B90PNAwXW1NI$QTys;|mB~D=6*Ux#gyDrjh&z1>{yV8mNB`vU}YvFpihu zV6;=xgO)tUsRVH5i=EO68FhO*=*-5X|tF=A}e+BqPG^-#IG^YyhI$=xb*-Y-Y z;u6^ldi&T0v66wVJI5Cq6x?8$`4*$P=A%2tJVyfy(u67;dNMi3guPe;$R*74rJ%=` zH4br8EwIgKe6o?l2OE`w$+;u?7%t~M;!ZmhhXLIlHOEWes`yfPUkj5J&S zxDd%r%p+FA@>H{4Lt1qw*9g^Z+bL)zCXGt|wOjY^uWj1%K|s`n$Tr_R5$XIXQ&4Ir zA$5Yj3@rZ}b57!Esu5sa@jx;LhGVV_&h(CMFCcJBu#B9c?EtKx0{Rt2hEvVEVJ(gOxEv&u&!_#e{cj5=e=e*TEP4gtH! zAZ7?aQ-kk9-7n&=*Cw}T3x=3b6Z38Mdy?-{wo^$AGlsLxCtvu^1Ds8R?ZK$E?)s}y zFI;j{Lf@ihGZ1b3f!^5H+>tZ-L$4vB^JO5p*wNsRN$ttrTF-PG5NQO+N7X&njB0O1 zso$&7yp@bb4&kO!H6ib@KTanrtmNC%!9{^c`eeqmP$DW?96%&axuWkRqMfdd9%Z`iXc;>Z zoiFWl=dd^I2$ijUz+%zxtpDa?Al_)cIVkDcYjLHQnL~&mS^~eNWg>RN(xGjKK;#Xc z8&5>kaIsDZ{looQ;|Q=}ky8r-xa&)6U^`9dsKczyKGZl~?>qS(Vy_d^vbD~j8WB~e z{QIIxQBG0CiR8Yd*r`bURfme8i^pH+@7NO@x4%wUf|v?^5$1Nqb81|KYUgRxqe{{Iwfh@xbJnTd^R$!uxH(d2=gj63ioN4u|~1}zp%?|Bu){MP;I8?*WKU>dnz1tqODQ4dX~ z73ReFH)Brm@6)A~T``WUvDUFU)bGP-!Ao{B5-InzrypNO{ny*v5(!FiKsf|&d2V!jZ&s+KQz^>xy8T&4N7{{|i5Gx$24$N-OH0wk zy)3;p!<5@a4&%ew%`)_8*X@9-vP=u64{c%}s|NAlax(DSGQKWJ(gBcv(W@FX?$s8Y z?5zO&_(QWliN7{N+RWe-dGKa76BV<~xN!fTXt@m6XpuE+xV4 z!{GgVEYC=0YMKlslE}Phg!EAIgs_VFHpHgroEuEhC7G|smw}SGZ|x%!DQ?u8Kf(QT zShCnr7408$MD^D=16v*;kU>?jK!F1dWiuysc|5QfYOq2X z+NPF+9(bHq+{14nx-A77p~plc#v%N_87$Teutu2SwLc5|rVsg=E9{xy>Q}3{Fj7$~ zV0s9A8hCuXvG!2*(93q(DiQjmBIN5#;Jo9Y7!QX34B|tfpIiC_&V#J5_cMH?UIWq! zVXlKY{pN!+mL^K609J3#|M|0{WEk?Q5CFsAH-{axX@g&kPV025SF zYY~sSY7ZfW5AeOirrv_IG9{eCH(sN*#GX!jcB`N&16ZDE54a4unNX_8(VKb{)b^N- zW`~Uyp4ad;7tJ*i&O=t%7HWZ4bZ=Zmav< z&zZvdpd9cUO}4Y0QQ_W#g;TiwZ3qdIn}zI;Zg9J8oW>8uhk3#$#N%_oZMOmq)@j#G z)!Y03KghxUzjGny`iS~6ce^2Bi%KCdc1^4WItcrV`R!eC;jTSw--HgFHcNz#NelU! zP}Kp7T5-+)R6yDi)aP?q&mDPc{e|d6iF``;@3K*Jsg+~iu^!v9HW{2fq#@bZ)n5Pb zz$hdic-wzUG7t_Y$)a)^JI_1ArlcQM`p3;8z^!exO)5XnM31Qu(v<(N>w}4`?$6wx z!|`rW+1fGH9vIx&Q4;1#G2d*f7IKl`>AR|ZQ*03K2) z@@JD$MU>C}pS=LdX+NjBE{xQSlBz3!{w=?*zjI=K#MWf2S4`@w+TkJboMX zF-FDzT*DB~+F*Hz*7YOZsb!#VeQ)N~%blei2RqY~kLDxNaA78iR@dNmf+tTc!sRtX zdrdq;blUMpb2-vkX8D-^A5U-D7FX9q>oznF!GpWIy9Xy|APMg7?(XgccL;>wu4&w* zvEWW{Y22YV&%4h)e_(x>^Qv02M%^`PJXosfM#n#X(Y`wcAx4FQ!?XhcA@U#bneh5< z53O^HB}?Cr$tBIp8Wwui(wTVEC|(_dR)v}$E?-Kl(!Wg5yDi^9?GrQHOzY!5dd9%5 zW{2vhZ3=LYJ0duJD_DB-E0{Vju0+BlLPyLsKq2jeq#PFv*T58b%}S6G)AiI)Q;lBT|VFkWJud_0OGd$6&M@tyh{}1M4zE>p)`%YJK9KLgqxm*IF)DgK7#+nNp zA>ReMcFdzzGo+3c?r+yFm&sCB7|2@4G5J!r%Y!XS;)r=s)qa|_#nm}+rc6QJ2Z=bp`D|ag~$5l zFFGh7y4y_;xjT&bmVzSHJ#&X_x@iVOa?+iRjZ_Age<-*> z$+HkzrflOXjROD!nBeuF-2FGkjpeI)`%+L7O0#u6@1bjbp+p4t*7v=1R<)|x1Rpd2 zzjbIcY$ujSc5u*Zzw@fVIc74ffg38Hr7sfKFmFj2X;w+o9=atd=zLnA z#;IMcw}7b$^tysmZ5_65D1Ux+d&&mgbj1aW`1TSRw9p#CiCyX0$21@aUR&|`TGB}- zD$06x$@v(O_=?hBBkWo#cDtuF2P*4tD(NzKN8+=e*O`PGBXiD;_A2 z3=I$!Es~AaC1$2aV(T3vKZ)L~f!f6D2+pS=9n6vLMF+6!=WxL;AX17127FXS@Y?m~ zkAQg1iEiuHc8IRItw+}dI+IR89E*zrwKMcd8+=v8&UNy(Gxxf)(&{WBrJ)#q9sEnGEWytP&r@wjumxw6eRo;O(t*}_3nRDJU4xedT_JP zEbmPIPL}mGX&1mYu^!+kGOKkcW(RF#lh26|kFcymc@SMf5)t02Y!c0QTXVu}(x^?v z#Gx2)SGZd?%s&kLDXcB~MK0&L=eGKWq{b?Hy?1qEc6%!i(}3QBiy@{N&_i7IDI39Y zbEdge6I|4_ORBB>#EB}q---Yk5vYB7Ut=+kD64CF{%bfXE^nYfP>y!Do&1{1J!_Bn zE%^T3c<O!l_|sS<{>7%r%75{W_aAzp%W+~O`WroH69wY6DK~}(fIAZ~_RVC8 z4J~kj4;t&l>p&@NHSp&RZEo(0NJ9fWwqBsF-rpycgGf26?5PU$npI*0psdwb%A?YC z0DZ^`Y1L;&Sa0wu(VOnQjB4cjMzpzP03v0VlQStdFu!eg<)VIZlT_@bL%4svH5dJ) z!f5Ls5|WdPgq;?v0K)IzX}cto-;E-eg^7xgw1g9Y7;SXSlnwdikj9a-?brrusB$0^ z{k!*@m0(e5=;bU0GebnikJ=|T5yNvrGOPg&+%PkKH1gQYJhCZu4GN~sJuu<$htg_p z(RvB-(V)wsL1L2D;_j*iAaLdw(7^Q<4Lh$|8QBEMZ{#pO!5 z7?N$Xw*M-SkqYrMBq_ANT9z&(*S)xeUd9mpYy{?yt1qb8etoDHbP$;=!d*-MzJ+%W z6VpHS@xuwd78%i>+->dEPwEz7ZMEGui-$5GqV8?C?u^G2<%}`H8MlRB;a@JFF}3+S z!X#SWH-1gqW*Q{d^#aezB;u#r^7IRBp5oYG5LR%AO0a z3p|i^^^5yCJAP<;lLw=hip7Z}U@qMRuGF-zZuK)FulnA2h9ZMstRd&EX_+t4*+Wj( zO_fvpEaDq#8B}OGdR&{Qm@8H6{Lid0B1DecylSbM;Adb|l+qB|E~mgL5r_gvcdAxV z;?#s`Xi^jXmf^7?0r!pgQCsF}Qktz|;cD__9jiwKF2OrSaH_VEf?Ia!XC4}6W2i|D zcuyDzkO$5D(;vivi_(@z`Z;Yoyp~!Y#?#^0ZmkGotBG)&sYV7aU)Xip>d|r^Jj>=H zMBdZi#HWrWG9#BQQx*gtS*)p;85NDr)|KXc)GK5;Wi1*FC%y1`I&5r z&~4zKI~H9%C~0!FN0*=a7*wULZYAvColvYUuSJIV?c80Qf!~eapSG%vEj{UTvrrIf z=J-O=2ojrNv0HJBe(<=pArvA=StG5?cCHDknc2=>vi7t9qRrskrp#&qHL8YS>fSj9 zKeNS3?v-$MnQcA8513}|1bPKm*Ft=pX56CVxoiqzb|>(_UXkW5ef-{YQ@K0|+yj_K z-*=i7LB$-QVe#v|dcl{(7~K?@%jI1FX`U>d!pcjg+)kauO6=}%3%idpH@0jEl+NyO zSoI8GB!LlWfP-XVEU9to~<5TIWIsp7&%2OG1*e{zlxBi_eoPvqjyj z!3t-eETd!@oQ5dnZJhdjJg}1K>HYb3yC6WO4lA4y@g`16{GL(JZcd5)FBWRvD6+DY zVxF`aUH*Ph4^|3ZK@gcpshWFJY$|zZEvUBJN{&m+%TeRgxO_J_eAg0(>XDQ_J z&InrmrUDShLlfync{lzDZx#2meW>Bzz~8t$9Pc|y9qwbFoa0=x<@x&mEYB|) z#BNQqBYA`C;O|Z%IL@A=!W4@!iOuH(bzlj$-daa-MJ@*4pn zKI5YeXQjXLIGukrlo0yo3~DVxj^+hsB39jB^8H*p^ENhM7-8jOq7;jo1}K1~S;pWv zuRwb~?1bThUYFgYp&|;?1w-L~!VAWL1Qdn-yN^)z@pQz+#r8Q^l%xn{OYVvtXB3uZ zSU2zcFE{r0^}F*n?r<UjW z1GC{=-CENXC>ZoMYPN_5>J({lGTN3X_+NEpg)|PxYyo73_W>R2Zc#ID{bhWYdD3lv zRNQ6;L_42Z8r|23EW#hgL6#67WlIbPr{suUWlCLZNRNOgrUztu_6|U9Dkj81+8c2p zSPu^qyb(8-o}2rdKm6g2zE2w$`ScL9R{cOOz0CJ;E)#+gcAjDBL90eer3FZ!aKjF8 zIB$&sMJr)Nhk=UMmJ}E;*gA%=r)q5^Jmpo0S$x2AP;T)p=?DfngbBF^ ztD&$licy1qski8?Yilk3371%TSndA*mPr6)FuMS%ShCDtt~Gq*2cL4XzW%%oCDN%& zqWgj%ozf4}C|cU_=Ykp-U7d&IBqlhKbtXEa1_^@IkT(bOlw{^mD1t;0K^+OGfGAwG zK9*zVL0^hV3AeHtz@*@M#mXqp!=FRT(UDk5v-?cFLRU&DLRGzCK<)sEijdOW-+ebC zOZ-%a>8+LgZ5}5vtoG0K-MauX*0$Fk4g~)93kE86tRzMOw=@HH`TI4yq14+q6&l9e z_AgU`uuaTDSeTkoSvM%3_z6V5<$uS>m1c0Ns0Cm=5-)h@EUyH_l{M8&s@BFxpHKpK_wITwPw8d1n-+Cyrfn_c?h7A zDP!EBZr3U*{Hot@dh5X46KKC=c=xu%xj`;~5fSRb2B^Jt4vbcAx-S6nTTpN@ELJaa zh>3l-#s|$*`5o&6zt0WMHs820O!KZQTw9NJyq8$2xYp zu+2XWd4Y$dKfXE&bU7L94$^af2;C|;4md9-_noZYzMn=#7QizmDi`_EV1I@&Wa??} zFtO+wm3Xr_tj1?eQiS+4``j-4@l99&$un03EAdU3GcV)RM)w0>^Z#NP@PC`LHuVOz z{Tmww55wJeC)<_Xd#2k@Cp8~(Cf%5h@dByQ0d@{K2gKm1_woFLmPfpktF>oW2|rZ6 zSvickKtIc{fToe2prmTVPK~NS`Y_k$6D^*L{2Z0=d(czO#br4JLv`KVlm%D7991cc z+IJ2s1lGw+Ve)M8InN%Knh`7 zwc~mmeu|e27)F*ZfreJxO(>**6x{msGKk&OO%l?!|e0K{xZ~G7~64$M*eK@e||}p6Xe9!C5F)b^>45b9i^RB zFOM-5+f*a>mz6j-P2Kd4x3*MqyNl?2dI#ewD@`#5ri7eC2x_RtQ}k1WXytHwV3UZj zvXrzaI*Yx> z7`Vluas06?0DY+=6`_X1J>#Wa8&R$j3iK0Xac&Sb8bk2f-eFpEJN9~b8ipWkUQPhF zSur;7YL-{yZ-L))LhCw(+dsJdwHQ1=$sEP|3}2-LN5Tq#k&mL2O#}{@IN(*SKn@%Q zjP_yZRW)1FaTf&SEpn@`96ymxhgDQpv!L{MhREjfE*>_TF&I686ctq?YT^GW1X>hB z#s1`JWj0=c7tCq#7Y^)EV21*9#bU#Cj|!K6_qJprO(Sj`fTm7xE@pH$%Rbl(w-6S5 zBjrYiUm~XI5JLRj0VeFBLFg=fJcznZpMjQY9Dzenq7fuF$1oRp8Q<}A9iX3wMx`z~ z3I;l%8f=jLkt(%th^#Q{&w#$t)dWe){H?Exh0UMc@g42CuY?PVI8k<-K3Db7CRhJx zy+C#cn8j*j9wN1W7yOcITCu=t@Z2{>HB+>BMWSiu0O1b!0YVqZq%q2E4p6Dmea9qD639^lU*G!y zlZzx7Gu#(n$Z7xjOnEwp`?aMF>Fq^}_uh=o2Hk@xO2Pxw%NNRn3V6it17@`J?iU-!8q5GMRjP z8>9@nvV~l$+H)nHZ}3ZSuE#=(;nB91T31EMFj#6AT=z5vSAM)3?$#!}>8_K-2Mt5M z^<>@>dKXFPc$+?IJ%pq)j`G#Lm|h7>zec7+)biXW?wJZD4WWsCGTzaQ8`AMlVVoO} zScnJc%NxSx6X{oWn+{F#%|KrWbKO_5^Z*q50upPnkl7JA0H)hFn6N2VUI8ggnk$MP z%T*+gd~A%5y?3tzW6;vT$e1_oI;r9^Mpn{&Nh#vwkEt`pUUEH5zui)jR`b=$zGMg* z->A1TY<;$9_+lhv(NX2f5N)I>a#K!UI7iW((2rH#SllgnsI(uory3C5W%?Ypt7Zl4 zILw?c)XSe&cTpu4xDc{k?~CUOZ9y9-d^8$GzcH~j~FCJQhUd~*E1 zg>1`z$#p40SMx}_6Mn128?}c=uoXbhrof6BFI&Zu!%7c;Regy^y976pz|}qBmBS0s zi=_Ih0C;tnpu-DY*0-pW*@E=ZBmL0DOj#QRx93e4ym>#BEF=T`o6WgV7>(YZcgo46 z@88LJhoIlf5n^9wK^Og+bAN@?`HoQT>ZbIm*hjRg-VdwKHBTB%lV(5$iH2LiwEC6` zOm{&xWOM4vx|UK_o(^D>AIqikzRE_7Imwf8QE8j!fPo=? zHOm6I(C$xTP8 zqiN9!UC5jxjIWt+3_r*c%^txHH|TlClZ(Hj*puKW0>WL$0~Zt%V_hDuf^dg|bR85d zdo_1x1>y#Fu)Ye>6PL>%0=CsS{V)4%;Oh4B2kaFdYxh&fm7+FhLgFL$*CmA7f6`u# z9ZNsq|6oc4h*}+h*ofZIlyj?$)jRoF05=WKY5Rt2OQ*u0Tqq z@{1_zi|y&F=acGD`@ms$+(LoxuEzGc#?jJ=Sq@9!e=Ryaw*PuTwu>|4;x^7M18aCJC-|owi;Ze%| zt+1uI@Bvu$-Uitom#{iy)_(jpF{TEHZ=N<$Q$Bdo$n2M?IVFA_)T6WHgT2g7BKhx($BpR7 z%LfISL&5s6ACM(HB&Y+%{WF3`=J6kDb}vRP79Wq{(LJ_wpF0mg!bcl`dufBr0!YT@H(Ko3=mXRI_N`A$DcpL(C zXs{zZIhOhCJXi@c_h20in;dk?kP~waKXaYO@k%!;kNuOi1=e}o=uqDVCVAR9sM$Pq zmtH}?-Uj}D>kfi`nPLUAk}UlPEg1<1=>I4mX9Pe``TK=8pvg{WM@P6p_A&2kx||TI z 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

_`VHH2YS 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%0A|GiDg+-P}( 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 literal 0 HcmV?d00001 diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/120-1.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/120-1.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/120-1.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/120-1.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/120.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/120.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/120.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/120.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/180.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/180.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/180.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/180.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/40.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/40.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/40.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/40.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/58.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/58.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/58.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/58.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/60.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/60.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/60.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/60.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/80.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/80.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/80.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/80.png diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/87.png b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/87.png similarity index 100% rename from Simple Anki/Assets.xcassets/AppIcon.appiconset/87.png rename to Simple Anki/Assets.xcassets/AppIcon 2.appiconset/87.png diff --git a/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/Contents.json b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/Contents.json new file mode 100644 index 0000000..42418d3 --- /dev/null +++ b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/Contents.json @@ -0,0 +1,67 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "120-1.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Simple Anki/Assets.xcassets/AppIcon.appiconset/Contents.json b/Simple Anki/Assets.xcassets/AppIcon.appiconset/Contents.json index bb1ad4d..cff1680 100644 --- a/Simple Anki/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Simple Anki/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,57 +1,9 @@ { "images" : [ - { - "filename" : "40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "120-1.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, { "filename" : "1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/Simple Anki/Base.lproj/LaunchScreen.storyboard b/Simple Anki/Base.lproj/LaunchScreen.storyboard index 2393c1d..c27a68f 100644 --- a/Simple Anki/Base.lproj/LaunchScreen.storyboard +++ b/Simple Anki/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - + @@ -16,22 +16,8 @@ - diff --git a/Simple Anki/Controllers/CardsTableViewController.swift b/Simple Anki/Controllers/CardsTableViewController.swift deleted file mode 100644 index b2dbe3c..0000000 --- a/Simple Anki/Controllers/CardsTableViewController.swift +++ /dev/null @@ -1,384 +0,0 @@ -// -// CardsTableViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 28.02.2021. -// - -import UIKit -import RealmSwift - -class CardsTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - private lazy var tableView: UITableView = { - let table = UITableView(frame: .zero) - table.register(UITableViewCell.self, forCellReuseIdentifier: K.cardCellIdentifier) - return table - }() - - private lazy var cardToolbar: UIToolbar = { - let toolbar = UIToolbar() - toolbar.sizeToFit() - toolbar.backgroundColor = .systemBackground - return toolbar - }() - - private lazy var segmentedControl: UISegmentedControl = { - let segmentedControl = UISegmentedControl(items: ["Learning", "Memorized"]) - segmentedControl.selectedSegmentIndex = 0 - return segmentedControl - }() - - var cards: Results? - var cardsToDisplay: Results? - - var selectedDeck: Deck? { - didSet { - loadCards() - } - } - - override func viewDidLoad() { - super.viewDidLoad() - navigationController?.navigationBar.prefersLargeTitles = true - title = selectedDeck?.name - view.addSubview(tableView) - tableView.frame = view.bounds - segmentedControl.addTarget(self, action: #selector(segmentedControlSwitched), for: .valueChanged) - navigationItem.titleView = segmentedControl - tableView.delegate = self - tableView.dataSource = self - tableView.frame = view.bounds - tableView.tableFooterView = UIView() - navigationItem.rightBarButtonItem = UIBarButtonItem( - image: UIImage(systemName: "plus"), - style: .plain, - target: self, - action: #selector(didTapPlus) - ) - navigationItem.leftBarButtonItem = UIBarButtonItem( - title: "Close", - style: .plain, - target: self, - action: #selector(didTapClose) - ) - configureToolbar() - tableView.contentInset.bottom = cardToolbar.constraints[1].constant - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - loadCards() - } - - // MARK: - Private methods - - private func configureToolbar() { - let gearButton = UIButton() - let gearImage = UIImage(systemName: "gearshape") - gearButton.configureIconButton(configuration: .tinted(), image: gearImage) - gearButton.frame = CGRect(x: 0, y: 0, width: 50, height: 50) - gearButton.addTarget(self, action: #selector(didLayoutTap), for: .touchUpInside) - let gear = UIBarButtonItem(customView: gearButton) - - let flexible = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil) - - let reviewButton = UIButton() - reviewButton.configureDefaultButton(title: "Review") - reviewButton.frame = CGRect(x: 0, y: 0, width: view.frame.width - 98, height: 50) - reviewButton.addTarget(self, action: #selector(reviewButtonTouchUpInside), for: .touchUpInside) - let review = UIBarButtonItem(customView: reviewButton) - - cardToolbar.items = [gear, flexible, review] - view.addSubview(cardToolbar) - cardToolbar.translatesAutoresizingMaskIntoConstraints = false - cardToolbar.widthAnchor.constraint(equalToConstant: view.frame.size.width).isActive = true - cardToolbar.heightAnchor.constraint(equalToConstant: 70).isActive = true - cardToolbar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -16).isActive = true - - } - - // MARK: - Actions - - @objc private func segmentedControlSwitched() { - print(segmentedControl.selectedSegmentIndex) - if segmentedControl.selectedSegmentIndex == 0 { - cardsToDisplay = cards?.where({ $0.memorized == false }) - } else { - cardsToDisplay = cards?.where({ $0.memorized == true }) - } - reload() - } - - @objc private func didTapClose() { - dismiss(animated: true) - } - - @objc private func reviewButtonTouchUpInside() { - let reviewVC = ReviewViewController() - guard let deck = cards?[0].deck.first else { return } - guard let cardsToReview = cards?.where({ $0.memorized == false }) else { return } - reviewVC.reviewManager = ReviewManager( - layout: deck.layout, - autoPlay: deck.autoplay, - cards: cardsToReview.shuffled() - ) - let navVC = UINavigationController(rootViewController: reviewVC) - navVC.modalPresentationStyle = .fullScreen - present(navVC, animated: true) - } - - @objc private func didLayoutTap() { - let alert = UIAlertController(title: "Set layout", message: nil, preferredStyle: .actionSheet) - let frontToBack = UIAlertAction(title: "Front-to-Back", style: .default) { (_) in - do { - try StorageManager.realm.write { - self.selectedDeck?.layout = K.Layout.frontToBack - } - } catch { print(error) } - } - - let backToFront = UIAlertAction(title: "Back-to-Front", style: .default) { (_) in - do { - try StorageManager.realm.write { - self.selectedDeck?.layout = K.Layout.backToFront - } - } catch { print(error) } - } - - let all = UIAlertAction(title: "All", style: .default) { (_) in - do { - try StorageManager.realm.write { - self.selectedDeck?.layout = K.Layout.all - } - } catch { print(error) } - } - - let cancel = UIAlertAction(title: "Cancel", style: .cancel) - let checked = "checked" - switch selectedDeck?.layout { - case K.Layout.frontToBack: - frontToBack.setValue(true, forKey: checked) - backToFront.setValue(false, forKey: checked) - all.setValue(false, forKey: checked) - case K.Layout.backToFront: - frontToBack.setValue(false, forKey: checked) - backToFront.setValue(true, forKey: checked) - all.setValue(false, forKey: checked) - case K.Layout.all: - frontToBack.setValue(false, forKey: checked) - backToFront.setValue(false, forKey: checked) - all.setValue(true, forKey: checked) - default: - break - } - - alert.addAction(frontToBack) - alert.addAction(backToFront) - alert.addAction(all) - alert.addAction(cancel) - present(alert, animated: true) - - } - - @objc private func didTapPlus() { - presentNewCardViewController() - } - - private func presentNewCardViewController() { - let newCardVC = NewCardViewController() - let navVC = UINavigationController(rootViewController: newCardVC) - navVC.modalPresentationStyle = .fullScreen - newCardVC.selectedDeck = selectedDeck - present(navVC, animated: true) - } - - // MARK: - Table view data source - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let cardsCount = cardsToDisplay?.count { - if segmentedControl.selectedSegmentIndex == 0 { - if cardsCount == 0 { - setEmptyState() - cardToolbar.isHidden = true - } else { - restore() - cardToolbar.isHidden = false - } - } else { - if cardsCount == 0 { - setEmptyStateForMemorizedCards() - cardToolbar.isHidden = true - } else { - restore() - cardToolbar.isHidden = true - } - } - } - return cardsToDisplay?.count ?? 0 - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 60 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: K.cardCellIdentifier, for: indexPath) - var content = cell.defaultContentConfiguration() - content.textProperties.lineBreakMode = .byTruncatingTail - content.textProperties.numberOfLines = 1 - content.secondaryTextProperties.lineBreakMode = .byTruncatingTail - content.secondaryTextProperties.numberOfLines = 1 - content.text = cardsToDisplay?[indexPath.row].front - content.secondaryText = cardsToDisplay?[indexPath.row].back - cell.contentConfiguration = content - return cell - } - - func tableView( - _ tableView: UITableView, - leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { - let config = UISwipeActionsConfiguration(actions: [makeCardMemorizedContextualAction(forRowAt: indexPath)]) - return config - } - - private func makeCardMemorizedContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction { - let memorizedContextualAction = UIContextualAction(style: .normal, title: "Memorized") { (_, _, completion) in - guard let cards = self.cardsToDisplay else { return } - let card = cards[indexPath.row] - do { - try StorageManager.realm.write { - if card.memorized == false { - card.memorized = true - } else { - card.memorized = false - } - } - } catch { - print(error) - } - - self.tableView.deleteRows(at: [indexPath], with: .automatic) - completion(true) - } - memorizedContextualAction.image = UIImage(systemName: "brain") - memorizedContextualAction.backgroundColor = .systemGreen - return memorizedContextualAction - } - - func tableView( - _ tableView: UITableView, - trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { - return UISwipeActionsConfiguration(actions: [makeDeleteContextualAction(forRowAt: indexPath)]) - } - - private func makeDeleteContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction { - let deleteContextualAction = UIContextualAction(style: .destructive, title: "Delete") { (_, _, completion) in - guard let cards = self.cardsToDisplay else { return } - let card = cards[indexPath.row] - StorageManager.delete(card) - self.tableView.deleteRows(at: [indexPath], with: .automatic) - completion(true) - } - deleteContextualAction.image = UIImage(systemName: "trash") - return deleteContextualAction - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let newCardVC = NewCardViewController() - let navVC = UINavigationController(rootViewController: newCardVC) - newCardVC.selectedCard = cardsToDisplay?[indexPath.row] - newCardVC.selectedDeck = selectedDeck - newCardVC.reloadData = { [weak self] in - self?.reload() - } - present(navVC, animated: true) - } - - func loadCards() { - cards = selectedDeck?.cards.sorted(byKeyPath: "dateCreated", ascending: true) - if segmentedControl.selectedSegmentIndex == 0 { - cardsToDisplay = cards?.where({ $0.memorized == false }) - } else { - cardsToDisplay = cards?.where({ $0.memorized == true }) - } - reload() - } -} - -extension CardsTableViewController: RefreshDataDelegate { - func reload() { - tableView.reloadData() - } -} - -extension CardsTableViewController: EmptyState { - func setEmptyStateForMemorizedCards() { - let imageView = UIImageView(image: UIImage(systemName: "brain")) - imageView.tintColor = .systemGray3 - imageView.contentMode = .scaleAspectFill - - let messageLabel = UILabel() - messageLabel.text = "No memorized cards in this deck." - messageLabel.numberOfLines = 0 - messageLabel.textAlignment = .center - messageLabel.font = .systemFont(ofSize: 20) - messageLabel.textColor = .systemGray - - let stackView = UIStackView(arrangedSubviews: [imageView, messageLabel]) - stackView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 180) - stackView.spacing = 20 - stackView.center = view.center - stackView.axis = .vertical - stackView.alignment = .center - - let emptyStateview = UIView() - emptyStateview.addSubview(stackView) - tableView.backgroundView = emptyStateview - tableView.isScrollEnabled = false - } - - func setEmptyState() { - let imageView = UIImageView(image: UIImage(systemName: "square.stack")) - imageView.contentMode = .scaleAspectFill - imageView.tintColor = .systemGray3 - - let messageLabel = UILabel() - messageLabel.text = "No cards in this deck." - messageLabel.numberOfLines = 0 - messageLabel.textAlignment = .center - messageLabel.font = .systemFont(ofSize: 20) - messageLabel.textColor = .systemGray - - let stackView = UIStackView(arrangedSubviews: [imageView, messageLabel]) - stackView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 200) - stackView.spacing = 20 - stackView.center = view.center - stackView.axis = .vertical - stackView.alignment = .center - - let button = UIButton() - button.configureDefaultButton(title: "Add a card") - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(didTapPlus), for: .touchUpInside) - - let emptyStateview = UIView() - emptyStateview.addSubview(stackView) - emptyStateview.addSubview(button) - button.translatesAutoresizingMaskIntoConstraints = false - button.widthAnchor.constraint(equalToConstant: 300).isActive = true - button.heightAnchor.constraint(equalToConstant: 50).isActive = true - button.centerXAnchor.constraint(equalTo: emptyStateview.centerXAnchor).isActive = true - button.safeTopAnchor.constraint(equalTo: emptyStateview.safeBottomAnchor, constant: -100).isActive = true - - tableView.backgroundView = emptyStateview - tableView.isScrollEnabled = false - } - - func restore() { - tableView.backgroundView = nil - tableView.isScrollEnabled = true - } -} diff --git a/Simple Anki/Controllers/Cells/BaseSettingsCell.swift b/Simple Anki/Controllers/Cells/BaseSettingsCell.swift deleted file mode 100644 index 65114ba..0000000 --- a/Simple Anki/Controllers/Cells/BaseSettingsCell.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// BaseSettingsCell.swift -// Simple Anki -// -// Created by Астемир Бозиев on 17.03.2022. -// - -import Foundation -import UIKit - -class BaseSettingsCell: UITableViewCell { - let iconContainer: UIView = { - let view = UIView() - view.clipsToBounds = true - view.layer.cornerRadius = 7 - view.layer.masksToBounds = true - return view - }() - - let iconImageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = .white - imageView.contentMode = .scaleAspectFit - return imageView - }() - - let label: UILabel = { - let label = UILabel() - label.numberOfLines = 1 - return label - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(label) - contentView.addSubview(iconContainer) - iconContainer.addSubview(iconImageView) - contentView.clipsToBounds = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - let size: CGFloat = contentView.frame.size.height - 12 - iconContainer.frame = CGRect(x: 15, y: 6, width: size, height: size) - - let imageSize: CGFloat = size / 1.5 - iconImageView.frame = CGRect(x: (size - imageSize) / 2, y: (size - imageSize) / 2, width: imageSize, height: imageSize) - - label.frame = CGRect( - x: 25 + iconContainer.frame.size.width, - y: 0, - width: contentView.frame.size.width - 20 - iconContainer.frame.size.width, - height: contentView.frame.size.height) - } - - override func prepareForReuse() { - super.prepareForReuse() - iconImageView.image = nil - label.text = nil - } -} diff --git a/Simple Anki/Controllers/Cells/DatePickerViewCell.swift b/Simple Anki/Controllers/Cells/DatePickerViewCell.swift deleted file mode 100644 index 24f4816..0000000 --- a/Simple Anki/Controllers/Cells/DatePickerViewCell.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// DatePickerViewCell.swift -// Simple Anki -// -// Created by Астемир Бозиев on 24.04.2022. -// - -import Foundation -import UIKit - -protocol DatePickerViewCellDelegate: AnyObject { - func datePicker(with cell: UITableViewCell) -} - -class DatePickerViewCell: UITableViewCell { - static let identifier = "DatePickerViewCell" - weak var delegate: DatePickerViewCellDelegate? - - let datePicker: UIDatePicker = { - let picker = UIDatePicker() - picker.datePickerMode = .time - picker.locale = .current - picker.preferredDatePickerStyle = .wheels - return picker - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(datePicker) - datePicker.addTarget(self, action: #selector(datePickerAction), for: .valueChanged) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - datePicker.frame = CGRect(x: 0, y: 0, width: contentView.frame.size.width, height: 200) - } - - @objc func datePickerAction() { - delegate?.datePicker(with: self) - } - - func configure(with model: DatePickerOption) { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm" - dateFormatter.timeZone = .current - let date = dateFormatter.date(from: model.date) ?? Date() - datePicker.date = date - } -} diff --git a/Simple Anki/Controllers/Cells/SettingsViewCell.swift b/Simple Anki/Controllers/Cells/SettingsViewCell.swift deleted file mode 100644 index 55238c8..0000000 --- a/Simple Anki/Controllers/Cells/SettingsViewCell.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SettingsTableViewCell.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit - -class SettingsTableViewCell: BaseSettingsCell { - static let identifier = "SettingsTableViewCell" - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - accessoryType = .disclosureIndicator - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func configure(with model: Option) { - label.text = model.title - iconImageView.image = model.icon - iconImageView.tintColor = .systemGray - } -} diff --git a/Simple Anki/Controllers/Cells/SwitchTableViewCell.swift b/Simple Anki/Controllers/Cells/SwitchTableViewCell.swift deleted file mode 100644 index 6ac05a4..0000000 --- a/Simple Anki/Controllers/Cells/SwitchTableViewCell.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// SettingsTableViewCell.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit - -protocol SwitchViewCellDelegate: AnyObject { - func switchAction(with cell: UITableViewCell) -} - -class SwitchTableViewCell: BaseSettingsCell { - static let identifier = "SwitchTableViewCell" - weak var delegate: SwitchViewCellDelegate? - - let mySwitch: UISwitch = { - return UISwitch() - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - contentView.addSubview(mySwitch) - accessoryType = .none - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func layoutSubviews() { - super.layoutSubviews() - mySwitch.sizeToFit() - mySwitch.frame = CGRect( - x: contentView.frame.size.width - mySwitch.frame.size.width - 20, - y: (contentView.frame.size.height - mySwitch.frame.size.height) / 2, - width: mySwitch.frame.size.width, - height: mySwitch.frame.size.height) - mySwitch.addTarget(self, action: #selector(switchAction), for: .valueChanged) - } - - override func prepareForReuse() { - super.prepareForReuse() - mySwitch.isOn = false - } - - public func configure(with model: SwitchOption) { - label.text = model.title - iconImageView.image = model.icon - iconImageView.tintColor = .systemGray - mySwitch.isOn = model.isOn - } - - @objc func switchAction() { - delegate?.switchAction(with: self) - } -} diff --git a/Simple Anki/Controllers/Decks/DecksTableViewController.swift b/Simple Anki/Controllers/Decks/DecksTableViewController.swift deleted file mode 100644 index 1b7156c..0000000 --- a/Simple Anki/Controllers/Decks/DecksTableViewController.swift +++ /dev/null @@ -1,256 +0,0 @@ -// -// DecksTableViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit -import RealmSwift - -class DecksTableViewController: UITableViewController { - - var viewModel = DecksViewModel() - - var isActive: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - title = "Decks" - navigationController?.navigationBar.prefersLargeTitles = true - congigureBarButtonItems() - configureTableView() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - let key = viewModel.getSortType() ?? .dateCreated - viewModel.loadDecks(by: key) - } - - @objc private func didTapImport() { - let importVC = ImportViewController() - importVC.reloadData = { [weak self] in - self?.reload() - } - let nav = UINavigationController(rootViewController: importVC) - nav.isModalInPresentation = true - if let sheetController = nav.sheetPresentationController { - sheetController.detents = [.medium()] - sheetController.prefersScrollingExpandsWhenScrolledToEdge = false - } - self.present(nav, animated: true) - } - - @objc private func didTapPlus() { - didTapCreateDeck() - } - - @objc private func didTapCreateDeck() { - let newDeckVC = NewDeckViewController() - newDeckVC.reloadData = { [weak self] in - self?.reload() - } - let navVC = UINavigationController(rootViewController: newDeckVC) - navVC.modalPresentationStyle = .fullScreen - present(navVC, animated: true) - } - - // MARK: - Configure UI - - private func configureTableView() { - tableView.tableFooterView = UIView() - tableView.register(UITableViewCell.self, forCellReuseIdentifier: K.deckCellIdentifier) - viewModel.delegate = self - } - - private func congigureBarButtonItems() { - let barButtonItems = [ - UIBarButtonItem( - image: UIImage(systemName: "plus"), - style: .plain, - target: self, - action: #selector(didTapPlus) - ), - UIBarButtonItem( - image: UIImage(systemName: "tray.and.arrow.down"), - style: .plain, - target: self, - action: #selector(didTapImport) - ) - ] - navigationItem.rightBarButtonItems = barButtonItems - } - - // MARK: - Table view data source - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let decksCount = viewModel.decks?.count { - switch decksCount { - case 0: - setEmptyState() - default: - restore() - } - } - return viewModel.decks?.count ?? 0 - } - - override func tableView( - _ tableView: UITableView, - trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { - let config = UISwipeActionsConfiguration(actions: [makeDeleteContextualAction(forRowAt: indexPath)]) - config.performsFirstActionWithFullSwipe = false - return config - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 55 - } - - override func tableView( - _ tableView: UITableView, - leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { - let config = UISwipeActionsConfiguration(actions: [makeEditDeckNameContextualAction(forRowAt: indexPath)]) - config.performsFirstActionWithFullSwipe = false - return config - } - - private func makeDeleteContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction { - let deleteContextualAction = UIContextualAction(style: .destructive, title: "Delete") { (_, _, completion) in - let deck = self.viewModel.decks[indexPath.row] - StorageManager.delete(deck) - self.tableView.deleteRows(at: [indexPath], with: .automatic) - completion(true) - } - deleteContextualAction.image = UIImage(systemName: "trash") - return deleteContextualAction - } - - private func makeEditDeckNameContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction { - let editContextualAction = UIContextualAction(style: .normal, title: "Edit") { (_, _, completion) in - var textField = UITextField() - let alert = UIAlertController(title: "Edit deck's name", message: "", preferredStyle: .alert) - - let editAction = UIAlertAction(title: "Change", style: .default) { (_) in - let deckName = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) - if !deckName!.isEmpty { - self.viewModel.saveDeck(at: indexPath.row, text: textField.text) - } - self.tableView.reloadData() - } - alert.addAction(editAction) - - let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) - alert.addAction(cancelAction) - - alert.addTextField { [weak self] (deckTextField) in - NotificationCenter.default.addObserver( - forName: UITextField.textDidChangeNotification, - object: deckTextField, - queue: OperationQueue.main) { _ in - let deckName = deckTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) - editAction.isEnabled = !deckName!.isEmpty - } - deckTextField.autocapitalizationType = .sentences - deckTextField.placeholder = "Type new name" - textField = deckTextField - textField.text = self?.viewModel.decks[indexPath.row].name - textField.clearButtonMode = .whileEditing - } - - DispatchQueue.main.async { - self.present(alert, animated: true, completion: nil) - } - - completion(true) - } - - editContextualAction.backgroundColor = .systemBlue - editContextualAction.image = UIImage(systemName: "pencil") - return editContextualAction - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: K.deckCellIdentifier, for: indexPath) - var content = cell.defaultContentConfiguration() - let deck = viewModel.decks[indexPath.row] - content.text = deck.name - let cardsCount = deck.cards.count - switch cardsCount { - case 0: - content.secondaryText = "No cards yet" - case 1: - content.secondaryText = "\(cardsCount) card" - default: - content.secondaryText = "\(cardsCount) cards" - } - cell.contentConfiguration = content - cell.accessoryType = .disclosureIndicator - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cardsVC = CardsTableViewController() - if let indexPath = tableView.indexPathForSelectedRow { - cardsVC.selectedDeck = viewModel.decks[indexPath.row] - } - let navVC = UINavigationController(rootViewController: cardsVC) - navVC.modalPresentationStyle = .fullScreen - tableView.deselectRow(at: indexPath, animated: true) - present(navVC, animated: true) - } -} - -extension DecksTableViewController: RefreshDataDelegate { - func reload() { - tableView.reloadData() - } -} - -extension DecksTableViewController: EmptyState { - func setEmptyState() { - let imageView = UIImageView(image: UIImage(systemName: "tray")) - imageView.center = CGPoint(x: view.frame.width / 2, - y: view.frame.height / 2) - imageView.bounds.size = CGSize(width: imageView.bounds.size.width * 5, - height: imageView.bounds.size.height * 5) - imageView.tintColor = .systemGray3 - - let messageLabel = UILabel(frame: CGRect(x: 0, y: 0, - width: view.frame.width, - height: view.frame.height)) - messageLabel.center = CGPoint(x: view.frame.width / 2, - y: view.frame.height / 2 + 65) - messageLabel.text = "You don't have decks yet." - messageLabel.numberOfLines = 0 - messageLabel.textAlignment = .center - messageLabel.font = .systemFont(ofSize: 20) - messageLabel.textColor = .systemGray - - let button = UIButton() - button.configureDefaultButton(title: "Create a deck") - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(didTapCreateDeck), for: .touchUpInside) - - let emptyStateview = UIView() - emptyStateview.addSubview(imageView) - emptyStateview.addSubview(messageLabel) - emptyStateview.addSubview(button) - - button.widthAnchor.constraint(equalToConstant: 300).isActive = true - button.heightAnchor.constraint(equalToConstant: 50).isActive = true - button.centerXAnchor.constraint(equalTo: emptyStateview.centerXAnchor).isActive = true - button.safeTopAnchor.constraint(equalTo: emptyStateview.safeBottomAnchor, constant: -100).isActive = true - - tableView.backgroundView = emptyStateview - tableView.isScrollEnabled = false - } - - func restore() { - tableView.backgroundView = nil - tableView.isScrollEnabled = true - } -} diff --git a/Simple Anki/Controllers/Decks/DecksViewModel.swift b/Simple Anki/Controllers/Decks/DecksViewModel.swift deleted file mode 100644 index f9a2fa6..0000000 --- a/Simple Anki/Controllers/Decks/DecksViewModel.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// DecksViewModel.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.02.2022. -// - -import Foundation -import RealmSwift - -enum SortType: String { - case name - case dateCreated -} - -protocol RefreshDataDelegate: AnyObject { - func reload() -} - -class DecksViewModel { - - var decks: Results! - - weak var delegate: RefreshDataDelegate? - - func loadDecks(by key: SortType) { - decks = StorageManager.realm.objects(Deck.self).sorted(byKeyPath: key.rawValue, ascending: true) - delegate?.reload() - } - - func saveDeck(at index: Int, text: String?) { - guard let text = text else { return } - - do { - try StorageManager.realm.write { - self.decks[index].name = text - } - } catch { - print(error) - } - } - - func setSort(by type: SortType) { - UserDefaults.standard.set(type.rawValue, forKey: "sort") - } - - func getSortType() -> SortType? { - guard let sortType = UserDefaults.standard.string(forKey: "sort") else { return nil } - return SortType(rawValue: sortType) - } -} diff --git a/Simple Anki/Controllers/Decks/Import/ImportViewController.swift b/Simple Anki/Controllers/Decks/Import/ImportViewController.swift deleted file mode 100644 index e5ff62a..0000000 --- a/Simple Anki/Controllers/Decks/Import/ImportViewController.swift +++ /dev/null @@ -1,323 +0,0 @@ -// -// ImportDeckViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 07.05.2022. -// - -import UIKit -import UniformTypeIdentifiers -import SwiftCSV - -enum ImportFileType: String { - case csv - case apkg -} - -class ImportViewController: UIViewController { - - let activityView = UIActivityIndicatorView(style: .medium) - let segmentedControl: UISegmentedControl = { - let segmentControl = UISegmentedControl(items: ["APKG", "CSV"]) - segmentControl.selectedSegmentIndex = 0 - for index in 0.. Void)? - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - navigationItem.titleView = segmentedControl - closeButton.addTarget(self, action: #selector(didTapCloseButton), for: .touchUpInside) - chooseFileButton.addTarget(self, action: #selector(didTapChooseFileButton), for: .touchUpInside) - segmentedControl.addTarget(self, action: #selector(didChangeSegmentedControl), for: .valueChanged) - - tabButton.addTarget(self, action: #selector(didTapTabButton), for: .touchUpInside) - commaButton.addTarget(self, action: #selector(didTapCommaButton), for: .touchUpInside) - semiColonButton.addTarget(self, action: #selector(didTapSemiColonButton), for: .touchUpInside) - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: closeButton) - - delimeterStackView.addArrangedSubview(semiColonButton) - delimeterStackView.addArrangedSubview(commaButton) - delimeterStackView.addArrangedSubview(tabButton) - semiColonButton.isSelected = true - semiColonButton.configuration?.baseForegroundColor = .white - - view.addSubview(delimeterStackView) - view.addSubview(chooseFileButton) - view.addSubview(apkgInfoLabel) - view.addSubview(csvInfoLabel) - view.addSubview(activityView) - view.addSubview(importingDeckLabel) - view.addSubview(dontCloseAppLabel) - csvInfoLabel.isHidden = true - delimeterStackView.isHidden = true - importingDeckLabel.isHidden = true - dontCloseAppLabel.isHidden = true - } - - @objc func didTapCloseButton() { - dismiss(animated: true) - } - - @objc func didTapSemiColonButton() { - selectedCSVDelimeter = .semicolon - semiColonButton.isSelected = true - commaButton.isSelected = false - tabButton.isSelected = false - tabButton.configuration?.baseForegroundColor = .systemBlue - commaButton.configuration?.baseForegroundColor = .systemBlue - semiColonButton.configuration?.baseForegroundColor = .white - } - - @objc func didTapCommaButton() { - selectedCSVDelimeter = .comma - semiColonButton.isSelected = false - commaButton.isSelected = true - tabButton.isSelected = false - tabButton.configuration?.baseForegroundColor = .systemBlue - commaButton.configuration?.baseForegroundColor = .white - semiColonButton.configuration?.baseForegroundColor = .systemBlue - } - - @objc func didTapTabButton() { - selectedCSVDelimeter = .tab - semiColonButton.isSelected = false - commaButton.isSelected = false - tabButton.isSelected = true - tabButton.configuration?.baseForegroundColor = .white - commaButton.configuration?.baseForegroundColor = .systemBlue - semiColonButton.configuration?.baseForegroundColor = .systemBlue - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - delimeterStackView.frame.size = CGSize(width: 300, height: 50) - delimeterStackView.center = view.center - delimeterStackView.frame.origin.y += 25 - - chooseFileButton.frame.size = CGSize(width: 300, height: 50) - chooseFileButton.center = view.center - chooseFileButton.frame.origin.y += 100 - - csvInfoLabel.frame.size = CGSize(width: 300, height: 300) - csvInfoLabel.center = view.center - csvInfoLabel.frame.origin.y -= 80 - - apkgInfoLabel.frame.size = CGSize(width: 300, height: 300) - apkgInfoLabel.center = view.center - apkgInfoLabel.frame.origin.y -= 50 - - dontCloseAppLabel.translatesAutoresizingMaskIntoConstraints = false - importingDeckLabel.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - importingDeckLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 190), - importingDeckLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40), - importingDeckLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), - - dontCloseAppLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 220), - dontCloseAppLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40), - dontCloseAppLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40) - ]) - - activityView.center = view.center - activityView.frame.origin.y -= 50 - - } - - @objc private func didTapChooseFileButton() { - self.presentDocumentPicker() - } - - @objc private func didChangeSegmentedControl() { - if segmentedControl.selectedSegmentIndex == 0 { - selectedFile = .apkg - delimeterStackView.isHidden = true - csvInfoLabel.isHidden = true - apkgInfoLabel.isHidden = false - } else { - selectedFile = .csv - delimeterStackView.isHidden = false - csvInfoLabel.isHidden = false - apkgInfoLabel.isHidden = true - } - } - - func presentImportedCards(deckName: String, _ cards: [Cards]) { - let importedCardsCV = ImportedCardsCollectionViewController() - importedCardsCV.importedCards = cards - importedCardsCV.deckName = deckName - importedCardsCV.reloadData = reloadData - let navVC = UINavigationController(rootViewController: importedCardsCV) - navVC.modalPresentationStyle = .popover - navVC.isModalInPresentation = true - present(navVC, animated: true) - } - - private func presentDocumentPicker() { - guard let fileType = UTType(filenameExtension: selectedFile.rawValue) else { return } - let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [fileType], asCopy: true) - documentPicker.delegate = self - documentPicker.allowsMultipleSelection = false - present(documentPicker, animated: true) - } - - func showActivityIndicator() { - activityView.startAnimating() - chooseFileButton.isHidden = true - apkgInfoLabel.isHidden = true - csvInfoLabel.isHidden = true - delimeterStackView.isHidden = true - importingDeckLabel.isHidden = false - dontCloseAppLabel.isHidden = false - } - - func hideActivityIndicator() { - if activityView.isAnimating { - activityView.stopAnimating() - } - } -} - -extension ImportViewController: UIDocumentPickerDelegate { - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let url = urls.first else { return } - showActivityIndicator() - switch selectedFile { - case .apkg: - var apkgCards: [Cards] = [] - let apkgManager = APKGManager(apkgURL: url) - DispatchQueue.global(qos: .userInitiated).async { - do { - apkgCards = try apkgManager.prepareAPKGCards() - } catch { - self.showAlert(with: "Error", message: "Could not import deck") - print(error) - } - DispatchQueue.main.async { - self.hideActivityIndicator() - self.presentImportedCards(deckName: url.lastPathComponent, apkgCards) - } - } - case .csv: - var csvCards: [Cards] = [] - DispatchQueue.global(qos: .userInitiated).async { - do { - let csv = try CSV(url: url, delimiter: self.selectedCSVDelimeter) - for row in csv.rows { - guard !row.isEmpty, row.count >= 2 else { continue } - let front = self.cleanString(row[0]) - let back = self.cleanString(row[1]) - csvCards.append(CSVCard(front: front, back: back)) - } - } catch { - self.showAlert(with: "Error", message: "Could not import deck") - print(error) - } - DispatchQueue.main.async { - self.hideActivityIndicator() - self.presentImportedCards(deckName: url.lastPathComponent, csvCards) - } - } - } - } - - private func showAlert(with title: String, message: String) { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let action = UIAlertAction(title: "OK", style: .cancel) { [weak self] _ in - self?.dismiss(animated: true, completion: nil) - } - alertController.addAction(action) - present(alertController, animated: true, completion: nil) - } - - private func cleanString(_ data: String) -> String { - return data.replaceOccurrences(of: "<[^>]+>", with: "").replaceOccurrences(of: "[\\[].*?[\\]]", with: "") - } -} - -extension String { - func replaceOccurrences(of this: String, with that: String) -> String { - return self.replacingOccurrences(of: this, with: that, options: .regularExpression, range: nil) - } -} diff --git a/Simple Anki/Controllers/Decks/Import/ImportedCardCollectionViewCell.swift b/Simple Anki/Controllers/Decks/Import/ImportedCardCollectionViewCell.swift deleted file mode 100644 index 92eb660..0000000 --- a/Simple Anki/Controllers/Decks/Import/ImportedCardCollectionViewCell.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// ImportedCardTableViewCell.swift -// Simple Anki -// -// Created by Астемир Бозиев on 09.05.2022. -// - -import UIKit - -class ImportedCardCollectionViewCell: UICollectionViewCell { - static let identifier = "ImportedCardTableViewCell" - - private let cardView: UIView = { - let view = UIView() - view.backgroundColor = .systemBackground - view.layer.cornerRadius = 10 - return view - }() - - let frontField: UITextField = { - let field = UITextField() - field.placeholder = "Front word" - field.font = .systemFont(ofSize: 26, weight: .bold) - field.returnKeyType = .next - return field - }() - - let backField: UITextField = { - let field = UITextField() - field.placeholder = "Back word" - field.font = .systemFont(ofSize: 26, weight: .bold) - field.returnKeyType = .done - return field - }() - - private let frontLabel: UILabel = { - let label = UILabel() - label.text = "Front" - label.textColor = .systemGray - return label - }() - - private let backLabel: UILabel = { - let label = UILabel() - label.text = "Back" - label.textColor = .systemGray - return label - }() - - private let separationLine: UIView = { - let view = UIView() - view.backgroundColor = .systemGray4 - return view - }() - - override init(frame: CGRect) { - super.init(frame: frame) - cardView.addSubview(separationLine) - cardView.addSubview(frontLabel) - cardView.addSubview(backLabel) - cardView.addSubview(frontField) - cardView.addSubview(backField) - contentView.addSubview(cardView) - frontField.isEnabled = false - backField.isEnabled = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - let leftPadding = 16.0 - let rightPadding = leftPadding * 2.0 - let labelHeight = 15.0 - let labelWidth = 100.0 - - cardView.frame = CGRect( - x: leftPadding, - y: 0, - width: contentView.bounds.width - rightPadding, - height: 200.0 - ) - separationLine.frame = CGRect( - x: leftPadding, - y: 200 / 2.0, - width: cardView.frame.width - rightPadding, - height: 1.0 - ) - frontLabel.frame = CGRect( - x: leftPadding, - y: 15.0, - width: labelWidth, - height: labelHeight - ) - backLabel.frame = CGRect( - x: leftPadding, - y: separationLine.frame.origin.y + 15.0, - width: labelWidth, - height: labelHeight - ) - frontField.frame = CGRect( - x: leftPadding, - y: 50.0, - width: cardView.frame.width - rightPadding, - height: 40.0 - ) - backField.frame = CGRect( - x: leftPadding, - y: separationLine.frame.origin.y + 50.0, - width: cardView.frame.width - rightPadding, - height: 40.0 - ) - } - - override func prepareForReuse() { - super.prepareForReuse() - frontField.text = nil - backField.text = nil - } - - public func configure(with model: Cards?) { - frontField.text = model?.front - backField.text = model?.back - } -} diff --git a/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift b/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift deleted file mode 100644 index 94c0d47..0000000 --- a/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// ImoptedDeckViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 09.05.2022. -// - -import UIKit - -class ImportedCardsCollectionViewController: UIViewController { - - var collectionView: UICollectionView! - var importedCards: [Cards]? - var deckName: String? - var reloadData: (() -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - title = "Preview imported cards" - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(didSaveTapped)) - configureCollectionView() - setupUI() - } - - private func setupUI() { - view.addSubview(collectionView) - collectionView.frame = view.bounds - collectionView.delegate = self - collectionView.dataSource = self - - } - - @objc private func didSaveTapped() { - guard let deckName = deckName, let cards = importedCards else { - return - } - let newDeck = Deck() - newDeck.name = deckName - for card in cards { - let newCard = Card() - newCard.front = card.front - newCard.back = card.back - newDeck.cards.append(newCard) - } - StorageManager.save(newDeck) - reloadData?() - self.view.window?.rootViewController?.dismiss(animated: true) - } - - private func configureCollectionView() { - let layout = UICollectionViewFlowLayout() - layout.sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 70, right: 16) - layout.itemSize = CGSize(width: view.frame.width, height: 200) - collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.allowsSelection = false - collectionView.backgroundColor = .secondarySystemBackground - collectionView.register(ImportedCardCollectionViewCell.self, forCellWithReuseIdentifier: ImportedCardCollectionViewCell.identifier) - } -} - -extension ImportedCardsCollectionViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return importedCards?.count ?? 0 - } -} - -extension ImportedCardsCollectionViewController: UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImportedCardCollectionViewCell.identifier, for: indexPath) - as? ImportedCardCollectionViewCell else { return UICollectionViewCell() } - cell.configure(with: importedCards?[indexPath.row]) - return cell - } -} diff --git a/Simple Anki/Controllers/Decks/NewDeckViewController.swift b/Simple Anki/Controllers/Decks/NewDeckViewController.swift deleted file mode 100644 index 6bfee8d..0000000 --- a/Simple Anki/Controllers/Decks/NewDeckViewController.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// NewDeckViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.04.2022. -// - -import UIKit -import SPIndicator - -class NewDeckViewController: UIViewController { - - var reloadData: (() -> Void)? - let indicatorView = SPIndicatorView(title: "Deck saved", preset: .done) - private let deckView: UIView = { - let view = UIView() - view.backgroundColor = .systemBackground - view.layer.cornerRadius = 10 - return view - }() - - private lazy var addCardsButton: UIButton = { - let button = UIButton() - let image = UIImage(systemName: "arrow.forward") - button.configureDefaultButton(title: "Add cards", image: image) - return button - }() - - private let textField: UITextField = { - let field = UITextField() - field.placeholder = "Name" - field.font = .systemFont(ofSize: 26, weight: .bold) - field.returnKeyType = .next - return field - }() - - override func viewDidLoad() { - super.viewDidLoad() - title = "New deck" - navigationItem.leftBarButtonItem = UIBarButtonItem( - title: "Cancel", - style: .plain, - target: self, - action: #selector(didCancelTapped) - ) - navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "Save", - style: .done, - target: self, - action: #selector(didSaveTapped) - ) - textField.addTarget(self, action: #selector(textFieldChanged), for: .editingChanged) - addCardsButton.addTarget(self, action: #selector(addCardsButtonTapped), for: .touchUpInside) - setupUI() - } - - private func setupUI() { - navigationItem.rightBarButtonItem?.isEnabled = false - addCardsButton.isEnabled = false - view.backgroundColor = .secondarySystemBackground - view.addSubview(deckView) - view.addSubview(addCardsButton) - deckView.addSubview(textField) - textField.becomeFirstResponder() - - let leftPadding = 16.0 - let rightPadding = 32.0 - - deckView.frame = CGRect(x: leftPadding, - y: 200.0, - width: view.bounds.width - rightPadding, - height: 80.0) - textField.frame = CGRect(x: leftPadding, - y: (deckView.frame.height - 40.0) / 2.0, - width: deckView.frame.width - rightPadding, - height: 40.0) - - addCardsButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - addCardsButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), - addCardsButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), - addCardsButton.heightAnchor.constraint(equalToConstant: 50), - addCardsButton.safeBottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -10) - ]) - } - - @objc func didCancelTapped() { - dismiss(animated: true) - } - - @objc func didSaveTapped() { - let newDeck = Deck() - guard let deckName = textField.text else { return } - newDeck.name = deckName.trimmingCharacters(in: .whitespacesAndNewlines) - StorageManager.save(newDeck) - reloadData?() - indicatorView.present(duration: 0.5, haptic: .success) - dismiss(animated: true) - } - - @objc func addCardsButtonTapped() { - let newCardVC = NewCardViewController() - let newDeck = Deck() - guard let deckName = textField.text else { return } - newDeck.name = deckName.trimmingCharacters(in: .whitespacesAndNewlines) - StorageManager.save(newDeck) - newCardVC.selectedDeck = newDeck - addCardsButton.isHidden = true - textField.resignFirstResponder() - navigationController?.pushViewController(newCardVC, animated: true) - } - - @objc func textFieldChanged() { - if textField.text?.isEmpty == false { - navigationItem.rightBarButtonItem?.isEnabled = true - addCardsButton.isEnabled = true - } else { - navigationItem.rightBarButtonItem?.isEnabled = false - addCardsButton.isEnabled = false - } - } - -} diff --git a/Simple Anki/Controllers/LaunchScreenViewController.swift b/Simple Anki/Controllers/LaunchScreenViewController.swift deleted file mode 100644 index fd5d4ab..0000000 --- a/Simple Anki/Controllers/LaunchScreenViewController.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// LaunchScreenViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 25.06.2022. -// - -import UIKit - -protocol DoneDelegate: AnyObject { - func vewController(isDismissed: Bool) -} - -class LaunchScreenViewController: UIViewController { - - weak var delegate: DoneDelegate? - - lazy var stackView: UIStackView = { - let stackView = UIStackView() - stackView.spacing = 10 - stackView.axis = .horizontal - stackView.alignment = .fill - stackView.distribution = .fill - return stackView - }() - - lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImage(named: "Image.png") - imageView.contentMode = .scaleAspectFit - return imageView - }() - - lazy var simpleLabel: UILabel = { - let label = UILabel() - label.text = "Simple" - label.font = UIFont.systemFont(ofSize: 50, weight: .bold) - label.minimumScaleFactor = 0.5 - return label - }() - - lazy var ankiLabel: UILabel = { - let label = UILabel() - label.text = "Anki" - label.font = UIFont.systemFont(ofSize: 50, weight: .bold) - label.textColor = .systemBlue - label.minimumScaleFactor = 0.5 - return label - }() - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - stackView.isHidden = true - } - - override func viewDidAppear(_ animated: Bool) { - self.dismiss(animated: false, completion: { - self.delegate?.vewController(isDismissed: true) - }) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - stackView.addArrangedSubview(simpleLabel) - stackView.addArrangedSubview(ankiLabel) - view.addSubview(imageView) - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - } -} diff --git a/Simple Anki/Controllers/MainTabBarViewController.swift b/Simple Anki/Controllers/MainTabBarViewController.swift deleted file mode 100644 index 559fd54..0000000 --- a/Simple Anki/Controllers/MainTabBarViewController.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// ViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit - -class MainTabBarViewController: UITabBarController { - - override func viewDidLoad() { - super.viewDidLoad() - configureTabBar() -// showOnboardingScreen() - } - - private func configureTabBar() { - let decksVC = UINavigationController(rootViewController: DecksTableViewController()) - let settingsVC = UINavigationController(rootViewController: SettingsViewController()) - - let decksItem = UITabBarItem( - title: "Decks", - image: UIImage(systemName: "tray.full"), - selectedImage: nil - ) - let settingsItem = UITabBarItem( - title: "Settings", - image: UIImage(systemName: "gear"), - selectedImage: nil - ) - - decksVC.tabBarItem = decksItem - settingsVC.tabBarItem = settingsItem - self.setViewControllers([decksVC, settingsVC], animated: false) - } - - private func showLoadingScreen() { - let loadingScreen = LaunchScreenViewController() - loadingScreen.delegate = self - loadingScreen.modalPresentationStyle = .fullScreen - present(loadingScreen, animated: false) - } -} - -extension MainTabBarViewController: DoneDelegate { - func vewController(isDismissed: Bool) { - if isDismissed && OnboardingManager.shared.isNewUser() { - showOnboardingScreen() - } - } - - private func showOnboardingScreen() { - let onboardingVC = OnboardingViewController() - let navVC = UINavigationController(rootViewController: onboardingVC) - onboardingVC.modalPresentationStyle = .popover - onboardingVC.isModalInPresentation = true - present(navVC, animated: true) - } -} diff --git a/Simple Anki/Controllers/NewCardViewController.swift b/Simple Anki/Controllers/NewCardViewController.swift deleted file mode 100644 index 997e6cd..0000000 --- a/Simple Anki/Controllers/NewCardViewController.swift +++ /dev/null @@ -1,463 +0,0 @@ -// -// NewCardViewControllerBeta.swift -// Simple Anki -// -// Created by Астемир Бозиев on 03.04.2022. -// - -import UIKit -import RealmSwift -import AVFoundation -import SPIndicator - -class NewCardViewController: UIViewController { - - var selectedCard: Card? - var selectedDeck: Deck? - - var audioRecorder: AVAudioRecorder! - var player: AVAudioPlayer! - var recordingSession = AVAudioSession.sharedInstance() - let indicatorView = SPIndicatorView(title: "Card added", preset: .done) - var recordFilePath: URL? - - var reloadData: (() -> Void)? - var isRecording: Bool = false - - private lazy var cardView: UIView = { - let view = UIView() - view.backgroundColor = .systemBackground - view.layer.cornerRadius = 10 - return view - }() - - private lazy var frontField: UITextField = { - let field = UITextField() - field.placeholder = "Front word" - field.font = .systemFont(ofSize: 26, weight: .bold) - field.returnKeyType = .next - return field - }() - - private lazy var backField: UITextField = { - let field = UITextField() - field.placeholder = "Back word" - field.font = .systemFont(ofSize: 26, weight: .bold) - field.returnKeyType = .done - return field - }() - - private lazy var frontLabel: UILabel = { - let label = UILabel() - label.text = "Front" - label.textColor = .systemGray - return label - }() - - private lazy var backLabel: UILabel = { - let label = UILabel() - label.text = "Back" - label.textColor = .systemGray - return label - }() - - private lazy var separationLine: UIView = { - let view = UIView() - view.backgroundColor = .systemGray4 - return view - }() - - private lazy var addAndNext: UIButton = { - let button = UIButton() - button.configureDefaultButton(title: "Add") - return button - }() - - private lazy var recordButton: UIButton = { - let button = UIButton() - let image = UIImage(systemName: "mic") - button.configureIconButton(configuration: .tinted(), image: image) - return button - }() - - private lazy var playButton: UIButton = { - let button = UIButton() - let image = UIImage(systemName: "speaker.wave.3") - button.configureIconButton(configuration: .tinted(), image: image) - return button - }() - - override func viewDidLoad() { - super.viewDidLoad() - title = "New card" - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(didDoneTapped)) - navigationItem.setHidesBackButton(true, animated: false) - - frontField.delegate = self - backField.delegate = self - - recordButton.addTarget(self, action: #selector(recordButtonTapped), for: .touchUpInside) - addAndNext.addTarget(self, action: #selector(didSaveAndNextTapped), for: .touchUpInside) - playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) - frontField.addTarget(self, action: #selector(textFieldChanged), for: .editingChanged) - - setupMenuForPlayButton() - - let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) - view.addGestureRecognizer(tap) - } - - override func viewWillAppear(_ animated: Bool) { - setupUI() - } - - private func setupEditScreen() { - if let card = selectedCard { - navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "Update", - style: .done, - target: self, - action: #selector(didSaveTapped) - ) - recordButton.isEnabled = true - title = selectedCard?.front - - frontField.text = card.front - backField.text = card.back - if let name = card.audioName { - recordFilePath = Utils.getAudioFilePath(with: name) - playButton.isHidden = false - recordButton.isHidden = true - } else { - playButton.isHidden = true - recordButton.isHidden = false - } - addAndNext.isHidden = true - } else { - frontField.becomeFirstResponder() - } - } - - private func setupUI() { - navigationItem.largeTitleDisplayMode = .never - view.backgroundColor = .secondarySystemBackground - - if frontField.text!.isEmpty { - addAndNext.isEnabled = false - recordButton.isEnabled = false - } else { - addAndNext.isEnabled = true - recordButton.isEnabled = true - } - playButton.isHidden = true - - view.addSubview(addAndNext) - view.addSubview(recordButton) - view.addSubview(playButton) - - recordButton.translatesAutoresizingMaskIntoConstraints = false - recordButton.safeTrailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: -16).isActive = true - recordButton.widthAnchor.constraint(equalToConstant: 50).isActive = true - recordButton.heightAnchor.constraint(equalToConstant: 50).isActive = true - - playButton.translatesAutoresizingMaskIntoConstraints = false - playButton.safeTrailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: -16).isActive = true - playButton.widthAnchor.constraint(equalToConstant: 50).isActive = true - playButton.heightAnchor.constraint(equalToConstant: 50).isActive = true - - addAndNext.translatesAutoresizingMaskIntoConstraints = false - addAndNext.safeLeadingAnchor.constraint(equalTo: view.safeLeadingAnchor, constant: 16).isActive = true - addAndNext.trailingAnchor.constraint(equalTo: recordButton.leadingAnchor, constant: -16).isActive = true - addAndNext.heightAnchor.constraint(equalToConstant: 50).isActive = true - - recordButton.safeBottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -10).isActive = true - playButton.safeBottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -10).isActive = true - addAndNext.safeBottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -10).isActive = true - configureCardView() - setupEditScreen() - } - - private func configureCardView() { - view.addSubview(cardView) - cardView.addSubview(separationLine) - cardView.addSubview(frontLabel) - cardView.addSubview(backLabel) - cardView.addSubview(frontField) - cardView.addSubview(backField) - - let leftPadding = 16.0 - let rightPadding = leftPadding * 2.0 - let labelHeight = 15.0 - let labelWidth = 100.0 - - cardView.frame = CGRect( - x: leftPadding, - y: 100.0, - width: view.bounds.width - rightPadding, - height: 200.0 - ) - separationLine.frame = CGRect( - x: leftPadding, - y: 200 / 2.0, - width: cardView.frame.width - rightPadding, - height: 1.0 - ) - frontLabel.frame = CGRect( - x: leftPadding, - y: 15.0, - width: labelWidth, - height: labelHeight - ) - backLabel.frame = CGRect( - x: leftPadding, - y: separationLine.frame.origin.y + 15.0, - width: labelWidth, - height: labelHeight - ) - frontField.frame = CGRect( - x: leftPadding, - y: 50.0, - width: cardView.frame.width - rightPadding, - height: 40.0 - ) - backField.frame = CGRect( - x: leftPadding, - y: separationLine.frame.origin.y + 50.0, - width: cardView.frame.width - rightPadding, - height: 40.0 - ) - } - - private func setupMenuForPlayButton() { - let delete = UIAction(title: "Delete", - image: UIImage(systemName: "trash")) { _ in - if let audioFilePath = self.recordFilePath { - Utils.deleteAudioFile(at: audioFilePath) - } - self.playButton.isHidden = true - self.recordButton.isHidden = false - self.recordButton.configuration?.baseForegroundColor = .systemBlue - } - playButton.menu = UIMenu(title: "", options: .destructive, children: [delete]) - } - - private func showSettingsAlert() { - let alert = UIAlertController( - title: "Microphone access required", - message: "Allow Simple Anki to use a microphone in the app settings, to be able to record the word pronounciation", - preferredStyle: .alert) - - let settingsAction = UIAlertAction(title: "Settings", style: .default) { _ in - guard let appSettingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } - - if UIApplication.shared.canOpenURL(appSettingsUrl) { - UIApplication.shared.open(appSettingsUrl) { (success) in - print("Settings opened: \(success)") - } - } - } - - let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil) - alert.addAction(cancelAction) - alert.addAction(settingsAction) - present(alert, animated: true, completion: nil) - } - - private func loadRecordingUI() { - UIView.animate(withDuration: 0.2) { - self.addAndNext.setTitle("Recording...", for: .normal) - self.recordButton.configuration?.image = UIImage(systemName: "square.fill") - self.recordButton.configuration?.baseForegroundColor = .systemRed - self.recordButton.configuration?.cornerStyle = .capsule - } - addAndNext.isEnabled = false - } - - private func loadPlaybackUI() { - guard let isEmpty = frontField.text?.isEmpty else { return } - if !isEmpty { - addAndNext.isEnabled = true - } - recordButton.configuration?.image = UIImage(systemName: "mic") - self.recordButton.configuration?.cornerStyle = .large - recordButton.isHidden = true - playButton.isHidden = false - addAndNext.setTitle("Add", for: .normal) - } - - // MARK: - Button handlers - - @objc func recordButtonTapped() { - switch self.recordingSession.recordPermission { - case .granted: - HapticManager.shared.vibrate(for: .success) - if !self.isRecording { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.09) { - self.isRecording = true - self.loadRecordingUI() - self.startRecording() - } - } else { - self.finishRecording() - self.loadPlaybackUI() - self.isRecording = false - } - case .denied: - HapticManager.shared.vibrate(for: .error) - self.showSettingsAlert() - case .undetermined: - self.recordingSession.requestRecordPermission { _ in } - default: - break - } - } - - @objc func playButtonTapped() { - guard let url = recordFilePath else { return } - play(with: url) - playButton.isEnabled = false - } - - @objc private func dismissKeyboard() { - view.endEditing(true) - } - - @objc private func didDoneTapped() { - if let audioFilePath = recordFilePath { - Utils.deleteAudioFile(at: audioFilePath) - } - dismiss(animated: true) - } - - @objc private func didSaveTapped() { - saveCard() - recordFilePath = nil - reloadData?() - dismiss(animated: true) - indicatorView.present(duration: 0.5, haptic: .success) - } - - @objc private func didSaveAndNextTapped() { - updateUIafterAddCard() - } - - private func updateUIafterAddCard() { - saveCard() - frontField.text?.removeAll() - backField.text?.removeAll() - frontField.becomeFirstResponder() - indicatorView.present(duration: 0.5, haptic: .success) - addAndNext.isEnabled = false - recordButton.isEnabled = false - recordButton.isHidden = false - recordButton.configuration?.baseForegroundColor = .systemBlue - playButton.isHidden = true - recordFilePath = nil - } - - func saveCard() { - let newCard = Card() - if let fronText = frontField.text { - newCard.front = fronText.trimmingCharacters(in: .whitespaces) - } - if let backText = backField.text { - newCard.back = backText.trimmingCharacters(in: .whitespaces) - } - if let path = recordFilePath { - if path.exists() { - newCard.audioName = recordFilePath?.lastPathComponent - } - } - if let card = selectedCard { - StorageManager.update(card, with: newCard) - } else { - StorageManager.save(newCard, to: selectedDeck) - } - } -} - -// MARK: - TextFieldDelegate Extension - -extension NewCardViewController: UITextFieldDelegate { - - @objc func textFieldChanged() { - if frontField.text?.isEmpty == false { - addAndNext.isEnabled = true - recordButton.isEnabled = true - } else { - addAndNext.isEnabled = false - recordButton.isEnabled = false - } - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - switch textField.returnKeyType { - case .next: - backField.becomeFirstResponder() - case .done: - backField.resignFirstResponder() - default: - break - } - return true - } -} - -extension NewCardViewController: AVAudioPlayerDelegate { - func play(with url: URL) { - do { - player = try AVAudioPlayer(contentsOf: url) - player.prepareToPlay() - player.delegate = self - player.play() - } catch { - print("error: \(error.localizedDescription)") - } - } - - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - playButton.isEnabled = true - } -} - -extension NewCardViewController: AVAudioRecorderDelegate { - - func startRecording() { - let recordSettings = [ - AVFormatIDKey: Int(kAudioFormatAppleLossless), - AVSampleRateKey: 44100, - AVEncoderBitRateKey: 320000, - AVNumberOfChannelsKey: 2, - AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue - ] - do { - try recordingSession.setCategory(.playAndRecord, mode: .spokenAudio, options: .defaultToSpeaker) - try recordingSession.setActive(true) - let url = Utils.generateNewRecordName() - audioRecorder = try AVAudioRecorder(url: url, settings: recordSettings) - audioRecorder.delegate = self - audioRecorder.record() - } catch { - print(error.localizedDescription) - } - } - - func finishRecording() { - audioRecorder.stop() - recordFilePath = audioRecorder.url - do { - try recordingSession.setCategory(.playback, mode: .default) - try recordingSession.setActive(false) - } catch { - print(error.localizedDescription) - } - } - - func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { - finishRecording() - } - - func audioRecorderBeginInterruption(_ recorder: AVAudioRecorder) { - finishRecording() - } -} diff --git a/Simple Anki/Controllers/OnboardingViewController.swift b/Simple Anki/Controllers/OnboardingViewController.swift deleted file mode 100644 index 2042bef..0000000 --- a/Simple Anki/Controllers/OnboardingViewController.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// OnboardingViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 18.06.2022. -// - -import UIKit - -class OnboardingViewController: UIViewController { - - let models: [Feature] = [ - Feature( - title: "Create collections", - desription: "Add decks and cards with minimum taps", - image: UIImage(systemName: "tray.full") - ), - Feature( - title: "Import collections", - desription: "Upload other anki decks in .apkg and .csv formats", - image: UIImage(systemName: "tray.and.arrow.down") - ), - Feature( - title: "Record pronunciation", - desription: "Add voice recording of the words you want to learn", - image: UIImage(systemName: "mic") - ), - Feature( - title: "Set up reminders", - desription: "Turn on notifications and study according to your schedule", - image: UIImage(systemName: "bell") - ) - ] - - let createFeature = FeatureView() - let importFeature = FeatureView() - let recordFeature = FeatureView() - let reminderFeature = FeatureView() - - lazy var welcomeLabel: UILabel = { - let label = UILabel() - label.frame = CGRect(x: 44, y: 0, width: 300, height: 300) - label.numberOfLines = 0 - label.textAlignment = .left - label.font = UIFont.systemFont(ofSize: 42, weight: .bold) - let attrString = NSMutableAttributedString(string: "Welcome to Simple Anki") - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor : UIColor.systemBlue - ] - attrString.addAttributes(attributes, range: NSRange(location: 11, length: 11)) - label.attributedText = attrString - return label - }() - - lazy var getStartedButton: UIButton = { - let button = UIButton() - button.configureDefaultButton(title: "Get started") - return button - }() - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - getStartedButton.addTarget(self, action: #selector(didTapContinue), for: .touchUpInside) - - } - - @objc private func didTapContinue() { - dismiss(animated: true) { - OnboardingManager.shared.setIsNotNewUser() - } - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - createFeature.configure(model: models[0]) - importFeature.configure(model: models[1]) - recordFeature.configure(model: models[2]) - reminderFeature.configure(model: models[3]) - - view.addSubview(createFeature) - view.addSubview(importFeature) - view.addSubview(recordFeature) - view.addSubview(reminderFeature) - view.addSubview(welcomeLabel) - view.addSubview(getStartedButton) - - createFeature.frame = CGRect( - x: 44, - y: 260, - width: view.bounds.width, - height: 30 - ) - importFeature.frame = CGRect( - x: 44, - y: createFeature.frame.origin.y + createFeature.frame.height + 50, - width: view.bounds.width, - height: 30) - recordFeature.frame = CGRect( - x: 44, - y: importFeature.frame.origin.y + importFeature.frame.height + 50, - width: view.bounds.width, - height: 30 - ) - reminderFeature.frame = CGRect( - x: 44, - y: recordFeature.frame.origin.y + recordFeature.frame.height + 50, - width: view.bounds.width, - height: 30 - ) - - getStartedButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - getStartedButton.widthAnchor.constraint(equalToConstant: 300), - getStartedButton.heightAnchor.constraint(equalToConstant: 50), - getStartedButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - getStartedButton.safeTopAnchor.constraint(equalTo: view.safeBottomAnchor, constant: -100) - ]) - } -} diff --git a/Simple Anki/Controllers/ReviewViewController.swift b/Simple Anki/Controllers/ReviewViewController.swift deleted file mode 100644 index a3d3d4d..0000000 --- a/Simple Anki/Controllers/ReviewViewController.swift +++ /dev/null @@ -1,221 +0,0 @@ -// -// ReviewViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 05.03.2021. -// - -import UIKit -import StoreKit -import RealmSwift -import AVFoundation - -class ReviewViewController: UIViewController { - - var deckLenght: Int? - var progress: Progress? - var reviewManager: ReviewManager? - var audioPlayer : AVAudioPlayer! - var audioFilePath: URL? - - let topWordLabel: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: CGFloat(36)) - label.numberOfLines = 0 - label.textAlignment = .center - label.textColor = .label - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.6 - return label - }() - - let bottomWordLabel: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: CGFloat(36)) - label.numberOfLines = 0 - label.textAlignment = .center - label.textColor = .label - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.6 - return label - }() - - let speakerButton: UIButton = { - let button = UIButton(frame: CGRect(x: 0, y: 0, width: 80, height: 80)) - let config = UIImage.SymbolConfiguration(pointSize: CGFloat(64), weight: .thin) - let image = UIImage(systemName: "speaker.wave.2.circle", withConfiguration: config) - button.setImage(image, for: .normal) - button.tintColor = .darkGray - return button - }() - - let progressBar: UIProgressView = { - let bar = UIProgressView(progressViewStyle: .bar) - bar.trackTintColor = .systemGray5 - return bar - }() - - let finishLabel: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: CGFloat(36)) - label.textAlignment = .center - label.textColor = .label - label.numberOfLines = 0 - label.text = "Finished!\nWant to repeat?" - return label - }() - - let repeatButton: UIButton = { - let button = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 40)) - let config = UIImage.SymbolConfiguration(pointSize: CGFloat(64), weight: .regular) - let image = UIImage(systemName: "repeat", withConfiguration: config) - button.setImage(image, for: .normal) - button.tintColor = .darkGray - return button - }() - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - speakerButton.addTarget(self, action: #selector(speakerButtonPressed), for: .touchUpInside) - repeatButton.addTarget(self, action: #selector(repeatButtonPressed), for: .touchUpInside) - let rightButton = UIBarButtonItem( - image: UIImage(systemName: "xmark"), - style: .plain, - target: self, - action: #selector(closeButtonTapped) - ) - navigationItem.rightBarButtonItem = rightButton - navigationItem.rightBarButtonItem?.tintColor = .lightGray - finishLabel.isHidden = true - repeatButton.isHidden = true - guard let numberOfCards = reviewManager?.numberOfCards else { return } - progress = Progress(totalUnitCount: numberOfCards) - topWordLabel.text = reviewManager?.currentCard?.front - bottomWordLabel.text = nil - setupSpeakerButton() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - let width = view.frame.size.width - 32 - let height = view.frame.size.height / 3 - 50 - topWordLabel.frame = CGRect(x: 16, y: 150, width: width, height: height) - bottomWordLabel.frame = CGRect(x: 16, y: view.frame.size.height / 2 - 40, width: width, height: height) - speakerButton.frame = CGRect(x: (view.frame.size.width - 70) / 2, y: height * 2 + 180, width: 70, height: 70) - progressBar.frame = CGRect(x: 16, y: view.frame.size.height - 32, width: view.frame.size.width - 32, height: 0) - finishLabel.frame = CGRect(x: 0, y: view.frame.size.height / 2 - 100, width: view.frame.size.width, - height: 100 - ) - repeatButton.frame = CGRect( - x: view.frame.size.width / 2 - 25, - y: view.frame.size.height / 2 + 50, - width: 50, - height: 40 - ) - view.addSubview(topWordLabel) - view.addSubview(bottomWordLabel) - view.addSubview(speakerButton) - view.addSubview(progressBar) - view.addSubview(finishLabel) - view.addSubview(repeatButton) - - let tap = UITapGestureRecognizer(target: self, action: #selector(screenTapped)) - view.isUserInteractionEnabled = true - view.addGestureRecognizer(tap) - } - - private func setupSpeakerButton() { - if getAudioFilePathForCurrentCard() != nil { - speakerButton.isHidden = false - } else { - speakerButton.isHidden = true - } - } - - @objc func screenTapped() { - if bottomWordLabel.text == nil { - bottomWordLabel.text = reviewManager?.currentCard?.back - updateProgressBar() - } else { - reviewManager?.pickCard() - if let card = reviewManager?.currentCard { - setupSpeakerButton() - topWordLabel.text = card.front - bottomWordLabel.text = nil - } else { - finishLabel.isHidden = false - repeatButton.isHidden = false - topWordLabel.isHidden = true - bottomWordLabel.isHidden = true - speakerButton.isHidden = true - RateManager.showRatesAlert(withoutCounter: false) - } - } - } - - @objc func closeButtonTapped() { - dismiss(animated: true) - } - - @objc func repeatButtonPressed(_ sender: Any) { - finishLabel.isHidden = true - repeatButton.isHidden = true - topWordLabel.isHidden = false - bottomWordLabel.isHidden = false - - progressBar.setProgress(0.0, animated: false) - progress?.completedUnitCount = 0 - progressBar.tintColor = UIColor(named: "systemBlue") - - reviewManager?.repeatReview() - reviewManager?.pickCard() - if let card = reviewManager?.currentCard { - setupSpeakerButton() - topWordLabel.text = card.front - bottomWordLabel.text = nil - } - } - - @objc private func speakerButtonPressed() { - if let audioName = reviewManager?.currentCard?.audioName { - let audioFilePath = Utils.getAudioFilePath(with: audioName) - if audioFilePath.exists() { - play(recordFilePath: audioFilePath) - } - } - } - - private func getAudioFilePathForCurrentCard() -> URL? { - if let audioName = reviewManager?.currentCard?.audioName { - let audioFilePath = Utils.getAudioFilePath(with: audioName) - if audioFilePath.exists() { - return audioFilePath - } - } - return nil - } - - private func updateProgressBar() { - progress?.completedUnitCount += 1 - guard let fractionCompleted = progress?.fractionCompleted else { return } - let progressFloat = Float(fractionCompleted) - progressBar.setProgress(progressFloat, animated: true) - - if progress?.completedUnitCount == progress?.totalUnitCount { - progressBar.tintColor = #colorLiteral(red: 0.2039215686, green: 0.7803921569, blue: 0.3490196078, alpha: 1) - } - } -} - -extension ReviewViewController: AVAudioPlayerDelegate { - func play(recordFilePath: URL) { - do { - audioPlayer = try AVAudioPlayer(contentsOf: recordFilePath) - audioPlayer.delegate = self - audioPlayer.play() - } catch { - print("error: \(error)") - } - } -} diff --git a/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift b/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift deleted file mode 100644 index d2af9ef..0000000 --- a/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// ReminderViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 24.04.2022. -// - -import UIKit - -class ReminderViewController: UIViewController { - - private let tableView: UITableView = { - let table = UITableView(frame: .zero, style: .insetGrouped) - table.register(DatePickerViewCell.self, forCellReuseIdentifier: DatePickerViewCell.identifier) - table.register(SwitchTableViewCell.self, forCellReuseIdentifier: SwitchTableViewCell.identifier) - table.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.identifier) - table.isScrollEnabled = false - return table - }() - - var models = [SectionOld]() - let remonderOn = UserDefaults.standard.bool(forKey: K.UserDefaultsKeys.reminder) - let reminderTime = UserDefaults.standard.string(forKey: K.UserDefaultsKeys.reminderTime) ?? "00:00" - let reminderIcon = ReminderManager.shared.isReminderOn() ? UIImage(systemName: "bell") : UIImage(systemName: "bell.slash") - - var selectedDays = [Weekday]() - - override func viewDidLoad() { - super.viewDidLoad() - title = "Reminder" - view.backgroundColor = .systemBackground - view.addSubview(tableView) - tableView.delegate = self - tableView.dataSource = self - tableView.frame = view.bounds - tableView.frame.origin.y -= 20 - navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "Done", - style: .done, - target: self, - action: #selector(doneButtonTapped) - ) - configure() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - selectedDays.removeAll() - selectedDays = ReminderManager.shared.collectSelectedWeekdays() - } - - private func configure() { - models.append(SectionOld(title: "", options: [ - .switchCell(model: SwitchOption( - title: K.Settings.reminderOn, - icon: reminderIcon, - isOn: remonderOn, - handler: nil - )), - .datePickerCell(model: DatePickerOption( - date: reminderTime - )), - .staticCell(model: Option( - title: "Repeat", - icon: UIImage(systemName: "repeat"), - handler: { - self.showWeekdaysViewController() - })) - ])) - } - - private func showWeekdaysViewController() { - let weekdaysVC = WeekdaysViewController() - navigationController?.pushViewController(weekdaysVC, animated: true) - } - - @objc private func doneButtonTapped() { - let notifications = ReminderManager.shared.getNotificationsCredentials(weekdays: selectedDays) - if ReminderManager.shared.isReminderOn() { - ReminderManager.shared.scheduleNotifications(notifications: notifications) - } else { - ReminderManager.shared.removeAllNotifications() - } - dismiss(animated: true) - } -} - -extension ReminderViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let type = models[indexPath.section].options[indexPath.row] - - switch type.self { - case .staticCell(let model): - model.handler?() - default: - break - } - } -} - -extension ReminderViewController: UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { - return models.count - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let model = models[section] - return model.title - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = models[indexPath.section].options[indexPath.row] - switch model.self { - case .datePickerCell: - return 200 - default: - break - } - return UITableViewCell().frame.height - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return models[section].options.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = models[indexPath.section].options[indexPath.row] - switch model.self { - case .staticCell(let model): - guard let cell = tableView.dequeueReusableCell( - withIdentifier: SettingsTableViewCell.identifier, - for: indexPath - ) as? SettingsTableViewCell else { return UITableViewCell() } - cell.configure(with: model) - return cell - - case .switchCell(let model): - guard let cell = tableView.dequeueReusableCell( - withIdentifier: SwitchTableViewCell.identifier, - for: indexPath - ) as? SwitchTableViewCell else { return UITableViewCell() } - cell.delegate = self - cell.selectionStyle = .none - cell.configure(with: model) - return cell - - case .datePickerCell(let model): - guard let cell = tableView.dequeueReusableCell( - withIdentifier: DatePickerViewCell.identifier, - for: indexPath - ) as? DatePickerViewCell else { return UITableViewCell() } - cell.delegate = self - cell.selectionStyle = .none - cell.configure(with: model) - return cell - } - } -} - -extension ReminderViewController: SwitchViewCellDelegate { - func switchAction(with cell: UITableViewCell) { - guard let switchCell = cell as? SwitchTableViewCell else { return } - if switchCell.mySwitch.isOn { - ReminderManager.shared.setReminderOn() - switchCell.iconImageView.image = UIImage(systemName: "bell") - } else { - ReminderManager.shared.setReminderOff() - switchCell.iconImageView.image = UIImage(systemName: "bell.slash") - } - } -} - -extension ReminderViewController: DatePickerViewCellDelegate { - func datePicker(with cell: UITableViewCell) { - guard let datePickerCell = cell as? DatePickerViewCell else { return } - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm" - let timeString = dateFormatter.string(from: datePickerCell.datePicker.date) - UserDefaults.standard.set(timeString, forKey: K.UserDefaultsKeys.reminderTime) - } -} diff --git a/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift b/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift deleted file mode 100644 index c8c73d5..0000000 --- a/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// WeekdaysViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 02.05.2022. -// - -import UIKit - -class WeekdaysViewController: UIViewController { - - private let tableView: UITableView = { - let table = UITableView(frame: .zero, style: .insetGrouped) - table.register(UITableViewCell.self, forCellReuseIdentifier: "cell") - table.isScrollEnabled = false - return table - }() - - override func viewDidLoad() { - super.viewDidLoad() - title = "Choose days" - view.addSubview(tableView) - view.backgroundColor = .systemBackground - tableView.frame = view.bounds - tableView.frame.origin.y -= 20 - tableView.delegate = self - tableView.dataSource = self - } -} - -extension WeekdaysViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return ReminderManager.shared.weekdays.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) - var content = cell.defaultContentConfiguration() - content.text = ReminderManager.shared.weekdays[indexPath.row].name - cell.contentConfiguration = content - if ReminderManager.shared.isDayInReminder(index: indexPath.row) { - cell.accessoryType = .checkmark - } - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let cell = tableView.cellForRow(at: indexPath) else { return } - if !ReminderManager.shared.isDayInReminder(index: indexPath.row) { - ReminderManager.shared.addWeekdayToReminder(index: indexPath.row) - cell.accessoryType = .checkmark - } else { - ReminderManager.shared.deleteDayFromReminder(index: indexPath.row) - cell.accessoryType = .none - } - tableView.deselectRow(at: indexPath, animated: true) - } -} diff --git a/Simple Anki/Controllers/Settings/SettingsTableViewController.swift b/Simple Anki/Controllers/Settings/SettingsTableViewController.swift deleted file mode 100644 index b93b798..0000000 --- a/Simple Anki/Controllers/Settings/SettingsTableViewController.swift +++ /dev/null @@ -1,188 +0,0 @@ -// -// SettingsViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit -import StoreKit - -class SettingsViewController: UIViewController { - - private let tableView: UITableView = { - let table = UITableView(frame: .zero, style: .insetGrouped) - table.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.identifier) - table.register(SwitchTableViewCell.self, forCellReuseIdentifier: SwitchTableViewCell.identifier) - return table - }() - - var models = [SectionOld]() - - let darkMode = UserDefaults.standard.bool(forKey: K.UserDefaultsKeys.darkMode) - - override func viewDidLoad() { - super.viewDidLoad() - title = "Settings" - navigationController?.navigationBar.prefersLargeTitles = true - configure() - view.addSubview(tableView) - tableView.delegate = self - tableView.dataSource = self - tableView.frame = view.bounds - } - - func configure() { - models.append(SectionOld(title: K.Settings.appearence, options: [ - .switchCell(model: SwitchOption( - title: K.Settings.darkMode, - icon: UIImage(systemName: K.Icon.lefthalf), - isOn: darkMode, - handler: nil)) - ])) - - models.append(SectionOld(title: K.Settings.support, options: [ - .staticCell(model: Option(title: K.Settings.rateThisApp, icon: UIImage(systemName: K.Icon.star)) { - RateManager.rateApp() - }), - .staticCell(model: Option(title: K.Settings.reportBug, icon: UIImage(systemName: K.Icon.ladybug)) { - EmailManager.prepareEmailForBugReport() - }), - .staticCell(model: Option(title: K.Settings.suggestFeature, icon: UIImage(systemName: K.Icon.chevron)) { - EmailManager.prepareEmailForFeatureSuggestion() - }), - .staticCell(model: Option(title: K.Settings.shareThisApp, icon: UIImage(systemName: K.Icon.share)) { - self.showActivityViewController() - }) - - ])) - - models.append(SectionOld(title: K.Settings.notifications, options: [ - .staticCell(model: Option(title: "Reminder", icon: UIImage(systemName: K.Icon.bell), handler: { - ReminderManager.shared.notificationCenter.requestAuthorization(options: [.alert, .sound]) { permissionGranted, error in - if let error = error { - print(error.localizedDescription) - } else if permissionGranted { - self.presentReminderViewController() - } else { - self.showSettingsAlert() - } - } - })) - ])) - } - - private func showSettingsAlert() { - let alert = UIAlertController( - title: "You turned off notifications :(", - message: "Open settings to allow Simple Anki send you notifications.", - preferredStyle: .alert - ) - - let settingsAction = UIAlertAction(title: "Settings", style: .default) { _ in - guard let appSettingsUrl = URL(string: UIApplication.openSettingsURLString) else { return } - if UIApplication.shared.canOpenURL(appSettingsUrl) { - UIApplication.shared.open(appSettingsUrl) { (success) in - print("Settings opened: \(success)") - } - } - } - - let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil) - alert.addAction(cancelAction) - alert.addAction(settingsAction) - - DispatchQueue.main.async { - self.present(alert, animated: true, completion: nil) - } - } - - private func presentReminderViewController() { - DispatchQueue.main.async { - let reminderVC = ReminderViewController() - let nav = UINavigationController(rootViewController: reminderVC) - nav.isModalInPresentation = true - if let sheetController = nav.sheetPresentationController { - sheetController.detents = [.medium()] - sheetController.prefersScrollingExpandsWhenScrolledToEdge = false - } - self.present(nav, animated: true) - } - } - - private func showActivityViewController() { - let items: [Any] = ["Check this out!", URL(string: K.appURL)!] - let avc = UIActivityViewController(activityItems: items, applicationActivities: nil) - self.present(avc, animated: true, completion: nil) - } -} - -extension SettingsViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let type = models[indexPath.section].options[indexPath.row] - - switch type.self { - case .staticCell(let model): - model.handler?() - default: - break - } - } -} - -extension SettingsViewController: UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { - return models.count - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let model = models[section] - return model.title - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return models[section].options.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = models[indexPath.section].options[indexPath.row] - - switch model.self { - case .staticCell(let model): - guard let cell = tableView.dequeueReusableCell( - withIdentifier: SettingsTableViewCell.identifier, - for: indexPath - ) as? SettingsTableViewCell else { return UITableViewCell() } - cell.configure(with: model) - return cell - - case .switchCell(let model): - guard let cell = tableView.dequeueReusableCell( - withIdentifier: SwitchTableViewCell.identifier, - for: indexPath - ) as? SwitchTableViewCell else { return UITableViewCell() } - cell.delegate = self - cell.selectionStyle = .none - cell.configure(with: model) - return cell - default: - return UITableViewCell() - } - } -} - -extension SettingsViewController: SwitchViewCellDelegate { - func switchAction(with cell: UITableViewCell) { - guard let switchCell = cell as? SwitchTableViewCell else { return } - if switchCell.mySwitch.isOn { - UserDefaults.standard.set(true, forKey: K.UserDefaultsKeys.darkMode) - view.window?.overrideUserInterfaceStyle = .dark - } else { - UserDefaults.standard.set(false, forKey: K.UserDefaultsKeys.darkMode) - view.window?.overrideUserInterfaceStyle = .light - } - } -} diff --git a/Simple Anki/Extensions/DateExtension.swift b/Simple Anki/Extensions/DateExtension.swift deleted file mode 100644 index 117f28d..0000000 --- a/Simple Anki/Extensions/DateExtension.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// DateExtension.swift -// Simple Anki -// -// Created by Астемир Бозиев on 03.05.2022. -// - -import Foundation diff --git a/Simple Anki/Extensions/UIApplicationExtension.swift b/Simple Anki/Extensions/UIApplicationExtension.swift deleted file mode 100644 index 7f916a5..0000000 --- a/Simple Anki/Extensions/UIApplicationExtension.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit - -extension UIApplication { - static var release: String { - return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "x.x" - } - - static var build: String { - return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "x" - } - - static var version: String { - return "\(release) \(build)" - } -} diff --git a/Simple Anki/Extensions/UIButtonExtension.swift b/Simple Anki/Extensions/UIButtonExtension.swift deleted file mode 100644 index 7bff2e7..0000000 --- a/Simple Anki/Extensions/UIButtonExtension.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// UIButtonExtension.swift -// Simple Anki -// -// Created by Астемир Бозиев on 13.05.2022. -// - -import Foundation -import UIKit - -extension UIButton { - func configureDefaultButton(title: String) { - defaultConfiguration() - self.configuration?.title = title - } - - func configureDefaultButton() { - defaultConfiguration() - } - - func configureDefaultButton(title: String, image: UIImage?) { - defaultConfiguration() - self.configuration?.title = title - self.configuration?.image = image - self.configuration?.imagePlacement = .trailing - self.configuration?.imagePadding = 10 - } - - private func defaultConfiguration() { - self.configuration = .filled() - self.configuration?.baseBackgroundColor = .systemBlue - self.configuration?.baseForegroundColor = .white - self.configuration?.cornerStyle = .large - self.configuration?.titleAlignment = .center - } - - func configureTintedButton(title: String, image: UIImage? = nil, color: UIColor? = .systemBlue) { - self.configuration = .tinted() - self.configuration?.baseBackgroundColor = color - self.configuration?.baseForegroundColor = color - self.configuration?.title = title - self.configuration?.cornerStyle = .large - self.configuration?.titleAlignment = .center - } - - func configureIconButton(configuration: Configuration, image: UIImage?) { - self.configuration = configuration - self.configuration?.cornerStyle = .large - self.configuration?.image = image - self.configuration?.titleAlignment = .center - self.configuration?.baseForegroundColor = .systemBlue - } - -} diff --git a/Simple Anki/Extensions/UILabelExtension.swift b/Simple Anki/Extensions/UILabelExtension.swift deleted file mode 100644 index 2979a04..0000000 --- a/Simple Anki/Extensions/UILabelExtension.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// UILabelExtension.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.06.2022. -// - -import UIKit - -extension UILabel { - - func addTrailing(image: UIImage, text: String) { - let attachment = NSTextAttachment() - attachment.image = image - - let attachmentString = NSAttributedString(attachment: attachment) - let string = NSMutableAttributedString(string: text, attributes: [:]) - - string.append(attachmentString) - self.attributedText = string - } - - func addLeading(image: UIImage, text: String) { - let attachment = NSTextAttachment() - attachment.image = image - - let attachmentString = NSMutableAttributedString(attachment: attachment) - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor : UIColor.systemBlue - ] - let mutableAttributedString = NSMutableAttributedString() - mutableAttributedString.append(attachmentString) - - let string = NSMutableAttributedString(string: text, attributes: [:]) - mutableAttributedString.append(string) - mutableAttributedString.addAttributes(attributes, range: NSRange(location: 0, length: attachmentString.length)) - self.attributedText = mutableAttributedString - } - - func generateProFeatureLabel(text: String) -> UILabel { - let checkMarkImage = UIImage(systemName: "checkmark")! - self.addLeading(image: checkMarkImage, text: text) - return self - } -} diff --git a/Simple Anki/Extensions/UIViewExtension.swift b/Simple Anki/Extensions/UIViewExtension.swift deleted file mode 100644 index 8adbd74..0000000 --- a/Simple Anki/Extensions/UIViewExtension.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// UIViewExtension.swift -// Simple Anki -// -// Created by Астемир Бозиев on 06.02.2022. -// - -import UIKit - -extension UIView { - var safeTopAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.topAnchor - } - return topAnchor - } - - var safeLeftAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.leftAnchor - } - return leftAnchor - } - - var safeTrailingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.trailingAnchor - } - return trailingAnchor - } - - var safeLeadingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.leadingAnchor - } - return leadingAnchor - } - - var safeRightAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.rightAnchor - } - return rightAnchor - } - - var safeBottomAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11.0, *) { - return safeAreaLayoutGuide.bottomAnchor - } - return bottomAnchor - } -} diff --git a/Simple Anki/Extensions/URLExtension.swift b/Simple Anki/Extensions/URLExtension.swift deleted file mode 100644 index b788068..0000000 --- a/Simple Anki/Extensions/URLExtension.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// URLExtention.swift -// Simple Anki -// -// Created by Астемир Бозиев on 05.06.2021. -// - -import Foundation - -extension URL { - func exists() -> Bool { - return FileManager.default.fileExists(atPath: self.path) - } -} diff --git a/Simple Anki/Managers/EmailManager.swift b/Simple Anki/Managers/EmailManager.swift deleted file mode 100644 index 9ac55bb..0000000 --- a/Simple Anki/Managers/EmailManager.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// EmailManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit - -class EmailManager { - - private static var emailUrl: URL! - - static func prepareEmailForBugReport() { - let appVersion = String(describing: UIApplication.version) - let subject = "Bug report. App version: \(appVersion)".addingPercentEncoding( - withAllowedCharacters: .urlQueryAllowed)! - prepareEmailUrl(with: subject) - UIApplication.shared.open(emailUrl, options: [:], completionHandler: nil) - } - - static func prepareEmailForFeatureSuggestion() { - let subject = "Feature request".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! - prepareEmailUrl(with: subject) - UIApplication.shared.open(emailUrl, options: [:], completionHandler: nil) - } - - private static func prepareEmailUrl(with subject: String) { - emailUrl = URL(string: "mailto:\(K.email)?subject=\(subject)")! - } -} diff --git a/Simple Anki/Managers/HapticManager.swift b/Simple Anki/Managers/HapticManager.swift deleted file mode 100644 index 9f44955..0000000 --- a/Simple Anki/Managers/HapticManager.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// HapticManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 11.06.2021. -// - -import UIKit - -final class HapticManager { - - static let shared = HapticManager() - - private init() {} - - public func vibrate(for type: UINotificationFeedbackGenerator.FeedbackType) { - let notificationGenerator = UINotificationFeedbackGenerator() - notificationGenerator.prepare() - notificationGenerator.notificationOccurred(type) - } -} diff --git a/Simple Anki/Managers/PlayerManager.swift b/Simple Anki/Managers/PlayerManager.swift deleted file mode 100644 index 208bda0..0000000 --- a/Simple Anki/Managers/PlayerManager.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// PlayerManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 05.06.2021. -// - -import Foundation -import AVFoundation - -class PlayerManager: NSObject, AVAudioPlayerDelegate { - - var audioPlayer : AVAudioPlayer! - - init(recordFilePath: URL) { - super.init() - do { - audioPlayer = try AVAudioPlayer(contentsOf: recordFilePath) - audioPlayer.delegate = self - } catch { - print("error: \(error)") - } - } - - func play(recordFilePath: URL) { - audioPlayer.play() - } -} diff --git a/Simple Anki/Managers/ReminderManager.swift b/Simple Anki/Managers/ReminderManager.swift deleted file mode 100644 index 735b2cf..0000000 --- a/Simple Anki/Managers/ReminderManager.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// ReminderManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 03.05.2022. -// - -import Foundation -import UserNotifications -import SwiftUI - -struct Weekday: Identifiable, Codable { - let id: Int - let name: String -} - -struct CustomNotification { - let title: String - let body: String - let weekday: Weekday -} - -class ReminderManager { - static let shared = ReminderManager() - let notificationCenter = UNUserNotificationCenter.current() - - let weekdays = [ - Weekday(id: 2, name: "Monday"), - Weekday(id: 3, name: "Tuesday"), - Weekday(id: 4, name: "Wednesday"), - Weekday(id: 5, name: "Thursday"), - Weekday(id: 6, name: "Friday"), - Weekday(id: 7, name: "Saturday"), - Weekday(id: 1, name: "Sunday") - ] - - var notifications = [CustomNotification]() - - private init() { } - - func addAllDaysToReminder() { - weekdays.forEach { day in - UserDefaults.standard.set(day.id, forKey: day.name) - } - } - - func addWeekdayToReminder(index: Int) { - UserDefaults.standard.set(weekdays[index].id, forKey: weekdays[index].name) - } - - func addWeekdayToReminder(weekday: Weekday) { - UserDefaults.standard.set(weekday.id, forKey: weekday.name) - } - - func deleteDayFromReminder(index: Int) { - UserDefaults.standard.set(0, forKey: weekdays[index].name) - } - - func isDayInReminder(index: Int) -> Bool { - return UserDefaults.standard.integer(forKey: weekdays[index].name) != 0 - } - - func isDayInReminder(weekday: Weekday) -> Bool { - return UserDefaults.standard.integer(forKey: weekday.name) != 0 - } - - func collectSelectedWeekdays() -> [Weekday] { - return weekdays.filter { isDayInReminder(weekday: $0) } - } - - func setReminderOn() { - UserDefaults.standard.set(true, forKey: K.UserDefaultsKeys.reminder) - } - - func setReminderOff() { - UserDefaults.standard.set(false, forKey: K.UserDefaultsKeys.reminder) - } - - func isReminderOn() -> Bool { - return UserDefaults.standard.bool(forKey: K.UserDefaultsKeys.reminder) - } - - func getReminderTime() -> Date? { - guard let timeString = UserDefaults.standard.string(forKey: K.UserDefaultsKeys.reminderTime) - else { return nil } - let dateFormatter = DateFormatter() - dateFormatter.timeZone = .current - dateFormatter.dateFormat = "HH:mm" - return dateFormatter.date(from: timeString) - } - - func getNotificationsCredentials(weekdays: [Weekday]) -> [CustomNotification] { - return weekdays.map { - CustomNotification(title: "Review time!", - body: "Your decks are wating to be reviewed.", - weekday: $0) - } - } - - func scheduleNotifications(notifications: [CustomNotification]) { - removeAllNotifications() - guard let time = getReminderTime() else { return } - for notification in notifications { - let content = UNMutableNotificationContent() - content.title = notification.title - content.body = notification.body - content.sound = .default - var dateComponents = DateComponents() - dateComponents.timeZone = TimeZone.current - dateComponents.hour = time.component(.hour) - dateComponents.minute = time.component(.minute) - dateComponents.weekday = notification.weekday.id - let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) - let request = UNNotificationRequest(identifier: notification.weekday.name, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request) { (error) in - if let error = error { - print("Uh oh! We had an error: \(error)") - } - } - } - } - - func removeAllNotifications() { - notificationCenter.removeAllPendingNotificationRequests() - } -} diff --git a/Simple Anki/Managers/ReviewManager.swift b/Simple Anki/Managers/ReviewManager.swift deleted file mode 100644 index 668c504..0000000 --- a/Simple Anki/Managers/ReviewManager.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// ReviewManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 31.03.2021. -// - -import Foundation - -struct ReviewCard { - let front: String - let back: String? - let audioName: String? -} - -class ReviewManager { - let layout: String! - let autoPlay: Bool! - var cardsForReview = [ReviewCard]() - var alreadyReviewed = [ReviewCard]() - var numberOfCards: Int64? - var currentCard: ReviewCard? - - init(layout: String, autoPlay: Bool, cards: [Card]) { - self.layout = layout - self.autoPlay = autoPlay - prepareCards(cards: cards) - pickCard() - cardsForReview.shuffle() - } - - func prepareCards(cards: [Card]) { - if layout == K.Layout.all { - for card in cards { - all(with: card) - } - } else if layout == K.Layout.backToFront { - for card in cards { - backToFront(with: card) - } - } else { - for card in cards { - frontToBack(with: card) - } - } - numberOfCards = Int64(cardsForReview.count) - } - - private func all(with card: Card) { - frontToBack(with: card) - backToFront(with: card) - } - - private func backToFront(with card: Card) { - cardsForReview.append(ReviewCard(front: card.back, back: card.front, audioName: card.audioName)) - } - - private func frontToBack(with card: Card) { - cardsForReview.append(ReviewCard(front: card.front, back: card.back, audioName: card.audioName)) - } - - func pickCard() { - if !cardsForReview.isEmpty { - currentCard = cardsForReview.popLast() - alreadyReviewed.append(currentCard!) - } else { - currentCard = nil - } - } - - func repeatReview() { - alreadyReviewed.shuffle() - cardsForReview.append(contentsOf: alreadyReviewed) - alreadyReviewed.removeAll() - } -} diff --git a/Simple Anki/Managers/StorageManager.swift b/Simple Anki/Managers/StorageManager.swift deleted file mode 100644 index 9c4bf27..0000000 --- a/Simple Anki/Managers/StorageManager.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// StorageManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 01.03.2021. -// - -import Foundation -import RealmSwift - -class StorageManager { - - static var realm: Realm! - - static func save(_ card: Card, to deck: Deck?) { - do { - try realm.write { - deck?.cards.append(card) - } - } catch { - print(error) - } - - } - - static func update(_ card: Card, with newCard: Card) { - do { - try realm.write { - card.front = newCard.front - card.back = newCard.back - card.audioName = newCard.audioName - } - } catch { - print(error) - } - - } - - static func save(_ deck: Deck) { - do { - try realm.write { - realm.add(deck) - } - } catch { - print(error) - } - - } - - static func deleteCard(at index: Int, from deck: Deck?) { - let card = deck?.cards[index] - if let name = card?.audioName { - Utils.deleteAudioFile(with: name) - } - do { - try realm.write { - deck?.cards.remove(at: index) - } - } catch { - print(error) - } - - } - - static func delete(_ card: Card) { - if let name = card.audioName { - Utils.deleteAudioFile(with: name) - } - do { - try realm.write { - realm.delete(card) - } - } catch { - print(error) - } - - } - - static func delete(_ deck: Deck) { - do { - try realm.write { - deck.cards.forEach { card in - if let name = card.audioName { - Utils.deleteAudioFile(with: name) - } - realm.delete(card) - } - realm.delete(deck) - } - } catch { - print(error) - } - - } -} diff --git a/Simple Anki/Managers/Utils.swift b/Simple Anki/Managers/Utils.swift deleted file mode 100644 index dad39f6..0000000 --- a/Simple Anki/Managers/Utils.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Utils.swift -// Simple Anki -// -// Created by Астемир Бозиев on 04.06.2021. -// - -import Foundation - -class Utils { - - private static let fileManager = FileManager.default - - static func generateNewRecordName() -> URL { - return getAudioFilePath(with: "\(UUID().uuidString).m4a") - } - - static func getDocumentsDirectory() -> URL { - return fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - } - - static func getAudioFilePath(with name: String) -> URL { - return getDocumentsDirectory().appendingPathComponent(name) - } - - static func deleteAudioFile(with name: String) { - let audioFilePath = getAudioFilePath(with: name) - if audioFilePath.exists() { - do { - try fileManager.removeItem(at: audioFilePath) - } catch { - print("Could not delete file: \(error)") - } - } - } - - static func deleteAudioFile(at path: URL) { - if path.exists() { - do { - try fileManager.removeItem(at: path) - } catch { - print("Could not delete file: \(error)") - } - } - } -} diff --git a/Simple Anki/Models/Card.swift b/Simple Anki/Models/Card.swift deleted file mode 100644 index b3e65af..0000000 --- a/Simple Anki/Models/Card.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Card.swift -// Simple Anki -// -// Created by Астемир Бозиев on 28.02.2021. -// - -import Foundation -import RealmSwift - -// class Card: Object { -// @objc dynamic var _id: ObjectId = ObjectId.generate() -// @objc dynamic var front: String = "" -// @objc dynamic var back: String = "" -// @objc dynamic var dateCreated: Date = Date() -// @objc dynamic var audioName: String? -// @objc dynamic var memorized: Bool = false -// var parentDeck = LinkingObjects(fromType: Deck.self, property: "cards") -// -// override static func primaryKey() -> String? { -// return "_id" -// } -// } diff --git a/Simple Anki/Models/Deck.swift b/Simple Anki/Models/Deck.swift deleted file mode 100644 index 5c60a8d..0000000 --- a/Simple Anki/Models/Deck.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Deck.swift -// Simple Anki -// -// Created by Астемир Бозиев on 28.02.2021. -// - -import Foundation -import RealmSwift - -// class Deck: Object { -// @objc dynamic var _id: ObjectId = ObjectId.generate() -// @objc dynamic var name: String = "" -// @objc dynamic var dateCreated: Date = Date() -// @objc dynamic var layout: String = "frontToBack" -// @objc dynamic var autoplay: Bool = false -// let cards = List() -// -// override static func primaryKey() -> String? { -// return "_id" -// } -// } diff --git a/Simple Anki/Models/Options.swift b/Simple Anki/Models/Options.swift deleted file mode 100644 index 32799f4..0000000 --- a/Simple Anki/Models/Options.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Section.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit - -struct Option { - let title: String - let icon: UIImage? - let handler: (() -> Void)? -} - -struct SwitchOption { - let title: String - let icon: UIImage? - let isOn: Bool - let handler: (() -> Void)? -} - -struct DatePickerOption { - let date: String -} - -enum OptionType { - case staticCell(model: Option) - case switchCell(model: SwitchOption) - case datePickerCell(model: DatePickerOption) -} - -enum SwitchType { - case darkMode - case reminder -} - -struct SectionOld { - let title: String - let options: [OptionType] -} diff --git a/Simple Anki/Protocols/EmptyState.swift b/Simple Anki/Protocols/EmptyState.swift deleted file mode 100644 index be19a8b..0000000 --- a/Simple Anki/Protocols/EmptyState.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// EmptyStateDelegate.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.04.2022. -// - -import Foundation - -protocol EmptyState { - func setEmptyState() - func setEmptyStateForMemorizedCards() - func restore() -} - -extension EmptyState { - func setEmptyStateForMemorizedCards() {} -} diff --git a/Simple Anki/SceneDelegate.swift b/Simple Anki/SceneDelegate.swift deleted file mode 100644 index be8f44f..0000000 --- a/Simple Anki/SceneDelegate.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// SceneDelegate.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - guard let windowScene = (scene as? UIWindowScene) else { return } - let window = UIWindow(windowScene: windowScene) - window.rootViewController = MainTabBarViewController() - window.makeKeyAndVisible() - self.window = window - - let darkModeIsOn = UserDefaults.standard.bool(forKey: K.UserDefaultsKeys.darkMode) - if darkModeIsOn { - self.window?.overrideUserInterfaceStyle = .dark - } else { - self.window?.overrideUserInterfaceStyle = .light - } - } - - func sceneDidDisconnect(_ scene: UIScene) { - - } - - func sceneDidBecomeActive(_ scene: UIScene) { - - } - - func sceneWillResignActive(_ scene: UIScene) { - - } - - func sceneWillEnterForeground(_ scene: UIScene) { - - } - - func sceneDidEnterBackground(_ scene: UIScene) { - - } -} diff --git a/Simple Anki/Managers/OnboardingManager.swift b/Simple Anki/SwiftUI/Managers/OnboardingManager.swift similarity index 100% rename from Simple Anki/Managers/OnboardingManager.swift rename to Simple Anki/SwiftUI/Managers/OnboardingManager.swift diff --git a/Simple Anki/Managers/RateManager.swift b/Simple Anki/SwiftUI/Managers/RateManager.swift similarity index 100% rename from Simple Anki/Managers/RateManager.swift rename to Simple Anki/SwiftUI/Managers/RateManager.swift diff --git a/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift b/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift index 6f8b6f4..ca791c6 100644 --- a/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift +++ b/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift @@ -8,6 +8,27 @@ import Foundation import UserNotifications +struct Weekday: Codable, Identifiable { + let id: Int + let name: String +} + +struct CustomNotification { + let title: String + let body: String + let weekday: Weekday +} + +let weekdays = [ + Weekday(id: 2, name: "Monday"), + Weekday(id: 3, name: "Tuesday"), + Weekday(id: 4, name: "Wednesday"), + Weekday(id: 5, name: "Thursday"), + Weekday(id: 6, name: "Friday"), + Weekday(id: 7, name: "Saturday"), + Weekday(id: 1, name: "Sunday") +] + @Observable class ReminderViewModel { diff --git a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift index 81777fc..ecdaac0 100644 --- a/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift +++ b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift @@ -8,18 +8,27 @@ import Foundation import RealmSwift -class ReviewManagerSUI: ObservableObject { - @Published var currentCard: Card? - @Published var isReviewing = false +@Observable +class ReviewManagerSUI { + var currentCard: Card? + var isReviewing = false + var isAutoplayOn: Bool { return deck.autoplay } private var currentIndex = 0 private var deck: Deck + private var cards: [Card] init(deck: Deck) { self.deck = deck + + if deck.shuffled { + self.cards = deck.cards.shuffled() + } else { + self.cards = Array(deck.cards) + } } func startReview() { @@ -28,15 +37,15 @@ class ReviewManagerSUI: ObservableObject { } currentIndex = 0 - currentCard = deck.cards[currentIndex] + currentCard = self.cards[currentIndex] isReviewing = true } func nextCard() { currentIndex += 1 - if currentIndex < deck.cards.count { - currentCard = deck.cards[currentIndex] + if currentIndex < self.cards.count { + currentCard = self.cards[currentIndex] } else { isReviewing = false } diff --git a/Simple Anki/SwiftUI/Managers/SoundManager.swift b/Simple Anki/SwiftUI/Managers/SoundManager.swift index e3ec0a2..c1c08b2 100644 --- a/Simple Anki/SwiftUI/Managers/SoundManager.swift +++ b/Simple Anki/SwiftUI/Managers/SoundManager.swift @@ -21,7 +21,7 @@ class SoundManager { private func setupPlayer() { DispatchQueue.global(qos: .background).async { do { - try self.audioSession.setCategory(.playAndRecord) + try self.audioSession.setCategory(.playback) try self.audioSession.setActive(true, options: .notifyOthersOnDeactivation) } catch { print(error.localizedDescription) @@ -31,8 +31,6 @@ class SoundManager { func play(sound: String) { let url = FileManager.documentsDirectory.appendingPathComponent(sound) - print(url) - DispatchQueue.global(qos: .background).async { do { self.player = try AVAudioPlayer(contentsOf: url) diff --git a/Simple Anki/SwiftUI/Models/CardSUI.swift b/Simple Anki/SwiftUI/Models/Card.swift similarity index 95% rename from Simple Anki/SwiftUI/Models/CardSUI.swift rename to Simple Anki/SwiftUI/Models/Card.swift index 2a48081..0997a37 100644 --- a/Simple Anki/SwiftUI/Models/CardSUI.swift +++ b/Simple Anki/SwiftUI/Models/Card.swift @@ -16,7 +16,6 @@ class Card: Object, ObjectKeyIdentifiable { @Persisted var dateCreated: Date = Date() @Persisted var audioName: String? @Persisted var memorized: Bool = false - @Persisted var shuffled: Bool = false @Persisted(originProperty: "cards") var deck: LinkingObjects convenience init(front: String, back: String, audioName: String? = nil, image: String? = nil) { diff --git a/Simple Anki/SwiftUI/Models/DeckSUI.swift b/Simple Anki/SwiftUI/Models/Deck.swift similarity index 96% rename from Simple Anki/SwiftUI/Models/DeckSUI.swift rename to Simple Anki/SwiftUI/Models/Deck.swift index 14fe7fa..edab777 100644 --- a/Simple Anki/SwiftUI/Models/DeckSUI.swift +++ b/Simple Anki/SwiftUI/Models/Deck.swift @@ -20,6 +20,7 @@ class Deck: Object, ObjectKeyIdentifiable { @Persisted(indexed: true) var dateCreated: Date = Date() @Persisted var layout: String = "frontToBack" @Persisted var autoplay: Bool = false + @Persisted var shuffled: Bool = false @Persisted var cards: List convenience init(name: String) { diff --git a/Simple Anki/SwiftUI/SimpleAnkiApp.swift b/Simple Anki/SwiftUI/SimpleAnkiApp.swift index e4ce582..91b77f0 100644 --- a/Simple Anki/SwiftUI/SimpleAnkiApp.swift +++ b/Simple Anki/SwiftUI/SimpleAnkiApp.swift @@ -17,8 +17,10 @@ struct SimpleAnkiApp: SwiftUI.App { MainView() .environment(\.realmConfiguration, Realm.Configuration(schemaVersion: 2, migrationBlock: { migration, oldSchemaVersion in if oldSchemaVersion < 2 { - migration.enumerateObjects(ofType: Card.className()) { _, newObject in + migration.enumerateObjects(ofType: Deck.className()) { _, newObject in newObject!["shuffled"] = false + } + migration.enumerateObjects(ofType: Card.className()) { _, newObject in newObject!["image"] = nil } } diff --git a/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift index 64aab75..af5257a 100644 --- a/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift @@ -22,7 +22,7 @@ struct CardView: View { @State private var selectedImage: PhotosPickerItem? @State private var isPreviewPresented: Bool = false @State private var showAlert: Bool = false - @State private var saveButtonTapped: Bool = false + @State private var addButtonTapped: Bool = false @FocusState private var focusedField: FocusableField? private var transaction: Transaction { @@ -82,18 +82,18 @@ struct CardView: View { recorder.setName(UUID().uuidString + ".m4a") } withAnimation { - saveButtonTapped.toggle() + addButtonTapped.toggle() } HapticManagerSUI.shared.impact(style: .heavy) } label: { - Text(viewModel.updating ? "Update" : "Save") + Text(viewModel.updating ? "Update" : "Add") } .changeEffect( .rise(origin: UnitPoint(x: 0.45, y: -11)) { Text(viewModel.updating ? "Updated" : "Added") .font(.system(size: 20, weight: .semibold, design: .rounded)) .foregroundStyle(.blue) - }, value: saveButtonTapped) + }, value: addButtonTapped) .controlSize(.extraLarge) .buttonStyle(.borderedProminent) } @@ -145,7 +145,6 @@ struct CardView: View { .alert("No access", isPresented: $showAlert, actions: { Button("Cancel", role: .cancel, action: {}) Button("Open settings", role: .none) { - } }) } @@ -181,12 +180,11 @@ struct CardView: View { } } - ToolbarItem(placement: .principal) { - } - ToolbarItem(placement: .cancellationAction) { Button { - FileManager.default.delete(viewModel.audioName) + if !viewModel.updating { + FileManager.default.delete(viewModel.audioName) + } dismiss() } label: { Image(systemName: "xmark") diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift index 9323ae2..a424957 100644 --- a/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift @@ -32,6 +32,12 @@ struct DeckSettingsView: View { }) } + Section("Cards order") { + Toggle(isOn: $deck.shuffled, label: { + Label("Shuffle", systemImage: "shuffle") + }) + } + Section { Button(action: { remove(deck: deck) diff --git a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift index 23ad59b..8907f0d 100644 --- a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift @@ -10,8 +10,8 @@ import SwiftUI struct ReviewView: View { @Environment(\.dismiss) var dismiss - @State private var isShowAnswer: Bool = false - @StateObject var reviewManager: ReviewManagerSUI + @State private var isAnswerPresented: Bool = false + @State var reviewManager: ReviewManagerSUI var body: some View { NavigationView { @@ -35,7 +35,7 @@ struct ReviewView: View { Divider() Text(reviewManager.currentCard?.back ?? "No back text") } - .opacity(isShowAnswer ? 1 : 0) + .opacity(isAnswerPresented ? 1 : 0) } else { Text("Finished!") .font(.system(size: 60, weight: .bold)) @@ -67,20 +67,20 @@ struct ReviewView: View { Spacer() if reviewManager.isReviewing { Button { - if isShowAnswer { + if isAnswerPresented { reviewManager.nextCard() - isShowAnswer = false + isAnswerPresented = false } else { - isShowAnswer = true + isAnswerPresented = true } HapticManagerSUI.shared.impact(style: .medium) } label: { - Image(systemName: isShowAnswer ? "arrow.right.circle.fill" : "eye.circle.fill") + Image(systemName: isAnswerPresented ? "arrow.right.circle.fill" : "eye.circle.fill") } } else { Button { reviewManager.startReview() - isShowAnswer = false + isAnswerPresented = false HapticManagerSUI.shared.impact(style: .medium) } label: { Image(systemName: "repeat.circle.fill") diff --git a/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift b/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift index 01e9d6e..c8c21b8 100644 --- a/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Settings/ReminderView.swift @@ -35,7 +35,7 @@ struct ReminderView: View { } Section { - ForEach(ReminderManager.shared.weekdays) { weekday in + ForEach(weekdays) { weekday in Button { if viewModel.isWeekdayInReminder(weekday: weekday) { viewModel.removeNotificationFromReminder(weekday: weekday) diff --git a/Simple Anki/UIComponents/FeatureView.swift b/Simple Anki/UIComponents/FeatureView.swift deleted file mode 100644 index 6f379d7..0000000 --- a/Simple Anki/UIComponents/FeatureView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// FeatureView.swift -// Simple Anki -// -// Created by Астемир Бозиев on 18.06.2022. -// - -import Foundation -import UIKit - -struct Feature { - let title: String - let desription: String - let image: UIImage? -} - -class FeatureView: UIView { - - lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = .systemBlue - imageView.contentMode = .scaleAspectFit - return imageView - }() - - lazy var featureTitle: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: 17, weight: .bold) - label.numberOfLines = 1 - return label - }() - - lazy var featureDescription: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: .regular) - label.numberOfLines = 0 - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - addSubview(imageView) - addSubview(featureTitle) - addSubview(featureDescription) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - imageView.frame = CGRect( - x: 0, - y: 10, - width: 60, - height: 50 - ) - featureTitle.frame = CGRect( - x: 70, - y: 0, - width: 210, - height: 32 - ) - featureDescription.frame = CGRect( - x: 70, - y: featureTitle.frame.origin.y + 14, - width: 230, - height: 64 - ) - } - - func configure(model: Feature) { - imageView.image = model.image - featureTitle.text = model.title - featureDescription.text = model.desription - } -} From 217168d4638509588f05ee592489950f58962ceb Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 11 Feb 2024 16:42:47 +0400 Subject: [PATCH 8/9] new workflow for building the app --- .github/workflows/build.yml | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb85408..68b8c8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,20 +1,30 @@ -name: Build +name: Xcode - Build and Analyze on: push: - branches: - - master + branches: [ "master" ] pull_request: - branches: - - master + branches: [ "master" ] jobs: build: - name: Build default scheme using any available iPhone simulator + name: Build and analyse default scheme using xcodebuild command runs-on: macos-latest steps: - name: Checkout uses: actions/checkout@v3 - - name: Build iOS Action - uses: sparkfabrik/ios-build-action@v2.0.0 + - name: Set Default Scheme + run: | + scheme_list=$(xcodebuild -list -json | tr -d "\n") + default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") + echo $default | cat >default + echo Using default scheme: $default + - name: Build + env: + scheme: ${{ 'default' }} + run: | + if [ $scheme = default ]; then scheme=$(cat default); fi + if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi + file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` + xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" | xcpretty && exit ${PIPESTATUS[0]} From 87cac0a75a8da4c2814a5d9001f903fcef2e1203 Mon Sep 17 00:00:00 2001 From: Astemir Boziev Date: Sun, 11 Feb 2024 17:55:12 +0400 Subject: [PATCH 9/9] some refactoring --- Simple Anki.xcodeproj/project.pbxproj | 16 +- Simple Anki/ConstantsOld.swift | 7 + .../SwiftUI/UI/Views/Card/CardView.swift | 15 +- .../{CardsView.swift => CardsListView.swift} | 44 ++-- .../SwiftUI/UI/Views/ContentView.swift | 234 ------------------ .../UI/Views/Deck/CreateDeckView.swift | 9 +- .../UI/Views/Deck/DeckSettingsView.swift | 13 +- .../{DecksView.swift => DecksListView.swift} | 78 +----- .../UI/Views/Deck/ImportedDeckView.swift | 2 +- .../SwiftUI/UI/Views/Deck/NewDeckView.swift | 121 --------- Simple Anki/SwiftUI/UI/Views/MainView.swift | 2 +- 11 files changed, 87 insertions(+), 454 deletions(-) rename Simple Anki/SwiftUI/UI/Views/Card/{CardsView.swift => CardsListView.swift} (72%) rename Simple Anki/SwiftUI/UI/Views/Deck/{DecksView.swift => DecksListView.swift} (60%) diff --git a/Simple Anki.xcodeproj/project.pbxproj b/Simple Anki.xcodeproj/project.pbxproj index 2593e4c..027a14b 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -23,11 +23,11 @@ C05BCFF62B78D12C0076CAD9 /* ImportedDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */; }; C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */; }; C067B6872A8C1251000AF881 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6862A8C1251000AF881 /* MainView.swift */; }; - C067B6892A8C139A000AF881 /* DecksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksView.swift */; }; + C067B6892A8C139A000AF881 /* DecksListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6882A8C139A000AF881 /* DecksListView.swift */; }; C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B68A2A8C13A5000AF881 /* SettingsView.swift */; }; C070008E263E86E0006DF020 /* RateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070008D263E86E0006DF020 /* RateManager.swift */; }; C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0730B162B77F26A00B43D42 /* NewDeckView.swift */; }; - C074581F2A9293AA0046F39D /* CardsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581E2A9293AA0046F39D /* CardsView.swift */; }; + C074581F2A9293AA0046F39D /* CardsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C074581E2A9293AA0046F39D /* CardsListView.swift */; }; C07458232A938CF90046F39D /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458222A938CF90046F39D /* UserSettings.swift */; }; C07458252A93943E0046F39D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458242A93943E0046F39D /* Constants.swift */; }; C07458282A939FA40046F39D /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07458272A939FA40046F39D /* LinkView.swift */; }; @@ -109,11 +109,11 @@ C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportedDeckView.swift; sourceTree = ""; }; C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnkiApp.swift; sourceTree = ""; }; C067B6862A8C1251000AF881 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; - C067B6882A8C139A000AF881 /* DecksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksView.swift; sourceTree = ""; }; + C067B6882A8C139A000AF881 /* DecksListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecksListView.swift; sourceTree = ""; }; C067B68A2A8C13A5000AF881 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; C070008D263E86E0006DF020 /* RateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateManager.swift; sourceTree = ""; }; C0730B162B77F26A00B43D42 /* NewDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckView.swift; sourceTree = ""; }; - C074581E2A9293AA0046F39D /* CardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsView.swift; sourceTree = ""; }; + C074581E2A9293AA0046F39D /* CardsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardsListView.swift; sourceTree = ""; }; C07458222A938CF90046F39D /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; C07458242A93943E0046F39D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; C07458272A939FA40046F39D /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = ""; }; @@ -260,7 +260,7 @@ children = ( C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */, C0730B162B77F26A00B43D42 /* NewDeckView.swift */, - C067B6882A8C139A000AF881 /* DecksView.swift */, + C067B6882A8C139A000AF881 /* DecksListView.swift */, C05BCFEF2B78CD3F0076CAD9 /* CreateDeckView.swift */, C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */, C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */, @@ -273,7 +273,7 @@ children = ( C00513FA2B517E91005F5815 /* CardPreviewView.swift */, C0CD12542AAB3F2700FE3BB6 /* CardView.swift */, - C074581E2A9293AA0046F39D /* CardsView.swift */, + C074581E2A9293AA0046F39D /* CardsListView.swift */, ); path = Card; sourceTree = ""; @@ -625,7 +625,7 @@ C0F730C42B752F7100127CB4 /* ReminderView.swift in Sources */, C0F730C22B7424D900127CB4 /* ReminderManagerSUI.swift in Sources */, C07458252A93943E0046F39D /* Constants.swift in Sources */, - C067B6892A8C139A000AF881 /* DecksView.swift in Sources */, + C067B6892A8C139A000AF881 /* DecksListView.swift in Sources */, C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */, C05BCFF22B78CF4D0076CAD9 /* ImportDeckView.swift in Sources */, @@ -650,7 +650,7 @@ C07458232A938CF90046F39D /* UserSettings.swift in Sources */, C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */, C05BCFF02B78CD3F0076CAD9 /* CreateDeckView.swift in Sources */, - C074581F2A9293AA0046F39D /* CardsView.swift in Sources */, + C074581F2A9293AA0046F39D /* CardsListView.swift in Sources */, C08903272A8D474900EFC51C /* Card.swift in Sources */, C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, diff --git a/Simple Anki/ConstantsOld.swift b/Simple Anki/ConstantsOld.swift index 1e014e5..560885a 100644 --- a/Simple Anki/ConstantsOld.swift +++ b/Simple Anki/ConstantsOld.swift @@ -31,6 +31,13 @@ struct K { } struct Icon { + static let recordButton = "waveform.badge.mic" + static let stopCircleFill = "stop.circle.fill" + static let playCircle = "play.circle" + static let plusCircleFill = "plus.circle.fill" + static let gearshape = "gearshape" + static let trash = "trash" + static let noCards = "rectangle.portrait.on.rectangle.portrait.angled" static let lefthalf = "circle.lefthalf.fill" static let ladybug = "ladybug" static let chevron = "chevron.left.slash.chevron.right" diff --git a/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift index af5257a..349e54b 100644 --- a/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift @@ -84,6 +84,7 @@ struct CardView: View { withAnimation { addButtonTapped.toggle() } + focusedField = .frontField HapticManagerSUI.shared.impact(style: .heavy) } label: { Text(viewModel.updating ? "Update" : "Add") @@ -107,14 +108,14 @@ struct CardView: View { Button { SoundManager.shared.play(sound: fileName) } label: { - Image(systemName: "play.circle") + Image(systemName: K.Icon.playCircle) .font(.system(size: 18)) } .contextMenu { Button(role: .destructive) { deleteAudioAndUpdateCard() } label: { - Label("Delete", systemImage: "trash") + Label("Delete", systemImage: K.Icon.trash) } } } else { @@ -137,7 +138,7 @@ struct CardView: View { } } } label: { - Image(systemName: recorder.isRecording ? "stop.circle.fill" : "waveform.badge.mic") + Image(systemName: recorder.isRecording ? K.Icon.stopCircleFill : K.Icon.recordButton) .foregroundStyle(recorder.isRecording ? .red : .blue) .font(.system(size: recorder.isRecording ? 35 : 18)) .contentTransition(.symbolEffect(.replace.offUp.wholeSymbol)) @@ -176,7 +177,13 @@ struct CardView: View { } .disabled(viewModel.incomplete) .fullScreenCover(isPresented: $isPreviewPresented) { - CardPreviewView(front: viewModel.frontWord, back: viewModel.backWord, image: viewModel.image, audio: viewModel.audioName, isPreviewPresented: $isPreviewPresented) + CardPreviewView( + front: viewModel.frontWord, + back: viewModel.backWord, + image: viewModel.image, + audio: viewModel.audioName, + isPreviewPresented: $isPreviewPresented + ) } } diff --git a/Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardsListView.swift similarity index 72% rename from Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift rename to Simple Anki/SwiftUI/UI/Views/Card/CardsListView.swift index 3031398..2330bf0 100644 --- a/Simple Anki/SwiftUI/UI/Views/Card/CardsView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardsListView.swift @@ -8,18 +8,19 @@ import SwiftUI import RealmSwift -struct CardsView: View { +struct CardsListView: View { + @ObservedRealmObject var deck: Deck @State private var isCardViewPresented: Bool = false @State private var isReviewPresented: Bool = false - @State private var isLayoutDialogPresented: Bool = false - @ObservedRealmObject var deck: Deck + @State private var isDeckSettingsPresented: Bool = false @Environment(\.realm) var realm + @Environment(\.dismiss) var dismiss var body: some View { ZStack { if deck.cards.isEmpty { ContentUnavailableView(label: { - Label("No cards", systemImage: "rectangle.portrait.on.rectangle.portrait.angled") + Label("No cards", systemImage: K.Icon.noCards) }, description: { Text("Cards will appear here.") }, actions: { @@ -38,8 +39,13 @@ struct CardsView: View { ForEach(deck.cards) { card in NavigationLink { CardView( - viewModel: CardViewModel(card: card, repository: CardRepository(deck: deck)), - recorder: AudioRecorder(fileName: card.audioName != nil ? card.audioName! : UUID().uuidString + ".m4a") + viewModel: CardViewModel( + card: card, + repository: CardRepository(deck: deck) + ), + recorder: AudioRecorder( + fileName: card.audioName != nil ? card.audioName! : UUID().uuidString + ".m4a" + ) ) .navigationBarBackButtonHidden() } label: { @@ -49,7 +55,7 @@ struct CardsView: View { Button { remove(card) } label: { - Image(systemName: "trash") + Image(systemName: K.Icon.trash) } .tint(.red) } @@ -62,16 +68,20 @@ struct CardsView: View { HStack(spacing: 10) { Button { - isLayoutDialogPresented.toggle() + isDeckSettingsPresented.toggle() HapticManagerSUI.shared.impact(style: .heavy) } label: { - Image(systemName: "gearshape") + Image(systemName: K.Icon.gearshape) .padding(.vertical, 8) .padding(.horizontal, 4) } .tint(.blue) .buttonStyle(.bordered) - .sheet(isPresented: $isLayoutDialogPresented, content: { + .sheet(isPresented: $isDeckSettingsPresented, onDismiss: { + if deckIsRemoved(deck: deck) { + dismiss() + } + }, content: { DeckSettingsView(deck: deck) .interactiveDismissDisabled(deck.name.isEmpty) }) @@ -88,7 +98,6 @@ struct CardsView: View { .fullScreenCover(isPresented: $isReviewPresented) { ReviewView(reviewManager: ReviewManagerSUI(deck: deck)) } - } .padding() .padding(.top) @@ -101,11 +110,14 @@ struct CardsView: View { Button { isCardViewPresented.toggle() } label: { - Image(systemName: "plus.circle.fill") + Image(systemName: K.Icon.plusCircleFill) } .sheet(isPresented: $isCardViewPresented) { NavigationView { - CardView(viewModel: CardViewModel(repositry: CardRepository(deck: deck)), recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a")) + CardView( + viewModel: CardViewModel(repositry: CardRepository(deck: deck)), + recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a") + ) } } } @@ -121,6 +133,10 @@ struct CardsView: View { LocalFileManager.shared.delete(audioName) } + private func deckIsRemoved(deck: Deck) -> Bool { + return realm.object(ofType: Deck.self, forPrimaryKey: deck._id) == nil + } + private func findIndex(of card: Card) -> Int? { return deck.cards.firstIndex(of: card) } @@ -129,7 +145,7 @@ struct CardsView: View { struct CardsView_Previews: PreviewProvider { static var previews: some View { NavigationView { - CardsView(deck: Deck.deck2) + CardsListView(deck: Deck.deck2) } } } diff --git a/Simple Anki/SwiftUI/UI/Views/ContentView.swift b/Simple Anki/SwiftUI/UI/Views/ContentView.swift index 60ebc97..b4f1ece 100644 --- a/Simple Anki/SwiftUI/UI/Views/ContentView.swift +++ b/Simple Anki/SwiftUI/UI/Views/ContentView.swift @@ -6,237 +6,3 @@ // import SwiftUI - -// enum TabbedItems: Int, CaseIterable { -// case home = 0 -// case favorite -// -// var title: String { -// switch self { -// case .home: -// return "Decks" -// case .favorite: -// return "Settings" -// } -// } -// -// var iconName: String { -// switch self { -// case .home: -// return "tray.full.fill" -// case .favorite: -// return "gear" -// } -// } -// } -// -// struct MainTabbedView: View { -// -// @State var selectedTab = 0 -// @State var next: Bool = false -// -// var body: some View { -// -// ZStack(alignment: .bottom) { -// TabView(selection: $selectedTab) { -// NavigationStack { -// Button("go to next") { -// next.toggle() -// }.navigationDestination(isPresented: $next) { -// DecksView() -// } -// -// -// } -// -// Text("Settings") -// .tag(1) -// } -// -// ZStack { -// HStack { -// ForEach((TabbedItems.allCases), id: \.self) { item in -// Button { -// selectedTab = item.rawValue -// } label: { -// customTabItem(imageName: item.iconName, title: item.title, isActive: (selectedTab == item.rawValue)) -// } -// } -// } -// .padding(6) -// } -// .frame(maxWidth: .infinity, maxHeight: 70) -// .background(.cyan.opacity(0.2)) -// .cornerRadius(35) -// .padding(.horizontal, 26) -// .offset(x: next == true ? -400 : 0) -// .animation(.bouncy(duration: 0.2), value: next) -// } -// } -// } -// -// extension MainTabbedView { -// func customTabItem(imageName: String, title: String, isActive: Bool) -> some View { -// VStack(spacing: 5) { -// Spacer() -// Image(systemName: imageName) -// .renderingMode(.template) -// .foregroundColor(isActive ? .black : .gray) -// Text(title) -// .font(.system(size: 10)) -// .foregroundColor(isActive ? .black : .gray) -// Spacer() -// } -// .padding() -// .frame(width: 100, height: 60) -// .background(isActive ? .cyan.opacity(0.4) : .clear) -// .cornerRadius(30) -// } -// } - -struct FirstView: View { - var body: some View { - List { - ForEach(0..<20) { item in - NavigationLink { - ThirdView() - } label: { - Text("\(item)") - } - - } - } - .listStyle(.plain) - .navigationTitle("First title") - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) - } -} - -struct SecondView: View { - var body: some View { - List { - ForEach(0..<20) { item in - NavigationLink { - ThirdView() - } label: { - Text("\(item)") - } - } - } - .navigationTitle("Title") - } -} - -struct ThirdView: View { - var body: some View { - VStack { - List { - ForEach(0..<20) { _ in - Text("Third View with tabBar hidden") - .font(.headline) - } - } - .listStyle(.plain) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) - - Divider() - - HStack { - Button(action: {}, label: { - Text("Button") - }) - } - .padding(0) - .frame(height: 42) - } - .navigationTitle("Cards") - } -} - -enum Tab: Int { - case first, second, third -} - -struct TabBarView: View { - - @State private var selectedTab: Tab = .first - - var body: some View { - VStack(spacing: 0) { - ZStack { - switch selectedTab { - case .first: - NavigationStack { - VStack(spacing: 0) { -// DecksView() - TabBarView() - } - } - case .second: - NavigationStack { - SettingsView() - } - case .third: - Text("Third") - } - } - - if selectedTab != .first { - TabBarView() - } - } - } - - @ViewBuilder - func TabBarView() -> some View { - VStack(spacing: 0) { - Divider() - - HStack { - Spacer() - TabItemView(tab: .first, title: "Decks", icon: "tray.full.fill", selectedTab: $selectedTab) - Spacer(minLength: 144) - TabItemView(tab: .second, title: "Settings", icon: "gear", selectedTab: $selectedTab) - Spacer() - } - .padding(.top, 8) - - } - .frame(height: 50) - } -} - -struct TabItemView2: View { - let title: String - let icon: String - let tab: Tab - - @Binding var selectedTab: Tab - - init(tab: Tab, title: String, icon: String, selectedTab: Binding) { - self.tab = tab - self.title = title - self.icon = icon - self._selectedTab = selectedTab - } - - var body: some View { - ZStack(alignment: .topTrailing) { - VStack(spacing: 4) { - Image(systemName: icon) - .font(.system(size: 24)) - Text(title) - .font(.system(size: 11)) - } - .foregroundColor(selectedTab == tab ? .blue : .secondary) - } - .frame(width: 65, height: 42) - .onTapGesture { - selectedTab = tab - } - } -} - -#Preview { - TabBarView() -} diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift index 27a347c..231169d 100644 --- a/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift @@ -9,7 +9,14 @@ import SwiftUI import RealmSwift struct CreateDeckView: View { - @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @ObservedResults( + Deck.self, + sortDescriptor: SortDescriptor( + keyPath: \Deck.dateCreated, + ascending: false + ) + ) + var decks @State private var deckName: String = "" @FocusState private var isFocused: Bool @Environment(\.dismiss) var dismiss diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift index a424957..bb0aba8 100644 --- a/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift @@ -9,6 +9,7 @@ import SwiftUI import RealmSwift struct DeckSettingsView: View { + @State private var isDeleteAlertPresented: Bool = false @ObservedRealmObject var deck: Deck @Environment(\.dismiss) var dismiss @Environment(\.realm) var realm @@ -19,6 +20,7 @@ struct DeckSettingsView: View { List { Section("Name") { TextField("Enter name", text: $deck.name) + .submitLabel(.done) } Section("Layout") { LayoutButton(labelText: "Front to Back", layout: K.Layout.frontToBack, deck: deck) @@ -40,12 +42,20 @@ struct DeckSettingsView: View { Section { Button(action: { - remove(deck: deck) + isDeleteAlertPresented = true + }, label: { Label("Delete deck", systemImage: "trash") .foregroundStyle(.red) }) .tint(.red) + .alert("Delete \(deck.name) deck?", isPresented: $isDeleteAlertPresented) { + Button(role: .destructive) { + remove(deck: deck) + } label: { + Text("Delete") + } + } } } } @@ -75,7 +85,6 @@ struct DeckSettingsView: View { } catch { print("Error: \(error)") } - } } diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/DecksListView.swift similarity index 60% rename from Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift rename to Simple Anki/SwiftUI/UI/Views/Deck/DecksListView.swift index 781927c..daf8942 100644 --- a/Simple Anki/SwiftUI/UI/Views/Deck/DecksView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DecksListView.swift @@ -8,11 +8,16 @@ import SwiftUI import RealmSwift -struct DecksView: View { - @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks +struct DecksListView: View { + @ObservedResults( + Deck.self, + sortDescriptor: SortDescriptor( + keyPath: \Deck.dateCreated, + ascending: false + ) + ) var decks @State private var isNewDeckViewPresented: Bool = false @State private var isDeleteDialogPresented = false - @Environment(\.realm) var realm var body: some View { @@ -40,7 +45,7 @@ struct DecksView: View { List { ForEach(decks) { deck in NavigationLink { - CardsView(deck: deck) + CardsListView(deck: deck) } label: { VStack(alignment: .leading) { Text(deck.name) @@ -61,16 +66,6 @@ struct DecksView: View { .navigationTitle("Decks") .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { -// Button { -// isImportDeckViewPresented.toggle() -// HapticManagerSUI.shared.impact(style: .heavy) -// } label: { -// Image(systemName: "tray.and.arrow.down") -// } -// .sheet(isPresented: $isImportDeckViewPresented) { -// ImportDeckView() -// } - Button { isNewDeckViewPresented.toggle() HapticManagerSUI.shared.impact(style: .heavy) @@ -86,67 +81,14 @@ struct DecksView: View { } } - @ViewBuilder - private func ImportDeckView() -> some View { - NavigationStack { - VStack { - Spacer() - Text("Hello world") - .padding(.bottom, 40) - Spacer() - Button(action: { - - }, label: { - Text("Choose file...") - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - }) - .buttonStyle(.borderedProminent) - } - .padding() - - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button(action: { - // dismiss - }, label: { - Image(systemName: "xmark") - }) - .tint(.secondary) - } - ToolbarItem(placement: .principal) { -// Picker("What is your favorite color?", selection: $selectedType) { -// Text("APKG").tag(0) -// Text("CSV").tag(1) -// } -// .frame(maxWidth: 150) -// .pickerStyle(.segmented) - } - } - } - .presentationDetents([.medium]) - } - @ViewBuilder private func cardsCount(of deck: Deck) -> some View { let cardCount = deck.cards.count Text(cardCount == 0 ? "No cards" : "^[\(cardCount) card](inflect: true)") } -// private func addDeck() { -// guard !deckName.isEmpty else { return } -// -// let name = deckName.trimmingCharacters(in: .whitespacesAndNewlines) -// let deck = Deck(name: name) -// $decks.append(deck) -// deckName = "" -// isNewDeckViewPresented = false -// } - private func remove(by deckID: ObjectId) { - do { - if let deckToDelete = realm.object(ofType: Deck.self, forPrimaryKey: deckID) { deckToDelete.cards.forEach { card in LocalFileManager.shared.delete(card.audioName) @@ -163,5 +105,5 @@ struct DecksView: View { } #Preview { - DecksView() + DecksListView() } diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift index ca81f82..62d6c20 100644 --- a/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Deck/ImportedDeckView.swift @@ -9,7 +9,7 @@ import SwiftUI import RealmSwift struct ImportedDeckView: View { - @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks + @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: \Deck.dateCreated, ascending: false)) var decks @Binding var deckName: String @Binding var importedCards: RealmSwift.List diff --git a/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift index 7867723..572a087 100644 --- a/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift +++ b/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift @@ -6,27 +6,10 @@ // import SwiftUI -import RealmSwift -import SwiftCSV - -enum Delimeter: String { - case comma = "," - case semicolon = ";" - case tab = "\t" - case none = "" -} struct NewDeckView: View { - @ObservedResults(Deck.self, sortDescriptor: SortDescriptor(keyPath: "dateCreated", ascending: false)) var decks - @State private var deckName: String = "" @State private var selectedSegment: Int = 0 - @State private var isFileImporterPresented: Bool = false - @State private var isImportedCardsViewPresented: Bool = false - @State private var selectedDelimeter: CSVDelimiter = .character("d") - @State private var importedCards: RealmSwift.List = RealmSwift.List() - @Environment(\.dismiss) var dismiss - @Environment(\.realm) var realm var body: some View { NavigationStack { @@ -50,110 +33,6 @@ struct NewDeckView: View { } } } - - @ViewBuilder - private func ImportDeckView() -> some View { - VStack { - List { - Section { - DelimeterButton(title: "Comma", delimeter: .comma) - DelimeterButton(title: "Semicolon", delimeter: .semicolon) - DelimeterButton(title: "Tab", delimeter: .tab) - } header: { - Text("Select delimeter") - } footer: { - HStack(spacing: 3) { - Text("More information about") - Link("CSV files.", destination: URL(string: "https://en.wikipedia.org/wiki/Comma-separated_values")!) - .font(.caption2) - } - } - - Section { - Button(action: { - isFileImporterPresented = true - }, label: { - Text("Choose file...") - }) - .disabled(selectedDelimeter == .character("d")) - .fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.commaSeparatedText], allowsMultipleSelection: false) { result in - switch result { - case .success(let success): - print(success) - deckName = success[0].lastPathComponent - do { - let csv = try CSV(url: success[0], delimiter: selectedDelimeter) - for row in csv.rows { - guard !row.isEmpty, row.count >= 2 else { continue } -// let front = self.cleanString(row[0]) -// let back = self.cleanString(row[1]) - importedCards.append(Card(front: row[0], back: row[1])) -// dismiss() - isImportedCardsViewPresented = true - } - } catch { -// self.showAlert(with: "Error", message: "Could not import deck") - print(error) - } - case .failure(let failure): - print(failure) - } - } - .sheet(isPresented: $isImportedCardsViewPresented) { - NavigationStack { - List { - Section("Edit name") { - TextField("Deck name", text: $deckName) - } - Section("Cards") { - ForEach(importedCards) { card in - Button(action: { - - }, label: { - Text("\(card.front) - \(card.back)") - }) - } - .onDelete(perform: { indexSet in - importedCards.remove(atOffsets: indexSet) - }) - } - } - .navigationTitle("Imported deck") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(action: { - var deck = Deck(name: deckName) - deck.cards = importedCards - $decks.append(deck) - dismiss() - }, label: { - Text("Done") - }) - } - } - } - } - } - } - } - } - - @ViewBuilder - private func DelimeterButton(title: String, delimeter: CSVDelimiter) -> some View { - Button(action: { - selectedDelimeter = delimeter - }, label: { - HStack { - Text(title) - Spacer() - Image(systemName: "checkmark") - .foregroundStyle(.blue) - .opacity(selectedDelimeter == delimeter ? 1: 0) - } - }) - .tint(.primary) - } } #Preview { diff --git a/Simple Anki/SwiftUI/UI/Views/MainView.swift b/Simple Anki/SwiftUI/UI/Views/MainView.swift index 4aea577..b0a4447 100644 --- a/Simple Anki/SwiftUI/UI/Views/MainView.swift +++ b/Simple Anki/SwiftUI/UI/Views/MainView.swift @@ -17,7 +17,7 @@ struct MainView: View { case .first: NavigationStack { VStack(spacing: 0) { - DecksView() + DecksListView() TabBarView() } }