└── FlashSpeak
├── FlashSpeak
├── AppDelegate
│ ├── ru.lproj
│ │ └── LaunchScreen.strings
│ └── Base.lproj
│ │ └── LaunchScreen.storyboard
├── Resources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── Color
│ │ │ ├── Contents.json
│ │ │ └── fiveBackground.colorset
│ │ │ │ └── Contents.json
│ │ ├── LanguageIcon
│ │ │ ├── Contents.json
│ │ │ ├── lang.icon.de.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── lang.icon.de.svg
│ │ │ ├── lang.icon.en.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── lang.icon.en.svg
│ │ │ ├── lang.icon.es.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── lang.icon.es.svg
│ │ │ ├── lang.icon.fr.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── lang.icon.fr.svg
│ │ │ ├── lang.icon.pt.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── lang.icon.pt.svg
│ │ │ └── lang.icon.ru.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── lang.icon.ru.svg
│ │ ├── Placeholder
│ │ │ ├── Contents.json
│ │ │ └── placeholder.imageset
│ │ │ │ └── Contents.json
│ │ ├── ChooseLanguageScreen
│ │ │ ├── Contents.json
│ │ │ ├── DE.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── Germany (DE).svg
│ │ │ ├── ES.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── FR.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── France (FR).svg
│ │ │ ├── pt.imageset
│ │ │ │ └── Contents.json
│ │ │ ├── ru.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── Russia (RU).svg
│ │ │ └── EN.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── United Kingdom (GB).svg
│ │ ├── AppIcon.appiconset
│ │ │ ├── 128.png
│ │ │ ├── 16.png
│ │ │ ├── 256.png
│ │ │ ├── 32.png
│ │ │ ├── 512.png
│ │ │ ├── 128@2x.png
│ │ │ ├── 16@2x.png
│ │ │ ├── 256@2x.png
│ │ │ ├── 32@2x.png
│ │ │ ├── 512@2x.png
│ │ │ ├── iPad_App_76_1x.png
│ │ │ ├── iPad_App_76_2x.png
│ │ │ ├── iPhone_App_60_2x.png
│ │ │ ├── iPhone_App_60_3x.png
│ │ │ ├── App_store_1024_1x.png
│ │ │ ├── iPad_Settings_29_1x.png
│ │ │ ├── iPad_Settings_29_2x.png
│ │ │ ├── iPad_Pro_App_83.5_2x.png
│ │ │ ├── iPad_Spotlight_40_1x.png
│ │ │ ├── iPad_Spotlight_40_2x.png
│ │ │ ├── iPhone_Settings_29_2x.png
│ │ │ ├── iPhone_Settings_29_3x.png
│ │ │ ├── iPad_Notifications_20_1x.png
│ │ │ ├── iPad_Notifications_20_2x.png
│ │ │ ├── iPhone_Spotlight_40_2x.png
│ │ │ ├── iPhone_Spotlight_40_3x.png
│ │ │ ├── iPhone_Notifications_20_2x.png
│ │ │ └── iPhone_Notifications_20_3x.png
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ └── Info.plist
├── FlashSpeak.entitlements
├── Flow
│ ├── Lists
│ │ ├── Card
│ │ │ ├── Model
│ │ │ │ ├── CardImage.swift
│ │ │ │ ├── CardViewModel.swift
│ │ │ │ └── CardError.swift
│ │ │ ├── Router
│ │ │ │ └── CardRouter.swift
│ │ │ ├── Builder
│ │ │ │ └── CardBuilder.swift
│ │ │ └── View
│ │ │ │ └── ImageCollectionView
│ │ │ │ ├── ImageCollectionDataSource.swift
│ │ │ │ ├── AddImageCell.swift
│ │ │ │ ├── ImageCollectionDelegate.swift
│ │ │ │ └── ImageCell.swift
│ │ ├── Learn
│ │ │ ├── View
│ │ │ │ └── Subviews
│ │ │ │ │ ├── Strategy
│ │ │ │ │ ├── AnswerView
│ │ │ │ │ │ ├── AnswerCell.swift
│ │ │ │ │ │ ├── Keyboard
│ │ │ │ │ │ │ ├── AnswerTextFieldDelegate.swift
│ │ │ │ │ │ │ ├── AnswerKeyboardViewDelegate.swift
│ │ │ │ │ │ │ ├── AnswerButtonCell.swift
│ │ │ │ │ │ │ └── AnswerKeyboardViewDataSource.swift
│ │ │ │ │ │ └── TestStrategy
│ │ │ │ │ │ │ ├── AnswerTestViewDataSource.swift
│ │ │ │ │ │ │ ├── AnswerTestViewStrategy.swift
│ │ │ │ │ │ │ └── AnswerTestViewDelegate.swift
│ │ │ │ │ └── QuestionView
│ │ │ │ │ │ ├── QuestionViewStrategy.swift
│ │ │ │ │ │ ├── QuestionWordViewStrategy.swift
│ │ │ │ │ │ ├── QuestionImageViewStrategy.swift
│ │ │ │ │ │ └── QuestionWordImageViewStrategy.swift
│ │ │ │ │ └── Progress
│ │ │ │ │ ├── ProgressViewDataSource.swift
│ │ │ │ │ ├── ProgressCell.swift
│ │ │ │ │ ├── ProgressViewDelegate.swift
│ │ │ │ │ └── ProgressView.swift
│ │ │ ├── Router
│ │ │ │ └── LearnRouter.swift
│ │ │ └── Builder
│ │ │ │ └── LearnBuilder.swift
│ │ ├── Hint
│ │ │ ├── View
│ │ │ │ └── HintGestureRecognizerDelegate.swift
│ │ │ ├── Router
│ │ │ │ └── HintRouter.swift
│ │ │ ├── Presenter
│ │ │ │ └── HintPresenter.swift
│ │ │ └── Builder
│ │ │ │ └── HintBuilder.swift
│ │ ├── Result
│ │ │ ├── View
│ │ │ │ ├── Model
│ │ │ │ │ ├── ResultViewModel.swift
│ │ │ │ │ └── WordCellModel.swift
│ │ │ │ ├── ResultTableVIew
│ │ │ │ │ ├── ResultTableViewDelegate.swift
│ │ │ │ │ ├── ResultTableViewDataSource.swift
│ │ │ │ │ └── ResultTableView.swift
│ │ │ │ ├── MistakeTableView
│ │ │ │ │ ├── MistakeTableViewDelegate.swift
│ │ │ │ │ ├── MistakeTableViewDataSource.swift
│ │ │ │ │ └── MistakeTableView.swift
│ │ │ │ └── ChartLearn
│ │ │ │ │ ├── ChartLearnViewModel.swift
│ │ │ │ │ └── ChartLearnView.swift
│ │ │ ├── Router
│ │ │ │ └── ResultRouter.swift
│ │ │ └── Builder
│ │ │ │ └── ResultBuilder.swift
│ │ ├── LearnSettings
│ │ │ ├── View
│ │ │ │ ├── SettingsTableView
│ │ │ │ │ ├── SettingsCellDelegate.swift
│ │ │ │ │ ├── SettingsTableViewDelegate.swift
│ │ │ │ │ └── SettingsTableViewDataSource.swift
│ │ │ │ └── LearnSettingsViewController.swift
│ │ │ ├── Router
│ │ │ │ └── LearnSettingsRouter.swift
│ │ │ ├── Builder
│ │ │ │ └── LearnSettingsBuilder.swift
│ │ │ └── Presenter
│ │ │ │ └── LearnSettingsPresenter.swift
│ │ ├── Language
│ │ │ ├── View
│ │ │ │ ├── LanguageGestureRecognizerDelegate.swift
│ │ │ │ ├── LanguageTableDelegate.swift
│ │ │ │ └── LanguageTableDataSource.swift
│ │ │ ├── Router
│ │ │ │ └── LanguageRouter.swift
│ │ │ ├── Builder
│ │ │ │ └── LanguageBuilder.swift
│ │ │ └── Presenter
│ │ │ │ └── LanguagePresenter.swift
│ │ ├── NewList
│ │ │ ├── View
│ │ │ │ ├── NewLisTextFieldDelegate.swift
│ │ │ │ ├── NewListGestureRecognizerDelegate.swift
│ │ │ │ ├── NewListColorCollectionDataSource.swift
│ │ │ │ ├── ColorCell.swift
│ │ │ │ └── NewListColorCollectionDelegate.swift
│ │ │ ├── Router
│ │ │ │ └── NewListRouter.swift
│ │ │ ├── Model
│ │ │ │ └── ListViewModel.swift
│ │ │ └── Biulder
│ │ │ │ └── NewListBuilder.swift
│ │ ├── Lists
│ │ │ ├── Error
│ │ │ │ └── ListError.swift
│ │ │ ├── Model
│ │ │ │ ├── ListCellModel.swift
│ │ │ │ └── ListMenu.swift
│ │ │ ├── Router
│ │ │ │ └── ListsRouter.swift
│ │ │ ├── Builder
│ │ │ │ └── ListsBuilder.swift
│ │ │ └── View
│ │ │ │ ├── ListSearchResultsController.swift
│ │ │ │ ├── ProfileButton.swift
│ │ │ │ └── ListsCollectionDataSource.swift
│ │ ├── WordCards
│ │ │ ├── Model
│ │ │ │ ├── WordCardCellModel.swift
│ │ │ │ ├── PaddingLabel.swift
│ │ │ │ └── WordMenu.swift
│ │ │ ├── Router
│ │ │ │ └── WordCardsRouter.swift
│ │ │ ├── Error
│ │ │ │ └── WordCardsError.swift
│ │ │ ├── Builder
│ │ │ │ └── WordCardsBuilder.swift
│ │ │ └── View
│ │ │ │ ├── WordCardsSearchBarDelegate.swift
│ │ │ │ ├── WordCardsCollectionDataSource.swift
│ │ │ │ └── ResultStackView
│ │ │ │ └── ResultStackView.swift
│ │ ├── ListMaker
│ │ │ ├── Router
│ │ │ │ └── ListMakerRouter.swift
│ │ │ ├── Error
│ │ │ │ └── ListMakerError.swift
│ │ │ ├── View
│ │ │ │ ├── ListMakerTextDropDelegate.swift
│ │ │ │ ├── LeftAlignedCollectionViewFlowLayout.swift
│ │ │ │ ├── ListMakerDragDelegate.swift
│ │ │ │ ├── ListMakerCollectionViewDelegate.swift
│ │ │ │ ├── ListMakerTokenFieldDelegate.swift
│ │ │ │ ├── TokenCollectionView
│ │ │ │ │ └── TokenCell.swift
│ │ │ │ └── ButtonCell.swift
│ │ │ └── Builder
│ │ │ │ └── ListMakerBuilder.swift
│ │ └── PrepareLearn
│ │ │ ├── Router
│ │ │ └── PrepareLearnRouter.swift
│ │ │ └── Builder
│ │ │ └── PrepareLearnBuilder.swift
│ └── Welcome
│ │ ├── Router
│ │ └── WelcomeRouter.swift
│ │ └── Builder
│ │ └── WelcomeBuilder.swift
├── Core
│ ├── Learn
│ │ ├── Model
│ │ │ ├── Answer
│ │ │ │ ├── KeyboardAnswer.swift
│ │ │ │ ├── Answer.swift
│ │ │ │ └── TestAnswer.swift
│ │ │ ├── Question
│ │ │ │ └── Question.swift
│ │ │ └── Exercise.swift
│ │ ├── Strategy
│ │ │ ├── AnswerStrategy
│ │ │ │ ├── AnswerStrategy.swift
│ │ │ │ └── KeyboardAnswerStrategy.swift
│ │ │ └── QuestionsStrategy
│ │ │ │ ├── QuestionsStrategy.swift
│ │ │ │ ├── WordQuestionsStrategy.swift
│ │ │ │ ├── ImageQuestionsStrategy.swift
│ │ │ │ └── WordImageQuestionsStrategy.swift
│ │ ├── Manager
│ │ │ └── LearnManagerError.swift
│ │ └── Caretaker
│ │ │ ├── LearnCaretaker.swift
│ │ │ └── WordCaretaker.swift
│ ├── LearnSettings
│ │ ├── Settings
│ │ │ ├── LearnSettingControl.swift
│ │ │ ├── LearnQuestion.swift
│ │ │ └── LearnSettingProtocol.swift
│ │ └── LearnSettings.swift
│ ├── CoreData
│ │ └── FlashSpeak.xcdatamodeld
│ │ │ └── .xccurrentversion
│ ├── Extensions
│ │ ├── StringExtension.swift
│ │ ├── UIColorExtension.swift
│ │ ├── UIFontExtension.swift
│ │ ├── UIImageExtension.swift
│ │ ├── UIViewExtension.swift
│ │ ├── URLSession.swift
│ │ └── UIButtonConfigurationExtension.swift
│ └── ImageCache
│ │ ├── ImageLoader.swift
│ │ └── ImageManager.swift
├── Helpers
│ ├── Settings.swift
│ ├── Constants.swift
│ ├── Layout.swift
│ ├── Autolayout.swift
│ ├── Grid.swift
│ └── URLConfiguration.swift
├── Model
│ ├── CoreDataModel
│ │ ├── Learn
│ │ │ ├── LearnCD+CoreDataClass.swift
│ │ │ └── LearnCD+CoreDataProperties.swift
│ │ ├── List
│ │ │ ├── ListCD+CoreDataClass.swift
│ │ │ └── ListCD+CoreDataProperties.swift
│ │ ├── Word
│ │ │ ├── WordCD+CoreDataClass.swift
│ │ │ └── WordCD+CoreDataProperties.swift
│ │ └── Study
│ │ │ ├── StudyCD+CoreDataClass.swift
│ │ │ └── StudyCD+CoreDataProperties.swift
│ ├── Analytics
│ │ └── AnalyticsEvent.swift
│ └── Models
│ │ ├── CardIndex.swift
│ │ ├── GradientStyle.swift
│ │ ├── ListMenu.swift
│ │ ├── TransalateResponse.swift
│ │ ├── ImageUrlModel.swift
│ │ ├── LearnResults.swift
│ │ ├── Word.swift
│ │ ├── Study.swift
│ │ ├── Learn.swift
│ │ └── List.swift
├── Wrappers
│ └── NetworkResponse.swift
├── Errors
│ ├── CoreDataError.swift
│ └── NetworkError.swift
├── GoogleService-Info.plist
├── Service
│ └── NetworkService.swift
└── Coordinator
│ └── Coordinator.swift
├── FlashSpeak.xcodeproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ ├── WorkspaceSettings.xcsettings
│ └── IDEWorkspaceChecks.plist
├── FlashSpeakUITests
├── FlashSpeakUITestsLaunchTests.swift
└── FlashSpeakUITests.swift
└── FlashSpeakTests
├── ListsPresenter
└── MockLictsViewController.swift
└── FlashSpeakTests.swift
/FlashSpeak/FlashSpeak/AppDelegate/ru.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/Color/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/Placeholder/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/128@2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/16@2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/256@2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/32@2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/512@2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_App_76_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_App_76_1x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_App_76_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_App_76_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/App_store_1024_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/App_store_1024_1x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_1x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Pro_App_83.5_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Pro_App_83.5_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Spotlight_40_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Spotlight_40_1x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Spotlight_40_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Spotlight_40_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/FlashSpeak.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_1x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DenDmitriev/Flashspeak/HEAD/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/Model/CardImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardImage.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct CardImage {
11 | var url: URL
12 | var image: UIImage?
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Model/Answer/KeyboardAnswer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAnswer.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct KeyboardAnswer: Answer {
11 | var answer: String?
12 | }
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Model/Question/Question.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Question.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct Question {
11 | var question: String
12 | var image: UIImage?
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Model/Answer/Answer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol Answer {
11 | /// User answer
12 | var answer: String? { get set }
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Model/Answer/TestAnswer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestAnswer.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TestAnswer: Answer {
11 | var answer: String?
12 | var words: [String]
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/AnswerCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 06.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol AnswerCell {
11 | var isRight: Bool? { get set }
12 | }
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Helpers/Settings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Settings.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Settings {
11 | static let minWordsInList: Int = 7
12 | static let imagesCountForSelectUser: Int = 7
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/LearnSettings/Settings/LearnSettingControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettingControl.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 09.06.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LearnSettingControl {
11 | case selector, switcher, switcherWithValue
12 | }
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Hint/View/HintGestureRecognizerDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HintGestureRecognizerDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Оксана Каменчук on 22.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | typealias HintGestureRecognizerDelegate = NewListGestureRecognizerDelegate
11 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/Model/ResultViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultViewModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ResultViewModel {
11 | let result: String
12 | let description: String
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/Learn/LearnCD+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnCD+CoreDataClass.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 08.05.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 |
13 | public class LearnCD: NSManagedObject {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/View/SettingsTableView/SettingsCellDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsCellDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol SettingsCellDelegate: AnyObject {
11 | func valueChanged()
12 | }
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Language/View/LanguageGestureRecognizerDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LanguageGestureRecognizerDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | typealias LanguageGestureRecognizerDelegate = NewListGestureRecognizerDelegate
11 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/List/ListCD+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListCD+CoreDataClass.swift
3 | // FlashSpeak
4 | //
5 | // Created by Anastasia Losikova on 16.04.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(ListCD)
13 | public class ListCD: NSManagedObject {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/Word/WordCD+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCD+CoreDataClass.swift
3 | // FlashSpeak
4 | //
5 | // Created by Anastasia Losikova on 16.04.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(WordCD)
13 | public class WordCD: NSManagedObject {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/Study/StudyCD+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StudyCD+CoreDataClass.swift
3 | // FlashSpeak
4 | //
5 | // Created by Anastasia Losikova on 16.04.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | @objc(StudyCD)
13 | public class StudyCD: NSManagedObject {
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Helpers/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // C.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Constants {
11 | static let clienID = "client_id"
12 |
13 | struct Analitics {
14 | static let someValue = "value"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Model/Exercise.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exercise.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Exercise {
11 | var word: Word
12 | var question: Question
13 | var answer: Answer
14 | // var settings: LearnSettings
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/CoreData/FlashSpeak.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | FlashSpeak.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/ResultTableVIew/ResultTableViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultTableViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 03.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class ResultTableViewDelegate: NSObject, UITableViewDelegate {
11 |
12 | weak var view: ResultTableView?
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Strategy/AnswerStrategy/AnswerStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol AnswerStrategy: AnyObject {
11 |
12 | func createAnswers(_ words: [Word], source: LearnLanguage.Language) -> [Answer]
13 | }
14 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/QuestionView/QuestionViewStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuestionViewStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol QuestionViewStrategy {
11 | var view: UIView { get }
12 |
13 | func set(question: Question)
14 | }
15 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/View/SettingsTableView/SettingsTableViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsTableViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 25.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class SettingsTableViewDelegate: NSObject, UITableViewDelegate {
11 | weak var view: SettingsTableView?
12 | }
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Strategy/QuestionsStrategy/QuestionsStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuestionsStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol QuestionsStrategy: AnyObject {
11 | func createQuestions(_ words: [Word], source: LearnLanguage.Language) -> [Question]
12 | }
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/MistakeTableView/MistakeTableViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MistakeTableViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 03.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class MistakeTableViewDelegate: NSObject, UITableViewDelegate {
11 |
12 | weak var view: MistakeTableViewProtocol?
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/DE.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Germany (DE).svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/ES.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Spain (ES).svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/FR.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "France (FR).svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/pt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Portugal (PT).svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/ru.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Russia (RU).svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/Placeholder/placeholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "placeholder.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/EN.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "United Kingdom (GB).svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Analytics/AnalyticsEvent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsEvent.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 24.06.2024.
6 | //
7 |
8 | import Foundation
9 |
10 | enum AnalyticsEvent: String {
11 | case selectLanguage
12 | case startLearn
13 | case createList
14 |
15 | var key: String {
16 | self.rawValue
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Helpers/Layout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Layout.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 27.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct Layout {
11 | static let separatorCollection: CGFloat = Grid.pt8
12 | static let insetsCollection = UIEdgeInsets(
13 | top: .zero,
14 | left: Grid.pt16,
15 | bottom: .zero,
16 | right: Grid.pt16
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.de.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lang.icon.de.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.en.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lang.icon.en.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.es.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lang.icon.es.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.fr.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lang.icon.fr.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.pt.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lang.icon.pt.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.ru.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "lang.icon.ru.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true,
14 | "template-rendering-intent" : "template"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/View/NewLisTextFieldDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewLisTextFieldDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class NewLisTextFieldDelegate: NSObject, UITextFieldDelegate {
11 |
12 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
13 | textField.resignFirstResponder()
14 | return true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/Error/ListError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ListError: LocalizedError {
11 | case delete(error: Error)
12 |
13 | var errorDescription: String? {
14 | switch self {
15 | case .delete(let error):
16 | return "\(error.localizedDescription)"
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Hint/Router/HintRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HintRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Оксана Каменчук on 22.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol HintEvent {
11 | var didSendEventClosure: ((HintRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct HintRouter: HintEvent {
15 |
16 | enum Event {
17 | case close
18 | }
19 |
20 | var didSendEventClosure: ((Event) -> Void)?
21 | }
22 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Strategy/AnswerStrategy/KeyboardAnswerStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardAnswerStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | final class KeyboardAnswerStrategy: AnswerStrategy {
11 | typealias Element = KeyboardAnswer
12 |
13 | func createAnswers(_ words: [Word], source: LearnLanguage.Language) -> [Answer] {
14 | return words.map { _ in KeyboardAnswer() }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/Router/NewListRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewListRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol NewListEvent {
11 | var didSendEventClosure: ((NewListRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct NewListRouter: NewListEvent {
15 | enum Event {
16 | case done(list: List), close
17 | }
18 |
19 | var didSendEventClosure: ((Event) -> Void)?
20 | }
21 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/Router/CardRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol CardEvent {
11 | var didSendEventClosure: ((CardRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct CardRouter: CardEvent {
15 | enum Event {
16 | case save(wordID: UUID?), error(error: LocalizedError)
17 | }
18 |
19 | var didSendEventClosure: ((Event) -> Void)?
20 | }
21 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/Router/LearnRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LearnEvent {
11 | var didSendEventClosure: ((LearnRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct LearnRouter: LearnEvent {
15 | enum Event {
16 | case complete(list: List, mistakes: [Word: String])
17 | }
18 |
19 | var didSendEventClosure: ((Event) -> Void)?
20 | }
21 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/View/NewListGestureRecognizerDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewListGestureRecognizerDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class NewListGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
11 |
12 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
13 | return touch.view == gestureRecognizer.view
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/Router/ResultRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ResultEvent {
11 | var didSendEventClosure: ((ResultRouter.Event) -> Void)? { get }
12 | }
13 |
14 | struct ResultRouter: ResultEvent {
15 | enum Event {
16 | case learn(list: List)
17 | case settings
18 | }
19 |
20 | var didSendEventClosure: ((Event) -> Void)?
21 | }
22 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/CardIndex.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardIndex.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 23.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CardIndex {
11 | let current: Int
12 | let count: Int
13 |
14 | var label: String {
15 | return "\(current) / \(count)"
16 | }
17 |
18 | var progress: Float {
19 | let current = Float(current)
20 | let total = Float(count)
21 | return current / total
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/GradientStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GradientStyle.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.04.2023.
6 | //
7 |
8 | import Foundation
9 | import UIKit.UIColor
10 |
11 | enum GradientStyle: Int, CaseIterable {
12 | case grey = 0
13 | case red = 1
14 | case orange = 2
15 | case yellow = 3
16 | case green = 4
17 | case blue = 5
18 | case violet = 6
19 |
20 | var color: UIColor {
21 | UIColor.color(by: self)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Manager/LearnManagerError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnManagerError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 12.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LearnManagerError: LocalizedError {
11 | case imageLoad(word: Word)
12 |
13 | var errorDescription: String? {
14 | switch self {
15 | case .imageLoad(let word):
16 | return NSLocalizedString("Image load error", comment: "Error") + word.source
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Language/View/LanguageTableDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LanguageTableDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class LanguageTableDelegate: NSObject, UITableViewDelegate {
11 |
12 | weak var viewInput: (UIViewController & LanguageViewInput)?
13 |
14 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
15 | viewInput?.didSelectItem(indexPath: indexPath)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/Router/LearnSettingsRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettingsRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LearnSettingsEvent {
11 | var didSendEventClosure: ((LearnSettingsRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct LearnSettingsRouter: LearnSettingsEvent {
15 |
16 | enum Event {
17 | case close
18 | }
19 |
20 | var didSendEventClosure: ((Event) -> Void)?
21 | }
22 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/DE.imageset/Germany (DE).svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Language/Router/LanguageRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LanguageRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LanguageEvent {
11 | var didSendEventClosure: ((LanguageRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct LanguageRouter: LanguageEvent {
15 |
16 | enum Event {
17 | case change(language: Language)
18 | case close
19 | }
20 |
21 | var didSendEventClosure: ((Event) -> Void)?
22 | }
23 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/Model/WordCardCellModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct WordCardCellModel {
11 | let source: String
12 | var translation: String
13 | var image: UIImage?
14 |
15 | static func modelFactory(word: Word) -> WordCardCellModel {
16 | return WordCardCellModel(
17 | source: word.source,
18 | translation: word.translation
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Welcome/Router/WelcomeRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WelcomeRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 28.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol WelcomeEvent {
11 | var didSendEventClosure: ((WelcomeRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct WelcomeRouter: WelcomeEvent {
15 |
16 | enum Event {
17 | case complete, source(language: Language), target(language: Language)
18 | }
19 |
20 | var didSendEventClosure: ((Event) -> Void)?
21 | }
22 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/Router/ListMakerRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ListMakerEvent {
11 | var didSendEventClosure: ((ListMakerRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | struct ListMakerRouter: ListMakerEvent {
15 |
16 | enum Event {
17 | case generate(list: List), error(error: LocalizedError)
18 | }
19 |
20 | var didSendEventClosure: ((Event) -> Void)?
21 | }
22 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Welcome/Builder/WelcomeBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WelcomeBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 28.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct WelcomeBuilder {
11 | static func build(router: WelcomeEvent) -> UIViewController & WelcomeViewInput {
12 | let presenter = WelcomePresenter(router: router)
13 | let viewController = WelcomeViewController(presenter: presenter)
14 | presenter.viewController = viewController
15 | return viewController
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/Model/CardViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardViewModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct CardViewModel {
11 | let source: String
12 | var translation: String
13 | var images: [UIImage]
14 |
15 | static func modelFactory(word: Word) -> CardViewModel {
16 | return CardViewModel(
17 | source: word.source,
18 | translation: word.translation,
19 | images: [UIImage]()
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/Model/ListCellModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListCellModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ListCellModel {
11 | let title: String
12 | let words: [String]
13 | let style: GradientStyle
14 |
15 | static func modelFactory(from model: List) -> ListCellModel {
16 | return ListCellModel(
17 | title: model.title,
18 | words: model.words.map({ $0.source }),
19 | style: model.style
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/Model/WordCellModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCellModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 27.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct WordCellModel {
11 | let source: String
12 | let translation: String
13 | let mistake: String
14 |
15 | static func modelFactory(word: Word, mistake: String) -> WordCellModel {
16 | return WordCellModel(
17 | source: word.source,
18 | translation: word.translation,
19 | mistake: mistake
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/Error/ListMakerError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ListMakerError: LocalizedError {
11 |
12 | case loadTransalte(description: String)
13 | case unknown
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .loadTransalte(let description):
18 | return description
19 | default:
20 | return NSLocalizedString("Unknown error", comment: "Error")
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/ru.imageset/Russia (RU).svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Wrappers/NetworkResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkResponse.swift
3 | // FlashSpeak
4 | //
5 | // Created by Алексей Ходаков on 17.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | @propertyWrapper public struct NetworkResponse: Decodable {
11 |
12 | // MARK: - Public properties
13 | public let wrappedValue: T?
14 |
15 | // MARK: - Initialisation
16 | public init(from decoder: Decoder) throws {
17 | wrappedValue = try? T(from: decoder)
18 | }
19 |
20 | public init(_ wrappedValue: T?) {
21 | self.wrappedValue = wrappedValue
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/ListMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMenu.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.05.2023.
6 | //
7 |
8 | import Foundation
9 | import UIKit.UIImage
10 |
11 | enum ListMenu: CaseIterable {
12 | case delete
13 |
14 | var title: String {
15 | switch self {
16 | case .delete:
17 | return NSLocalizedString("Удалить", comment: "Menu")
18 | }
19 | }
20 |
21 | var image: UIImage? {
22 | switch self {
23 | case .delete:
24 | return UIImage(systemName: "trash")
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/Model/CardError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum CardError: LocalizedError {
11 |
12 | case imageURL(error: Error)
13 | case save(error: Error)
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .imageURL(let error):
18 | return NSLocalizedString("Image URL error", comment: "Error") + error.localizedDescription
19 | case .save(let error):
20 | return "\(error)"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/Router/WordCardsRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCardsRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 26.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol WordCardsEvent {
11 | var didSendEventClosure: ((WordCardsRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | class WordCardsRouter: WordCardsEvent {
15 |
16 | enum Event {
17 | case word(word: Word)
18 | case edit
19 | case editListProperties(list: List)
20 | case error(error: LocalizedError)
21 | }
22 |
23 | var didSendEventClosure: ((Event) -> Void)?
24 | }
25 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/FR.imageset/France (FR).svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/LearnSettings/LearnSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettings.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 09.06.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LearnSettings: Int, CaseIterable {
11 | case mode, question, answer
12 |
13 | var title: String {
14 | switch self {
15 | case .mode:
16 | return NSLocalizedString("Lesson Mode", comment: "Title")
17 | case .question:
18 | return NSLocalizedString("Question Mode", comment: "Title")
19 | case .answer:
20 | return NSLocalizedString("Answer Mode", comment: "Title")
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Helpers/Autolayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Autolayout.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 25.06.2024.
6 | //
7 |
8 | import UIKit
9 |
10 | @propertyWrapper
11 | public struct Autolayout {
12 | public var wrappedValue: T {
13 | didSet {
14 | translatesAutoresizingMaskIntoConstraints()
15 | }
16 | }
17 |
18 | public init(wrappedValue: T) {
19 | self.wrappedValue = wrappedValue
20 | translatesAutoresizingMaskIntoConstraints()
21 | }
22 |
23 | private func translatesAutoresizingMaskIntoConstraints() {
24 | wrappedValue.translatesAutoresizingMaskIntoConstraints = false
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/StringExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringExtension.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 22.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 |
12 | /// Cleaning text punctuation marks
13 | func cleanText() -> String {
14 | var output = [String]()
15 | self.enumerateSubstrings(
16 | in: self.startIndex ..< self.endIndex,
17 | options: .byWords
18 | ) { substring, _, _, _ in
19 | if let substring = substring {
20 | return output.append(substring)
21 | }
22 | }
23 | return output.joined(separator: " ")
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/Builder/CardBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | struct CardBuilder {
12 | static func build(word: Word, style: GradientStyle, router: CardEvent) -> UIViewController & CardViewInput {
13 |
14 | let presenter = CardPresenter(word: word, style: style, router: router)
15 |
16 | let viewController = CardViewController(presenter: presenter)
17 |
18 | presenter.viewController = viewController
19 |
20 | return viewController
21 | }
22 | }
23 |
24 | // swiftlint: enable line_length
25 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/Builder/LearnBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct LearnBuilder {
11 | static func build(
12 | router: LearnEvent,
13 | list: List
14 | ) -> UIViewController & LearnViewInput {
15 |
16 | let presenter = LearnPresenter(router: router, list: list)
17 |
18 | let viewController = LearnViewController(
19 | presenter: presenter,
20 | answersCount: list.words.count
21 | )
22 |
23 | presenter.viewController = viewController
24 |
25 | return viewController
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/Router/ListsRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListsRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 25.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ListsEvent {
11 | var didSendEventClosure: ((ListsRouter.Event) -> Void)? { get set }
12 | }
13 |
14 | class ListsRouter: ListsEvent {
15 |
16 | enum Event {
17 | case prepareLearn(list: List)
18 | case newList
19 | case changeLanguage(language: Language)
20 | case editList(list: List)
21 | case editWords(list: List)
22 | case transfer(list: List)
23 | case error(error: LocalizedError)
24 | }
25 |
26 | var didSendEventClosure: ((ListsRouter.Event) -> Void)?
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/LearnSettings/Settings/LearnQuestion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnQuestion.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 09.06.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LearnQuestion {
11 | case word, image, wordImage
12 |
13 | static func adapter(word: LearnWord.Word, image: LearnImage.Image) -> Self {
14 | if word == .word,
15 | image == .image {
16 | return .wordImage
17 | } else if word == .word,
18 | image == .noImage {
19 | return .word
20 | } else if word == .noWord,
21 | image == .image {
22 | return .image
23 | } else {
24 | return .wordImage
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/Learn/LearnCD+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnCD+CoreDataProperties.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 08.05.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 |
13 | extension LearnCD {
14 |
15 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
16 | return NSFetchRequest(entityName: "LearnCD")
17 | }
18 |
19 | @NSManaged public var id: UUID
20 | @NSManaged public var startTime: Date
21 | @NSManaged public var finishTime: Date
22 | @NSManaged public var result: Int16
23 | @NSManaged public var count: Int16
24 | @NSManaged public var listCD: ListCD?
25 |
26 | }
27 |
28 | extension LearnCD: Identifiable {
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/TransalateResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransalateResponse.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.06.2023.
6 | //
7 | // Parsing Google API Translation Response
8 |
9 | import Foundation
10 |
11 | struct TransalateResponse: Decodable {
12 | var data: Translations
13 | }
14 |
15 | struct Translations: Decodable {
16 | var translations: [TranslatedText]
17 | }
18 |
19 | struct TranslatedText: Decodable {
20 | var translatedText: String
21 | }
22 |
23 | /*
24 | {
25 | "data": {
26 | "translations": [
27 | {
28 | "translatedText": "собака"
29 | },
30 | {
31 | "translatedText": "кот"
32 | }
33 | ]
34 | }
35 | }
36 | */
37 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/Builder/ResultBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | struct ResultBuilder {
12 |
13 | static func build(router: ResultEvent, list: List, mistakes: [Word: String]) -> (UIViewController & ResultViewInput) {
14 |
15 | let presenter = ResultPresenter(router: router, list: list, mistakes: mistakes)
16 |
17 | let viewController = ResultViewController(
18 | presenter: presenter
19 | )
20 |
21 | presenter.viewController = viewController
22 |
23 | return viewController
24 | }
25 | }
26 |
27 | // swiftlint: enable line_length
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/Error/WordCardsError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCardsError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum WordCardsError: LocalizedError {
11 | case imageURL(error: Error)
12 | case loadImage
13 | case save(error: Error)
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .imageURL(let error):
18 | return NSLocalizedString("Image URL error", comment: "Error") + error.localizedDescription
19 | case .loadImage:
20 | return NSLocalizedString("Image load error", comment: "Error")
21 | case .save(error: let error):
22 | return "\(error.localizedDescription)"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Errors/CoreDataError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoreDataError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 08.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum CoreDataError: LocalizedError {
11 | case listNotFounded(id: UUID)
12 | case wordNotFounded(id: UUID)
13 | case save(description: String)
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .listNotFounded(let id):
18 | return NSLocalizedString("List not found in CoreData", comment: "Error") + "id: \(id)"
19 | case .wordNotFounded(let id):
20 | return NSLocalizedString("Word not found in CoreData", comment: "Error") + "id: \(id)"
21 | case .save(let description):
22 | return "\(description)"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Hint/Presenter/HintPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HintPresenter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Оксана Каменчук on 22.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol HintViewInput {
11 | func didTabBackground()
12 | }
13 |
14 | protocol HintViewOutput {
15 | var router: HintEvent? { get }
16 |
17 | func viewDidTapBackground()
18 | }
19 |
20 | class HintPresenter: ObservableObject {
21 |
22 | var router: HintEvent?
23 | weak var viewInput: (UIViewController & HintViewInput)?
24 |
25 |
26 | init(router: HintEvent) {
27 | self.router = router
28 | }
29 | }
30 |
31 | extension HintPresenter: HintViewOutput {
32 |
33 | func viewDidTapBackground() {
34 | router?.didSendEventClosure?(.close)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/PrepareLearn/Router/PrepareLearnRouter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrepareLearnRouter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 25.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol PrepareLearnEvent {
11 | var didSendEventClosure: ((PrepareLearnRouter.Action) -> Void)? { get set }
12 | }
13 |
14 | struct PrepareLearnRouter: PrepareLearnEvent {
15 |
16 | enum Action {
17 | case close
18 | case error(error: LocalizedError)
19 | case learn(list: List)
20 | case editWords(list: List)
21 | case editCards(list: List)
22 | case showCards(list: List)
23 | case showStatistic(list: List)
24 | case showSettings(list: List)
25 | }
26 |
27 | var didSendEventClosure: ((Action) -> Void)?
28 | }
29 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/UIColorExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColorExtension.swift
3 | // Lingocard
4 | //
5 | // Created by Denis Dmitriev on 12.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 |
12 | enum TypeColor {
13 | case dark, light
14 | }
15 |
16 | static var fiveBackgroundColor: UIColor = .init(named: "fiveBackground") ?? .tertiarySystemBackground
17 |
18 | static func color(by style: GradientStyle, type: TypeColor? = .dark) -> UIColor {
19 | switch type {
20 | case .dark:
21 | return CAGradientLayer.darkColor(for: style)
22 | case .light:
23 | return CAGradientLayer.lightColor(for: style)
24 | default:
25 | return CAGradientLayer.darkColor(for: style)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Hint/Builder/HintBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HintBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Оксана Каменчук on 22.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct HintBuilder {
11 |
12 | static func build(
13 | router: HintEvent,
14 | title: String? = nil,
15 | paragraphOne: String? = nil
16 | ) -> (UIViewController & HintViewInput) {
17 | let presenter = HintPresenter(router: router)
18 | let gestureRecognizerDelegate = HintGestureRecognizerDelegate()
19 |
20 | let viewInput = HintController(
21 | presenter: presenter,
22 | gestureRecognizerDelegate: gestureRecognizerDelegate
23 | )
24 | presenter.viewInput = viewInput
25 |
26 | return viewInput
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/Builder/LearnSettingsBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettingsBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct LearnSettingsBuilder {
11 | static func build(router: LearnSettingsEvent, imageFlag: Bool) -> (UIViewController & LearnSettingsViewInput) {
12 | let presenter = LearnSettingsPresenter(router: router)
13 | let learnSettingsManager = LearnSettingsManager(imageFlag: imageFlag)
14 |
15 | let viewController = LearnSettingsViewController(
16 | presenter: presenter,
17 | settingsManager: learnSettingsManager
18 | )
19 |
20 | presenter.viewInput = viewController
21 |
22 | return viewController
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Strategy/QuestionsStrategy/WordQuestionsStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordQuestionsStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | final class WordQuestionsStrategy: QuestionsStrategy {
11 |
12 | func createQuestions(_ words: [Word], source: LearnLanguage.Language) -> [Question] {
13 | let questions: [Question] = words.map { word in
14 | var question: Question
15 | switch source {
16 | case .source:
17 | question = Question(question: word.source)
18 | case .target:
19 | question = Question(question: word.translation)
20 | }
21 | return question
22 |
23 | }
24 | return questions
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/Word/WordCD+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCD+CoreDataProperties.swift
3 | // FlashSpeak
4 | //
5 | // Created by Anastasia Losikova on 16.04.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 |
13 | extension WordCD {
14 |
15 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
16 | return NSFetchRequest(entityName: "WordCD")
17 | }
18 |
19 | @NSManaged public var id: UUID
20 | @NSManaged public var imageURL: URL?
21 | @NSManaged public var numberOfRightAnswers: Int16
22 | @NSManaged public var numberOfWrongAnsewrs: Int16
23 | @NSManaged public var title: String
24 | @NSManaged public var translation: String?
25 | @NSManaged public var listCD: ListCD?
26 |
27 | }
28 |
29 | extension WordCD: Identifiable {
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/PrepareLearn/Builder/PrepareLearnBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrepareLearnBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 25.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct PrepareLearnBuilder {
11 | static func build(router: PrepareLearnEvent, list: List) -> UIViewController & PrepareLearnInput {
12 | let coreData = CoreDataManager.instance
13 | let presenter = PrepareLearnPresenter(
14 | router: router,
15 | list: list,
16 | fetchedListResultsController: coreData.initListFetchedResultsController()
17 | )
18 |
19 | let viewController = PrepareLearnViewController(
20 | presenter: presenter
21 | )
22 |
23 | presenter.viewController = viewController
24 |
25 | return viewController
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/QuestionView/QuestionWordViewStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuestionWordView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct QuestionWordViewStrategy: QuestionViewStrategy {
11 | var view: UIView = {
12 | let label = UILabel()
13 | label.translatesAutoresizingMaskIntoConstraints = false
14 | label.font = .titleBold1
15 | label.textColor = .label
16 | label.textAlignment = .center
17 | label.numberOfLines = 0
18 | label.adjustsFontSizeToFitWidth = true
19 | label.minimumScaleFactor = Grid.factor50
20 | label.isUserInteractionEnabled = true
21 | return label
22 | }()
23 |
24 | func set(question: Question) {
25 | (view as? UILabel)?.text = question.question
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/ChooseLanguageScreen/EN.imageset/United Kingdom (GB).svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/ImageUrlModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageUrlModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Алексей Ходаков on 17.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ImageUrlModel
11 | struct ImageUrlModel: Codable {
12 | let results: [Result]
13 | }
14 |
15 | // MARK: - Result
16 | struct Result: Codable {
17 | let urls: Urls
18 | }
19 |
20 | // MARK: - Urls
21 | struct Urls: Codable {
22 | /// Height 2000+ px
23 | let full: URL
24 | /// Height 1080 px
25 | let regular: URL
26 | /// Height 400 px
27 | let small: URL
28 | /// Height 200 px
29 | let thumb: URL
30 | }
31 |
32 | // MARK: - Alias name
33 | typealias ImageUrl = ImageUrlModel
34 |
35 |
36 | /*
37 | {
38 | "total": 85,
39 | "total_pages": 22,
40 | "results": [
41 | {
42 | "urls": {
43 | "raw":
44 | */
45 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Strategy/QuestionsStrategy/ImageQuestionsStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageQuestionsStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | class ImageQuestionsStrategy: QuestionsStrategy {
11 |
12 | func createQuestions(_ words: [Word], source: LearnLanguage.Language) -> [Question] {
13 | let questions: [Question] = words.map { word in
14 |
15 | var question: Question
16 |
17 | switch source {
18 | case .source:
19 | question = Question(question: word.translation, image: nil)
20 | case .target:
21 | question = Question(question: word.source, image: nil)
22 | }
23 |
24 | return question
25 |
26 | }
27 | return questions
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Strategy/QuestionsStrategy/WordImageQuestionsStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordImageQuestionsStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class WordImageQuestionsStrategy: QuestionsStrategy {
11 |
12 | func createQuestions(_ words: [Word], source: LearnLanguage.Language) -> [Question] {
13 | let questions: [Question] = words.map { word in
14 |
15 | var question: Question
16 |
17 | switch source {
18 | case .source:
19 | question = Question(question: word.source, image: nil)
20 | case .target:
21 | question = Question(question: word.translation, image: nil)
22 | }
23 |
24 | return question
25 | }
26 |
27 | return questions
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/Presenter/LearnSettingsPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettingsPresenter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol LearnSettingsViewInput {
11 | var learnSettingsManager: LearnSettingsManager { get set }
12 | }
13 |
14 | protocol LearnSettingsViewOutput {
15 |
16 | }
17 |
18 | class LearnSettingsPresenter {
19 |
20 | // MARK: - Properties
21 | var router: LearnSettingsEvent?
22 | weak var viewInput: (UIViewController & LearnSettingsViewInput)?
23 |
24 | // MARK: - Private properties
25 |
26 | // MARK: - Constraction
27 |
28 | init(router: LearnSettingsEvent) {
29 | self.router = router
30 | }
31 |
32 | // MARK: - Private functions
33 | }
34 |
35 | // MARK: - Functions
36 |
37 | extension LearnSettingsPresenter: LearnSettingsViewOutput {
38 | }
39 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/ListMakerTextDropDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerTextDropDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 24.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ListMakerTextDropDelegate: NSObject, UITextDropDelegate {
12 |
13 | weak var viewController: (UIViewController & ListMakerViewInput)?
14 |
15 | func textDroppableView(_ textDroppableView: UIView & UITextDroppable, dropSessionDidEnd session: UIDropSession) {
16 | session.items.forEach { dragitem in
17 | guard
18 | let item = dragitem.localObject as? String,
19 | let textField = textDroppableView as? UITextField,
20 | textField.text == item
21 | else { return }
22 | viewController?.deleteToken(token: item)
23 | }
24 | }
25 | }
26 |
27 | // swiftlint:enable line_length
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/Model/ListViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListViewModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.06.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ListViewModel {
11 | var title: String
12 | var style: GradientStyle
13 | var imageFlag: Bool
14 |
15 | static func modelFactory(list: List?) -> ListViewModel {
16 | let title = list?.title ?? ""
17 | let style = list?.style ?? .grey
18 | let imageFlag = list?.addImageFlag ?? true
19 | return ListViewModel(title: title, style: style, imageFlag: imageFlag)
20 | }
21 |
22 | func isEquals(list: List?) -> Bool? {
23 | guard let list = list else { return nil }
24 | guard
25 | list.title == title,
26 | list.addImageFlag == imageFlag,
27 | list.style == style
28 | else { return true }
29 | return false
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/Color/fiveBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.941",
9 | "green" : "0.937",
10 | "red" : "0.937"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "display-p3",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.122",
27 | "green" : "0.114",
28 | "red" : "0.114"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CLIENT_ID
6 | $(CLIENT_ID)
7 | GOOGLE_API_KEY
8 | $(GOOGLE_API_KEY)
9 | ITSAppUsesNonExemptEncryption
10 |
11 | UIApplicationSceneManifest
12 |
13 | UIApplicationSupportsMultipleScenes
14 |
15 | UISceneConfigurations
16 |
17 | UIWindowSceneSessionRoleApplication
18 |
19 |
20 | UISceneConfigurationName
21 | Default Configuration
22 | UISceneDelegateClassName
23 | $(PRODUCT_MODULE_NAME).SceneDelegate
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/LearnResults.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Results.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum LearnResults: CaseIterable {
11 | /// Workout duration
12 | case duration
13 | /// Number of correct answers
14 | case rights
15 | /// Number of training sessions
16 | case passed
17 | /// Total training time
18 | case time
19 |
20 | var description: String {
21 | switch self {
22 | case .duration:
23 | return NSLocalizedString("Last time", comment: "Description")
24 | case .rights:
25 | return NSLocalizedString("Last result", comment: "Description")
26 | case .passed:
27 | return NSLocalizedString("Total passed", comment: "Description")
28 | case .time:
29 | return NSLocalizedString("Total time", comment: "Description")
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeakUITests/FlashSpeakUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlashSpeakUITestsLaunchTests.swift
3 | // FlashSpeakUITests
4 | //
5 | // Created by Denis Dmitriev on 12.04.2023.
6 | //
7 |
8 | import XCTest
9 |
10 | final class FlashSpeakUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Progress/ProgressViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressViewDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 31.05.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | class ProgressViewDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | weak var view: ProgressViewInput?
14 |
15 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
16 | return view?.count ?? .zero
17 | }
18 |
19 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
20 | guard
21 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProgressCell.identifier, for: indexPath) as? ProgressCell
22 | else { return UICollectionViewCell() }
23 | cell.configure()
24 | return cell
25 | }
26 | }
27 |
28 | // swiftlint: enable line_length
29 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | API_KEY
6 | AIzaSyD3XO2HyGPvg6Uv4XCTItsyVTjQpcWRXK0
7 | GCM_SENDER_ID
8 | 775154845236
9 | PLIST_VERSION
10 | 1
11 | BUNDLE_ID
12 | DenisDmitriev.FlashSpeak
13 | PROJECT_ID
14 | flashspeak-7d5e6
15 | STORAGE_BUCKET
16 | flashspeak-7d5e6.appspot.com
17 | IS_ADS_ENABLED
18 |
19 | IS_ANALYTICS_ENABLED
20 |
21 | IS_APPINVITE_ENABLED
22 |
23 | IS_GCM_ENABLED
24 |
25 | IS_SIGNIN_ENABLED
26 |
27 | GOOGLE_APP_ID
28 | 1:775154845236:ios:a4a517809cb0821702ac61
29 |
30 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/LeftAlignedCollectionViewFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LeftAlignedCollectionViewFlowLayout.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
11 |
12 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
13 | let attributes = super.layoutAttributesForElements(in: rect)
14 |
15 | var leftMargin = sectionInset.left
16 | var maxY: CGFloat = -1.0
17 | attributes?.forEach { layoutAttribute in
18 | if layoutAttribute.frame.origin.y >= maxY {
19 | leftMargin = sectionInset.left
20 | }
21 |
22 | layoutAttribute.frame.origin.x = leftMargin
23 |
24 | leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
25 | maxY = max(layoutAttribute.frame.maxY, maxY)
26 | }
27 |
28 | return attributes
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/LearnSettings/Settings/LearnSettingProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettingProtocol.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 09.06.2023.
6 | //
7 |
8 | import Foundation
9 | import UIKit.UIImage
10 |
11 | protocol LearnSettingsDelegate: AnyObject {
12 | }
13 |
14 | protocol LearnSettingProtocol {
15 | associatedtype Setting: TitleImageable, RawRepresentable
16 |
17 | var active: Setting { get set }
18 | var isHidden: Bool { get }
19 | var value: Int? { get set }
20 | var all: [Setting] { get }
21 | var title: String { get }
22 | var image: UIImage? { get }
23 | var controller: LearnSettingControl { get }
24 | var delegate: LearnSettingsDelegate? { get set }
25 |
26 | static func fromUserDefaults() -> Setting
27 | func saveToUserDefaults(with value: Int?)
28 | func changed(controlValue: T)
29 | func getControlValue() -> T?
30 | }
31 |
32 | protocol TitleImageable: CaseIterable {
33 | var title: String { get }
34 | var image: UIImage? { get }
35 | }
36 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/Keyboard/AnswerTextFieldDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerTextFieldDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class AnswerTextFieldDelegate: NSObject, UITextFieldDelegate {
12 |
13 | weak var view: AnswerKeyboardViewStrategy?
14 |
15 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
16 | view?.didAnswer()
17 | return true
18 | }
19 |
20 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
21 | guard let text = textField.text?.lowercased() else { return true }
22 | var cleanedText = (text + string).cleanText()
23 | if cleanedText.last == " " {
24 | cleanedText.removeLast()
25 | }
26 | view?.answer?.answer = cleanedText
27 | return true
28 | }
29 | }
30 |
31 | // swiftlint:enable line_length
32 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/ListMakerDragDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerDragDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 24.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ListMakerDragDelegate: NSObject, UICollectionViewDragDelegate {
12 |
13 | weak var viewController: (UIViewController & ListMakerViewInput)?
14 |
15 | func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
16 | guard let item = viewController?.tokens[indexPath.item] else { return [] }
17 | let itemProvider = NSItemProvider(object: item as NSString)
18 | let dragItem = UIDragItem(itemProvider: itemProvider)
19 | dragItem.localObject = item
20 | // highlight
21 | viewController?.highlightTokenField(isActive: true)
22 | viewController?.highlightRemoveArea(isActive: true)
23 | return [dragItem]
24 | }
25 | }
26 |
27 | // swiftlint:enable line_length
28 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/ResultTableVIew/ResultTableViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultTableViewDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 03.06.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | class ResultTableViewDataSource: NSObject, UITableViewDataSource {
12 |
13 | weak var view: ResultTableView?
14 |
15 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
16 | view?.resultViewModels.count ?? .zero
17 | }
18 |
19 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
20 | guard
21 | let cell = tableView.dequeueReusableCell(withIdentifier: ResultTableViewCell.identifier, for: indexPath) as? ResultTableViewCell,
22 | let viewModel = view?.resultViewModels[indexPath.item]
23 | else { return UITableViewCell() }
24 | cell.configure(viewModel: viewModel)
25 | return cell
26 | }
27 | }
28 |
29 | // swiftlint: enable line_length
30 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/MistakeTableView/MistakeTableViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MistakeTableViewDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 03.06.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | class MistakeTableViewDataSource: NSObject, UITableViewDataSource {
12 |
13 | weak var view: MistakeTableViewProtocol?
14 |
15 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
16 | return view?.mistakeViewModels.count ?? .zero
17 | }
18 |
19 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
20 | guard
21 | let cell = tableView.dequeueReusableCell(withIdentifier: MistakeTableViewCell.identifier, for: indexPath) as? MistakeTableViewCell,
22 | let viewModel = view?.mistakeViewModels[indexPath.item]
23 | else { return UITableViewCell() }
24 | cell.configure(viewModel: viewModel)
25 | return cell
26 | }
27 | }
28 |
29 | // swiftlint: enable line_length
30 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/UIFontExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Fonts.swift
3 | // CoreDataTest
4 | //
5 | // Created by Denis Dmitriev on 17.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIFont {
11 | static var titleBold1: UIFont { UIFont.boldSystemFont(ofSize: 32) }
12 |
13 | static var title1: UIFont { UIFont.systemFont(ofSize: 32) }
14 |
15 | static var titleBold2: UIFont { UIFont.boldSystemFont(ofSize: 26) }
16 |
17 | static var title2: UIFont { UIFont.systemFont(ofSize: 26) }
18 |
19 | static var titleBold3: UIFont { UIFont.boldSystemFont(ofSize: 20) }
20 |
21 | static var title3: UIFont { UIFont.systemFont(ofSize: 20) }
22 |
23 | static var titleLight4: UIFont { UIFont.systemFont(ofSize: 20, weight: .light) }
24 |
25 | static var captionBold1: UIFont { UIFont.boldSystemFont(ofSize: 12) }
26 |
27 | static var caption2: UIFont { UIFont.systemFont(ofSize: 10) }
28 |
29 | static var subhead: UIFont { UIFont.systemFont(ofSize: 16) }
30 |
31 | static var regular: UIFont { UIFont.systemFont(ofSize: 12) }
32 | }
33 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.pt.imageset/lang.icon.pt.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/Word.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Word.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 16.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Word: Hashable {
11 | var id: UUID = UUID()
12 | var source: String
13 | var translation: String
14 | var imageURL: URL?
15 |
16 | var rightAnswers: Int = 0
17 | var wrongAnswers: Int = 0
18 |
19 | init(wordCD: WordCD) {
20 | self.id = wordCD.id
21 | self.source = wordCD.title
22 | self.translation = wordCD.translation ?? ""
23 | self.imageURL = wordCD.imageURL
24 | self.rightAnswers = Int(wordCD.numberOfRightAnswers)
25 | self.wrongAnswers = Int(wordCD.numberOfWrongAnsewrs)
26 | }
27 |
28 | init(source: String, translation: String) {
29 | self.source = source
30 | self.translation = translation
31 | }
32 |
33 | func nameForCustomImage() -> String {
34 | return id.uuidString
35 | }
36 |
37 | func learned() -> Bool {
38 | return rightAnswers - wrongAnswers > 0 ? true : false
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.de.imageset/lang.icon.de.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Caretaker/LearnCaretaker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnCaretaker.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | class LearnCaretaker {
11 |
12 | // MARK: - Propetes
13 |
14 | var learn: Learn
15 | var listID: UUID
16 |
17 | // MARK: - Constraction
18 |
19 | init(wordsCount: Int, listID: UUID) {
20 | self.learn = Learn(
21 | startTime: Date.now,
22 | finishTime: Date.now,
23 | result: .zero,
24 | count: wordsCount
25 | )
26 | self.listID = listID
27 | }
28 |
29 | // MARK: - Functions
30 |
31 | func addResult(answer: Bool) {
32 | if answer {
33 | learn.result += 1
34 | }
35 | }
36 |
37 | func finish() {
38 | learn.finishTime = Date.now
39 | saveLearnToCD(learn, for: listID)
40 | }
41 |
42 | // MARK: - Private Functions
43 |
44 | private func saveLearnToCD(_ learn: Learn, for listID: UUID) {
45 | CoreDataManager.instance.createLearn(learn, for: listID)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/Study.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Study.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 16.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Study {
11 | var id: UUID = UUID()
12 | var started: Date
13 | var sourceLanguage: Language
14 | var targetLanguage: Language
15 | var lists: [List]
16 |
17 | init(studyCD: StudyCD) {
18 | self.id = studyCD.id
19 | self.started = studyCD.startDate
20 | self.sourceLanguage = Language(rawValue: Int(studyCD.sourceLanguage)) ?? .russian
21 | self.targetLanguage = Language(rawValue: Int(studyCD.targetLanguage)) ?? .russian
22 | var lists = [List]()
23 | studyCD.listsCD?.forEach {
24 | if let list = $0 as? ListCD {
25 | let list = List(listCD: list)
26 | lists.append(list)
27 | }
28 | }
29 | self.lists = lists
30 | }
31 |
32 | init(sourceLanguage: Language, targerLanguage: Language) {
33 | self.id = UUID()
34 | self.started = Date.now
35 | self.sourceLanguage = sourceLanguage
36 | self.targetLanguage = targerLanguage
37 | self.lists = []
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeakTests/ListsPresenter/MockLictsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockLictsViewController.swift
3 | // FlashSpeakTests
4 | //
5 | // Created by Denis Dmitriev on 29.04.2023.
6 | //
7 |
8 | import UIKit
9 | @testable import FlashSpeak
10 |
11 | class MockLictsViewController: UIViewController & ListsViewInput {
12 |
13 | var listCellModels = [ListCellModel]()
14 | var presenter: ListsViewOutput?
15 |
16 | init(
17 | presenter: ListsViewOutput
18 | ) {
19 | self.presenter = presenter
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | required init?(coder: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | func didSelectList(indexPath: IndexPath) {
28 | presenter?.editList(at: indexPath)
29 | }
30 |
31 | func didTapLanguage() {
32 | presenter?.changeLanguage()
33 | }
34 |
35 | func didTapNewList() {
36 | presenter?.newList()
37 | }
38 |
39 | func reloadListsView() {
40 | // reload collection view
41 | }
42 |
43 | func configureLanguageButton(language: FlashSpeak.Language) {
44 | presenter?.changeLanguage()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/Study/StudyCD+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StudyCD+CoreDataProperties.swift
3 | // FlashSpeak
4 | //
5 | // Created by Anastasia Losikova on 16.04.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 |
13 | extension StudyCD {
14 |
15 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
16 | return NSFetchRequest(entityName: "StudyCD")
17 | }
18 |
19 | @NSManaged public var id: UUID
20 | @NSManaged public var sourceLanguage: Int16
21 | @NSManaged public var startDate: Date
22 | @NSManaged public var targetLanguage: Int16
23 | @NSManaged public var listsCD: NSSet?
24 |
25 | }
26 |
27 | // MARK: Generated accessors for listsCD
28 | extension StudyCD {
29 |
30 | @objc(addListsCDObject:)
31 | @NSManaged public func addToListsCD(_ value: ListCD)
32 |
33 | @objc(removeListsCDObject:)
34 | @NSManaged public func removeFromListsCD(_ value: ListCD)
35 |
36 | @objc(addListsCD:)
37 | @NSManaged public func addToListsCD(_ values: NSSet)
38 |
39 | @objc(removeListsCD:)
40 | @NSManaged public func removeFromListsCD(_ values: NSSet)
41 |
42 | }
43 |
44 | extension StudyCD: Identifiable {
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.en.imageset/lang.icon.en.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/UIImageExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageExtension.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 12.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIImage {
11 |
12 | func roundedImage(cornerRadius: CGFloat) -> UIImage? {
13 | let size = self.size
14 |
15 | // create image layer
16 | let imageLayer = CALayer()
17 | imageLayer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
18 | imageLayer.contents = self.cgImage
19 |
20 | // set radius
21 | imageLayer.masksToBounds = true
22 | imageLayer.cornerRadius = cornerRadius
23 |
24 | // get rounded image
25 | UIGraphicsBeginImageContext(size)
26 | if let context = UIGraphicsGetCurrentContext() {
27 | imageLayer.render(in: context)
28 | }
29 | let roundImage = UIGraphicsGetImageFromCurrentImageContext()
30 | UIGraphicsEndImageContext()
31 |
32 | return roundImage
33 | }
34 |
35 | func imageResized(to size: CGSize) -> UIImage {
36 | return UIGraphicsImageRenderer(size: size).image { _ in
37 | draw(in: CGRect(origin: .zero, size: size))
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Language/Builder/LanguageBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LanguageBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct LanguageBuilder {
11 |
12 | static func build(
13 | router: LanguageEvent,
14 | language: Language,
15 | title: String? = nil,
16 | description: String? = nil
17 | ) -> (UIViewController & LanguageViewInput) {
18 | let presenter = LanguagePresenter(router: router, language: language)
19 | let tableDataSource = LanguageTableDataSource()
20 | let tableDelegate = LanguageTableDelegate()
21 | let gestureRecognizerDelegate = LanguageGestureRecognizerDelegate()
22 |
23 | let viewInput = LanguageController(
24 | presenter: presenter,
25 | languageTableDataSource: tableDataSource,
26 | languageTableDelegate: tableDelegate,
27 | gestureRecognizerDelegate: gestureRecognizerDelegate
28 | )
29 | viewInput.setTitle(title, description: description)
30 |
31 | presenter.viewInput = viewInput
32 | tableDelegate.viewInput = viewInput
33 | tableDataSource.viewInput = viewInput
34 |
35 | return viewInput
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Language/View/LanguageTableDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LanguageTableDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class LanguageTableDataSource: NSObject, UITableViewDataSource {
11 |
12 | weak var viewInput: (UIViewController & LanguageViewInput)?
13 |
14 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
15 | return Language.allCases.count
16 | }
17 |
18 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
19 | guard
20 | let cell = tableView.dequeueReusableCell(
21 | withIdentifier: LanguageCell.identifier,
22 | for: indexPath
23 | ) as? LanguageCell,
24 | let language = viewInput?.languages[indexPath.row]
25 | else { return UITableViewCell() }
26 |
27 | cell.configure(language: language)
28 |
29 | if let targetLanguage = viewInput?.language {
30 | if language.code == targetLanguage.code {
31 | tableView.selectRow(at: indexPath, animated: false, scrollPosition: .bottom)
32 | }
33 | }
34 |
35 | return cell
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.fr.imageset/lang.icon.fr.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Helpers/Grid.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Grid.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Grid {
11 | static let cr4: CGFloat = 4
12 | static let cr8: CGFloat = 8
13 | static let cr12: CGFloat = 12
14 | static let cr16: CGFloat = 16
15 |
16 | static let pt1: CGFloat = 1
17 | static let pt2: CGFloat = 2
18 | static let pt4: CGFloat = 4
19 | static let pt6: CGFloat = 6
20 | static let pt8: CGFloat = 8
21 | static let pt12: CGFloat = 12
22 | static let pt16: CGFloat = 16
23 | static let pt24: CGFloat = 24
24 | static let pt28: CGFloat = 28
25 | static let pt32: CGFloat = 32
26 | static let pt36: CGFloat = 36
27 | static let pt44: CGFloat = 44
28 | static let pt48: CGFloat = 48
29 | static let pt64: CGFloat = 64
30 | static let pt80: CGFloat = 80
31 | static let pt96: CGFloat = 96
32 | static let pt128: CGFloat = 128
33 | static let pt256: CGFloat = 256
34 |
35 | static let factor100: CGFloat = 1
36 | static let factor85: CGFloat = 0.85
37 | static let factor75: CGFloat = 0.75
38 | static let factor50: CGFloat = 0.5
39 | static let factor35: CGFloat = 0.35
40 | static let factor25: CGFloat = 0.25
41 | static let factor20: CGFloat = 0.20
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Progress/ProgressCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 31.05.2023.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | class ProgressCell: UICollectionViewCell {
12 |
13 | static let identifier = "ProgressCell"
14 |
15 | @Published var isRight: Bool?
16 | private var store = Set()
17 |
18 | override init(frame: CGRect) {
19 | super.init(frame: frame)
20 | backgroundColor = .systemGray4
21 | layer.cornerRadius = frame.height / 2
22 | }
23 |
24 | required init?(coder: NSCoder) {
25 | fatalError("init(coder:) has not been implemented")
26 | }
27 |
28 | override func prepareForReuse() {
29 | super.prepareForReuse()
30 | isRight = nil
31 | backgroundColor = .systemGray4
32 | }
33 |
34 | func configure() {
35 | self.$isRight
36 | .receive(on: RunLoop.main)
37 | .sink { [weak self] isRight in
38 | guard let isRight = isRight else { return }
39 | UIView.animate(withDuration: 0.3) {
40 | self?.backgroundColor = isRight ? .systemGreen : .systemRed
41 | }
42 | }
43 | .store(in: &store)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.ru.imageset/lang.icon.ru.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/Model/PaddingLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PaddingLabel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 01.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class PaddingLabel: UILabel {
11 |
12 | var topInset: CGFloat
13 | var bottomInset: CGFloat
14 | var leftInset: CGFloat
15 | var rightInset: CGFloat
16 |
17 | required init(withInsets top: CGFloat, _ bottom: CGFloat, _ left: CGFloat, _ right: CGFloat) {
18 | self.topInset = top
19 | self.bottomInset = bottom
20 | self.leftInset = left
21 | self.rightInset = right
22 | super.init(frame: CGRect.zero)
23 | }
24 |
25 | required init?(coder aDecoder: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | override func drawText(in rect: CGRect) {
30 | let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset)
31 | super.drawText(in: rect.inset(by: insets))
32 | }
33 |
34 | override var intrinsicContentSize: CGSize {
35 | get {
36 | var contentSize = super.intrinsicContentSize
37 | contentSize.height += topInset + bottomInset
38 | contentSize.width += leftInset + rightInset
39 | return contentSize
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Learn/Caretaker/WordCaretaker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Caretaker.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 04.05.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | class WordCaretaker {
11 |
12 | // MARK: - Propetes
13 |
14 | var words: [Word]
15 | var mistakeWords = [Word: String]()
16 |
17 | // MARK: - Constraction
18 |
19 | init(words: [Word]) {
20 | self.words = words
21 | }
22 |
23 | // MARK: - Functions
24 |
25 | func addResult(answer: Bool, for wordID: UUID, mistake: String) {
26 | guard
27 | let index = words.firstIndex(where: { $0.id == wordID })
28 | else { return }
29 | if answer {
30 | words[index].rightAnswers += 1
31 | } else {
32 | words[index].wrongAnswers += 1
33 | mistakeWords[words[index]] = mistake
34 | if mistake.isEmpty {
35 | mistakeWords[words[index]] = NSLocalizedString("Empty", comment: "description").lowercased()
36 | }
37 | }
38 | }
39 |
40 | func finish() {
41 | updateWordInCD()
42 | }
43 |
44 | // MARK: - Private Functions
45 |
46 | private func updateWordInCD() {
47 | words.forEach { word in
48 | CoreDataManager.instance.updateWord(word, by: word.id)
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/Builder/ListsBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListsBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct ListsBuilder {
11 |
12 | static func build(router: ListsEvent) -> UIViewController & ListsViewInput {
13 | let coreData = CoreDataManager.instance
14 | let presenter = ListsPresenter(
15 | fetchedListsResultController: coreData.initListFetchedResultsController(),
16 | router: router
17 | )
18 | let listsCollectionDataSource = ListsCollectionDataSource()
19 | let listsCollectionDelegate = ListsCollectionDelegate()
20 | let searchResultsUpdating = ListSearchResultsController()
21 |
22 | let viewController = ListsViewController(
23 | presenter: presenter,
24 | listsCollectionDataSource: listsCollectionDataSource,
25 | listsCollectionDelegate: listsCollectionDelegate,
26 | searchResultsController: searchResultsUpdating
27 | )
28 |
29 | presenter.viewController = viewController
30 | listsCollectionDelegate.viewController = viewController
31 | listsCollectionDataSource.viewController = viewController
32 | searchResultsUpdating.viewController = viewController
33 |
34 | return viewController
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/TestStrategy/AnswerTestViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerViewDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class AnswerTestViewDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | weak var view: AnswerTestViewStrategy?
14 |
15 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
16 | return AnswerTestViewStrategy.numberOfItemInSections
17 | }
18 |
19 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
20 | guard
21 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AnswerWordCell.identifier, for: indexPath) as? AnswerWordCell,
22 | let testAnswer = view?.answer as? TestAnswer,
23 | testAnswer.words.count >= AnswerTestViewStrategy.numberOfItemInSections
24 | else {
25 | return collectionView.dequeueReusableCell(withReuseIdentifier: AnswerWordCell.identifier, for: indexPath) // UICollectionViewCell()
26 | }
27 | let text = testAnswer.words[indexPath.item]
28 | cell.configure(text: text)
29 | return cell
30 | }
31 | }
32 |
33 | // swiftlint:enable line_length
34 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/View/NewListColorCollectionDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewListColorCollectionDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class NewListColorCollectionDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | weak var viewInput: (UIViewController & NewListViewInput)?
14 |
15 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
16 | return viewInput?.styles.count ?? .zero
17 | }
18 |
19 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
20 | guard
21 | let cell = collectionView.dequeueReusableCell(
22 | withReuseIdentifier: ColorCell.identifier,
23 | for: indexPath
24 | ) as? ColorCell,
25 | let style = viewInput?.styles[indexPath.item]
26 | else { return UICollectionViewCell() }
27 |
28 | cell.configure(style: style)
29 | if style == viewInput?.viewModel.style {
30 | cell.isSelected = true
31 | collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredHorizontally)
32 | }
33 | return cell
34 | }
35 | }
36 |
37 | // swiftlint:enable line_length
38 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/Biulder/NewListBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewListBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct NewListBuilder {
11 |
12 | static func build(router: NewListEvent, list: List? = nil) -> (UIViewController & NewListViewInput) {
13 | let presenter = NewListPresenter(router: router, list: list)
14 | let colorCollectionDelegate = NewListColorCollectionDelegate()
15 | let colorCollectionDataSource = NewListColorCollectionDataSource()
16 | let gestureRecognizerDelegate = NewListGestureRecognizerDelegate()
17 | let textFieldDelegate = NewLisTextFieldDelegate()
18 | let viewModel: ListViewModel = .modelFactory(list: list)
19 |
20 | let viewController = NewListViewController(
21 | presenter: presenter,
22 | viewModel: viewModel,
23 | newListColorCollectionDelegate: colorCollectionDelegate,
24 | newListColorCollectionDataSource: colorCollectionDataSource,
25 | gestureRecognizerDelegate: gestureRecognizerDelegate,
26 | textFieldDelegate: textFieldDelegate
27 | )
28 |
29 | presenter.viewInput = viewController
30 | colorCollectionDelegate.viewInput = viewController
31 | colorCollectionDataSource.viewInput = viewController
32 |
33 | return viewController
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/QuestionView/QuestionImageViewStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuestionImageView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class QuestionImageViewStrategy: QuestionViewStrategy {
11 | lazy var view: UIView = {
12 | let stackView = UIStackView(arrangedSubviews: [
13 | questionImageView
14 | ])
15 | stackView.translatesAutoresizingMaskIntoConstraints = false
16 | stackView.alignment = .fill
17 | stackView.distribution = .fill
18 | stackView.spacing = Grid.pt8
19 | stackView.axis = .vertical
20 | stackView.isUserInteractionEnabled = true
21 | return stackView
22 | }()
23 |
24 | private let questionImageView: UIImageView = {
25 | let imageView = UIImageView()
26 | imageView.translatesAutoresizingMaskIntoConstraints = false
27 | imageView.contentMode = .scaleAspectFit
28 | imageView.layer.cornerRadius = Grid.cr12
29 | imageView.layer.masksToBounds = true
30 | return imageView
31 | }()
32 |
33 | func set(question: Question) {
34 | var image = question.image
35 | let cornerRadius = (question.image?.size.width ?? view.frame.width) / view.frame.width * Grid.cr12
36 | image = image?.roundedImage(cornerRadius: cornerRadius)
37 | questionImageView.image = image
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeakTests/FlashSpeakTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlashSpeakTests.swift
3 | // FlashSpeakTests
4 | //
5 | // Created by Denis Dmitriev on 12.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import XCTest
10 | @testable import FlashSpeak
11 |
12 | final class FlashSpeakTests: XCTestCase {
13 |
14 | override func setUpWithError() throws {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() throws {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | // Any test you write for XCTest can be annotated as throws and async.
26 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
27 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
28 | }
29 |
30 | func testPerformanceExample() throws {
31 | // This is an example of a performance test case.
32 | self.measure {
33 | // Put the code you want to measure the time of here.
34 | }
35 | }
36 |
37 | }
38 |
39 | // swiftlint:enable line_length
40 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/Keyboard/AnswerKeyboardViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerKeyboardViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class AnswerKeyboardViewDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var view: AnswerKeyboardViewStrategy?
14 |
15 | }
16 |
17 | extension AnswerKeyboardViewDelegate: UICollectionViewDelegateFlowLayout {
18 |
19 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
20 |
21 | let fullWidth = view?.collectionView.frame.width ?? UIScreen.main.bounds.width
22 |
23 | let width: CGFloat = fullWidth
24 | let height: CGFloat = AnswerKeyboardViewStrategy.height
25 |
26 | return CGSize(width: width, height: height)
27 | }
28 |
29 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
30 | if section == AnswerKeyboardViewStrategy.buttonSection {
31 | var edgeInsets = UIEdgeInsets()
32 | edgeInsets.top = Grid.pt8
33 | return edgeInsets
34 | } else {
35 | return UIEdgeInsets()
36 | }
37 | }
38 | }
39 |
40 | // swiftlint:enable line_length
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Errors/NetworkError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkError.swift
3 | // FlashSpeak
4 | //
5 | // Created by Алексей Ходаков on 17.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | enum NetworkError: LocalizedError {
11 | case network(description: String)
12 | case unreachableAddress(url: String)
13 | case emptyURL
14 | case invalidResponse
15 | case decodingError
16 | case unwrap
17 | case unknownError(error: Error)
18 | case imageDecodingError(error: Error)
19 |
20 | var errorDescription: String? {
21 | switch self {
22 | case .network(description: let description):
23 | return description
24 | case .unreachableAddress(let url):
25 | return NSLocalizedString("Unreachable url", comment: "Error") + url
26 | case .emptyURL:
27 | return NSLocalizedString("URL is nil", comment: "Error")
28 | case .decodingError:
29 | return NSLocalizedString("Decoding error", comment: "Error")
30 | case .invalidResponse:
31 | return NSLocalizedString("Response with mistake", comment: "Error")
32 | case .unknownError(let error):
33 | return error.localizedDescription
34 | case .imageDecodingError(let error):
35 | return NSLocalizedString("Image decoding error", comment: "Error") + error.localizedDescription
36 | case .unwrap:
37 | return NSLocalizedString("Data must not be nil", comment: "Error")
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Service/NetworkService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkService.swift
3 | // FlashSpeak
4 | //
5 | // Created by Алексей Ходаков on 17.04.2023.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import UIKit
11 |
12 | // MARK: - NetworkServiceProtocol
13 | protocol NetworkServiceProtocol {
14 | func translateWordsWithGoogle(url: URL) -> AnyPublisher
15 | func getImageURL(url: URL) -> AnyPublisher
16 | func imageLoader(url: URL) -> AnyPublisher
17 | }
18 |
19 | class NetworkService: NetworkServiceProtocol {
20 |
21 | // MARK: - Public functions
22 | func translateWordsWithGoogle(url: URL) -> AnyPublisher {
23 | URLSession.shared.publisher(for: url, queue: "translateWords")
24 | }
25 |
26 | func getImageURL(url: URL) -> AnyPublisher {
27 | URLSession.shared.publisher(for: url, queue: "getImageUrl")
28 | }
29 |
30 | func imageLoader(url: URL) -> AnyPublisher {
31 | URLSession.shared
32 | .dataTaskPublisher(for: url)
33 | .mapError({ error -> NetworkError in
34 | switch error {
35 | default:
36 | return NetworkError.unknownError(error: error)
37 | }
38 | })
39 | .map { data, _ in UIImage(data: data) }
40 | .eraseToAnyPublisher()
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/View/ColorCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 17.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class ColorCell: UICollectionViewCell {
11 |
12 | static let identifier = "ColorCell"
13 |
14 | var style: GradientStyle = .grey
15 |
16 | private lazy var gradientLayer: CAGradientLayer = {
17 | let layer = CAGradientLayer.gradientLayer(for: style, in: contentView.frame)
18 | layer.borderColor = UIColor.tintColor.cgColor
19 | return layer
20 | }()
21 |
22 | override init(frame: CGRect) {
23 | super.init(frame: frame)
24 | }
25 |
26 | required init?(coder: NSCoder) {
27 | fatalError("init(coder:) has not been implemented")
28 | }
29 |
30 | override func layoutSubviews() {
31 | super.layoutSubviews()
32 | setupStyle()
33 | }
34 |
35 | func configure(style: GradientStyle) {
36 | self.style = style
37 | }
38 |
39 | override var isSelected: Bool {
40 | willSet {
41 | super.isSelected = newValue
42 | if newValue {
43 | gradientLayer.borderWidth = Grid.pt4
44 | } else {
45 | gradientLayer.borderWidth = .zero
46 | }
47 | }
48 | }
49 |
50 | private func setupStyle() {
51 | gradientLayer.cornerRadius = Grid.cr4
52 | self.contentView.layer.insertSublayer(gradientLayer, at: 0)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/Builder/WordCardsBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCardsBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct WordCardsBuilder {
11 |
12 | static func build(list: List, router: WordCardsEvent) -> (UIViewController & WordCardsViewInput) {
13 | let coreData = CoreDataManager.instance
14 | let presenter = WordCardsPresenter(
15 | list: list,
16 | router: router,
17 | fetchedListResultsController: coreData.initListFetchedResultsController()
18 | )
19 | let collectionDelegate = WordCardsCollectionDelegate()
20 | let collectionDataSource = WordCardsCollectionDataSource()
21 | let searchBarDelegate = WordCardsSearchBarDelegate()
22 |
23 | let viewController = WordCardsViewController(
24 | title: list.title,
25 | style: list.style,
26 | presenter: presenter,
27 | wordCardsCollectionDataSource: collectionDataSource,
28 | wordCardsCollectionDelegate: collectionDelegate,
29 | searchBarDelegate: searchBarDelegate,
30 | imageFlag: list.addImageFlag
31 | )
32 |
33 | presenter.viewInput = viewController
34 | collectionDelegate.viewInput = viewController
35 | collectionDataSource.viewInput = viewController
36 | searchBarDelegate.viewController = viewController
37 |
38 | return viewController
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/Model/WordMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordMenu.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 24.05.2023.
6 | //
7 |
8 | import Foundation
9 | import UIKit.UIImage
10 |
11 | struct WordMenu {
12 |
13 | enum Action: CaseIterable {
14 | case edit
15 | case delete
16 |
17 | var title: String {
18 | switch self {
19 | case .delete:
20 | return NSLocalizedString("Delete", comment: "Menu")
21 | case .edit:
22 | return NSLocalizedString("Edit", comment: "Menu")
23 | }
24 | }
25 |
26 | var image: UIImage? {
27 | switch self {
28 | case .delete:
29 | return UIImage(systemName: "minus.circle")
30 | case .edit:
31 | return UIImage(systemName: "pencil")
32 | }
33 | }
34 | }
35 |
36 | func menu(closure: ((Action) -> Void)?) -> UIMenu {
37 | var menuElements = [UIMenuElement]()
38 | Action.allCases.forEach { wordMenu in
39 | let action = UIAction(title: wordMenu.title, image: wordMenu.image) { _ in
40 | switch wordMenu {
41 | case .edit:
42 | closure?(.edit)
43 | case .delete:
44 | closure?(.delete)
45 | }
46 | }
47 | menuElements.append(action)
48 | }
49 | return UIMenu(children: menuElements)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/Learn.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Learn.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 27.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Learn {
11 | var id: UUID = UUID()
12 | var startTime: Date
13 | var finishTime: Date
14 | var result: Int
15 | var count: Int
16 |
17 | init(learnCD: LearnCD) {
18 | self.id = learnCD.id
19 | self.startTime = learnCD.startTime
20 | self.finishTime = learnCD.finishTime
21 | self.result = Int(learnCD.result)
22 | self.count = Int(learnCD.count)
23 | }
24 |
25 | init(startTime: Date, finishTime: Date, result: Int, count: Int) {
26 | self.startTime = startTime
27 | self.finishTime = finishTime
28 | self.result = result
29 | self.count = count
30 | }
31 |
32 | func duration() -> String {
33 | let formatter = DateComponentsFormatter()
34 | switch finishTime.timeIntervalSince(startTime) {
35 | case ...60:
36 | formatter.allowedUnits = [.second]
37 | default:
38 | formatter.allowedUnits = [.minute, .second]
39 | }
40 | formatter.unitsStyle = .abbreviated
41 | formatter.zeroFormattingBehavior = [.default]
42 | let duration = formatter.string(from: startTime, to: finishTime) ?? "0"
43 | return duration
44 | }
45 |
46 | func timeInterval() -> TimeInterval {
47 | let timeInterval = finishTime.timeIntervalSince(startTime)
48 | return timeInterval
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/UIViewExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewExtension.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 23.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 | func addDashedBorder(
12 | color: UIColor,
13 | width: CGFloat = 1,
14 | dashPattern: [NSNumber] = [3, 6],
15 | cornerRadius: CGFloat = 0
16 | ) {
17 | let shapeLayer = CAShapeLayer()
18 | let shapeBounds = CGRect(
19 | x: width / 2,
20 | y: width / 2,
21 | width: bounds.width - width,
22 | height: bounds.height - width
23 | )
24 | shapeLayer.bounds = shapeBounds
25 | shapeLayer.position = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
26 | shapeLayer.fillColor = UIColor.clear.cgColor
27 | shapeLayer.strokeColor = color.cgColor
28 | shapeLayer.lineWidth = width
29 | shapeLayer.lineJoin = .round
30 | shapeLayer.lineDashPattern = dashPattern
31 | shapeLayer.path = UIBezierPath(roundedRect: shapeBounds, cornerRadius: cornerRadius).cgPath
32 | self.layer.addSublayer(shapeLayer)
33 | }
34 |
35 | func shake() {
36 | let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
37 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
38 | animation.duration = 0.6
39 | animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
40 | layer.add(animation, forKey: "shake")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/URLSession.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLSession.swift
3 | // FlashSpeak
4 | //
5 | // Created by Алексей Ходаков on 17.04.2023.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | // MARK: - URLSession
12 | extension URLSession {
13 | func publisher(
14 | for url: URL,
15 | queue label: String,
16 | responseType: T.Type = T.self,
17 | decoder: JSONDecoder = .init()
18 | ) -> AnyPublisher {
19 | dataTaskPublisher(for: url)
20 | .receive(on: DispatchQueue(label: label, qos: .background, attributes: .concurrent))
21 | .map(\.data)
22 | .decode(type: NetworkResponse.self, decoder: decoder)
23 | .mapError({ error -> NetworkError in
24 | switch error {
25 | case is URLError:
26 | return NetworkError.network(description: error.localizedDescription)
27 | case is DecodingError:
28 | return NetworkError.decodingError
29 | default:
30 | return NetworkError.invalidResponse
31 | }
32 | })
33 | .flatMap({ response -> AnyPublisher in
34 | guard let value = response.wrappedValue else {
35 | return Fail(error: NetworkError.unwrap).eraseToAnyPublisher()
36 | }
37 | return Just(value)
38 | .setFailureType(to: NetworkError.self)
39 | .eraseToAnyPublisher()
40 | })
41 | .eraseToAnyPublisher()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/ResultTableVIew/ResultTableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultTableView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 03.06.2023.
6 | //
7 | // swiftlint:disable weak_delegate
8 |
9 | import UIKit
10 |
11 | protocol ResultTableViewProtocol: AnyObject {
12 | var resultViewModels: [ResultViewModel] { get set }
13 | }
14 |
15 | class ResultTableView: UITableView {
16 |
17 | var resultViewModels = [ResultViewModel]()
18 |
19 | private let resultTableViewDataSource: UITableViewDataSource
20 | private let resultTableViewDelegate: UITableViewDelegate
21 |
22 | override init(frame: CGRect, style: UITableView.Style) {
23 | let resultTableViewDataSource = ResultTableViewDataSource()
24 | let resultTableViewDelegate = ResultTableViewDelegate()
25 | self.resultTableViewDataSource = resultTableViewDataSource
26 | self.resultTableViewDelegate = resultTableViewDelegate
27 | super.init(frame: frame, style: style)
28 | resultTableViewDelegate.view = self
29 | resultTableViewDataSource.view = self
30 | configure()
31 | }
32 |
33 | required init?(coder: NSCoder) {
34 | fatalError("init(coder:) has not been implemented")
35 | }
36 |
37 | private func configure() {
38 | delegate = resultTableViewDelegate
39 | dataSource = resultTableViewDataSource
40 | register(ResultTableViewCell.self, forCellReuseIdentifier: ResultTableViewCell.identifier)
41 | }
42 |
43 | }
44 |
45 | extension ResultTableView: ResultTableViewProtocol {
46 |
47 | }
48 |
49 | // swiftlint:enable weak_delegate
50 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeakUITests/FlashSpeakUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FlashSpeakUITests.swift
3 | // FlashSpeakUITests
4 | //
5 | // Created by Denis Dmitriev on 12.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import XCTest
10 |
11 | final class FlashSpeakUITests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 |
16 | // In UI tests it is usually best to stop immediately when a failure occurs.
17 | continueAfterFailure = false
18 |
19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
20 | }
21 |
22 | override func tearDownWithError() throws {
23 | // Put teardown code here. This method is called after the invocation of each test method in the class.
24 | }
25 |
26 | func testExample() throws {
27 | // UI tests must launch the application that they test.
28 | let app = XCUIApplication()
29 | app.launch()
30 |
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
44 | // swiftlint:enable line_length
45 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/MistakeTableView/MistakeTableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MistakeTableView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 03.06.2023.
6 | //
7 | // swiftlint: disable weak_delegate
8 |
9 | import UIKit
10 |
11 | protocol MistakeTableViewProtocol: AnyObject {
12 | var mistakeViewModels: [WordCellModel] { get set }
13 | }
14 |
15 | class MistakeTableView: UITableView {
16 |
17 | var mistakeViewModels = [WordCellModel]()
18 |
19 | private let mistakeTableViewDataSource: UITableViewDataSource
20 | private let mistakeTableViewDelegate: UITableViewDelegate
21 |
22 | override init(frame: CGRect, style: UITableView.Style) {
23 | let mistakeTableViewDataSource = MistakeTableViewDataSource()
24 | let mistakeTableViewViewDelegate = MistakeTableViewDelegate()
25 | self.mistakeTableViewDataSource = mistakeTableViewDataSource
26 | self.mistakeTableViewDelegate = mistakeTableViewViewDelegate
27 | super.init(frame: frame, style: .plain)
28 | mistakeTableViewDataSource.view = self
29 | mistakeTableViewViewDelegate.view = self
30 | configure()
31 | }
32 |
33 | required init?(coder: NSCoder) {
34 | fatalError("init(coder:) has not been implemented")
35 | }
36 |
37 | private func configure() {
38 | delegate = mistakeTableViewDelegate
39 | dataSource = mistakeTableViewDataSource
40 | register(MistakeTableViewCell.self, forCellReuseIdentifier: MistakeTableViewCell.identifier)
41 | }
42 | }
43 |
44 | extension MistakeTableView: MistakeTableViewProtocol {
45 |
46 | }
47 |
48 | // swiftlint: enable weak_delegate
49 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/ChartLearn/ChartLearnViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartLearnViewModel.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.06.2023.
6 | //
7 |
8 | import Foundation
9 | import Charts
10 |
11 | enum LearnStat: String {
12 | case rights = "Rights answers"
13 | case duration = "Exercise duration"
14 | }
15 |
16 | extension LearnStat: Plottable {
17 | var primitivePlottable: String {
18 | return NSLocalizedString(rawValue, comment: "title")
19 | }
20 | }
21 |
22 | struct ChartLearnViewModel {
23 | let stat: LearnStat
24 | let date: Date
25 | let result: Int
26 |
27 | static func modelFactory(learnings: [Learn], stats: [LearnStat]) -> [ChartLearnViewModel] {
28 | var viewModels = [ChartLearnViewModel]()
29 | stats.forEach { stat in
30 | let results: [ChartLearnViewModel]
31 | switch stat {
32 | case .rights:
33 | results = learnings
34 | .map { ChartLearnViewModel(
35 | stat: .rights,
36 | date: $0.finishTime,
37 | result: $0.result
38 | )
39 | }
40 | case .duration:
41 | results = learnings
42 | .map { ChartLearnViewModel(
43 | stat: .duration,
44 | date: $0.finishTime,
45 | result: Int($0.timeInterval())
46 | )
47 | }
48 | }
49 | viewModels.append(contentsOf: results)
50 | }
51 | return viewModels
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Resources/Assets.xcassets/LanguageIcon/lang.icon.es.imageset/lang.icon.es.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Progress/ProgressViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 31.05.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | class ProgressViewDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var view: ProgressViewInput?
14 | }
15 |
16 | extension ProgressViewDelegate: UICollectionViewDelegateFlowLayout {
17 |
18 | enum Layout {
19 | static let seporator = Grid.pt2
20 | static let aspect: CGFloat = 3
21 | }
22 |
23 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
24 | let totalWidth = collectionView.frame.width
25 | let count: CGFloat = CGFloat(view?.count ?? 1)
26 | var width = (totalWidth - (Layout.seporator * (count - 1))) / count
27 | let height = collectionView.frame.height
28 | if width < height * Layout.aspect {
29 | width = height * Layout.aspect
30 | }
31 | return CGSize(width: width, height: height)
32 | }
33 |
34 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
35 | return Layout.seporator
36 | }
37 |
38 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
39 | return Layout.seporator
40 | }
41 | }
42 |
43 | // swiftlint: enable line_length
44 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/NewList/View/NewListColorCollectionDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewListColorCollectionDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class NewListColorCollectionDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var viewInput: (UIViewController & NewListViewInput)?
14 |
15 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
16 | let style = viewInput?.styles[indexPath.item]
17 | viewInput?.viewModel.style = style ?? .grey
18 | }
19 | }
20 |
21 | extension NewListColorCollectionDelegate: UICollectionViewDelegateFlowLayout {
22 |
23 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
24 | let itemCount = CGFloat(GradientStyle.allCases.count)
25 | let spaceWidth = collectionView.frame.width - (itemCount - 1) * Grid.pt8
26 | let width = spaceWidth / itemCount
27 | let height = width
28 | return CGSize(width: width, height: height)
29 | }
30 |
31 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
32 | return Grid.pt8
33 | }
34 |
35 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
36 | return Grid.pt8
37 | }
38 | }
39 |
40 | // swiftlint:enable line_length
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/ImageCache/ImageLoader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageLoader.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.05.2023.
6 | //
7 |
8 | import Foundation
9 | import UIKit.UIImage
10 | import Combine
11 |
12 | public final class ImageLoader {
13 | public static let shared = ImageLoader()
14 |
15 | private let cache: ImageCacheType
16 | private lazy var backgroundQueue: OperationQueue = {
17 | let queue = OperationQueue()
18 | queue.maxConcurrentOperationCount = 5
19 | return queue
20 | }()
21 |
22 | public init(cache: ImageCacheType = ImageCache()) {
23 | self.cache = cache
24 | }
25 |
26 | func loadImage(from url: URL) -> AnyPublisher {
27 | if let image = cache[url] {
28 | return Just(image)
29 | .eraseToAnyPublisher()
30 | }
31 | if url.isFileURL {
32 | let image = ImageManager.shared.getImage(by: url)
33 | return Just(image)
34 | .eraseToAnyPublisher()
35 | }
36 | return URLSession.shared.dataTaskPublisher(for: url)
37 | .map { data, _ -> UIImage? in
38 | return UIImage(data: data)
39 | }
40 | .catch { _ in
41 | return Just(nil)
42 | }
43 | .handleEvents(receiveOutput: {[unowned self] image in
44 | guard let image = image else { return }
45 | self.cache[url] = image
46 | })
47 | // .print("Image loading \(url):")
48 | .subscribe(on: backgroundQueue)
49 | .receive(on: RunLoop.main)
50 | .eraseToAnyPublisher()
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/TestStrategy/AnswerTestViewStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerTestViewStrategy.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class AnswerTestViewStrategy: AnswerViewStrategy {
11 |
12 | /// Section for test answers
13 | static let numberOfItemInSections = 6
14 | static let itemPerRow: CGFloat = 2
15 | static let itemPerColumn: CGFloat = 3
16 |
17 | override init(delegate: AnswerViewControllerDelegate? = nil, color: UIColor? = nil) {
18 | super.init(delegate: delegate)
19 | self.collectionViewDataSource = AnswerTestViewDataSource()
20 | self.collectionViewDelegate = AnswerTestViewDelegate()
21 | self.collectionView.register(
22 | AnswerWordCell.self,
23 | forCellWithReuseIdentifier: AnswerWordCell.identifier
24 | )
25 | self.collectionView.dataSource = collectionViewDataSource
26 | self.collectionView.delegate = collectionViewDelegate
27 | (self.collectionViewDataSource as? AnswerTestViewDataSource)?.view = self
28 | (self.collectionViewDelegate as? AnswerTestViewDelegate)?.view = self
29 | }
30 |
31 | override func set(answer: Answer) {
32 | self.answer = answer
33 | collectionView.reloadData()
34 | }
35 |
36 | override func didAnswer(indexPath: IndexPath?) {
37 | guard
38 | let indexPath = indexPath,
39 | var testAnswer = answer as? TestAnswer
40 | else { return }
41 | testAnswer.answer = testAnswer.words[indexPath.item]
42 | delegate?.didAnswer(testAnswer)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/View/LearnSettingsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LearnSettingsViewController.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 07.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class LearnSettingsViewController: UIViewController, ObservableObject {
11 |
12 | // MARK: - Properties
13 | var learnSettingsManager: LearnSettingsManager
14 |
15 | // MARK: - Private properties
16 | private var presenter: LearnSettingsViewOutput
17 |
18 | // MARK: - Constraction
19 |
20 | init(
21 | presenter: LearnSettingsViewOutput,
22 | settingsManager: LearnSettingsManager
23 | ) {
24 | self.presenter = presenter
25 | self.learnSettingsManager = settingsManager
26 | super.init(nibName: nil, bundle: nil)
27 | }
28 |
29 | required init?(coder: NSCoder) {
30 | fatalError("init(coder:) has not been implemented")
31 | }
32 |
33 | private var learnSettingsView: LearnSettingsView {
34 | return self.view as? LearnSettingsView ?? LearnSettingsView(
35 | settingsManager: learnSettingsManager
36 | )
37 | }
38 |
39 | // MARK: - Lifecycle
40 |
41 | override func loadView() {
42 | super.loadView()
43 | self.view = LearnSettingsView(
44 | settingsManager: learnSettingsManager
45 | )
46 | }
47 |
48 | override func viewDidLoad() {
49 | super.viewDidLoad()
50 | // presenter.configureView()
51 | }
52 |
53 | // MARK: - Private functions
54 |
55 | // MARK: - Actions
56 | }
57 |
58 | // MARK: - Functions
59 |
60 | extension LearnSettingsViewController: LearnSettingsViewInput {
61 | }
62 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/ImageCache/ImageManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageManager.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 01.06.2023.
6 | //
7 |
8 | import Foundation
9 | import FirebaseCrashlytics
10 | import UIKit.UIImage
11 |
12 | public final class ImageManager {
13 | public static let shared = ImageManager()
14 | static let compressionQuality = 0.5
15 |
16 | func saveImage(image: UIImage, name: String) {
17 | saveJPG(image, name: name)
18 | }
19 |
20 | func getImage(by url: URL) -> UIImage? {
21 | guard
22 | let url = documentDirectoryPath()?.appendingPathComponent(url.lastPathComponent)
23 | else { return nil }
24 |
25 | do {
26 | let imageData = try Data(contentsOf: url)
27 | return UIImage(data: imageData)
28 | } catch {
29 | Crashlytics.crashlytics().record(error: error)
30 | print("Error loading image : \(error)")
31 | return nil
32 | }
33 | }
34 |
35 | func urlForFile(by name: String) -> URL? {
36 | return documentDirectoryPath()?
37 | .appendingPathComponent("\(name).jpg")
38 | }
39 |
40 | private func documentDirectoryPath() -> URL? {
41 | let path = FileManager.default.urls(
42 | for: .documentDirectory,
43 | in: .userDomainMask
44 | )
45 | return path.first
46 | }
47 |
48 | private func saveJPG(_ image: UIImage, name: String) {
49 | if let jpgData = image.jpegData(compressionQuality: ImageManager.compressionQuality),
50 | let path = documentDirectoryPath()?.appendingPathComponent("\(name).jpg") {
51 | try? jpgData.write(to: path)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/AppDelegate/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/TestStrategy/AnswerTestViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerTestViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class AnswerTestViewDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var view: AnswerTestViewStrategy?
14 |
15 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
16 | view?.didAnswer(indexPath: indexPath)
17 | }
18 | }
19 |
20 | extension AnswerTestViewDelegate: UICollectionViewDelegateFlowLayout {
21 |
22 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
23 |
24 | let fullWidth = view?.collectionView.frame.width ?? UIScreen.main.bounds.width
25 |
26 | let width: CGFloat = (fullWidth - (AnswerTestViewStrategy.itemPerRow - 1) * AnswerTestViewStrategy.separator) / AnswerTestViewStrategy.itemPerRow
27 | let height: CGFloat = AnswerTestViewStrategy.height
28 |
29 | return CGSize(width: width, height: height)
30 | }
31 |
32 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
33 | return AnswerTestViewStrategy.separator
34 | }
35 |
36 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
37 | return AnswerTestViewStrategy.separator
38 | }
39 | }
40 |
41 | // swiftlint:enable line_length
42 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/Builder/ListMakerBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerBuilder.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 21.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | struct ListMakerBuilder {
11 | static func build(list: List, router: ListMakerEvent) -> (UIViewController & ListMakerViewInput) {
12 | let presenter = ListMakerPresenter(list: list, router: router)
13 | let tokenFieldDelegate = ListMakerTokenFieldDelegate()
14 | let collectionDataSource = ListMakerCollectionViewDataSource()
15 | let collectionDelegate = ListMakerCollectionViewDelegate()
16 | let collectionDragDelegate = ListMakerDragDelegate()
17 | let collectionDropDelegate = ListMakerDropDelegate()
18 | let textDropDelegate = ListMakerTextDropDelegate()
19 |
20 | let viewController = ListMakerViewController(
21 | presenter: presenter,
22 | tokenFieldDelegate: tokenFieldDelegate,
23 | collectionDataSource: collectionDataSource,
24 | collectionDelegate: collectionDelegate,
25 | collectionDragDelegate: collectionDragDelegate,
26 | collectionDropDelegate: collectionDropDelegate,
27 | textDropDelegate: textDropDelegate
28 | )
29 |
30 | presenter.viewController = viewController
31 | tokenFieldDelegate.viewController = viewController
32 | collectionDataSource.viewController = viewController
33 | collectionDelegate.viewController = viewController
34 | collectionDragDelegate.viewController = viewController
35 | collectionDropDelegate.viewController = viewController
36 | textDropDelegate.viewController = viewController
37 |
38 | return viewController
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/View/ImageCollectionView/ImageCollectionDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageCollectionDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ImageCollectionDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | enum StaticCell {
14 | static let count: Int = 1
15 | }
16 |
17 | weak var view: ImageCollectionViewInput?
18 |
19 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
20 | let cellsCount = view?.images.count ?? .zero
21 | return cellsCount + StaticCell.count
22 | }
23 |
24 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
25 | if indexPath.item == view?.images.count ?? .zero {
26 | guard
27 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddImageCell.identifier, for: indexPath) as? AddImageCell
28 | else { return UICollectionViewCell() }
29 | cell.button.addTarget(self, action: #selector(addImage(sender:)), for: .touchUpInside)
30 | return cell
31 | } else {
32 | guard
33 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCell.identifier, for: indexPath) as? ImageCell,
34 | let image = view?.images[indexPath.item]
35 | else { return UICollectionViewCell() }
36 | cell.configure(image: image)
37 | return cell
38 | }
39 | }
40 |
41 | @objc func addImage(sender: UIButton) {
42 | view?.didTapAddImage()
43 | }
44 | }
45 |
46 | // swiftlint:enable line_length
47 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/View/ImageCollectionView/AddImageCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddImageCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 01.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class AddImageCell: UICollectionViewCell {
11 |
12 | // MARK: - Properties
13 | static let identifier: String = "AddImageCell"
14 |
15 | // MARK: - Subviews
16 |
17 | let button: UIButton = {
18 | var configure: UIButton.Configuration = .borderless()
19 | configure.image = UIImage(systemName: "plus")
20 | configure.baseForegroundColor = .tintColor
21 | let button = UIButton(configuration: configure)
22 | button.translatesAutoresizingMaskIntoConstraints = false
23 | return button
24 | }()
25 |
26 | // MARK: - Constraction
27 |
28 | override init(frame: CGRect) {
29 | super.init(frame: frame)
30 | self.configureUI()
31 | backgroundColor = .fiveBackgroundColor
32 | layer.cornerRadius = Grid.cr8
33 | layer.masksToBounds = true
34 | }
35 |
36 | required init?(coder: NSCoder) {
37 | fatalError("init(coder:) has not been implemented")
38 | }
39 |
40 | // MARK: - UI
41 |
42 | private func configureUI() {
43 | contentView.addSubview(button)
44 | setupConstraints()
45 | }
46 |
47 | private func setupConstraints() {
48 | NSLayoutConstraint.activate([
49 | button.topAnchor.constraint(equalTo: contentView.topAnchor),
50 | button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
51 | button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
52 | button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
53 | ])
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/View/ListSearchResultsController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListSearchResultsController.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class ListSearchResultsController: NSObject, UISearchResultsUpdating {
11 |
12 | weak var viewController: (UIViewController & ListsViewInput)?
13 |
14 | func updateSearchResults(for searchController: UISearchController) {
15 | guard
16 | let searchText = searchController.searchBar.text?.lowercased()
17 | else { return }
18 | let isSearching = !searchText.isEmpty
19 | viewController?.isSearching = isSearching
20 | if isSearching {
21 | viewController?.serachListCellModels.removeAll()
22 | let viewModels = viewController?.listCellModels
23 | .filter({
24 | $0.title.contains(searchText) ||
25 | $0.words.joined(separator: " ").contains(searchText)
26 | }) ?? []
27 | viewController?.serachListCellModels = viewModels
28 | } else {
29 | viewController?.serachListCellModels.removeAll()
30 | let viewModels = viewController?.listCellModels ?? []
31 | viewController?.serachListCellModels = viewModels
32 | }
33 | viewController?.reloadListsView()
34 | }
35 | }
36 |
37 | extension ListSearchResultsController: UISearchBarDelegate {
38 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
39 | viewController?.isSearching = false
40 | viewController?.serachListCellModels.removeAll()
41 | let viewModels = viewController?.listCellModels
42 | viewController?.serachListCellModels = viewModels ?? []
43 | viewController?.reloadListsView()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/View/ImageCollectionView/ImageCollectionDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageCollectionDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ImageCollectionDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var view: ImageCollectionViewInput?
14 |
15 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
16 | view?.didSelectImage(indexPath: indexPath)
17 | }
18 | }
19 |
20 | extension ImageCollectionDelegate: UICollectionViewDelegateFlowLayout {
21 |
22 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
23 | let height: CGFloat = collectionView.frame.height
24 | let width: CGFloat
25 | guard
26 | let cell = collectionView.cellForItem(at: indexPath) as? ImageCell,
27 | let imageSize = cell.imageSize()
28 | else {
29 | width = height
30 | return CGSize(width: width, height: height)
31 | }
32 | let aspect = imageSize.width / imageSize.height
33 | width = aspect * height
34 | return CGSize(width: width, height: height)
35 | }
36 |
37 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
38 | return Grid.pt8
39 | }
40 |
41 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
42 | return Grid.pt8
43 | }
44 | }
45 |
46 | // swiftlint:enable line_length
47 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/CoreDataModel/List/ListCD+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListCD+CoreDataProperties.swift
3 | // FlashSpeak
4 | //
5 | // Created by Anastasia Losikova on 16.04.2023.
6 | //
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 |
13 | extension ListCD {
14 |
15 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
16 | return NSFetchRequest(entityName: "ListCD")
17 | }
18 |
19 | @NSManaged public var addImageFlag: Bool
20 | @NSManaged public var creationDate: Date
21 | @NSManaged public var id: UUID
22 | @NSManaged public var style: Int16
23 | @NSManaged public var title: String
24 | @NSManaged public var studyCD: StudyCD?
25 | @NSManaged public var wordsCD: NSSet?
26 | @NSManaged public var learnsCD: NSSet?
27 |
28 | }
29 |
30 | // MARK: Generated accessors for wordsCD
31 | extension ListCD {
32 |
33 | @objc(addWordsCDObject:)
34 | @NSManaged public func addToWordsCD(_ value: WordCD)
35 |
36 | @objc(removeWordsCDObject:)
37 | @NSManaged public func removeFromWordsCD(_ value: WordCD)
38 |
39 | @objc(addWordsCD:)
40 | @NSManaged public func addToWordsCD(_ values: NSSet)
41 |
42 | @objc(removeWordsCD:)
43 | @NSManaged public func removeFromWordsCD(_ values: NSSet)
44 |
45 | }
46 |
47 | // MARK: Generated accessors for learnsCD
48 | extension ListCD {
49 |
50 | @objc(addLearnsCDObject:)
51 | @NSManaged public func addToLearnsCD(_ value: LearnCD)
52 |
53 | @objc(removeLearnsCDObject:)
54 | @NSManaged public func removeFromLearnsCD(_ value: LearnCD)
55 |
56 | @objc(addLearnsCD:)
57 | @NSManaged public func addToLearnsCD(_ values: NSSet)
58 |
59 | @objc(removeLearnsCD:)
60 | @NSManaged public func removeFromLearnsCD(_ values: NSSet)
61 |
62 | }
63 |
64 | extension ListCD: Identifiable {
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/ListMakerCollectionViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerCollectionViewDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 24.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ListMakerCollectionViewDelegate: NSObject, UICollectionViewDelegate {
12 |
13 | weak var viewController: (UIViewController & ListMakerViewInput)?
14 |
15 | }
16 |
17 | extension ListMakerCollectionViewDelegate: UICollectionViewDelegateFlowLayout {
18 |
19 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
20 | return Grid.pt8
21 | }
22 |
23 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
24 | return Grid.pt8
25 | }
26 |
27 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
28 | switch collectionView.tag {
29 | case ListMakerView.Initial.tokenCollectionTag:
30 | let label = UILabel(frame: CGRect.zero)
31 | label.font = TokenCell().tokenLabel.font
32 | label.text = viewController?.tokens[indexPath.item]
33 | label.sizeToFit()
34 | return CGSize(width: label.frame.width + Grid.pt16, height: label.frame.height + Grid.pt8)
35 | case ListMakerView.Initial.removeCollectionTag:
36 | return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
37 | default:
38 | return CGSize()
39 | }
40 |
41 | }
42 | }
43 |
44 | // swiftlint:enable line_length
45 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/View/WordCardsSearchBarDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCardsSearchBarDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 14.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class WordCardsSearchBarDelegate: NSObject, UISearchBarDelegate {
11 | weak var viewController: (UIViewController & WordCardsViewInput)?
12 |
13 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
14 | viewController?.isSearching = false
15 | viewController?.searchingWordCardCellModels.removeAll()
16 | let viewModels = viewController?.wordCardCellModels ?? []
17 | viewController?.searchingWordCardCellModels = viewModels
18 | viewController?.reloadWordCardsCollection()
19 | }
20 |
21 | }
22 |
23 | extension WordCardsSearchBarDelegate: UISearchResultsUpdating {
24 | func updateSearchResults(for searchController: UISearchController) {
25 | guard
26 | let searchText = searchController.searchBar.text?.lowercased()
27 | else { return }
28 | let isSearching = !searchText.isEmpty
29 | viewController?.isSearching = isSearching
30 | if isSearching {
31 | viewController?.searchingWordCardCellModels.removeAll()
32 | let filteredViewModels = viewController?.wordCardCellModels
33 | .filter({
34 | $0.source.contains(searchText) ||
35 | $0.translation.contains(searchText)
36 | }) ?? []
37 | viewController?.searchingWordCardCellModels = filteredViewModels
38 | } else {
39 | viewController?.searchingWordCardCellModels.removeAll()
40 | let viewModels = viewController?.wordCardCellModels ?? []
41 | viewController?.searchingWordCardCellModels = viewModels
42 | }
43 | viewController?.reloadWordCardsCollection()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Coordinator/Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | // MARK: - Coordinator
11 | protocol Coordinator: AnyObject {
12 | var finishDelegate: CoordinatorFinishDelegate? { get set }
13 | // Каждому координатору назначен один навигационный контроллер
14 | var navigationController: UINavigationController { get set }
15 | /// Массив для всех дочерних координаторов
16 | var childCoordinators: [Coordinator] { get set }
17 | /// Определенный тип потока
18 | var type: CoordinatorType { get }
19 | /// Место, где можно поставить логику, чтобы начать поток
20 | func start()
21 | /// Место, где можно поставить логику, чтобы закончить поток,
22 | /// очистить всех дочерних координаторов и уведомить родителя о том,
23 | /// что этот координатор готов к завершению
24 | func finish()
25 |
26 | func reload()
27 |
28 | init(_ navigationController: UINavigationController)
29 | }
30 |
31 | extension Coordinator {
32 | func finish() {
33 | childCoordinators.removeAll()
34 | finishDelegate?.coordinatorDidFinish(childCoordinator: self)
35 | }
36 |
37 | func reload() {
38 | childCoordinators.removeAll()
39 | finishDelegate?.coordinatorDidReload(childCoordinator: self)
40 | }
41 | }
42 |
43 | // MARK: - CoordinatorOutput
44 | /// Протокол делегата, помогающий родительскому координатору узнать, когда его дочерний готов к завершению
45 | protocol CoordinatorFinishDelegate: AnyObject {
46 | func coordinatorDidFinish(childCoordinator: Coordinator)
47 | func coordinatorDidReload(childCoordinator: Coordinator)
48 | }
49 |
50 | // MARK: - CoordinatorType
51 | /// Используя эту структуру, мы можем определить, какой тип потока мы можем использовать в приложении
52 | enum CoordinatorType {
53 | case welcome, lists, statistic
54 | }
55 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/Keyboard/AnswerButtonCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerButtonCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 06.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class AnswerButtonCell: UICollectionViewCell {
11 |
12 | // MARK: - Propetes
13 |
14 | static let identifier = "AnswerButtonCell"
15 |
16 | // MARK: - Subviews
17 |
18 | let button: UIButton = {
19 | let button = UIButton(configuration: .appFilled())
20 | button.translatesAutoresizingMaskIntoConstraints = false
21 | let title = NSLocalizedString("Check", comment: "Button")
22 | button.setTitle(title, for: .normal)
23 | return button
24 | }()
25 |
26 | // MARK: - Init
27 |
28 | override init(frame: CGRect) {
29 | super.init(frame: frame)
30 | configureView()
31 | configureSubviews()
32 | addConstraints()
33 | }
34 |
35 | // MARK: - Lifecycle
36 |
37 | required init?(coder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | // MARK: - UI
42 |
43 | private func configureView() {
44 | }
45 |
46 | private func configureSubviews() {
47 | contentView.addSubview(button)
48 | }
49 |
50 | // MARK: - Methods
51 |
52 | func configure(color: UIColor?) {
53 | button.tintColor = color
54 | }
55 |
56 | // MARK: - Constraints
57 |
58 | private func addConstraints() {
59 | NSLayoutConstraint.activate([
60 |
61 | button.topAnchor.constraint(equalTo: contentView.topAnchor),
62 | button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
63 | button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
64 | button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
65 | ])
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/ListMakerTokenFieldDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMakerTokenFieldDelegate.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 21.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ListMakerTokenFieldDelegate: NSObject, UITextFieldDelegate {
12 |
13 | weak var viewController: (UIViewController & ListMakerViewInput)?
14 |
15 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
16 | switch string {
17 | /// Paste text seporated by ","
18 | case UIPasteboard.general.string:
19 | let wordsByComma = string.components(separatedBy: ",")
20 | addTokens(wordsByComma)
21 | return false
22 |
23 | /// Paste text seporated by new line "\n"
24 | case UIPasteboard.general.string?.components(separatedBy: "\n").joined(separator: " "):
25 | let wordsByNewLine = string.components(separatedBy: " ")
26 | addTokens(wordsByNewLine)
27 | return false
28 |
29 | /// Keyboard typing with "," action
30 | case ",":
31 | addToken(textField.text)
32 | return false
33 | default:
34 | return true
35 | }
36 | }
37 |
38 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
39 | addToken(textField.text)
40 | return true
41 | }
42 |
43 | // MARK: - Private functions
44 |
45 | private func addTokens(_ tokens: [String]) {
46 | tokens.forEach { word in
47 | viewController?.addToken(token: word.lowercased())
48 | }
49 | }
50 |
51 | private func addToken(_ token: String?) {
52 | guard
53 | let token = token
54 | else { return }
55 | addTokens([token])
56 | }
57 | }
58 |
59 | // swiftlint:enable line_length
60 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/AnswerView/Keyboard/AnswerKeyboardViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnswerKeyboardViewDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class AnswerKeyboardViewDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | weak var view: AnswerKeyboardViewStrategy?
14 |
15 | func numberOfSections(in collectionView: UICollectionView) -> Int {
16 | return AnswerKeyboardViewStrategy.numberOfSections
17 | }
18 |
19 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
20 | return AnswerKeyboardViewStrategy.numberOfItemsInSection
21 | }
22 |
23 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
24 | switch indexPath.section {
25 | case AnswerKeyboardViewStrategy.textFiledSection:
26 | guard
27 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AnswerKeyboardCell.identifier, for: indexPath) as? AnswerKeyboardCell
28 | else { return UICollectionViewCell() }
29 | cell.answerTextField.delegate = view?.textFieldDelegate
30 | return cell
31 | case AnswerKeyboardViewStrategy.buttonSection:
32 | guard
33 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AnswerButtonCell.identifier, for: indexPath) as? AnswerButtonCell
34 | else { return UICollectionViewCell() }
35 | cell.configure(color: view?.color)
36 | cell.button.addTarget(self, action: #selector(buttonDidTap(sender:)), for: .touchUpInside)
37 | return cell
38 | default:
39 | return UICollectionViewCell()
40 | }
41 | }
42 |
43 | @objc func buttonDidTap(sender: UIButton) {
44 | view?.answerDidTap()
45 | }
46 | }
47 |
48 | // swiftlint:enable line_length
49 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/View/WordCardsCollectionDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WordCardsCollectionDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class WordCardsCollectionDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | weak var viewInput: (UIViewController & WordCardsViewInput)?
14 |
15 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
16 | if viewInput?.isSearching ?? false {
17 | return viewInput?.searchingWordCardCellModels.count ?? 0
18 | } else {
19 | return viewInput?.wordCardCellModels.count ?? 0
20 | }
21 | }
22 |
23 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
24 | guard
25 | let cell = collectionView.dequeueReusableCell(
26 | withReuseIdentifier: WordCardViewCell.identifier,
27 | for: indexPath
28 | ) as? WordCardViewCell,
29 | let wordCardCellModel = (viewInput?.isSearching ?? false) ?
30 | viewInput?.searchingWordCardCellModels[indexPath.item] :
31 | viewInput?.wordCardCellModels[indexPath.item],
32 | let style = viewInput?.style
33 | else { return UICollectionViewCell() }
34 | let menu = menu(indexPath: indexPath)
35 | cell.configure(wordCardCellModel: wordCardCellModel, style: style, menu: menu, imageFlag: viewInput?.imageFlag ?? false)
36 | return cell
37 | }
38 |
39 | private func menu(indexPath: IndexPath) -> UIMenu {
40 | let closure: (WordMenu.Action) -> Void = { [weak self] action in
41 | switch action {
42 | case .edit:
43 | self?.viewInput?.presenter.edit(by: indexPath)
44 | case .delete:
45 | self?.viewInput?.presenter.deleteWords(by: [indexPath])
46 | }
47 | }
48 | return WordMenu().menu(closure: closure)
49 | }
50 | }
51 |
52 | // swiftlint:enable line_length
53 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/TokenCollectionView/TokenCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TokenCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 22.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class TokenCell: UICollectionViewCell {
11 |
12 | static let identifier = "TokenCell"
13 |
14 | // MARK: - Subviews
15 |
16 | let tokenLabel: UILabel = {
17 | let label = UILabel()
18 | label.translatesAutoresizingMaskIntoConstraints = false
19 | label.font = .subhead
20 | label.textColor = .white
21 | label.textAlignment = .center
22 | label.backgroundColor = .tintColor
23 | label.numberOfLines = 1
24 | label.layer.masksToBounds = true
25 | return label
26 | }()
27 |
28 | // MARK: - Constraction
29 |
30 | override init(frame: CGRect) {
31 | super.init(frame: frame)
32 | configureView()
33 | configureSubviews()
34 | addConstraints()
35 | }
36 |
37 | required init?(coder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | override func draw(_ rect: CGRect) {
42 | super.draw(rect)
43 | }
44 |
45 | // MARK: - UI
46 |
47 | private func configureSubviews() {
48 | contentView.addSubview(tokenLabel)
49 | }
50 |
51 | private func configureView() {
52 | layer.cornerRadius = frame.height / 2
53 | layer.masksToBounds = true
54 | }
55 |
56 | // MARK: - Methods
57 |
58 | func configure(text: String, color: UIColor?) {
59 | tokenLabel.backgroundColor = color
60 | tokenLabel.text = text
61 | }
62 |
63 | // MARK: - Constraints
64 |
65 | private func addConstraints() {
66 | NSLayoutConstraint.activate([
67 | tokenLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
68 | tokenLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
69 | tokenLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
70 | tokenLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
71 | ])
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/View/ProfileButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileButton.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 17.06.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class ProfileButton: UIButton {
11 |
12 | override init(frame: CGRect) {
13 | super.init(frame: frame)
14 | var configuration: UIButton.Configuration = .plain()
15 | configuration.cornerStyle = .medium
16 | configuration.buttonSize = .small
17 | configuration.imagePlacement = .leading
18 | configuration.imagePadding = Grid.pt8
19 | // configuration.subtitle = NSLocalizedString("Profile", comment: "button")
20 | self.configuration = configuration
21 | self.imageView?.contentMode = .scaleAspectFit
22 | self.imageView?.layer.cornerRadius = Grid.cr4
23 | self.imageView?.layer.masksToBounds = true
24 | // self.imageView?.layer.borderWidth = Grid.pt2
25 | // self.imageView?.layer.borderColor = UIColor.tintColor.cgColor
26 | }
27 |
28 | required init?(coder: NSCoder) {
29 | fatalError("init(coder:) has not been implemented")
30 | }
31 |
32 | override func draw(_ rect: CGRect) {
33 | // Drawing code
34 | }
35 |
36 | func update(by language: Language) {
37 | self.configurationUpdateHandler = { button in
38 | if let image = UIImage(named: language.code) {
39 | var configuration = button.configuration
40 | let aspect = image.size.width / image.size.height
41 | let height = Grid.pt28
42 | let width = height * aspect
43 | let imageSize = CGSize(width: width, height: height)
44 | configuration?.image = image.imageResized(to: imageSize)
45 | var container = AttributeContainer()
46 | container.font = .boldSystemFont(ofSize: 16)
47 | configuration?.attributedTitle = AttributedString(language.description, attributes: container)
48 |
49 | button.configuration = configuration
50 | // button.imageView?.layer.cornerRadius = height / 2
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/Model/ListMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListMenuAction.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.05.2023.
6 | //
7 |
8 | import Foundation
9 | import UIKit.UIImage
10 |
11 | struct ListMenu {
12 |
13 | enum Action: CaseIterable {
14 | case editCards
15 | case editWords
16 | case transfer
17 | case delete
18 |
19 | var title: String {
20 | switch self {
21 | case .delete:
22 | return NSLocalizedString("Delete", comment: "Menu")
23 | case .editCards:
24 | return NSLocalizedString("Edit cards", comment: "Menu")
25 | case .editWords:
26 | return NSLocalizedString("Edit list", comment: "Menu")
27 | case .transfer:
28 | return NSLocalizedString("Move to another profile", comment: "Menu")
29 | }
30 | }
31 |
32 | var image: UIImage? {
33 | switch self {
34 | case .delete:
35 | return UIImage(systemName: "minus.circle")
36 | case .editCards:
37 | return UIImage(systemName: "square.and.pencil")
38 | case .editWords:
39 | return UIImage(systemName: "pencil")
40 | case .transfer:
41 | return UIImage(systemName: "arrow.turn.up.right")
42 | }
43 | }
44 | }
45 |
46 | func menu(closure: ((Action) -> Void)?) -> UIMenu {
47 | var menuElements = [UIMenuElement]()
48 | Action.allCases.forEach { listMenu in
49 | let action = UIAction(title: listMenu.title, image: listMenu.image) { _ in
50 | switch listMenu {
51 | case .editCards:
52 | closure?(.editCards)
53 | case .editWords:
54 | closure?(.editWords)
55 | case .transfer:
56 | closure?(.transfer)
57 | case .delete:
58 | closure?(.delete)
59 | }
60 | }
61 | menuElements.append(action)
62 | }
63 | return UIMenu(children: menuElements)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Strategy/QuestionView/QuestionWordImageViewStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuestionWordImageView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 19.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class QuestionWordImageViewStrategy: QuestionViewStrategy {
11 | lazy var view: UIView = {
12 | let stackView = UIStackView(arrangedSubviews: [
13 | questionImageView,
14 | questionLabel
15 | ])
16 | stackView.translatesAutoresizingMaskIntoConstraints = false
17 | stackView.alignment = .fill
18 | stackView.distribution = .fill
19 | stackView.spacing = Grid.pt8
20 | stackView.axis = .vertical
21 | stackView.layoutMargins = UIEdgeInsets(
22 | top: Grid.pt8,
23 | left: Grid.pt8,
24 | bottom: Grid.pt8,
25 | right: Grid.pt8
26 | )
27 | stackView.isLayoutMarginsRelativeArrangement = true
28 | stackView.isUserInteractionEnabled = true
29 | return stackView
30 | }()
31 |
32 | private let questionLabel: UILabel = {
33 | let label = UILabel()
34 | label.translatesAutoresizingMaskIntoConstraints = false
35 | label.font = .titleBold1
36 | label.textColor = .label
37 | label.textAlignment = .center
38 | label.numberOfLines = 0
39 | label.adjustsFontSizeToFitWidth = true
40 | label.minimumScaleFactor = Grid.factor50
41 | return label
42 | }()
43 |
44 | private let questionImageView: UIImageView = {
45 | let imageView = UIImageView()
46 | imageView.translatesAutoresizingMaskIntoConstraints = false
47 | imageView.contentMode = .scaleAspectFit
48 | imageView.layer.cornerRadius = Grid.cr12
49 | imageView.layer.masksToBounds = true
50 | return imageView
51 | }()
52 |
53 | func set(question: Question) {
54 | questionLabel.text = question.question
55 | var image = question.image
56 | let cornerRadius = (question.image?.size.width ?? view.frame.width) / view.frame.width * Grid.cr12
57 | image = image?.roundedImage(cornerRadius: cornerRadius)
58 | questionImageView.image = image
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Core/Extensions/UIButtonConfigurationExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIButtonConfigurationExtension.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 21.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIButton.Configuration {
11 | public static func appFilled() -> UIButton.Configuration {
12 | var configuration = UIButton.Configuration.filled()
13 | configuration.cornerStyle = .large
14 | configuration.buttonSize = .medium
15 | configuration.titleTextAttributesTransformer = .init({ incoming in
16 | var outgoing = incoming
17 | outgoing.font = UIFont.title3
18 | return outgoing
19 | })
20 | return configuration
21 | }
22 |
23 | public static func appGray() -> UIButton.Configuration {
24 | var configuration = UIButton.Configuration.gray()
25 | configuration.cornerStyle = .large
26 | configuration.buttonSize = .medium
27 | configuration.titleTextAttributesTransformer = .init({ incoming in
28 | var outgoing = incoming
29 | outgoing.font = UIFont.title3
30 | return outgoing
31 | })
32 | return configuration
33 | }
34 |
35 | public static func appTinted() -> UIButton.Configuration {
36 | var configuration = UIButton.Configuration.tinted()
37 | configuration.cornerStyle = .large
38 | configuration.buttonSize = .medium
39 | configuration.titleTextAttributesTransformer = .init({ incoming in
40 | var outgoing = incoming
41 | outgoing.font = UIFont.title3
42 | return outgoing
43 | })
44 | return configuration
45 | }
46 |
47 | public static func appFilledInvert() -> UIButton.Configuration {
48 | var configuration = UIButton.Configuration.filled()
49 | configuration.baseBackgroundColor = .fiveBackgroundColor
50 | configuration.cornerStyle = .large
51 | configuration.buttonSize = .medium
52 | configuration.titleTextAttributesTransformer = .init({ incoming in
53 | var outgoing = incoming
54 | outgoing.font = UIFont.title3
55 | return outgoing
56 | })
57 | return configuration
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Lists/View/ListsCollectionDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListsCollectionDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 18.04.2023.
6 | //
7 | // swiftlint:disable line_length
8 |
9 | import UIKit
10 |
11 | class ListsCollectionDataSource: NSObject, UICollectionViewDataSource {
12 |
13 | weak var viewController: (UIViewController & ListsViewInput)?
14 |
15 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
16 | let isSearching = viewController?.isSearching ?? false
17 | if isSearching {
18 | return viewController?.serachListCellModels.count ?? 0
19 | } else {
20 | return viewController?.listCellModels.count ?? 0
21 | }
22 | }
23 |
24 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
25 | guard
26 | let cell = collectionView.dequeueReusableCell(
27 | withReuseIdentifier: ListCell.identifier,
28 | for: indexPath
29 | ) as? ListCell,
30 | let isSearching = viewController?.isSearching,
31 | let listCellModel = isSearching ? viewController?.serachListCellModels[indexPath.row] : viewController?.listCellModels[indexPath.row]
32 | else { return UICollectionViewCell() }
33 | let menu = menu(indexPath: indexPath)
34 | cell.configure(listCellModel: listCellModel, menu: menu)
35 | return cell
36 | }
37 |
38 | private func menu(indexPath: IndexPath) -> UIMenu {
39 | let closure: (ListMenu.Action) -> Void = { [weak self] action in
40 | switch action {
41 | case .editCards:
42 | self?.viewController?.presenter.editList(at: indexPath)
43 | case .editWords:
44 | self?.viewController?.presenter.editWords(at: indexPath)
45 | case .transfer:
46 | self?.viewController?.presenter.transfer(at: indexPath)
47 | case .delete:
48 | self?.viewController?.presenter.deleteList(at: indexPath)
49 | }
50 | }
51 | return ListMenu().menu(closure: closure)
52 | }
53 | }
54 |
55 | // swiftlint:enable line_length
56 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Language/Presenter/LanguagePresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LanguagePresenter.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 20.04.2023.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | protocol LanguageViewInput {
12 | var languages: [Language] { get }
13 | var language: Language? { get set }
14 |
15 | func setTitle(_ title: String?, description: String?)
16 | func didSelectItem(indexPath: IndexPath)
17 | func didTabBackground()
18 | }
19 |
20 | protocol LanguageViewOutput {
21 | var router: LanguageEvent? { get }
22 | var language: Language? { get set }
23 |
24 | func viewDidSelectedLanguage(language: Language)
25 | func viewDidTapBackground()
26 | func subscribe()
27 | }
28 |
29 | class LanguagePresenter: ObservableObject {
30 |
31 | @Published var language: Language?
32 | var router: LanguageEvent?
33 | weak var viewInput: (UIViewController & LanguageViewInput)?
34 |
35 | private var store = Set()
36 |
37 | init(router: LanguageEvent, language: Language) {
38 | self.router = router
39 | self.language = language
40 | }
41 |
42 | private func getLocalLanguage() {
43 | let localLanguageCode = Locale.current.language.languageCode?.identifier ?? "ru"
44 | let sourceLanguage = Language.language(by: localLanguageCode) ?? .russian
45 | language = sourceLanguage
46 | }
47 |
48 | private func changeStudy(to language: Language) {
49 | // Сhange study function
50 | // check for exsist model Study by selected language or create new model Study language
51 | // Save to core data
52 | }
53 |
54 | }
55 |
56 | extension LanguagePresenter: LanguageViewOutput {
57 |
58 | func subscribe() {
59 | self.$language
60 | .receive(on: RunLoop.main)
61 | .sink { language in
62 | self.viewInput?.language = language
63 | }
64 | .store(in: &store)
65 | }
66 |
67 | func viewDidTapBackground() {
68 | router?.didSendEventClosure?(.close)
69 | }
70 |
71 | func viewDidSelectedLanguage(language: Language) {
72 | changeStudy(to: language)
73 | router?.didSendEventClosure?(.change(language: language))
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/WordCards/View/ResultStackView/ResultStackView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultStackView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 27.04.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol ResultableView {
11 | var kind: ResultStackView.Result { get }
12 | var titleLabel: UILabel { get }
13 | var resultLabel: UILabel { get }
14 | }
15 |
16 | final class ResultStackView: UIStackView, ResultableView {
17 |
18 | enum Result {
19 | case learns, result, time
20 |
21 | var title: String {
22 | switch self {
23 | case .learns:
24 | return NSLocalizedString("Workouts", comment: "Title")
25 | case .result:
26 | return NSLocalizedString("Last result", comment: "Title")
27 | case .time:
28 | return NSLocalizedString("Last time", comment: "Title")
29 | }
30 | }
31 | }
32 |
33 | var kind: Result
34 |
35 | // MARK: - Subviews
36 |
37 | let titleLabel: UILabel = {
38 | let label = UILabel()
39 | label.translatesAutoresizingMaskIntoConstraints = false
40 | label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
41 | return label
42 | }()
43 |
44 | let resultLabel: UILabel = {
45 | let label = UILabel()
46 | label.translatesAutoresizingMaskIntoConstraints = false
47 | return label
48 | }()
49 |
50 | // MARK: - Constraction
51 |
52 | required init(kind: Result) {
53 | self.kind = kind
54 | super.init(frame: .zero)
55 | configure()
56 | }
57 |
58 | required init(coder: NSCoder) {
59 | fatalError("init(coder:) has not been implemented")
60 | }
61 |
62 | // MARK: - Lifecycle
63 |
64 | override func draw(_ rect: CGRect) {
65 | super.draw(rect)
66 | }
67 |
68 | // MARK: - Private functions
69 |
70 | private func configure() {
71 | self.addArrangedSubview(titleLabel)
72 | self.addArrangedSubview(resultLabel)
73 | self.translatesAutoresizingMaskIntoConstraints = false
74 | self.axis = .horizontal
75 | self.distribution = .fill
76 | self.alignment = .leading
77 | self.titleLabel.text = kind.title + ": "
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Model/Models/List.swift:
--------------------------------------------------------------------------------
1 | //
2 | // List.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct List {
11 | var id: UUID = UUID()
12 | var title: String
13 | var words: [Word]
14 | var style: GradientStyle
15 | var created: Date
16 | var addImageFlag: Bool
17 | var learns: [Learn]
18 |
19 | init(listCD: ListCD) {
20 | self.id = listCD.id
21 | self.title = listCD.title
22 | self.style = GradientStyle(rawValue: Int(listCD.style)) ?? .grey
23 | self.created = listCD.creationDate
24 | self.addImageFlag = listCD.addImageFlag
25 | var words = [Word]()
26 | listCD.wordsCD?.forEach {
27 | if let word = $0 as? WordCD {
28 | let word = Word(wordCD: word)
29 | words.append(word)
30 | }
31 | }
32 | self.words = words
33 | var learns = [Learn]()
34 | listCD.learnsCD?.forEach {
35 | if let learn = $0 as? LearnCD {
36 | let learn = Learn(learnCD: learn)
37 | learns.append(learn)
38 | }
39 | }
40 | self.learns = learns
41 | }
42 |
43 | init(title: String,
44 | words: [Word],
45 | style: GradientStyle,
46 | created: Date,
47 | addImageFlag: Bool,
48 | learns: [Learn]) {
49 | self.title = title
50 | self.words = words
51 | self.style = style
52 | self.created = created
53 | self.addImageFlag = addImageFlag
54 | self.learns = learns
55 | }
56 | }
57 |
58 | extension List {
59 | static let placeholder: List = .init(
60 | title: "Placeholder",
61 | words: [
62 | Word(source: "Red", translation: "Красный"),
63 | Word(source: "Orange", translation: "Оранжевый"),
64 | Word(source: "Yellow", translation: "Желтый"),
65 | Word(source: "Green", translation: "Зеленый"),
66 | Word(source: "Cyen", translation: "Голубой"),
67 | Word(source: "Blue", translation: "Синий"),
68 | Word(source: "Purple", translation: "Фиолетовый")
69 | ],
70 | style: .blue,
71 | created: Date(),
72 | addImageFlag: false,
73 | learns: []
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Helpers/URLConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UrlConfiguration.swift
3 | // FlashSpeak
4 | //
5 | // Created by Алексей Ходаков on 14.04.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | class URLConfiguration {
11 |
12 | // MARK: - Properties
13 | static let shared = URLConfiguration()
14 |
15 | // MARK: - Public functions
16 |
17 | func translateURLGoogle(words: [String], targetLang: Language, sourceLang: Language) -> URL? {
18 | guard
19 | let key = Bundle.main.object(forInfoDictionaryKey: "GOOGLE_API_KEY"),
20 | let str = key as? String
21 | else { return nil }
22 | var queryItems = [
23 | URLQueryItem(name: "key", value: str),
24 | URLQueryItem(name: "target", value: targetLang.code),
25 | URLQueryItem(name: "source", value: sourceLang.code),
26 | URLQueryItem(name: "format", value: "text")
27 | ]
28 | words.forEach { word in
29 | queryItems.append(URLQueryItem(name: "q", value: word))
30 | }
31 | var components = URLComponents()
32 | components.scheme = "https"
33 | components.host = "translation.googleapis.com"
34 | components.path = "/language/translate/v2"
35 | components.queryItems = queryItems
36 | let url = components.url
37 | return url
38 | }
39 |
40 | func imageURL(word: String, language: Language, count: Int = 1) -> URL? {
41 | guard
42 | let clientId = Bundle.main.object(forInfoDictionaryKey: "CLIENT_ID"),
43 | let str = clientId as? String
44 | else { return nil }
45 | let queryItems = [
46 | URLQueryItem(name: Constants.clienID, value: str),
47 | URLQueryItem(name: "query", value: word),
48 | URLQueryItem(name: "page", value: "1"),
49 | URLQueryItem(name: "per_page", value: String(count)),
50 | URLQueryItem(name: "lang", value: language.code),
51 | URLQueryItem(name: "content_filter", value: "high"),
52 | URLQueryItem(name: "orientation", value: "squarish")
53 | ]
54 | guard
55 | var urlComps = URLComponents(
56 | string: "https://api.unsplash.com/search/photos?"
57 | ) else { return nil }
58 | urlComps.queryItems = queryItems
59 | let result = urlComps.url
60 | return result
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Learn/View/Subviews/Progress/ProgressView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProgressView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 31.05.2023.
6 | //
7 | // swiftlint: disable weak_delegate
8 |
9 | import UIKit
10 |
11 | protocol ProgressViewInput: AnyObject {
12 | var count: Int { get set }
13 |
14 | func setAnswer(isRight: Bool, index: Int)
15 | func scrollToCenter(by index: Int)
16 | }
17 |
18 | class ProgressView: UICollectionView {
19 |
20 | // MARK: - Properties
21 | var count: Int = .zero
22 |
23 | // MARK: - Private properties
24 | private let collectionDataSource: UICollectionViewDataSource?
25 | private let collectionDelegate: UICollectionViewDelegate?
26 |
27 | // MARK: - Constraction
28 |
29 | init() {
30 | let collectionDelegate = ProgressViewDelegate()
31 | let collectionDataSource = ProgressViewDataSource()
32 | self.collectionDataSource = collectionDataSource
33 | self.collectionDelegate = collectionDelegate
34 | let layout = UICollectionViewFlowLayout()
35 | layout.scrollDirection = .horizontal
36 | super.init(frame: .zero, collectionViewLayout: layout)
37 | collectionDelegate.view = self
38 | collectionDataSource.view = self
39 | configure()
40 | }
41 |
42 | required init?(coder: NSCoder) {
43 | fatalError("init(coder:) has not been implemented")
44 | }
45 |
46 | private func configure() {
47 | showsHorizontalScrollIndicator = false
48 | backgroundColor = .clear
49 | delegate = collectionDelegate
50 | dataSource = collectionDataSource
51 | register(ProgressCell.self, forCellWithReuseIdentifier: ProgressCell.identifier)
52 | isUserInteractionEnabled = false
53 | }
54 | }
55 |
56 | extension ProgressView: ProgressViewInput {
57 |
58 | func setAnswer(isRight: Bool, index: Int) {
59 | let indexPath = IndexPath(item: index, section: .zero)
60 | guard let cell = cellForItem(at: indexPath) as? ProgressCell else { return }
61 | cell.isRight = isRight
62 | }
63 |
64 | func scrollToCenter(by index: Int) {
65 | self.scrollToItem(
66 | at: IndexPath(item: index, section: .zero),
67 | at: .centeredHorizontally,
68 | animated: true
69 | )
70 | }
71 | }
72 |
73 | // swiftlint: enable weak_delegate
74 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/LearnSettings/View/SettingsTableView/SettingsTableViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsTableViewDataSource.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 25.05.2023.
6 | //
7 | // swiftlint: disable line_length
8 |
9 | import UIKit
10 |
11 | class SettingsTableViewDataSource: NSObject, UITableViewDataSource {
12 |
13 | weak var view: SettingsTableView?
14 |
15 | func numberOfSections(in tableView: UITableView) -> Int {
16 | let sections = view?.settingsManager?.count()
17 | return sections ?? .zero
18 | }
19 |
20 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
21 | let title = view?.settingsManager?.title(section)
22 | return title
23 | }
24 |
25 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
26 | let count = view?.settingsManager?.countInSection(section)
27 | return count ?? .zero
28 | }
29 |
30 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
31 | guard
32 | let setting = view?.settingsManager?.settings(indexPath.section)[indexPath.item]
33 | else { return UITableViewCell() }
34 | switch setting.controller {
35 | case .selector:
36 | guard
37 | let cell = tableView.dequeueReusableCell(withIdentifier: SegmentedControlCell.identifier, for: indexPath) as? SegmentedControlCell
38 | else { return UITableViewCell() }
39 | cell.configure(setting: setting)
40 | cell.delegate = view
41 | return cell
42 | case .switcher:
43 | guard
44 | let cell = tableView.dequeueReusableCell(withIdentifier: SwitchCell.identifier, for: indexPath) as? SwitchCell
45 | else { return UITableViewCell() }
46 | cell.configure(setting: setting)
47 | cell.delegate = view
48 | return cell
49 | case .switcherWithValue:
50 | guard
51 | let cell = tableView.dequeueReusableCell(withIdentifier: SwitchValueCell.identifier, for: indexPath) as? SwitchValueCell
52 | else { return UITableViewCell() }
53 | cell.configure(setting: setting)
54 | cell.delegate = view
55 | return cell
56 | }
57 | }
58 | }
59 |
60 | // swiftlint: enable line_length
61 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Result/View/ChartLearn/ChartLearnView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ChartLearnView.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 13.06.2023.
6 | //
7 |
8 | import SwiftUI
9 | import Charts
10 |
11 | struct ChartLearnView: View {
12 | var viewModels: [ChartLearnViewModel]
13 | var color: Color = .green
14 |
15 | var body: some View {
16 | Chart(viewModels.sorted(by: { $0.date < $1.date }), id: \.date) { model in
17 | LineMark(
18 | x: .value("Date", model.date),
19 | y: .value("Result", model.result)
20 | )
21 | .foregroundStyle(color)
22 | // .foregroundStyle(by: .value("Result", model.stat))
23 |
24 | PointMark(
25 | x: .value("Date", model.date),
26 | y: .value("Result", model.result)
27 | )
28 | .foregroundStyle(color)
29 | // .foregroundStyle(by: .value("Result", model.stat))
30 |
31 | AreaMark(
32 | x: .value("Date", model.date),
33 | y: .value("Result", model.result)
34 | )
35 | .foregroundStyle(Gradient(colors: [color, .clear]))
36 | }
37 | .chartForegroundStyleScale([
38 | "\(viewModels.first?.stat.primitivePlottable ?? "Result")": color
39 | ])
40 | .chartYAxisLabel(position: .trailing, alignment: .center) {
41 | Text("Result")
42 | }
43 | // .chartXAxisLabel(position: .bottom, alignment: .center) {
44 | // Text("Date")
45 | // }
46 | }
47 | }
48 |
49 | struct ChartLearnView_Previews: PreviewProvider {
50 | static var previews: some View {
51 | ChartLearnView(viewModels: [
52 | ChartLearnViewModel(stat: .rights, date: Date.now - 100, result: .random(in: 0...50)),
53 | ChartLearnViewModel(stat: .rights, date: Date.now, result: .random(in: 0...50)),
54 | ChartLearnViewModel(stat: .rights, date: Date.now + 100, result: .random(in: 0...50)),
55 | ChartLearnViewModel(stat: .duration, date: Date.now - 100, result: .random(in: 0...100)),
56 | ChartLearnViewModel(stat: .duration, date: Date.now, result: .random(in: 0...100)),
57 | ChartLearnViewModel(stat: .duration, date: Date.now + 100, result: .random(in: 0...100))
58 | ])
59 | .previewLayout(.fixed(width: 300, height: 150))
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/ListMaker/View/ButtonCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 17.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class ButtonCell: UICollectionViewCell {
11 | // MARK: - Propetes
12 |
13 | static let identifier = "ButtonCell"
14 |
15 | // MARK: - Subviews
16 |
17 | let button: UIButton = {
18 | var configuration = UIButton.Configuration.appGray()
19 | configuration.imagePadding = Grid.pt8
20 | let button = UIButton(configuration: configuration)
21 | button.configuration?.background.backgroundColor = .systemRed.withAlphaComponent(Grid.factor35)
22 | button.translatesAutoresizingMaskIntoConstraints = false
23 | button.isEnabled = false
24 | button.tintColor = .systemRed
25 | let title = NSLocalizedString("Delete word", comment: "title")
26 | button.setTitle(title, for: .normal)
27 | return button
28 | }()
29 |
30 | // MARK: - Init
31 |
32 | override init(frame: CGRect) {
33 | super.init(frame: frame)
34 | configureView()
35 | configureSubviews()
36 | addConstraints()
37 | }
38 |
39 | // MARK: - Lifecycle
40 |
41 | required init?(coder: NSCoder) {
42 | fatalError("init(coder:) has not been implemented")
43 | }
44 |
45 | // MARK: - Functions
46 |
47 | func configure(image: UIImage?) {
48 | button.setImage(image, for: .normal)
49 | }
50 |
51 | func highlight(_ isActive: Bool) {
52 | // let color: UIColor = isActive ? .systemRed.withAlphaComponent(Grid.factor35) : .fiveBackgroundColor
53 | // button.configuration?.background.backgroundColor = color
54 | }
55 |
56 | // MARK: - UI
57 |
58 | private func configureView() {
59 |
60 | }
61 |
62 | private func configureSubviews() {
63 | contentView.addSubview(button)
64 | }
65 |
66 | // MARK: - Methods
67 |
68 | // MARK: - Constraints
69 |
70 | private func addConstraints() {
71 | NSLayoutConstraint.activate([
72 | button.topAnchor.constraint(equalTo: contentView.topAnchor),
73 | button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
74 | button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
75 | button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
76 | ])
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/FlashSpeak/FlashSpeak/Flow/Lists/Card/View/ImageCollectionView/ImageCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageCell.swift
3 | // FlashSpeak
4 | //
5 | // Created by Denis Dmitriev on 15.05.2023.
6 | //
7 |
8 | import UIKit
9 |
10 | class ImageCell: UICollectionViewCell {
11 |
12 | // MARK: - Properties
13 | static let identifier: String = "ImageCell"
14 |
15 | // MARK: - Subviews
16 |
17 | private let imageView: UIImageView = {
18 | let imageView = UIImageView()
19 | imageView.translatesAutoresizingMaskIntoConstraints = false
20 | imageView.layer.cornerRadius = Grid.cr8
21 | imageView.layer.masksToBounds = true
22 | imageView.layer.borderColor = UIColor.tintColor.cgColor
23 | return imageView
24 | }()
25 |
26 | // MARK: - Constraction
27 |
28 | override init(frame: CGRect) {
29 | super.init(frame: frame)
30 | self.configureUI()
31 | }
32 |
33 | required init?(coder: NSCoder) {
34 | fatalError("init(coder:) has not been implemented")
35 | }
36 |
37 | // MARK: - Lifecycle
38 |
39 | override func layoutSubviews() {
40 | super.layoutSubviews()
41 | }
42 |
43 | override func prepareForReuse() {
44 | super.prepareForReuse()
45 | imageView.image = nil
46 | }
47 |
48 | override var isSelected: Bool {
49 | willSet {
50 | super.isSelected = newValue
51 | if newValue {
52 | imageView.layer.borderWidth = Grid.pt4
53 | } else {
54 | imageView.layer.borderWidth = .zero
55 | }
56 | }
57 | }
58 |
59 | // MARK: - Functions
60 |
61 | func configure(image: UIImage) {
62 | imageView.image = image
63 | }
64 |
65 | func imageSize() -> CGSize? {
66 | return imageView.image?.size
67 | }
68 |
69 | // MARK: - UI
70 |
71 | private func configureUI() {
72 | contentView.addSubview(imageView)
73 | setupConstraints()
74 | }
75 |
76 | private func setupConstraints() {
77 | NSLayoutConstraint.activate([
78 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
79 | imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
80 | imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
81 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
82 | ])
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------