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]} 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 b49607f..027a14b 100644 --- a/Simple Anki.xcodeproj/project.pbxproj +++ b/Simple Anki.xcodeproj/project.pbxproj @@ -7,67 +7,60 @@ objects = { /* Begin PBXBuildFile section */ - 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 */; }; - C02348CB2861F47E002FFBDF /* UILabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C02348CA2861F47E002FFBDF /* UILabelExtension.swift */; }; + C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00513FA2B517E91005F5815 /* CardPreviewView.swift */; }; + C00BD2642B5A8EA900ACD977 /* CardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00BD2632B5A8EA900ACD977 /* CardRepository.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 */; }; - C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.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 */; }; + C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */; }; + C067B6872A8C1251000AF881 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C067B6862A8C1251000AF881 /* MainView.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 */; }; - 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 */; }; + C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0730B162B77F26A00B43D42 /* NewDeckView.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 */; }; + C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.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 /* 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 */; }; 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 */; }; - 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 */; }; + 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 */; }; 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 */; }; + C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E5546C2AB59C9C000A3CA4 /* RecordingButton.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 */; }; @@ -77,7 +70,9 @@ 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 */; }; + 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 */ /* Begin PBXContainerItemProxy section */ @@ -98,31 +93,40 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 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 = ""; }; - C02348CA2861F47E002FFBDF /* UILabelExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabelExtension.swift; sourceTree = ""; }; + C00513FA2B517E91005F5815 /* CardPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPreviewView.swift; sourceTree = ""; }; + C00BD2632B5A8EA900ACD977 /* CardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardRepository.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 = ""; }; - C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSettingsCell.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 = ""; }; + 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 /* 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 = ""; }; - 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 = ""; }; + C0730B162B77F26A00B43D42 /* NewDeckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeckView.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 = ""; }; + C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManagerSUI.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 /* 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 = ""; }; 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 = ""; }; @@ -132,32 +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 = ""; }; - 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 = ""; }; - C0CBE86F266B7A2900B83253 /* PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManager.swift; sourceTree = ""; }; - C0CBE871266BA50900B83253 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.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 = ""; }; - 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 = ""; }; - 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 = ""; }; + 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 = ""; }; + C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingButton.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 = ""; }; @@ -167,7 +155,9 @@ 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 = ""; }; + 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 */ /* Begin PBXFrameworksBuildPhase section */ @@ -176,11 +166,11 @@ buildActionMask = 2147483647; files = ( C02348D3286343F4002FFBDF /* StoreKit.framework in Frameworks */, + C09AE07E2B6A726B00DB3E28 /* Pow in Frameworks */, C0B85474282AB18F009816D0 /* SQLite in Frameworks */, C0DD10BB2823F6D10058F96B /* SwiftCSV in Frameworks */, C0B5FFAC25E98362001D8D83 /* Realm in Frameworks */, C0B5FFAA25E98362001D8D83 /* RealmSwift in Frameworks */, - C0D1C1702815757A00350862 /* FirebaseAnalytics in Frameworks */, C0E8477827F9C9EA006AEED1 /* SPIndicator in Frameworks */, C0B85477282ABA14009816D0 /* ZIPFoundation in Frameworks */, ); @@ -203,15 +193,28 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - C00648A62681F26000265EB8 /* Cells */ = { + C00BD2622B5A8E9400ACD977 /* Repositories */ = { isa = PBXGroup; children = ( - C05F7C4727E3A29700F4FAB4 /* BaseSettingsCell.swift */, - C0F4FD1B27BD84DA00814BD6 /* SettingsViewCell.swift */, - C0D1C1732815CB9100350862 /* DatePickerViewCell.swift */, - C02D41CF28218E3C005E9835 /* SwitchTableViewCell.swift */, + C00BD2632B5A8EA900ACD977 /* CardRepository.swift */, ); - path = Cells; + path = Repositories; + sourceTree = ""; + }; + C01290712A8F822700ECF9D7 /* Managers */ = { + isa = PBXGroup; + children = ( + C070008D263E86E0006DF020 /* RateManager.swift */, + C0B6D150285DE01A00E354BA /* OnboardingManager.swift */, + C07458222A938CF90046F39D /* UserSettings.swift */, + C0A0C6492A9DFC210015C65E /* SoundManager.swift */, + C0750ECD2A9E9B8F00089B29 /* HapticManagerSUI.swift */, + C0750ECF2AA3A60400089B29 /* AudioRecorder.swift */, + C0750ED72AA3D4AC00089B29 /* ReviewManagerSUI.swift */, + C0CD12522AA9069400FE3BB6 /* LocalFileManager.swift */, + C0F730C12B7424D900127CB4 /* ReminderManagerSUI.swift */, + ); + path = Managers; sourceTree = ""; }; C02348D1286343F4002FFBDF /* Frameworks */ = { @@ -222,79 +225,105 @@ name = Frameworks; sourceTree = ""; }; - C05AA2B025E9891E00E40346 /* Controllers */ = { + C025748A2B4B1CE900F8EE29 /* ViewModels */ = { + isa = PBXGroup; + children = ( + C025748C2B4B1D4000F8EE29 /* CardViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + C025748B2B4B1CFB00F8EE29 /* UI */ = { + isa = PBXGroup; + children = ( + C00BD2622B5A8E9400ACD977 /* Repositories */, + C07458262A939F800046F39D /* UIComponents */, + C0750ED92AA492BD00089B29 /* Views */, + ); + path = UI; + sourceTree = ""; + }; + C067B6822A8C10D0000AF881 /* SwiftUI */ = { + isa = PBXGroup; + children = ( + C025748B2B4B1CFB00F8EE29 /* UI */, + C025748A2B4B1CE900F8EE29 /* ViewModels */, + C01290712A8F822700ECF9D7 /* Managers */, + C08903232A8D443900EFC51C /* Models */, + C067B6842A8C1235000AF881 /* SimpleAnkiApp.swift */, + ); + path = SwiftUI; + sourceTree = ""; + }; + C0730B182B77F28900B43D42 /* Deck */ = { 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 */, + C03B91C72B6FA60500EA483F /* DeckSettingsView.swift */, + C0730B162B77F26A00B43D42 /* NewDeckView.swift */, + C067B6882A8C139A000AF881 /* DecksListView.swift */, + C05BCFEF2B78CD3F0076CAD9 /* CreateDeckView.swift */, + C05BCFF12B78CF4D0076CAD9 /* ImportDeckView.swift */, + C05BCFF52B78D12C0076CAD9 /* ImportedDeckView.swift */, ); - path = Controllers; + path = Deck; sourceTree = ""; }; - C078B5F9281EA89500DE5A86 /* Reminder */ = { + C0730B192B77F2A200B43D42 /* Card */ = { isa = PBXGroup; children = ( - C0C043E228206B1F00A8AC6D /* WeekdaysViewController.swift */, - C0D1C1712815A57600350862 /* ReminderViewController.swift */, + C00513FA2B517E91005F5815 /* CardPreviewView.swift */, + C0CD12542AAB3F2700FE3BB6 /* CardView.swift */, + C074581E2A9293AA0046F39D /* CardsListView.swift */, ); - path = Reminder; + path = Card; sourceTree = ""; }; - C078B5FA281EA8B300DE5A86 /* Settings */ = { + C0730B1A2B77F2B700B43D42 /* Settings */ = { isa = PBXGroup; children = ( - C0F4FD2827BD855A00814BD6 /* SettingsTableViewController.swift */, - C078B5F9281EA89500DE5A86 /* Reminder */, + C0F730C32B752F7100127CB4 /* ReminderView.swift */, + C067B68A2A8C13A5000AF881 /* SettingsView.swift */, ); path = Settings; sourceTree = ""; }; - C0ADAD5625EC19D3005DE503 /* Extensions */ = { + C07458262A939F800046F39D /* UIComponents */ = { isa = PBXGroup; children = ( - C0CBE871266BA50900B83253 /* URLExtension.swift */, - C00648A92682060F00265EB8 /* UIApplicationExtension.swift */, - C0F4FD1427BD82DF00814BD6 /* UIViewExtension.swift */, - C02D41C2282146BF005E9835 /* DateExtension.swift */, - C00D75CB282E68A7004E92B2 /* UIButtonExtension.swift */, - C02348CA2861F47E002FFBDF /* UILabelExtension.swift */, + C0750ED52AA3C39400089B29 /* CircleButton.swift */, + C07458272A939FA40046F39D /* LinkView.swift */, + C0E5546C2AB59C9C000A3CA4 /* RecordingButton.swift */, + C0CC42522B5052C000F683CE /* CardToolbarView.swift */, + C094794F2B518EFC00C44BCF /* ImagePickerButton.swift */, + C03A02602B5299EF00576543 /* CardViewState.swift */, + C02404072B6835930017EF2E /* TabItemView.swift */, + C03B91C92B6FA82400EA483F /* LayoutButton.swift */, + C05BCFF32B78D04D0076CAD9 /* DelimeterButton.swift */, ); - path = Extensions; + path = UIComponents; sourceTree = ""; }; - C0AF0E7B2614F1AD00853FA7 /* Managers */ = { + C0750ED92AA492BD00089B29 /* Views */ = { isa = PBXGroup; children = ( - C0CBE86D266AAA9A00B83253 /* Utils.swift */, - C0CBE86F266B7A2900B83253 /* PlayerManager.swift */, - C0C081B325ED379600DF1083 /* StorageManager.swift */, - C0AF0E7C2614F1E000853FA7 /* ReviewManager.swift */, - C070008D263E86E0006DF020 /* RateManager.swift */, - C0DDE0F22658608C0051059B /* RecorderManager.swift */, - C0DBE8312673E9E00074DC13 /* HapticManager.swift */, - C00648AB2682070200265EB8 /* EmailManager.swift */, - C0A70E5F28211E620020D533 /* ReminderManager.swift */, - C0B6D150285DE01A00E354BA /* OnboardingManager.swift */, + C0730B1A2B77F2B700B43D42 /* Settings */, + C0730B192B77F2A200B43D42 /* Card */, + C0730B182B77F28900B43D42 /* Deck */, + C067B6862A8C1251000AF881 /* MainView.swift */, + C07458242A93943E0046F39D /* Constants.swift */, + C0750ED32AA3C01800089B29 /* ReviewView.swift */, + C05695D32B501AAA00033AF2 /* ContentView.swift */, ); - path = Managers; + path = Views; sourceTree = ""; }; - C0B1275227C3F0CB008E412D /* Decks */ = { + C08903232A8D443900EFC51C /* Models */ = { isa = PBXGroup; children = ( - C0C9BA8B2848B73C0020E555 /* Import */, - C0B1275427C3F0E7008E412D /* DecksViewModel.swift */, - C0B1275327C3F0D4008E412D /* DecksTableViewController.swift */, - C028C4C92811C0F000F6894E /* NewDeckViewController.swift */, + C08903242A8D444F00EFC51C /* Deck.swift */, + C08903262A8D474900EFC51C /* Card.swift */, ); - path = Decks; + path = Models; sourceTree = ""; }; C0B5FF6925E981A8001D8D83 = { @@ -324,20 +353,12 @@ C0B5FF7425E981A8001D8D83 /* Simple Anki */ = { isa = PBXGroup; 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 */, - C0C9BABF2848BE2F0020E555 /* GoogleService-Info.plist */, - C0FA7EA925F511FE00710F0D /* Constants.swift */, + C0FA7EA925F511FE00710F0D /* ConstantsOld.swift */, ); path = "Simple Anki"; sourceTree = ""; @@ -364,14 +385,6 @@ path = "Simple AnkiUITests"; sourceTree = ""; }; - C0B6D154285E2DD100E354BA /* UIComponents */ = { - isa = PBXGroup; - children = ( - C0B6D155285E2DFD00E354BA /* FeatureView.swift */, - ); - path = UIComponents; - sourceTree = ""; - }; C0C9BA702848AC300020E555 /* .github */ = { isa = PBXGroup; children = ( @@ -389,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 = ( @@ -409,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 = ( @@ -484,10 +469,10 @@ C0B5FFA925E98362001D8D83 /* RealmSwift */, C0B5FFAB25E98362001D8D83 /* Realm */, C0E8477727F9C9EA006AEED1 /* SPIndicator */, - C0D1C16F2815757A00350862 /* FirebaseAnalytics */, C0DD10BA2823F6D10058F96B /* SwiftCSV */, C0B85473282AB18F009816D0 /* SQLite */, C0B85476282ABA14009816D0 /* ZIPFoundation */, + C09AE07D2B6A726B00DB3E28 /* Pow */, ); productName = "Simple Anki"; productReference = C0B5FF7225E981A8001D8D83 /* Simple Anki.app */; @@ -535,8 +520,9 @@ C0B5FF6A25E981A8001D8D83 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1340; + LastUpgradeCheck = 1430; TargetAttributes = { C0B5FF7125E981A8001D8D83 = { CreatedOnToolsVersion = 12.4; @@ -563,10 +549,10 @@ packageReferences = ( C0B5FFA825E98362001D8D83 /* XCRemoteSwiftPackageReference "realm-cocoa" */, C0E8477627F9C9EA006AEED1 /* XCRemoteSwiftPackageReference "SPIndicator" */, - C0D1C16E2815757A00350862 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, C0DD10B92823F6D10058F96B /* XCRemoteSwiftPackageReference "SwiftCSV" */, C0B85472282AB18F009816D0 /* XCRemoteSwiftPackageReference "SQLite.swift" */, C0B85475282ABA14009816D0 /* XCRemoteSwiftPackageReference "ZIPFoundation" */, + C09AE07C2B6A726B00DB3E28 /* XCRemoteSwiftPackageReference "Pow" */, ); productRefGroup = C0B5FF7325E981A8001D8D83 /* Products */; projectDirPath = ""; @@ -588,7 +574,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 */, ); @@ -636,52 +621,48 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C0ADAD4225EC0E0B005DE503 /* Deck.swift in Sources */, C0C9BAB62848B9F20020E555 /* APKGModel.swift in Sources */, - C02D41C3282146BF005E9835 /* DateExtension.swift in Sources */, - C0A70E6028211E620020D533 /* ReminderManager.swift in Sources */, - C0CBE872266BA50900B83253 /* URLExtension.swift in Sources */, - C0FA7EAA25F511FE00710F0D /* Constants.swift in Sources */, - C0C043E328206B1F00A8AC6D /* WeekdaysViewController.swift in Sources */, - C0F4FD1F27BD84DA00814BD6 /* SettingsViewCell.swift in Sources */, - C02D41D028218E3C005E9835 /* SwitchTableViewCell.swift in Sources */, - C0B1275527C3F0E7008E412D /* DecksViewModel.swift in Sources */, - C0AF0E7D2614F1E000853FA7 /* ReviewManager.swift in Sources */, - C0F4FD3027BD855A00814BD6 /* SettingsTableViewController.swift in Sources */, - C0D1C1722815A57600350862 /* ReminderViewController.swift in Sources */, - C0DBE8322673E9E00074DC13 /* HapticManager.swift in Sources */, - C0B1275E27C3FC4A008E412D /* DecksTableViewController.swift in Sources */, - C0CBE86E266AAA9A00B83253 /* Utils.swift in Sources */, - C02348D728671E16002FFBDF /* LaunchScreenViewController.swift in Sources */, + C0F730C42B752F7100127CB4 /* ReminderView.swift in Sources */, + C0F730C22B7424D900127CB4 /* ReminderManagerSUI.swift in Sources */, + C07458252A93943E0046F39D /* Constants.swift in Sources */, + C067B6892A8C139A000AF881 /* DecksListView.swift in Sources */, + C0FA7EAA25F511FE00710F0D /* ConstantsOld.swift in Sources */, + C09479502B518EFC00C44BCF /* ImagePickerButton.swift in Sources */, + C05BCFF22B78CF4D0076CAD9 /* ImportDeckView.swift in Sources */, + C025748D2B4B1D4000F8EE29 /* CardViewModel.swift in Sources */, + C0CC42532B5052C000F683CE /* CardToolbarView.swift in Sources */, + C0750ED82AA3D4AC00089B29 /* ReviewManagerSUI.swift in Sources */, + C05BCFF42B78D04D0076CAD9 /* DelimeterButton.swift in Sources */, + C0750ED02AA3A60400089B29 /* AudioRecorder.swift in Sources */, + C067B68B2A8C13A5000AF881 /* SettingsView.swift in Sources */, + C0750ED62AA3C39400089B29 /* CircleButton.swift in Sources */, C0C9BAAB2848B9410020E555 /* APKGManager.swift in Sources */, - C028C4CA2811C0F000F6894E /* NewDeckViewController.swift in Sources */, - C00648AA2682060F00265EB8 /* UIApplicationExtension.swift in Sources */, - C0CBE870266B7A2900B83253 /* PlayerManager.swift in Sources */, - C0C9BAA52848B9120020E555 /* ImportedCardsCollectionViewController.swift in Sources */, + C0CD12532AA9069400FE3BB6 /* LocalFileManager.swift in Sources */, + C03B91C82B6FA60500EA483F /* DeckSettingsView.swift in Sources */, + C0E5546D2AB59C9C000A3CA4 /* RecordingButton.swift in Sources */, C070008E263E86E0006DF020 /* RateManager.swift in Sources */, - C0C9BAA42848B9120020E555 /* ImportedCardCollectionViewCell.swift in Sources */, - C0B1275727C3FB7A008E412D /* CardsTableViewController.swift in Sources */, - C00648AC2682070200265EB8 /* EmailManager.swift in Sources */, - C0C081B425ED379600DF1083 /* StorageManager.swift in Sources */, - 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 */, - C0B6D156285E2DFD00E354BA /* FeatureView.swift in Sources */, + C0A0C64A2A9DFC210015C65E /* SoundManager.swift in Sources */, + C067B6852A8C1235000AF881 /* SimpleAnkiApp.swift in Sources */, + C067B6872A8C1251000AF881 /* MainView.swift in Sources */, + C00BD2642B5A8EA900ACD977 /* CardRepository.swift in Sources */, + C03A02612B5299EF00576543 /* CardViewState.swift in Sources */, + C05695D42B501AAA00033AF2 /* ContentView.swift in Sources */, + C07458232A938CF90046F39D /* UserSettings.swift in Sources */, C0C9BABE2848BD460020E555 /* APKGDatabase.swift in Sources */, - C0ADAD4725EC0E62005DE503 /* Card.swift in Sources */, - C0F4FD3127BD855A00814BD6 /* MainTabBarViewController.swift in Sources */, - C0D1C1742815CB9100350862 /* DatePickerViewCell.swift in Sources */, - C05F7C4827E3A29700F4FAB4 /* BaseSettingsCell.swift in Sources */, - C0C9BAB82848BA160020E555 /* ImportViewController.swift in Sources */, - C0B5FF7625E981A8001D8D83 /* AppDelegate.swift in Sources */, - C00648AF2682140800265EB8 /* Options.swift in Sources */, - C00D75CC282E68A7004E92B2 /* UIButtonExtension.swift in Sources */, - C0B5FF7825E981A8001D8D83 /* SceneDelegate.swift in Sources */, + C05BCFF02B78CD3F0076CAD9 /* CreateDeckView.swift in Sources */, + C074581F2A9293AA0046F39D /* CardsListView.swift in Sources */, + C08903272A8D474900EFC51C /* Card.swift in Sources */, + C0730B172B77F26A00B43D42 /* NewDeckView.swift in Sources */, C0B6D151285DE01A00E354BA /* OnboardingManager.swift in Sources */, - C028C4CC2811C71500F6894E /* EmptyState.swift in Sources */, - C0E8477527F9AEF4006AEED1 /* NewCardViewController.swift in Sources */, + C0750ED42AA3C01800089B29 /* ReviewView.swift in Sources */, + C03B91CA2B6FA82400EA483F /* LayoutButton.swift in Sources */, + C00513FB2B517E91005F5815 /* CardPreviewView.swift in Sources */, + C08903252A8D444F00EFC51C /* Deck.swift in Sources */, + C05BCFF62B78D12C0076CAD9 /* ImportedDeckView.swift in Sources */, + C02404082B6835930017EF2E /* TabItemView.swift in Sources */, + C07458282A939FA40046F39D /* LinkView.swift in Sources */, + C0750ECE2A9E9B8F00089B29 /* HapticManagerSUI.swift in Sources */, + C0CD12552AAB3F2700FE3BB6 /* CardView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -787,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; @@ -842,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; @@ -860,19 +841,24 @@ 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"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(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; }; @@ -886,19 +872,24 @@ 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"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(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; }; @@ -1034,6 +1025,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"; @@ -1058,14 +1057,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"; @@ -1085,6 +1076,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" */; @@ -1105,11 +1101,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 @@ ]+>", 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 d39de54..0000000 --- a/Simple Anki/AppDelegate.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// AppDelegate.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit -import RealmSwift -import FirebaseCore -import Firebase -import FirebaseAnalytics - -@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, -// 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 0000000..3b86708 Binary files /dev/null and b/Simple Anki/Assets.xcassets/AppIcon 2.appiconset/1024.png differ 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/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/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/Constants.swift b/Simple Anki/ConstantsOld.swift similarity index 76% rename from Simple Anki/Constants.swift rename to Simple Anki/ConstantsOld.swift index 0ce44b7..560885a 100644 --- a/Simple Anki/Constants.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" @@ -46,5 +53,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/Controllers/CardsTableViewController.swift b/Simple Anki/Controllers/CardsTableViewController.swift deleted file mode 100644 index 622d552..0000000 --- a/Simple Anki/Controllers/CardsTableViewController.swift +++ /dev/null @@ -1,391 +0,0 @@ -// -// CardsTableViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 28.02.2021. -// - -import UIKit -import RealmSwift -import FirebaseAnalytics - -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].parentDeck.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 - } - if let deck = selectedDeck { - Analytics.logEvent("deck_layout", parameters: [ - "layout": deck.layout as NSObject, - "name": deck.name - ]) - } - - 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 de6a8ad..0000000 --- a/Simple Anki/Controllers/Cells/DatePickerViewCell.swift +++ /dev/null @@ -1,57 +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 prepareForReuse() { - super.prepareForReuse() - } - - 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 6adf433..0000000 --- a/Simple Anki/Controllers/Decks/DecksTableViewController.swift +++ /dev/null @@ -1,258 +0,0 @@ -// -// DecksTableViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit -import RealmSwift -import FirebaseAnalytics - -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() { - Analytics.logEvent("import_deck_tapped", parameters: nil) - 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 139662b..0000000 --- a/Simple Anki/Controllers/Decks/Import/ImportViewController.swift +++ /dev/null @@ -1,329 +0,0 @@ -// -// ImportDeckViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 07.05.2022. -// - -import UIKit -import UniformTypeIdentifiers -import SwiftCSV -import FirebaseAnalytics - -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) - Analytics.logEvent("close_import", parameters: nil) - } - - @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() - // MARK: Log event - Analytics.logEvent("import_deck_tapped", parameters: [ - "file_type" : self.selectedFile.rawValue as NSObject - ]) - } - - @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 65fb7e0..0000000 --- a/Simple Anki/Controllers/Decks/Import/ImportedCardsCollectionViewController.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// ImoptedDeckViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 09.05.2022. -// - -import UIKit -import FirebaseAnalytics - -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) - } - 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) - } - - 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 56ba4f7..0000000 --- a/Simple Anki/Controllers/MainTabBarViewController.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 21.11.2021. -// - -import UIKit - -class MainTabBarViewController: UITabBarController { - - override func viewDidLoad() { - super.viewDidLoad() - configureTabBar() -// showOnboardingScreen() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - } - - 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 a96d2e6..0000000 --- a/Simple Anki/Controllers/Settings/Reminder/ReminderViewController.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// ReminderViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 24.04.2022. -// - -import UIKit -import FirebaseAnalytics - -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 = [Section]() - 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(Section(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") - } - Analytics.logEvent("reminder", parameters: [ - "is_on" : switchCell.mySwitch.isOn as NSObject - ]) - } -} - -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) - 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 deleted file mode 100644 index f5fc28b..0000000 --- a/Simple Anki/Controllers/Settings/Reminder/WeekdaysViewController.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// WeekdaysViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 02.05.2022. -// - -import UIKit -import FirebaseAnalytics - -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 - 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 deleted file mode 100644 index 2fc7db7..0000000 --- a/Simple Anki/Controllers/Settings/SettingsTableViewController.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// SettingsViewController.swift -// Simple Anki -// -// Created by Астемир Бозиев on 22.06.2021. -// - -import UIKit -import StoreKit -import FirebaseAnalytics - -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 = [Section]() - - 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(Section(title: K.Settings.appearence, options: [ - .switchCell(model: SwitchOption( - title: K.Settings.darkMode, - icon: UIImage(systemName: K.Icon.lefthalf), - isOn: darkMode, - handler: nil)) - ])) - - models.append(Section(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(Section(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): - Analytics.logEvent(model.title, parameters: nil) - 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 - } - Analytics.logEvent("dark_mode", parameters: [ - "is_on" : switchCell.mySwitch.isOn as NSObject - ]) - } -} diff --git a/Simple Anki/Extensions/DateExtension.swift b/Simple Anki/Extensions/DateExtension.swift deleted file mode 100644 index 006d072..0000000 --- a/Simple Anki/Extensions/DateExtension.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// DateExtension.swift -// Simple Anki -// -// Created by Астемир Бозиев on 03.05.2022. -// - -import Foundation - -extension Date { - func component(_ component: Calendar.Component) -> Int { - Calendar.current.component(component, from: self) - } -} 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/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/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/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/Managers/ReminderManager.swift b/Simple Anki/Managers/ReminderManager.swift deleted file mode 100644 index d854ed3..0000000 --- a/Simple Anki/Managers/ReminderManager.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// ReminderManager.swift -// Simple Anki -// -// Created by Астемир Бозиев on 03.05.2022. -// - -import Foundation -import UserNotifications - -struct Weekday { - 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 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 de32a4a..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 36a2483..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 44fae8e..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 Section { - 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/SwiftUI/Managers/AudioRecorder.swift b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift new file mode 100644 index 0000000..69ec5d1 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/AudioRecorder.swift @@ -0,0 +1,112 @@ +// +// RecorderManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 02.09.2023. +// + +import Foundation +import AVFoundation + +@Observable +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() + } + + private let settings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue, + AVSampleRateKey: 44100.0, + AVNumberOfChannelsKey: 2 + ] + + func startRecording() { + do { + 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 + audioRecorder?.prepareToRecord() + audioRecorder?.record() + isRecording = true + } catch { + print("Error setting up audio recorder: \(error.localizedDescription)") + } + } + + func stopRecording(completion: @escaping (String) -> Void) { + audioRecorder?.stop() + + isRecording = false + isPlaybackReady = true + completion(fileName) + } + + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if flag { + isPlaybackReady = true + } + } + + func checkRecordPermission(completion: @escaping (Bool) -> Void) { + switch AVAudioApplication.shared.recordPermission { + case .granted: + completion(true) + case .denied: + completion(false) + case .undetermined: + AVAudioApplication.requestRecordPermission { granted in + DispatchQueue.main.async { + completion(granted) + } + } + default: + completion(false) + } + } + + func deleteRecording() { + let url = FileManager.documentsDirectory.appendingPathComponent(fileName) + + do { + try FileManager.default.removeItem(at: url) + } catch { + print(error) + } + } + + func setName(_ name: String) { + fileName = name + } + + 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)") + } + } +} diff --git a/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift b/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift new file mode 100644 index 0000000..b53fa01 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/HapticManagerSUI.swift @@ -0,0 +1,26 @@ +// +// HapticManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 30.08.2023. +// + +import Foundation +import SwiftUI + +class HapticManagerSUI { + + static let shared = HapticManagerSUI() + + private init() { } + + 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..52c8fd4 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/LocalFileManager.swift @@ -0,0 +1,67 @@ +// +// LocalFileManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 06.09.2023. +// + +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] + } + + func delete(_ fileName: String?) { + guard let fileName = fileName else { return } + do { + try Self.default.removeItem(at: Self.documentsDirectory.appendingPathComponent(fileName)) + } 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/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 new file mode 100644 index 0000000..ca791c6 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/ReminderManagerSUI.swift @@ -0,0 +1,184 @@ +// +// ReminderManagerSUI.swift +// Simple Anki +// +// Created by Астемир Бозиев on 08.02.2024. +// + +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 { + + @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 new file mode 100644 index 0000000..ecdaac0 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/ReviewManagerSUI.swift @@ -0,0 +1,53 @@ +// +// ReviewManagerSUI.swift +// Simple Anki +// +// Created by Астемир Бозиев on 03.09.2023. +// + +import Foundation +import RealmSwift + +@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() { + guard !deck.cards.isEmpty else { + return + } + + currentIndex = 0 + currentCard = self.cards[currentIndex] + isReviewing = true + } + + func nextCard() { + currentIndex += 1 + + 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 new file mode 100644 index 0000000..c1c08b2 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/SoundManager.swift @@ -0,0 +1,56 @@ +// +// SoundManager.swift +// Simple Anki +// +// Created by Астемир Бозиев on 29.08.2023. +// + +import Foundation +import AVKit + +class SoundManager { + + static let shared = SoundManager() + private var player: AVAudioPlayer? + private var audioSession = AVAudioSession.sharedInstance() + + private init() { + setupPlayer() + } + + private func setupPlayer() { + DispatchQueue.global(qos: .background).async { + do { + try self.audioSession.setCategory(.playback) + try self.audioSession.setActive(true, options: .notifyOthersOnDeactivation) + } catch { + print(error.localizedDescription) + } + } + } + + func play(sound: String) { + let url = FileManager.documentsDirectory.appendingPathComponent(sound) + 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) { + 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/Managers/UserSettings.swift b/Simple Anki/SwiftUI/Managers/UserSettings.swift new file mode 100644 index 0000000..fb42aa6 --- /dev/null +++ b/Simple Anki/SwiftUI/Managers/UserSettings.swift @@ -0,0 +1,13 @@ +// +// UserSettings.swift +// Simple Anki +// +// Created by Астемир Бозиев on 21.08.2023. +// + +import Foundation +import SwiftUI + +class UserSettings: ObservableObject { + @AppStorage("colorScheme") var colorScheme: Bool = false +} diff --git a/Simple Anki/SwiftUI/Models/Card.swift b/Simple Anki/SwiftUI/Models/Card.swift new file mode 100644 index 0000000..0997a37 --- /dev/null +++ b/Simple Anki/SwiftUI/Models/Card.swift @@ -0,0 +1,32 @@ +// +// 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 image: 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, audioName: String? = nil, image: String? = nil) { + self.init() + self.front = front + self.back = back + self.audioName = audioName + self.image = image + } +} + +extension Card { + static var card: Card = Card(front: "front", back: "back") +} diff --git a/Simple Anki/SwiftUI/Models/Deck.swift b/Simple Anki/SwiftUI/Models/Deck.swift new file mode 100644 index 0000000..edab777 --- /dev/null +++ b/Simple Anki/SwiftUI/Models/Deck.swift @@ -0,0 +1,43 @@ +// +// 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(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) { + self.init() + 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 new file mode 100644 index 0000000..91b77f0 --- /dev/null +++ b/Simple Anki/SwiftUI/SimpleAnkiApp.swift @@ -0,0 +1,34 @@ +// +// SimpleAnkiApp.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI +import RealmSwift + +@main +struct SimpleAnkiApp: SwiftUI.App { + @StateObject var userSettings = UserSettings() + + var body: some Scene { + WindowGroup { + MainView() + .environment(\.realmConfiguration, Realm.Configuration(schemaVersion: 2, migrationBlock: { migration, oldSchemaVersion in + if oldSchemaVersion < 2 { + migration.enumerateObjects(ofType: Deck.className()) { _, newObject in + newObject!["shuffled"] = false + } + migration.enumerateObjects(ofType: Card.className()) { _, newObject in + newObject!["image"] = nil + } + } + })) + .preferredColorScheme(userSettings.colorScheme ? .dark : .light) + .onAppear { + print(NSHomeDirectory()) + } + } + } +} diff --git a/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift b/Simple Anki/SwiftUI/UI/Repositories/CardRepository.swift new file mode 100644 index 0000000..6c9f525 --- /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 fetchCard(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 fetchCard(by id: ObjectId) -> Card? { + return realm.object(ofType: Card.self, forPrimaryKey: id) + } + + func add(card: Card) { + guard let deck = fetchDeck(by: deck._id) else { return } + try! realm.write { + deck.cards.append(card) + } + } + + func update(card: Card) { + guard let cardToUpdate = fetchCard(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 fetchDeck(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/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/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/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/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/ImagePickerButton.swift b/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift new file mode 100644 index 0000000..876a8fb --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/ImagePickerButton.swift @@ -0,0 +1,58 @@ +//// +//// 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 { + 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(image: .constant(UIImage(data: Data()))) +} 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/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..84ff7a7 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/UIComponents/RecordingButton.swift @@ -0,0 +1,47 @@ +// +// RecordingButton.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.09.2023. +// + +import SwiftUI +import RealmSwift +import AVFoundation + +// 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/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/Card/CardPreviewView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardPreviewView.swift new file mode 100644 index 0000000..a373c37 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardPreviewView.swift @@ -0,0 +1,98 @@ +// +// 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 ?? "") + } + } + .lineLimit(2) + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + .onTapGesture { + SoundManager.shared.play(sound: audio ?? "") + } + .font(.system(size: 35, weight: .medium)) + .padding() + + VStack { + Spacer() + + 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) { + 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/Card/CardView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift new file mode 100644 index 0000000..349e54b --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardView.swift @@ -0,0 +1,225 @@ +// +// CardView.swift +// Simple Anki +// +// Created by Astemir Boziev on 08.09.2023. +// + +import SwiftUI +import RealmSwift +import PhotosUI +import Pow + +enum FocusableField: Hashable { + case frontField + case backField +} + +struct CardView: View { + @State var viewModel: CardViewModel + @State var recorder: AudioRecorder + + @State private var selectedImage: PhotosPickerItem? + @State private var isPreviewPresented: Bool = false + @State private var showAlert: Bool = false + @State private var addButtonTapped: Bool = false + @FocusState private var focusedField: FocusableField? + + private var transaction: Transaction { + var transaction = Transaction() + transaction.disablesAnimations = true + return transaction + } + + @Environment(\.dismiss) private var dismiss + + var body: some View { + // MARK: VSTACK START + VStack { + Spacer() + + // MARK: VSTACK START + 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) + } // MARK: VSTACK END + .font(.system(size: 35, weight: .medium)) + .multilineTextAlignment(.center) + .padding() + .onAppear { + focusedField = .frontField + } + + Spacer() + + // MARK: ZSTACK START + ZStack { + // MARK: HSTACK START + HStack { + Group { + ImagePickerButton(image: $viewModel.image) + .hidden() + + Spacer() + + Button { + guard !viewModel.frontWord.isEmpty else { return } + if viewModel.updating { + viewModel.updateCard() + } else { + viewModel.addCard() + viewModel.clear() + recorder.setName(UUID().uuidString + ".m4a") + } + withAnimation { + addButtonTapped.toggle() + } + focusedField = .frontField + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + 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: addButtonTapped) + .controlSize(.extraLarge) + .buttonStyle(.borderedProminent) + } + .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 { + SoundManager.shared.play(sound: fileName) + } label: { + Image(systemName: K.Icon.playCircle) + .font(.system(size: 18)) + } + .contextMenu { + Button(role: .destructive) { + deleteAudioAndUpdateCard() + } label: { + Label("Delete", systemImage: K.Icon.trash) + } + } + } else { + Button { + HapticManagerSUI.shared.impact(style: .heavy) + + if recorder.isRecording { + recorder.stopRecording { fileName in + viewModel.audioName = fileName + } + } else { + recorder.checkRecordPermission { granted in + if granted { + DispatchQueue.global(qos: .background).async { + recorder.startRecording() + } + } else { + showAlert.toggle() + } + } + } + } label: { + 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)) + } + .alert("No access", isPresented: $showAlert, actions: { + Button("Cancel", role: .cancel, action: {}) + Button("Open settings", role: .none) { + } + }) + } + } // MARK: HSTACK END + .padding(.horizontal) + .padding(.vertical, 7) + .frame(maxWidth: .infinity) + + Text("Recording...") + .padding(.bottom, 3) + .foregroundStyle(.gray) + .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) { + Task { + let data = try? await selectedImage?.loadTransferable(type: Data.self) + viewModel.image = UIImage(data: data ?? Data()) + } + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Preview") { + withTransaction(transaction) { + isPreviewPresented.toggle() + } + } + .disabled(viewModel.incomplete) + .fullScreenCover(isPresented: $isPreviewPresented) { + CardPreviewView( + front: viewModel.frontWord, + back: viewModel.backWord, + image: viewModel.image, + audio: viewModel.audioName, + isPreviewPresented: $isPreviewPresented + ) + } + } + + ToolbarItem(placement: .cancellationAction) { + Button { + if !viewModel.updating { + FileManager.default.delete(viewModel.audioName) + } + 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") + } + } + + private func deleteAudioAndUpdateCard() { + viewModel.audioName = nil + recorder.deleteRecording() + viewModel.updateCard() + } +} + +struct CardView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + CardView(viewModel: CardViewModel(repositry: CardRepository(deck: Deck.deck1)), recorder: AudioRecorder(fileName: UUID().uuidString + ".m4a")) + } + } +} diff --git a/Simple Anki/SwiftUI/UI/Views/Card/CardsListView.swift b/Simple Anki/SwiftUI/UI/Views/Card/CardsListView.swift new file mode 100644 index 0000000..2330bf0 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Card/CardsListView.swift @@ -0,0 +1,151 @@ +// +// CardsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 20.08.2023. +// + +import SwiftUI +import RealmSwift + +struct CardsListView: View { + @ObservedRealmObject var deck: Deck + @State private var isCardViewPresented: Bool = false + @State private var isReviewPresented: Bool = false + @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: K.Icon.noCards) + }, 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: K.Icon.trash) + } + .tint(.red) + } + } + .onMove(perform: $deck.cards.move) + } + .listStyle(.plain) + + Divider() + + HStack(spacing: 10) { + Button { + isDeckSettingsPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: K.Icon.gearshape) + .padding(.vertical, 8) + .padding(.horizontal, 4) + } + .tint(.blue) + .buttonStyle(.bordered) + .sheet(isPresented: $isDeckSettingsPresented, onDismiss: { + if deckIsRemoved(deck: deck) { + dismiss() + } + }, 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: K.Icon.plusCircleFill) + } + .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 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) + } +} + +struct CardsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + CardsListView(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..b4f1ece --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/ContentView.swift @@ -0,0 +1,8 @@ +// +// TransitionDemo.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.01.2024. +// + +import SwiftUI 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..231169d --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/CreateDeckView.swift @@ -0,0 +1,76 @@ +// +// CreateDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 11.02.2024. +// + +import SwiftUI +import RealmSwift + +struct CreateDeckView: View { + @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 + + 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..bb0aba8 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DeckSettingsView.swift @@ -0,0 +1,93 @@ +// +// DeckSettingsView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 04.02.2024. +// + +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 + + var body: some View { + NavigationStack { + VStack { + List { + Section("Name") { + TextField("Enter name", text: $deck.name) + .submitLabel(.done) + } + 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("Cards order") { + Toggle(isOn: $deck.shuffled, label: { + Label("Shuffle", systemImage: "shuffle") + }) + } + + Section { + Button(action: { + 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") + } + } + } + } + } + .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/DecksListView.swift b/Simple Anki/SwiftUI/UI/Views/Deck/DecksListView.swift new file mode 100644 index 0000000..daf8942 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/DecksListView.swift @@ -0,0 +1,109 @@ +// +// DecksView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI +import RealmSwift + +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 { + 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 { + CardsListView(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 { + isNewDeckViewPresented.toggle() + HapticManagerSUI.shared.impact(style: .heavy) + } label: { + Image(systemName: "plus.circle.fill") + } + .sheet(isPresented: $isNewDeckViewPresented) { + NewDeckView() + .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 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 { + DecksListView() +} 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..62d6c20 --- /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: \Deck.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..572a087 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/Deck/NewDeckView.swift @@ -0,0 +1,40 @@ +// +// NewDeckView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 10.02.2024. +// + +import SwiftUI + +struct NewDeckView: View { + @State private var selectedSegment: Int = 0 + @Environment(\.dismiss) var dismiss + + 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) + } + } + } + } +} + +#Preview { + NewDeckView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/MainView.swift b/Simple Anki/SwiftUI/UI/Views/MainView.swift new file mode 100644 index 0000000..b0a4447 --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/MainView.swift @@ -0,0 +1,56 @@ +// +// MainView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 16.08.2023. +// + +import SwiftUI + +struct MainView: View { + @State private var selectedTab: Tab = .first + + var body: some View { + VStack(spacing: 0) { + ZStack { + switch selectedTab { + case .first: + NavigationStack { + VStack(spacing: 0) { + DecksListView() + TabBarView() + } + } + case .second: + 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) + } +} + +#Preview { + MainView() +} diff --git a/Simple Anki/SwiftUI/UI/Views/ReviewView.swift b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift new file mode 100644 index 0000000..8907f0d --- /dev/null +++ b/Simple Anki/SwiftUI/UI/Views/ReviewView.swift @@ -0,0 +1,129 @@ +// +// ReviewView.swift +// Simple Anki +// +// Created by Астемир Бозиев on 02.09.2023. +// + +import SwiftUI + +struct ReviewView: View { + @Environment(\.dismiss) var dismiss + + @State private var isAnswerPresented: Bool = false + @State var reviewManager: ReviewManagerSUI + + 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 ?? "") + + Group { + Divider() + Text(reviewManager.currentCard?.back ?? "No back text") + } + .opacity(isAnswerPresented ? 1 : 0) + } else { + Text("Finished!") + .font(.system(size: 60, weight: .bold)) + } + } + .scaledToFit() + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + .onTapGesture { + if reviewManager.isReviewing { + playPronunciation() + } + } + .font(.system(size: 40, weight: .medium)) + .padding() + .onAppear { + reviewManager.startReview() + if reviewManager.isAutoplayOn { + playPronunciation() + } + } + .onChange(of: reviewManager.currentCard) { + if reviewManager.isAutoplayOn { + playPronunciation() + } + } + + VStack { + Spacer() + if reviewManager.isReviewing { + Button { + if isAnswerPresented { + reviewManager.nextCard() + isAnswerPresented = false + } else { + isAnswerPresented = true + } + HapticManagerSUI.shared.impact(style: .medium) + } label: { + Image(systemName: isAnswerPresented ? "arrow.right.circle.fill" : "eye.circle.fill") + } + } else { + Button { + reviewManager.startReview() + isAnswerPresented = false + HapticManagerSUI.shared.impact(style: .medium) + } label: { + Image(systemName: "repeat.circle.fill") + } + } + } + .font(.system(size: 70)) + .foregroundColor(.gray.opacity(0.6)) + .padding(.bottom) + } + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .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 { + DispatchQueue.global(qos: .background).async { + SoundManager.shared.play(sound: audioName) + } + } + } +} + +struct ReviewView_Previews: PreviewProvider { + static var previews: some View { + 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..c8c21b8 --- /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(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/ViewModels/CardViewModel.swift b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift new file mode 100644 index 0000000..1a79b0a --- /dev/null +++ b/Simple Anki/SwiftUI/ViewModels/CardViewModel.swift @@ -0,0 +1,75 @@ +// +// CardViewModel.swift +// Simple Anki +// +// Created by Астемир Бозиев on 07.01.2024. +// + +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 } + + @ObservationIgnored + private var cardRepository: CardRepository + + init(repositry: CardRepository) { + self.cardRepository = repositry + } + + 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() { + frontWord = "" + backWord = "" + audioName = nil + } + + 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 + cardRepository.update(card: card) + } + + func deleteCard() { + guard let id = id else { return } + cardRepository.delete(by: id) + } + + private func cleanWords() { + frontWord = frontWord.trimmingCharacters(in: .whitespacesAndNewlines) + backWord = backWord.trimmingCharacters(in: .whitespacesAndNewlines) + } +} 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 - } -} 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