└── 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | 2 | 3 | 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 | --------------------------------------------------------------------------------