├── .DS_Store ├── .gitattributes ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Modules ├── .DS_Store ├── Anime │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── Anime │ │ │ ├── Data │ │ │ ├── GetAnimeRankingRepository.swift │ │ │ ├── GetAnimeRepository.swift │ │ │ ├── GetFavoriteAnimesRepository.swift │ │ │ ├── Locale │ │ │ │ ├── Entity │ │ │ │ │ └── AnimeModuleEntity.swift │ │ │ │ ├── GetAnimeListLocaleDataSource.swift │ │ │ │ ├── GetAnimeLocaleDataSource.swift │ │ │ │ ├── GetAnimeRankingLocaleDataSource.swift │ │ │ │ └── GetFavoriteAnimeLocaleDataSource.swift │ │ │ ├── Remote │ │ │ │ ├── GetAnimeListRemoteDataSource.swift │ │ │ │ ├── GetAnimeRankingRemoteDataSource.swift │ │ │ │ ├── GetAnimeRemoteDataSource.swift │ │ │ │ ├── Request │ │ │ │ │ ├── AnimeListRemoteRequest.swift │ │ │ │ │ └── AnimeRankingRemoteRequest.swift │ │ │ │ └── Response │ │ │ │ │ ├── AnimeDataResponse.swift │ │ │ │ │ └── AnimeResponse.swift │ │ │ ├── SearchAnimeRepository.swift │ │ │ └── UpdateFavoriteAnimeRepository.swift │ │ │ ├── Domain │ │ │ └── AnimeDomainModel.swift │ │ │ ├── Mapper │ │ │ ├── AnimeDataTransformer.swift │ │ │ ├── AnimeTransformer.swift │ │ │ └── AnimesTransformer.swift │ │ │ ├── Presentation │ │ │ └── AnimePresenter.swift │ │ │ ├── Request │ │ │ ├── AnimeListRequest.swift │ │ │ ├── AnimeRankingRequest.swift │ │ │ └── AnimeRequest.swift │ │ │ └── Supporting Files │ │ │ └── Localization │ │ │ ├── en.lproj │ │ │ ├── Localizable.strings │ │ │ └── Localizable.stringsdict │ │ │ └── id.lproj │ │ │ ├── Localizable.strings │ │ │ └── Localizable.stringsdict │ └── Tests │ │ └── AnimeTests │ │ └── AnimeTests.swift ├── AnimeDetail │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── AnimeDetail │ │ │ ├── Presentation │ │ │ └── View │ │ │ │ ├── AnimeDetailView.swift │ │ │ │ ├── AnimeInformationItem.swift │ │ │ │ └── AnimeStatItem.swift │ │ │ └── Supporting Files │ │ │ └── Localization │ │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ │ └── id.lproj │ │ │ └── Localizable.strings │ └── Tests │ │ └── AnimeDetailTests │ │ └── AnimeDetailTests.swift ├── Common │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── Common │ │ │ ├── Supporting Files │ │ │ ├── Assets.xcassets │ │ │ │ ├── CaretLeftIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── caret-left.svg │ │ │ │ ├── CloseIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── close.svg │ │ │ │ ├── Contents.json │ │ │ │ ├── CrownOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── crown.svg │ │ │ │ ├── HeartIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── heart (1).svg │ │ │ │ ├── HeartOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── heart.svg │ │ │ │ ├── HouseIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── house.svg │ │ │ │ ├── HouseOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── house (1).svg │ │ │ │ ├── ImageOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── image.svg │ │ │ │ ├── InfoOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── info.svg │ │ │ │ ├── SearchIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── search.svg │ │ │ │ ├── StarOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── star.svg │ │ │ │ ├── TrendingUpIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── trending-up.svg │ │ │ │ ├── UserIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── user (1).svg │ │ │ │ ├── UserOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── user.svg │ │ │ │ └── UsersOutlinedIcon.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── users.svg │ │ │ └── Localization │ │ │ │ ├── en.lproj │ │ │ │ └── Localizable.strings │ │ │ │ └── id.lproj │ │ │ │ └── Localizable.strings │ │ │ └── Utils │ │ │ ├── Extension │ │ │ └── Bundle+Ext.swift │ │ │ ├── Theme │ │ │ ├── Icons.swift │ │ │ ├── Shape.swift │ │ │ └── Space.swift │ │ │ └── View │ │ │ ├── AnimeCardItem.swift │ │ │ ├── AnimeItem.swift │ │ │ ├── AnimeRow.swift │ │ │ ├── AppBar.swift │ │ │ ├── Button.swift │ │ │ ├── CircleImage.swift │ │ │ ├── CustomEmptyView.swift │ │ │ ├── IconView.swift │ │ │ ├── ImagePlaceholder.swift │ │ │ ├── NoInternetView.swift │ │ │ ├── ObservableScrollView.swift │ │ │ ├── ProgressIndicator.swift │ │ │ ├── RefreshableScrollView.swift │ │ │ ├── SearchBar.swift │ │ │ ├── Snackbar.swift │ │ │ ├── TabBar.swift │ │ │ ├── TabItem.swift │ │ │ └── YumeDivider.swift │ └── Tests │ │ └── CommonTests │ │ └── CommonTests.swift ├── Favorite │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── Favorite │ │ │ ├── Presentation │ │ │ └── View │ │ │ │ └── FavoriteView.swift │ │ │ └── Supporting Files │ │ │ └── Localization │ │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ │ └── id.lproj │ │ │ └── Localizable.strings │ └── Tests │ │ └── FavoriteTests │ │ └── FavoriteTests.swift ├── Home │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── Home │ │ │ ├── Presentation │ │ │ ├── Presenter │ │ │ │ └── HomePresenter.swift │ │ │ └── View │ │ │ │ └── HomeView.swift │ │ │ └── Supporting Files │ │ │ └── Localization │ │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ │ └── id.lproj │ │ │ └── Localizable.strings │ └── Tests │ │ └── HomeTests │ │ └── HomeTests.swift ├── Profile │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── Profile │ │ │ ├── Presentation │ │ │ └── View │ │ │ │ └── ProfileView.swift │ │ │ └── Supporting Files │ │ │ └── Assets.xcassets │ │ │ └── ProfilePicture.imageset │ │ │ ├── Contents.json │ │ │ ├── profile.png │ │ │ ├── profile@2x.png │ │ │ └── profile@3x.png │ └── Tests │ │ └── ProfileTests │ │ └── ProfileTests.swift ├── Search │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ │ └── Search │ │ │ ├── Presentation │ │ │ ├── Presenter │ │ │ │ └── SearchPresenter.swift │ │ │ └── View │ │ │ │ └── SearchView.swift │ │ │ └── Supporting Files │ │ │ └── Localization │ │ │ ├── en.lproj │ │ │ └── Localizable.strings │ │ │ └── id.lproj │ │ │ └── Localizable.strings │ └── Tests │ │ └── SearchTests │ │ └── SearchTests.swift └── SeeAllAnime │ ├── .gitignore │ ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── Package.swift │ ├── README.md │ ├── Sources │ └── SeeAllAnime │ │ └── Presentation │ │ └── View │ │ └── SeeAllAnimeView.swift │ └── Tests │ └── SeeAllAnimeTests │ └── SeeAllAnimeTests.swift ├── README.md ├── Yume.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── Yume - ID.xcscheme │ └── Yume.xcscheme ├── Yume ├── .DS_Store ├── App │ ├── ContentView.swift │ ├── Router.swift │ └── YumeApp.swift ├── Core │ ├── .DS_Store │ ├── DI │ │ └── Injection.swift │ └── Utils │ │ ├── .DS_Store │ │ ├── Extensions │ │ ├── File+Ext.swift │ │ └── UINavigationController+Ext.swift │ │ └── Network │ │ └── APICall.swift ├── Preview Content │ ├── Preview Assets.xcassets │ │ └── Contents.json │ ├── PreviewData copy.swift │ └── top_all_anime_response.json └── Supporting Files │ ├── .DS_Store │ ├── Assets.xcassets │ ├── .DS_Store │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 172.png │ │ ├── 180.png │ │ ├── 196.png │ │ ├── 20.png │ │ ├── 216.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 48.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 55.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 66.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 88.png │ │ ├── 92.png │ │ └── Contents.json │ ├── ColorBackground.colorset │ │ └── Contents.json │ ├── ColorInverseOnSurface.colorset │ │ └── Contents.json │ ├── ColorInversePrimary.colorset │ │ └── Contents.json │ ├── ColorInverseSurface.colorset │ │ └── Contents.json │ ├── ColorOnBackground.colorset │ │ └── Contents.json │ ├── ColorOnPrimary.colorset │ │ └── Contents.json │ ├── ColorOnSecondaryContainer.colorset │ │ └── Contents.json │ ├── ColorOnSurface.colorset │ │ └── Contents.json │ ├── ColorOnSurfaceVariant.colorset │ │ └── Contents.json │ ├── ColorOutline.colorset │ │ └── Contents.json │ ├── ColorOutlineVariant.colorset │ │ └── Contents.json │ ├── ColorPrimary.colorset │ │ └── Contents.json │ ├── ColorSecondaryContainer.colorset │ │ └── Contents.json │ ├── ColorSurface.colorset │ │ └── Contents.json │ ├── ColorSurface2.colorset │ │ └── Contents.json │ ├── ColorSurfaceVariant.colorset │ │ └── Contents.json │ └── Contents.json │ ├── Font │ ├── Nunito-Black.ttf │ ├── Nunito-BlackItalic.ttf │ ├── Nunito-Bold.ttf │ ├── Nunito-BoldItalic.ttf │ ├── Nunito-ExtraBold.ttf │ ├── Nunito-ExtraBoldItalic.ttf │ ├── Nunito-ExtraLight.ttf │ ├── Nunito-ExtraLightItalic.ttf │ ├── Nunito-Italic.ttf │ ├── Nunito-Light.ttf │ ├── Nunito-LightItalic.ttf │ ├── Nunito-Medium.ttf │ ├── Nunito-MediumItalic.ttf │ ├── Nunito-Regular.ttf │ ├── Nunito-SemiBold.ttf │ └── Nunito-SemiBoldItalic.ttf │ ├── Info.plist │ ├── Keys-Example.plist │ ├── en.lproj │ └── Localizable.strings │ └── id.lproj │ └── Localizable.strings ├── YumeTests └── YumeTests.swift ├── YumeUITests ├── YumeUITests.swift └── YumeUITestsLaunchTests.swift ├── codemagic.yaml └── readme ├── .DS_Store ├── dependency-diagram.png ├── feature-graphic.jpg ├── screen-1.jpg ├── screen-2.jpg ├── screen-3.jpg ├── screen-4.jpg └── screen-5.jpg /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Custom 6 | Keys.plist 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - identifier_name 3 | - generic_type_name 4 | - force_cast 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bryan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Modules/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Modules/.DS_Store -------------------------------------------------------------------------------- /Modules/Anime/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/Anime/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/Anime/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Anime", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "Anime", 14 | targets: ["Anime"]) 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | .package(url: "https://github.com/realm/realm-cocoa.git", .upToNextMajor(from: "10.0.0")), 19 | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.0.0")), 20 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")) 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Anime", 27 | dependencies: [ 28 | .product(name: "RealmSwift", package: "realm-cocoa"), 29 | .product(name: "Core", package: "Yume-Core-Module"), 30 | "Alamofire" 31 | ]), 32 | .testTarget( 33 | name: "AnimeTests", 34 | dependencies: ["Anime"]) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Modules/Anime/README.md: -------------------------------------------------------------------------------- 1 | # Anime 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/GetAnimeRankingRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeRankingRepository.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | import Combine 9 | import Core 10 | 11 | public struct GetAnimeRankingRepository< 12 | AnimeLocaleDataSource: LocaleDataSource, 13 | RemoteDataSource: DataSource, 14 | Transformer: Mapper>: Repository 15 | where AnimeLocaleDataSource.Request == AnimeRankingRequest, 16 | AnimeLocaleDataSource.Response == AnimeModuleEntity, 17 | RemoteDataSource.Request == AnimeRankingRequest, 18 | RemoteDataSource.Response == [AnimeDataResponse], 19 | Transformer.Request == Any, 20 | Transformer.Response == [AnimeDataResponse], 21 | Transformer.Entity == [AnimeModuleEntity], 22 | Transformer.Domain == [AnimeDomainModel] { 23 | 24 | public typealias Request = AnimeRankingRequest 25 | public typealias Response = [AnimeDomainModel] 26 | 27 | private let _localeDataSource: AnimeLocaleDataSource 28 | private let _remoteDataSource: RemoteDataSource 29 | private let _mapper: Transformer 30 | 31 | public init( 32 | localeDataSource: AnimeLocaleDataSource, 33 | remoteDataSource: RemoteDataSource, 34 | mapper: Transformer) { 35 | _localeDataSource = localeDataSource 36 | _remoteDataSource = remoteDataSource 37 | _mapper = mapper 38 | } 39 | 40 | public func execute(request: AnimeRankingRequest?) -> AnyPublisher<[AnimeDomainModel], Error> { 41 | guard let request = request else { 42 | fatalError("Request cannot be empty") 43 | } 44 | 45 | return _localeDataSource.list(request: request) 46 | .flatMap { result -> AnyPublisher<[AnimeDomainModel], Error> in 47 | if result.isEmpty || request.refresh { 48 | return _remoteDataSource.execute(request: request) 49 | .map { _mapper.transformResponseToEntity(request: request, response: $0) } 50 | .flatMap { _localeDataSource.add(entities: $0) } 51 | .filter { $0 } 52 | .flatMap { _ in _localeDataSource.list(request: request) 53 | .map { _mapper.transformEntityToDomain(entity: $0) } 54 | } 55 | .catch { error in 56 | if result.isEmpty { 57 | // First time request and no cache 58 | return Fail<[AnimeDomainModel], Error>(error: error) 59 | .eraseToAnyPublisher() 60 | } else { 61 | // Failed to refresh 62 | return _localeDataSource.list(request: request) 63 | .map { _mapper.transformEntityToDomain(entity: $0) } 64 | .eraseToAnyPublisher() 65 | } 66 | } 67 | .eraseToAnyPublisher() 68 | } else { 69 | return _localeDataSource.list(request: request) 70 | .map { _mapper.transformEntityToDomain(entity: $0) } 71 | .eraseToAnyPublisher() 72 | } 73 | }.eraseToAnyPublisher() 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/GetAnimeRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeRepository.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Combine 9 | import Core 10 | 11 | public struct GetAnimeRepository< 12 | AnimeLocaleDataSource: LocaleDataSource, 13 | RemoteDataSource: DataSource, 14 | Transformer: Mapper>: Repository 15 | where AnimeLocaleDataSource.Request == Int, 16 | AnimeLocaleDataSource.Response == AnimeModuleEntity, 17 | RemoteDataSource.Request == AnimeRequest, 18 | RemoteDataSource.Response == AnimeResponse, 19 | Transformer.Request == Any, 20 | Transformer.Response == AnimeResponse, 21 | Transformer.Entity == AnimeModuleEntity, 22 | Transformer.Domain == AnimeDomainModel { 23 | 24 | public typealias Request = AnimeRequest 25 | public typealias Response = AnimeDomainModel 26 | 27 | private let _localeDataSource: AnimeLocaleDataSource 28 | private let _remoteDataSource: RemoteDataSource 29 | private let _mapper: Transformer 30 | 31 | public init( 32 | localeDataSource: AnimeLocaleDataSource, 33 | remoteDataSource: RemoteDataSource, 34 | mapper: Transformer) { 35 | _localeDataSource = localeDataSource 36 | _remoteDataSource = remoteDataSource 37 | _mapper = mapper 38 | } 39 | 40 | public func execute(request: AnimeRequest?) -> AnyPublisher { 41 | guard let request = request else { 42 | fatalError("Request cannot be empty") 43 | } 44 | 45 | return _localeDataSource.get(id: request.animeId) 46 | .flatMap { entity -> AnyPublisher in 47 | // Refresh anime if cached more than 24 hours 48 | if entity.updatedAt.isExpired() { 49 | return _remoteDataSource.execute(request: request) 50 | .map { _mapper.transformResponseToEntity(request: request, response: $0) } 51 | .flatMap { _localeDataSource.add(entities: [$0]) } 52 | .filter { $0 } 53 | .flatMap { _ in _localeDataSource.get(id: request.animeId) 54 | .map { _mapper.transformEntityToDomain(entity: $0) } 55 | } 56 | .catch { _ in 57 | return _localeDataSource.get(id: request.animeId) 58 | .map { _mapper.transformEntityToDomain(entity: $0) } 59 | .eraseToAnyPublisher() 60 | } 61 | .eraseToAnyPublisher() 62 | } else { 63 | return _localeDataSource.get(id: request.animeId) 64 | .map { _mapper.transformEntityToDomain(entity: $0) } 65 | .eraseToAnyPublisher() 66 | } 67 | } 68 | .catch { _ in 69 | return _remoteDataSource.execute(request: request) 70 | .map { _mapper.transformResponseToEntity(request: request, response: $0) } 71 | .flatMap { _localeDataSource.add(entities: [$0]) } 72 | .filter { $0 } 73 | .flatMap { _ in _localeDataSource.get(id: request.animeId) 74 | .map { _mapper.transformEntityToDomain(entity: $0) } 75 | } 76 | .eraseToAnyPublisher() 77 | } 78 | .eraseToAnyPublisher() 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/GetFavoriteAnimesRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetFavoriteAnimesRepository.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Core 9 | import Combine 10 | 11 | public struct GetFavoriteAnimesRepository< 12 | GetFavoriteAnimeLocaleDataSource: LocaleDataSource, 13 | Transformer: Mapper>: Repository 14 | where GetFavoriteAnimeLocaleDataSource.Request == Int, 15 | GetFavoriteAnimeLocaleDataSource.Response == AnimeModuleEntity, 16 | Transformer.Request == Any, 17 | Transformer.Response == [AnimeDataResponse], 18 | Transformer.Entity == [AnimeModuleEntity], 19 | Transformer.Domain == [AnimeDomainModel] { 20 | 21 | public typealias Request = Int 22 | public typealias Response = [AnimeDomainModel] 23 | 24 | private let _localeDataSource: GetFavoriteAnimeLocaleDataSource 25 | private let _mapper: Transformer 26 | 27 | public init( 28 | localeDataSource: GetFavoriteAnimeLocaleDataSource, 29 | mapper: Transformer) { 30 | 31 | _localeDataSource = localeDataSource 32 | _mapper = mapper 33 | } 34 | 35 | public func execute(request: Int?) -> AnyPublisher<[AnimeDomainModel], Error> { 36 | return _localeDataSource.list(request: nil) 37 | .map { _mapper.transformEntityToDomain(entity: $0) } 38 | .eraseToAnyPublisher() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Locale/Entity/AnimeModuleEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeModuleEntity.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | public class AnimeModuleEntity: Object { 12 | 13 | @Persisted(primaryKey: true) var id: Int = 0 14 | @Persisted var title: String = "" 15 | @Persisted var mainPicture: String = "" 16 | @Persisted var alternativeTitleSynonyms: List = List() 17 | @Persisted var alternativeTitleEnglish: String = "" 18 | @Persisted var alternativeTitleJapanese: String = "" 19 | @Persisted var startDate: String = "" 20 | @Persisted var endDate: String = "" 21 | @Persisted var synopsis: String = "" 22 | @Persisted var rating: Double = 0 23 | @Persisted var rank: Int = 0 24 | @Persisted var popularity: Int = 0 25 | @Persisted var userAmount: Int = 0 26 | @Persisted var favoriteAmount: Int = 0 27 | @Persisted var nsfw: String = "" 28 | @Persisted var genre: List = List() 29 | @Persisted var mediaType: String = "" 30 | @Persisted var status: String = "" 31 | @Persisted var episodeAmount: Int = 0 32 | @Persisted var startSeason: String = "" 33 | @Persisted var startSeasonYear: String = "" 34 | @Persisted var source: String = "" 35 | @Persisted var episodeDuration: Int = 0 36 | @Persisted var studios: List = List() 37 | @Persisted var isFavorite: Bool = false 38 | @Persisted var createdAt: Date = Date() 39 | @Persisted var updatedAt: Date = Date() 40 | 41 | public override static func primaryKey() -> String? { 42 | return "id" 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Locale/GetAnimeListLocaleDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeListLocaleDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 02/02/23. 6 | // 7 | 8 | import Core 9 | import Combine 10 | import Foundation 11 | import RealmSwift 12 | 13 | public struct GetAnimeListLocaleDataSource: LocaleDataSource { 14 | 15 | public typealias Request = AnimeListRequest 16 | public typealias Response = AnimeModuleEntity 17 | 18 | private let _realm: Realm 19 | 20 | public init(realm: Realm) { 21 | _realm = realm 22 | } 23 | 24 | public func list(request: AnimeListRequest?) -> AnyPublisher<[AnimeModuleEntity], Error> { 25 | fatalError() 26 | } 27 | 28 | public func add(entities: [AnimeModuleEntity]) -> AnyPublisher { 29 | return Future { completion in 30 | do { 31 | try _realm.write { 32 | for anime in entities { 33 | if let animeEntity = _realm.object(ofType: AnimeModuleEntity.self, forPrimaryKey: anime.id) { 34 | anime.isFavorite = animeEntity.isFavorite 35 | _realm.add(anime, update: .all) 36 | } else { 37 | _realm.add(anime) 38 | } 39 | } 40 | completion(.success(true)) 41 | } 42 | } catch { 43 | completion(.failure(DatabaseError.requestFailed)) 44 | } 45 | }.eraseToAnyPublisher() 46 | } 47 | 48 | public func get(id: Int) -> AnyPublisher { 49 | fatalError() 50 | } 51 | 52 | public func update(id: Int, entity: AnimeModuleEntity) -> AnyPublisher { 53 | fatalError() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Locale/GetAnimeLocaleDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeLocaleDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Core 9 | import Combine 10 | import Foundation 11 | import RealmSwift 12 | 13 | public struct GetAnimeLocaleDataSource: LocaleDataSource { 14 | 15 | public typealias Request = Int 16 | public typealias Response = AnimeModuleEntity 17 | 18 | private let _realm: Realm 19 | 20 | public init(realm: Realm) { 21 | _realm = realm 22 | } 23 | 24 | public func list(request: Int?) -> AnyPublisher<[AnimeModuleEntity], Error> { 25 | fatalError() 26 | } 27 | 28 | public func add(entities: [AnimeModuleEntity]) -> AnyPublisher { 29 | return Future { completion in 30 | do { 31 | try _realm.write { 32 | for anime in entities { 33 | if let animeEntity = _realm.object(ofType: AnimeModuleEntity.self, forPrimaryKey: anime.id) { 34 | anime.isFavorite = animeEntity.isFavorite 35 | _realm.add(anime, update: .all) 36 | } else { 37 | _realm.add(anime) 38 | } 39 | } 40 | completion(.success(true)) 41 | } 42 | } catch { 43 | completion(.failure(DatabaseError.requestFailed)) 44 | } 45 | }.eraseToAnyPublisher() 46 | } 47 | 48 | public func get(id: Int) -> AnyPublisher { 49 | return Future { completion in 50 | if let animeEntity = _realm.object(ofType: AnimeModuleEntity.self, forPrimaryKey: id) { 51 | completion(.success(animeEntity)) 52 | } else { 53 | completion(.failure(DatabaseError.requestFailed)) 54 | } 55 | }.eraseToAnyPublisher() 56 | } 57 | 58 | public func update(id: Int, entity: AnimeModuleEntity) -> AnyPublisher { 59 | fatalError() 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Locale/GetAnimeRankingLocaleDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeRankingLocaleDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 08/01/23. 6 | // 7 | 8 | import Core 9 | import Combine 10 | import Foundation 11 | import RealmSwift 12 | 13 | public struct GetAnimeRankingLocaleDataSource: LocaleDataSource { 14 | 15 | public typealias Request = AnimeRankingRequest 16 | public typealias Response = AnimeModuleEntity 17 | 18 | private let _realm: Realm 19 | 20 | public init(realm: Realm) { 21 | _realm = realm 22 | } 23 | 24 | public func list(request: AnimeRankingRequest?) -> AnyPublisher<[AnimeModuleEntity], Error> { 25 | return Future<[AnimeModuleEntity], Error> { completion in 26 | guard let request = request else { 27 | return completion(.failure(URLError.invalidRequest)) 28 | } 29 | 30 | let animes: Results = { 31 | switch request.rankingType { 32 | case RankingTypeRequest.airing: 33 | return _realm.objects(AnimeModuleEntity.self) 34 | .where { 35 | $0.status == Status.currentlyAiring.name 36 | && $0.rank != 0 37 | } 38 | .sorted(byKeyPath: request.rankingType.sortKey) 39 | case .upcoming: 40 | return _realm.objects(AnimeModuleEntity.self) 41 | .where { $0.status == Status.notYetAired.name } 42 | .sorted(byKeyPath: request.rankingType.sortKey) 43 | case .byPopularity: 44 | return _realm.objects(AnimeModuleEntity.self) 45 | .where { $0.popularity != 0 } 46 | .sorted(byKeyPath: request.rankingType.sortKey) 47 | case .favorite: 48 | return _realm.objects(AnimeModuleEntity.self) 49 | .sorted(byKeyPath: request.rankingType.sortKey, ascending: false) 50 | default: 51 | // All 52 | return _realm.objects(AnimeModuleEntity.self) 53 | .where { $0.rank != 0 } 54 | .sorted(byKeyPath: request.rankingType.sortKey) 55 | } 56 | }() 57 | completion(.success(animes.toArray(ofType: AnimeModuleEntity.self))) 58 | }.eraseToAnyPublisher() 59 | } 60 | 61 | public func add(entities: [AnimeModuleEntity]) -> AnyPublisher { 62 | return Future { completion in 63 | do { 64 | try _realm.write { 65 | for anime in entities { 66 | if let animeEntity = _realm.object(ofType: AnimeModuleEntity.self, forPrimaryKey: anime.id) { 67 | anime.isFavorite = animeEntity.isFavorite 68 | _realm.add(anime, update: .all) 69 | } else { 70 | _realm.add(anime) 71 | } 72 | } 73 | completion(.success(true)) 74 | } 75 | } catch { 76 | completion(.failure(DatabaseError.requestFailed)) 77 | } 78 | }.eraseToAnyPublisher() 79 | } 80 | 81 | public func get(id: Int) -> AnyPublisher { 82 | fatalError() 83 | } 84 | 85 | public func update(id: Int, entity: AnimeModuleEntity) -> AnyPublisher { 86 | fatalError() 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Locale/GetFavoriteAnimeLocaleDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetFavoriteAnimeLocaleDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Combine 9 | import Core 10 | import RealmSwift 11 | 12 | public struct GetFavoriteAnimeLocaleDataSource: LocaleDataSource { 13 | 14 | public typealias Request = Int 15 | public typealias Response = AnimeModuleEntity 16 | 17 | private let _realm: Realm 18 | 19 | public init(realm: Realm) { 20 | _realm = realm 21 | } 22 | 23 | public func list(request: Int?) -> AnyPublisher<[AnimeModuleEntity], Error> { 24 | return Future<[AnimeModuleEntity], Error> { completion in 25 | let animes: Results = { 26 | _realm.objects(AnimeModuleEntity.self) 27 | .where { $0.isFavorite == true } 28 | .sorted(byKeyPath: "title") 29 | }() 30 | completion(.success(animes.toArray(ofType: AnimeModuleEntity.self))) 31 | }.eraseToAnyPublisher() 32 | } 33 | 34 | public func add(entities: [AnimeModuleEntity]) -> AnyPublisher { 35 | fatalError() 36 | } 37 | 38 | public func get(id: Int) -> AnyPublisher { 39 | return Future { completion in 40 | if let animeEntity = _realm.object(ofType: AnimeModuleEntity.self, forPrimaryKey: id) { 41 | do { 42 | try _realm.write { 43 | animeEntity.setValue(!animeEntity.isFavorite, forKey: "isFavorite") 44 | } 45 | completion(.success(animeEntity)) 46 | } catch { 47 | completion(.failure(DatabaseError.requestFailed)) 48 | } 49 | } else { 50 | completion(.failure(DatabaseError.requestFailed)) 51 | } 52 | }.eraseToAnyPublisher() 53 | } 54 | 55 | public func update(id: Int, entity: AnimeModuleEntity) -> AnyPublisher { 56 | fatalError() 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Remote/GetAnimeListRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeListRemoteDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 09/01/23. 6 | // 7 | 8 | import Alamofire 9 | import Core 10 | import Combine 11 | import Foundation 12 | 13 | public struct GetAnimeListRemoteDataSource: DataSource { 14 | 15 | public typealias Request = AnimeListRequest 16 | public typealias Response = [AnimeDataResponse] 17 | 18 | private let _endpoint: String 19 | private let _encoder: ParameterEncoder 20 | private let _headers: HTTPHeaders 21 | 22 | public init( 23 | endpoint: String, 24 | encoder: ParameterEncoder, 25 | headers: HTTPHeaders 26 | ) { 27 | _endpoint = endpoint 28 | _encoder = encoder 29 | _headers = headers 30 | } 31 | 32 | public func execute(request: AnimeListRequest?) -> AnyPublisher<[AnimeDataResponse], Error> { 33 | return Future<[AnimeDataResponse], Error> { completion in 34 | guard let request = request else { 35 | return completion(.failure(URLError.invalidRequest)) 36 | } 37 | 38 | let remoteRequest = AnimeListRemoteRequest( 39 | title: request.q, 40 | limit: request.limit, 41 | offset: request.offset, 42 | fields: request.fields, 43 | nsfw: request.nsfw 44 | ) 45 | 46 | if let url = URL(string: _endpoint) { 47 | AF.request( 48 | url, 49 | parameters: remoteRequest, 50 | encoder: _encoder, 51 | headers: _headers 52 | ) 53 | .validate() 54 | .responseDecodable(of: AnimesResponse.self) { response in 55 | switch response.result { 56 | case .success(let value): 57 | completion(.success(value.animes)) 58 | case .failure(let error): 59 | if let error = error.underlyingError as? Foundation.URLError, error.code == .notConnectedToInternet { 60 | // No internet connection 61 | completion(.failure(URLError.notConnectedToInternet)) 62 | } else { 63 | completion(.failure(URLError.invalidResponse)) 64 | } 65 | } 66 | } 67 | } 68 | }.eraseToAnyPublisher() 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Remote/GetAnimeRankingRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeRankingRemoteDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | import Alamofire 9 | import Core 10 | import Combine 11 | import Foundation 12 | 13 | public struct GetAnimeRankingRemoteDataSource: DataSource { 14 | 15 | public typealias Request = AnimeRankingRequest 16 | public typealias Response = [AnimeDataResponse] 17 | 18 | private let _endpoint: String 19 | private let _encoder: ParameterEncoder 20 | private let _headers: HTTPHeaders 21 | 22 | public init( 23 | endpoint: String, 24 | encoder: ParameterEncoder, 25 | headers: HTTPHeaders 26 | ) { 27 | _endpoint = endpoint 28 | _encoder = encoder 29 | _headers = headers 30 | } 31 | 32 | public func execute(request: AnimeRankingRequest?) -> AnyPublisher<[AnimeDataResponse], Error> { 33 | return Future<[AnimeDataResponse], Error> { completion in 34 | guard let request = request else { 35 | return completion(.failure(URLError.invalidRequest)) 36 | } 37 | 38 | let remoteRequest = AnimeRankingRemoteRequest( 39 | type: request.rankingType.name, 40 | limit: request.limit, 41 | offset: request.offset, 42 | fields: request.fields, 43 | nsfw: request.nsfw 44 | ) 45 | 46 | if let url = URL(string: _endpoint) { 47 | AF.request( 48 | url, 49 | parameters: remoteRequest, 50 | encoder: _encoder, 51 | headers: _headers 52 | ) 53 | .validate() 54 | .responseDecodable(of: AnimesResponse.self) { response in 55 | switch response.result { 56 | case .success(let value): 57 | completion(.success(value.animes)) 58 | case .failure(let error): 59 | if let error = error.underlyingError as? Foundation.URLError, error.code == .notConnectedToInternet { 60 | // No internet connection 61 | completion(.failure(URLError.notConnectedToInternet)) 62 | } else { 63 | completion(.failure(URLError.invalidResponse)) 64 | } 65 | } 66 | } 67 | } 68 | }.eraseToAnyPublisher() 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Remote/GetAnimeRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetAnimeRemoteDataSource.swift 3 | // 4 | // 5 | // Created by Bryan on 09/01/23. 6 | // 7 | 8 | import Alamofire 9 | import Core 10 | import Combine 11 | import Foundation 12 | 13 | public struct GetAnimeRemoteDataSource: DataSource { 14 | 15 | public typealias Request = AnimeRequest 16 | public typealias Response = AnimeResponse 17 | 18 | private let _endpoint: String 19 | private let _encoder: ParameterEncoder 20 | private let _headers: HTTPHeaders 21 | 22 | public init( 23 | endpoint: String, 24 | encoder: ParameterEncoder, 25 | headers: HTTPHeaders 26 | ) { 27 | _endpoint = endpoint 28 | _encoder = encoder 29 | _headers = headers 30 | } 31 | 32 | public func execute(request: AnimeRequest?) -> AnyPublisher { 33 | return Future { completion in 34 | guard let request = request else { 35 | return completion(.failure(URLError.invalidRequest)) 36 | } 37 | 38 | if let url = URL(string: "\(_endpoint)/\(request.animeId)") { 39 | AF.request( 40 | url, 41 | parameters: request, 42 | encoder: _encoder, 43 | headers: _headers 44 | ) 45 | .validate() 46 | .responseDecodable(of: AnimeResponse.self) { response in 47 | switch response.result { 48 | case .success(let value): 49 | completion(.success(value)) 50 | case .failure(let error): 51 | if let error = error.underlyingError as? Foundation.URLError, error.code == .notConnectedToInternet { 52 | // No internet connection 53 | completion(.failure(URLError.notConnectedToInternet)) 54 | } else { 55 | completion(.failure(URLError.invalidResponse)) 56 | } 57 | } 58 | } 59 | } 60 | }.eraseToAnyPublisher() 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Remote/Request/AnimeListRemoteRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeListRemoteRequest.swift 3 | // 4 | // 5 | // Created by Bryan on 01/02/23. 6 | // 7 | 8 | public struct AnimeListRemoteRequest: Encodable { 9 | let q: String 10 | let limit: Int 11 | let offset: Int 12 | let fields: String 13 | let nsfw: Bool 14 | 15 | public init( 16 | title q: String, 17 | limit: Int?, 18 | offset: Int?, 19 | fields: String?, 20 | nsfw: Bool? 21 | ) { 22 | self.q = q 23 | self.limit = limit ?? 100 24 | self.offset = offset ?? 0 25 | self.fields = fields ?? ( 26 | "alternative_titles,start_date,end_date,synopsis,mean," 27 | + "rank,popularity,num_list_users,num_favorites,genres,media_type," 28 | + "status,num_episodes,start_season,source,average_episode_duration,studios" 29 | ) 30 | self.nsfw = nsfw ?? true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Remote/Request/AnimeRankingRemoteRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeRankingRemoteRequest.swift 3 | // 4 | // 5 | // Created by Bryan on 01/02/23. 6 | // 7 | 8 | public struct AnimeRankingRemoteRequest: Encodable { 9 | let rankingType: String 10 | let limit: Int 11 | let offset: Int 12 | let fields: String 13 | let nsfw: Bool 14 | 15 | public init( 16 | type rankingType: String, 17 | limit: Int?, 18 | offset: Int?, 19 | fields: String?, 20 | nsfw: Bool? 21 | ) { 22 | self.rankingType = rankingType 23 | self.limit = limit ?? 20 24 | self.offset = offset ?? 0 25 | self.fields = fields ?? ( 26 | "alternative_titles,start_date,end_date,synopsis,mean," 27 | + "rank,popularity,num_list_users,num_favorites,genres,media_type," 28 | + "status,num_episodes,start_season,source,average_episode_duration,studios" 29 | ) 30 | self.nsfw = nsfw ?? true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/Remote/Response/AnimeDataResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeRankingResponse.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | // MARK: - AnimeRankingResponse 9 | public struct AnimesResponse: Codable { 10 | let animes: [AnimeDataResponse] 11 | 12 | private enum CodingKeys: String, CodingKey { 13 | case animes = "data" 14 | } 15 | } 16 | 17 | // MARK: - AnimeRankingResponse 18 | public struct AnimeDataResponse: Codable { 19 | let anime: AnimeResponse 20 | 21 | private enum CodingKeys: String, CodingKey { 22 | case anime = "node" 23 | } 24 | } 25 | 26 | // MARK: - Ranking 27 | public struct Ranking: Codable { 28 | let rank: Int 29 | } 30 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/SearchAnimeRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchAnimeRepository.swift 3 | // 4 | // 5 | // Created by Bryan on 09/01/23. 6 | // 7 | 8 | import Combine 9 | import Core 10 | 11 | public struct SearchAnimeRepository< 12 | AnimeLocaleDataSource: LocaleDataSource, 13 | RemoteDataSource: DataSource, 14 | Transformer: Mapper>: Repository 15 | where AnimeLocaleDataSource.Request == AnimeListRequest, 16 | AnimeLocaleDataSource.Response == AnimeModuleEntity, 17 | RemoteDataSource.Request == AnimeListRequest, 18 | RemoteDataSource.Response == [AnimeDataResponse], 19 | Transformer.Request == Any, 20 | Transformer.Response == [AnimeDataResponse], 21 | Transformer.Entity == [AnimeModuleEntity], 22 | Transformer.Domain == [AnimeDomainModel] { 23 | 24 | public typealias Request = AnimeListRequest 25 | public typealias Response = [AnimeDomainModel] 26 | 27 | private let _localeDataSource: AnimeLocaleDataSource 28 | private let _remoteDataSource: RemoteDataSource 29 | private let _mapper: Transformer 30 | 31 | public init( 32 | localeDataSource: AnimeLocaleDataSource, 33 | remoteDataSource: RemoteDataSource, 34 | mapper: Transformer) { 35 | _localeDataSource = localeDataSource 36 | _remoteDataSource = remoteDataSource 37 | _mapper = mapper 38 | } 39 | 40 | public func execute(request: AnimeListRequest?) -> AnyPublisher<[AnimeDomainModel], Error> { 41 | return _remoteDataSource.execute(request: request) 42 | .map { _mapper.transformResponseToEntity(request: request, response: $0) } 43 | .flatMap { result -> AnyPublisher<[AnimeDomainModel], Error> in 44 | _localeDataSource.add(entities: result) 45 | .filter { $0 } 46 | .map { _ in _mapper.transformEntityToDomain(entity: result) } 47 | .eraseToAnyPublisher() 48 | } 49 | .eraseToAnyPublisher() 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Data/UpdateFavoriteAnimeRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateFavoriteAnimeRepository.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Combine 9 | import Core 10 | 11 | public struct UpdateFavoriteAnimeRepository< 12 | AnimeLocaleDataSource: LocaleDataSource, 13 | Transformer: Mapper>: Repository 14 | where AnimeLocaleDataSource.Request == Int, 15 | AnimeLocaleDataSource.Response == AnimeModuleEntity, 16 | Transformer.Request == Any, 17 | Transformer.Response == AnimeDataResponse, 18 | Transformer.Entity == AnimeModuleEntity, 19 | Transformer.Domain == AnimeDomainModel { 20 | 21 | public typealias Request = Int 22 | public typealias Response = AnimeDomainModel 23 | 24 | private let _localeDataSource: AnimeLocaleDataSource 25 | private let _mapper: Transformer 26 | 27 | public init( 28 | localeDataSource: AnimeLocaleDataSource, 29 | mapper: Transformer) { 30 | _localeDataSource = localeDataSource 31 | _mapper = mapper 32 | } 33 | 34 | public func execute(request: Int?) -> AnyPublisher { 35 | guard let request = request else { 36 | fatalError("Request cannot be empty") 37 | } 38 | 39 | return _localeDataSource.get(id: request) 40 | .map { _mapper.transformEntityToDomain(entity: $0) } 41 | .eraseToAnyPublisher() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Domain/AnimeDomainModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeDomainModel.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | public struct AnimeDomainModel: Equatable, Identifiable { 9 | 10 | public let id: Int 11 | public let title: String 12 | public let mainPicture: String 13 | public let alternativeTitleSynonyms: [String] 14 | public let alternativeTitleEnglish: String 15 | public let alternativeTitleJapanese: String 16 | public var startDate: String 17 | public var endDate: String 18 | public let airedDate: String 19 | public let synopsis: String 20 | public let rating: Double 21 | public let rank: Int 22 | public let popularity: Int 23 | public let userAmount: Int 24 | public let favoriteAmount: Int 25 | public let nsfw: String 26 | public let genre: [String] 27 | public let mediaType: String 28 | public let status: String 29 | public let episodeAmount: Int 30 | public let startSeason: String 31 | public let startSeasonYear: String 32 | public let source: String 33 | public let episodeDuration: Int 34 | public let episodeDurationText: String 35 | public let studios: [String] 36 | public var isFavorite: Bool 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Mapper/AnimeDataTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeDataTransformer.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Core 9 | import Foundation 10 | 11 | public struct AnimeDataTransformer: Mapper { 12 | public typealias Request = Any 13 | public typealias Response = AnimeDataResponse 14 | public typealias Entity = AnimeModuleEntity 15 | public typealias Domain = AnimeDomainModel 16 | 17 | public init() {} 18 | 19 | public func transformResponseToEntity(request: Any?, response: AnimeDataResponse) -> Entity { 20 | let animeEntity = AnimeModuleEntity() 21 | animeEntity.id = response.anime.id 22 | animeEntity.title = response.anime.title 23 | animeEntity.mainPicture = response.anime.mainPicture?.medium ?? "Unknown" 24 | animeEntity.alternativeTitleSynonyms.append(objectsIn: response.anime.alternativeTitles?.synonyms ?? []) 25 | animeEntity.alternativeTitleEnglish = response.anime.alternativeTitles?.english ?? "Unknown" 26 | animeEntity.alternativeTitleJapanese = response.anime.alternativeTitles?.japanese ?? "Unknown" 27 | animeEntity.startDate = response.anime.startDate ?? "Unknown" 28 | animeEntity.endDate = response.anime.endDate ?? "Unknown" 29 | animeEntity.synopsis = response.anime.synopsis ?? "Unknown" 30 | animeEntity.rating = response.anime.rating ?? 0 31 | animeEntity.rank = response.anime.rank ?? 0 32 | animeEntity.popularity = response.anime.popularity ?? 0 33 | animeEntity.userAmount = response.anime.userAmount 34 | animeEntity.favoriteAmount = response.anime.favoriteAmount 35 | animeEntity.nsfw = response.anime.nsfw?.name ?? "Unknown" 36 | animeEntity.genre.append(objectsIn: response.anime.genres?.map { $0.name } ?? []) 37 | animeEntity.mediaType = response.anime.mediaType.name 38 | animeEntity.status = response.anime.status.name 39 | animeEntity.episodeAmount = response.anime.episodeAmount 40 | animeEntity.startSeason = response.anime.startSeason?.season.name ?? "Unknown" 41 | animeEntity.startSeasonYear = response.anime.startSeason?.year.description ?? "" 42 | animeEntity.source = response.anime.source?.name ?? "Unknown" 43 | animeEntity.episodeDuration = response.anime.episodeDuration ?? 0 44 | animeEntity.studios.append(objectsIn: response.anime.studios.map { $0.name }) 45 | animeEntity.updatedAt = Date() 46 | return animeEntity 47 | } 48 | 49 | public func transformEntityToDomain(entity: AnimeModuleEntity) -> AnimeDomainModel { 50 | return AnimeDomainModel( 51 | id: entity.id, 52 | title: entity.title, 53 | mainPicture: entity.mainPicture, 54 | alternativeTitleSynonyms: Array(entity.alternativeTitleSynonyms), 55 | alternativeTitleEnglish: entity.alternativeTitleEnglish, 56 | alternativeTitleJapanese: entity.alternativeTitleJapanese, 57 | startDate: entity.startDate, 58 | endDate: entity.endDate, 59 | airedDate: AnimeTransformer.transformToAiredDate(startDate: entity.startDate, endDate: entity.endDate), 60 | synopsis: entity.synopsis, 61 | rating: entity.rating, 62 | rank: entity.rank, 63 | popularity: entity.popularity, 64 | userAmount: entity.userAmount, 65 | favoriteAmount: entity.favoriteAmount, 66 | nsfw: entity.nsfw, 67 | genre: Array(entity.genre), 68 | mediaType: entity.mediaType, 69 | status: entity.status, 70 | episodeAmount: entity.episodeAmount, 71 | startSeason: entity.startSeason, 72 | startSeasonYear: entity.startSeasonYear, 73 | source: entity.source, 74 | episodeDuration: entity.episodeDuration, 75 | episodeDurationText: AnimeTransformer.transformToDurationText(duration: entity.episodeDuration), 76 | studios: Array(entity.studios), 77 | isFavorite: entity.isFavorite 78 | ) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Mapper/AnimesTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimesTransformer.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | import Core 9 | import Foundation 10 | 11 | public struct AnimesTransformer: Mapper { 12 | public typealias Request = Any 13 | public typealias Response = [AnimeDataResponse] 14 | public typealias Entity = [AnimeModuleEntity] 15 | public typealias Domain = [AnimeDomainModel] 16 | 17 | public init() {} 18 | 19 | public func transformResponseToEntity(request: Any?, response: [AnimeDataResponse]) -> Entity { 20 | return response.map { result in 21 | let animeEntity = AnimeModuleEntity() 22 | animeEntity.id = result.anime.id 23 | animeEntity.title = result.anime.title 24 | animeEntity.mainPicture = result.anime.mainPicture?.medium ?? "Unknown" 25 | animeEntity.alternativeTitleSynonyms.append(objectsIn: result.anime.alternativeTitles?.synonyms ?? []) 26 | animeEntity.alternativeTitleEnglish = result.anime.alternativeTitles?.english ?? "Unknown" 27 | animeEntity.alternativeTitleJapanese = result.anime.alternativeTitles?.japanese ?? "Unknown" 28 | animeEntity.startDate = result.anime.startDate ?? "Unknown" 29 | animeEntity.endDate = result.anime.endDate ?? "Unknown" 30 | animeEntity.synopsis = result.anime.synopsis ?? "Unknown" 31 | animeEntity.rating = result.anime.rating ?? 0 32 | animeEntity.rank = result.anime.rank ?? 0 33 | animeEntity.popularity = result.anime.popularity ?? 0 34 | animeEntity.userAmount = result.anime.userAmount 35 | animeEntity.favoriteAmount = result.anime.favoriteAmount 36 | animeEntity.nsfw = result.anime.nsfw?.name ?? "Unknown" 37 | animeEntity.genre.append(objectsIn: result.anime.genres?.map { $0.name } ?? []) 38 | animeEntity.mediaType = result.anime.mediaType.name 39 | animeEntity.status = result.anime.status.name 40 | animeEntity.episodeAmount = result.anime.episodeAmount 41 | animeEntity.startSeason = result.anime.startSeason?.season.name ?? "Unknown" 42 | animeEntity.startSeasonYear = result.anime.startSeason?.year.description ?? "" 43 | animeEntity.source = result.anime.source?.name ?? "Unknown" 44 | animeEntity.episodeDuration = result.anime.episodeDuration ?? 0 45 | animeEntity.studios.append(objectsIn: result.anime.studios.map { $0.name }) 46 | animeEntity.updatedAt = Date() 47 | return animeEntity 48 | } 49 | } 50 | 51 | public func transformEntityToDomain(entity: [AnimeModuleEntity]) -> [AnimeDomainModel] { 52 | return entity.map { result in 53 | return AnimeDomainModel( 54 | id: result.id, 55 | title: result.title, 56 | mainPicture: result.mainPicture, 57 | alternativeTitleSynonyms: Array(result.alternativeTitleSynonyms), 58 | alternativeTitleEnglish: result.alternativeTitleEnglish, 59 | alternativeTitleJapanese: result.alternativeTitleJapanese, 60 | startDate: result.startDate, 61 | endDate: result.endDate, 62 | airedDate: AnimeTransformer.transformToAiredDate(startDate: result.startDate, endDate: result.endDate), 63 | synopsis: result.synopsis, 64 | rating: result.rating, 65 | rank: result.rank, 66 | popularity: result.popularity, 67 | userAmount: result.userAmount, 68 | favoriteAmount: result.favoriteAmount, 69 | nsfw: result.nsfw, 70 | genre: Array(result.genre), 71 | mediaType: result.mediaType, 72 | status: result.status, 73 | episodeAmount: result.episodeAmount, 74 | startSeason: result.startSeason, 75 | startSeasonYear: result.startSeasonYear, 76 | source: result.source, 77 | episodeDuration: result.episodeDuration, 78 | episodeDurationText: AnimeTransformer.transformToDurationText(duration: result.episodeDuration), 79 | studios: Array(result.studios), 80 | isFavorite: result.isFavorite 81 | ) 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Presentation/AnimePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimePresenter.swift 3 | // 4 | // 5 | // Created by Bryan on 07/01/23. 6 | // 7 | 8 | import Combine 9 | import Core 10 | import Foundation 11 | 12 | public class AnimePresenter< 13 | AnimeUseCase: UseCase, 14 | FavoriteUseCase: UseCase>: ObservableObject 15 | where AnimeUseCase.Request == AnimeRequest, 16 | AnimeUseCase.Response == AnimeDomainModel, 17 | FavoriteUseCase.Request == Int, 18 | FavoriteUseCase.Response == AnimeDomainModel { 19 | 20 | private var cancellables: Set = [] 21 | 22 | private let _animeUseCase: AnimeUseCase 23 | private let _favoriteUseCase: FavoriteUseCase 24 | 25 | @Published public var item: AnimeDomainModel? 26 | @Published public var errorMessage: String = "" 27 | @Published public var isLoading: Bool = false 28 | @Published public var isError: Bool = false 29 | 30 | public init( 31 | animeUseCase: AnimeUseCase, 32 | favoriteUseCase: FavoriteUseCase 33 | ) { 34 | _animeUseCase = animeUseCase 35 | _favoriteUseCase = favoriteUseCase 36 | } 37 | 38 | public func getAnime(request: AnimeUseCase.Request) { 39 | isLoading = true 40 | _animeUseCase.execute(request: request) 41 | .receive(on: RunLoop.main) 42 | .sink(receiveCompletion: { completion in 43 | switch completion { 44 | case .failure(let error): 45 | self.errorMessage = error.localizedDescription 46 | self.isError = true 47 | self.isLoading = false 48 | case .finished: 49 | self.isLoading = false 50 | } 51 | }, receiveValue: { item in 52 | self.isError = false 53 | 54 | var anime = item 55 | 56 | anime.startDate = item.startDate.apiFullStringDateToFullStringDate() 57 | anime.endDate = item.endDate.apiFullStringDateToFullStringDate() 58 | 59 | self.item = anime 60 | }) 61 | .store(in: &cancellables) 62 | } 63 | 64 | public func updateFavoriteAnime(request: FavoriteUseCase.Request) { 65 | _favoriteUseCase.execute(request: request) 66 | .receive(on: RunLoop.main) 67 | .sink(receiveCompletion: { completion in 68 | switch completion { 69 | case .failure(let error): 70 | self.errorMessage = error.localizedDescription 71 | self.isError = true 72 | self.isLoading = false 73 | case .finished: 74 | self.isLoading = false 75 | } 76 | }, receiveValue: { item in 77 | self.isError = false 78 | self.item = item 79 | }) 80 | .store(in: &cancellables) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Request/AnimeListRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeListRequest.swift 3 | // 4 | // 5 | // Created by Bryan on 08/01/23. 6 | // 7 | 8 | public struct AnimeListRequest { 9 | let q: String 10 | let limit: Int? 11 | let offset: Int? 12 | let fields: String? 13 | let nsfw: Bool? 14 | let refresh: Bool 15 | 16 | public init( 17 | title q: String, 18 | limit: Int? = nil, 19 | offset: Int? = nil, 20 | fields: String? = nil, 21 | nsfw: Bool? = nil, 22 | refresh: Bool = false 23 | ) { 24 | self.q = q 25 | self.limit = limit 26 | self.offset = offset 27 | self.fields = fields 28 | self.nsfw = nsfw 29 | self.refresh = refresh 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Request/AnimeRankingRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeRankingRequest.swift 3 | // 4 | // 5 | // Created by Bryan on 06/01/23. 6 | // 7 | 8 | public struct AnimeRankingRequest { 9 | let rankingType: RankingTypeRequest 10 | let limit: Int? 11 | let offset: Int? 12 | let fields: String? 13 | let nsfw: Bool? 14 | let refresh: Bool 15 | 16 | public init( 17 | type rankingType: RankingTypeRequest, 18 | limit: Int? = nil, 19 | offset: Int? = nil, 20 | fields: String? = nil, 21 | nsfw: Bool? = nil, 22 | refresh: Bool = false 23 | ) { 24 | self.rankingType = rankingType 25 | self.limit = limit 26 | self.offset = offset 27 | self.fields = fields 28 | self.nsfw = nsfw 29 | self.refresh = refresh 30 | } 31 | } 32 | 33 | public enum RankingTypeRequest: String { 34 | case all 35 | case airing 36 | case upcoming 37 | case tv 38 | case ova 39 | case movie 40 | case special 41 | case byPopularity 42 | case favorite 43 | 44 | var name: String { 45 | return rawValue.lowercased() 46 | } 47 | 48 | var sortKey: String { 49 | switch self { 50 | case .all, .airing, .tv, .ova, .movie, .special: 51 | return "rank" 52 | case .upcoming, .byPopularity: 53 | return "popularity" 54 | case .favorite: 55 | return "favoriteAmount" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Request/AnimeRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeRequest.swift 3 | // 4 | // 5 | // Created by Bryan on 09/01/23. 6 | // 7 | 8 | public struct AnimeRequest: Encodable { 9 | let animeId: Int 10 | let fields: String 11 | 12 | public init( 13 | id animeId: Int, 14 | fields: String = "alternative_titles,start_date,end_date,synopsis,mean," 15 | + "rank,popularity,num_list_users,num_favorites,genres,media_type," 16 | + "status,num_episodes,start_season,source,average_episode_duration,studios" 17 | ) { 18 | self.animeId = animeId 19 | self.fields = fields 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Supporting Files/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 02/02/23. 6 | 7 | */ 8 | 9 | "unknown_label" = "Unknown"; 10 | 11 | "hours_label" = "hours"; 12 | "minutes_label" = "minutes"; 13 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Supporting Files/Localization/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | number_duration_label %lld %lld %lld 6 | 7 | NSStringLocalizedFormatKey 8 | %#@hours@%#@minutes@%#@seconds@ 9 | hours 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | lld 15 | zero 16 | 17 | one 18 | 1 hour 19 | other 20 | %lld hours 21 | 22 | minutes 23 | 24 | NSStringFormatSpecTypeKey 25 | NSStringPluralRuleType 26 | NSStringFormatValueTypeKey 27 | lld 28 | zero 29 | 30 | one 31 | 1 minute 32 | other 33 | %lld minutes 34 | 35 | seconds 36 | 37 | NSStringFormatSpecTypeKey 38 | NSStringPluralRuleType 39 | NSStringFormatValueTypeKey 40 | lld 41 | zero 42 | 43 | one 44 | 1 second 45 | other 46 | %lld seconds 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Supporting Files/Localization/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 02/02/23. 6 | 7 | */ 8 | 9 | "unknown_label" = "Tidak diketahui"; 10 | 11 | "hours_label" = "jam"; 12 | "minutes_label" = "menit"; 13 | -------------------------------------------------------------------------------- /Modules/Anime/Sources/Anime/Supporting Files/Localization/id.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | number_duration_label %lld %lld %lld 6 | 7 | NSStringLocalizedFormatKey 8 | %#@hours@%#@minutes@%#@seconds@ 9 | hours 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | lld 15 | zero 16 | 17 | other 18 | %lld jam 19 | 20 | minutes 21 | 22 | NSStringFormatSpecTypeKey 23 | NSStringPluralRuleType 24 | NSStringFormatValueTypeKey 25 | lld 26 | zero 27 | 28 | other 29 | %lld menit 30 | 31 | seconds 32 | 33 | NSStringFormatSpecTypeKey 34 | NSStringPluralRuleType 35 | NSStringFormatValueTypeKey 36 | lld 37 | zero 38 | 39 | other 40 | %lld detik 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Modules/Anime/Tests/AnimeTests/AnimeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Anime 3 | 4 | final class AnimeTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(Anime().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AnimeDetail", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "AnimeDetail", 14 | targets: ["AnimeDetail"]) 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | .package(url: "https://github.com/SDWebImage/SDWebImageSwiftUI.git", .upToNextMajor(from: "2.0.0")), 19 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 20 | .package(path: "../Anime"), 21 | .package(path: "../Common") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "AnimeDetail", 28 | dependencies: [ 29 | .product(name: "Core", package: "Yume-Core-Module"), 30 | "Anime", 31 | "Common", 32 | "SDWebImageSwiftUI" 33 | ]), 34 | .testTarget( 35 | name: "AnimeDetailTests", 36 | dependencies: ["AnimeDetail"]) 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/README.md: -------------------------------------------------------------------------------- 1 | # AnimeDetail 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/Sources/AnimeDetail/Presentation/View/AnimeInformationItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeInformationItem.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Common 9 | import Core 10 | import SwiftUI 11 | 12 | public struct AnimeInformationItem: View { 13 | @State var label: String 14 | @State var value: String 15 | 16 | public var body: some View { 17 | VStack(alignment: .leading, spacing: Space.none) { 18 | Text(label) 19 | .typography(.body(color: YumeColor.onSurfaceVariant)) 20 | Text(value) 21 | .typography(.body(color: YumeColor.onBackground)) 22 | } 23 | } 24 | } 25 | 26 | struct AnimeInformationItem_Previews: PreviewProvider { 27 | static var previews: some View { 28 | AnimeInformationItem(label: "Episodes", value: "13") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/Sources/AnimeDetail/Presentation/View/AnimeStatItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeStatItem.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Common 9 | import Core 10 | import SwiftUI 11 | 12 | public struct AnimeStatItem: View { 13 | let icon: UIImage 14 | let iconColor: Color 15 | @State var label: String 16 | @State var value: String 17 | 18 | public var body: some View { 19 | GeometryReader { geo in 20 | VStack(spacing: Space.tiny) { 21 | IconView( 22 | icon: icon, 23 | color: iconColor 24 | ) 25 | Text(value) 26 | .typography(.subheadline(color: YumeColor.onBackground)) 27 | Text(label) 28 | .typography(.caption(color: YumeColor.onSurfaceVariant)) 29 | } 30 | .frame(width: geo.size.width, height: geo.size.height) 31 | } 32 | } 33 | } 34 | 35 | struct AnimeStatItem_Previews: PreviewProvider { 36 | static var previews: some View { 37 | AnimeStatItem(icon: Icons.starOutlined, iconColor: .yellow, label: "Score", value: "9.8") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/Sources/AnimeDetail/Supporting Files/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "score_label" = "Score"; 10 | "rank_label" = "Rank"; 11 | "popularity_label" = "Popularity"; 12 | "members_label" = "Members"; 13 | "favorites_label" = "Favorites"; 14 | 15 | "synopsis_label" = "Synopsis"; 16 | "information_label" = "Information"; 17 | "episodes_label" = "Episodes"; 18 | "duration_label" = "Duration"; 19 | "aired_label" = "Aired"; 20 | "studios_label" = "Studios"; 21 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/Sources/AnimeDetail/Supporting Files/Localization/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "score_label" = "Skor"; 10 | "rank_label" = "Peringkat"; 11 | "popularity_label" = "Popularitas"; 12 | "members_label" = "Anggota"; 13 | "favorites_label" = "Favorit"; 14 | 15 | "synopsis_label" = "Sinopsis"; 16 | "information_label" = "Informasi"; 17 | "episodes_label" = "Jumlah Episode"; 18 | "duration_label" = "Durasi"; 19 | "aired_label" = "Tayang"; 20 | "studios_label" = "Studio"; 21 | -------------------------------------------------------------------------------- /Modules/AnimeDetail/Tests/AnimeDetailTests/AnimeDetailTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AnimeDetail 3 | 4 | final class AnimeDetailTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(AnimeDetail().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modules/Common/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/Common/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/Common/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Common", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "Common", 14 | targets: ["Common"]) 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | .package(url: "https://github.com/SDWebImage/SDWebImageSwiftUI.git", .upToNextMajor(from: "2.0.0")), 19 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 20 | .package(path: "../Anime") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Common", 27 | dependencies: [ 28 | .product(name: "Core", package: "Yume-Core-Module"), 29 | "Anime", 30 | "SDWebImageSwiftUI" 31 | ]), 32 | .testTarget( 33 | name: "CommonTests", 34 | dependencies: ["Common"]) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Modules/Common/README.md: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/CaretLeftIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "caret-left.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/CaretLeftIcon.imageset/caret-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/CloseIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "close.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/CloseIcon.imageset/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/CrownOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "crown.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/CrownOutlinedIcon.imageset/crown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HeartIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heart (1).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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HeartIcon.imageset/heart (1).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HeartOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "heart.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HeartOutlinedIcon.imageset/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HouseIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "house.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HouseIcon.imageset/house.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HouseOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "house (1).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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/HouseOutlinedIcon.imageset/house (1).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/ImageOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/ImageOutlinedIcon.imageset/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/InfoOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "info.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/InfoOutlinedIcon.imageset/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/SearchIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "search.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/SearchIcon.imageset/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/StarOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "star.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/StarOutlinedIcon.imageset/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/TrendingUpIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "trending-up.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/TrendingUpIcon.imageset/trending-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/UserIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "user (1).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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/UserIcon.imageset/user (1).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/UserOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "user.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/UserOutlinedIcon.imageset/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/UsersOutlinedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "users.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 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Assets.xcassets/UsersOutlinedIcon.imageset/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "loading_label" = "Loading"; 10 | "unknown_label" = "Unknown"; 11 | 12 | "home_title" = "Home"; 13 | "search_title" = "Search"; 14 | "favorite_title" = "Favorite"; 15 | "profile_title" = "Profile"; 16 | 17 | "now_airing_title" = "Now Airing"; 18 | "upcoming_title" = "Upcoming"; 19 | "most_popular_title" = "Most Popular"; 20 | "top_rated_title" = "Top Rated"; 21 | 22 | "no_internet_label" = "No internet"; 23 | "retry_label" = "Retry"; 24 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Supporting Files/Localization/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "loading_label" = "Memuat"; 10 | "unknown_label" = "Tidak diketahui"; 11 | 12 | "home_title" = "Beranda"; 13 | "search_title" = "Cari"; 14 | "favorite_title" = "Favorit"; 15 | "profile_title" = "Profil"; 16 | 17 | "now_airing_title" = "Sedang Tayang"; 18 | "upcoming_title" = "Mendatang"; 19 | "most_popular_title" = "Paling Populer"; 20 | "top_rated_title" = "Peringkat Teratas"; 21 | 22 | "no_internet_label" = "Tidak ada koneksi internet"; 23 | "retry_label" = "Coba lagi"; 24 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/Extension/Bundle+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Ext.swift 3 | // 4 | // 5 | // Created by Bryan on 09/01/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | public static var common: Bundle = .module 12 | } 13 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/Theme/Icons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icons.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | import UIKit 9 | 10 | public struct Icons { 11 | public static let caretLeft = UIImage(named: "CaretLeftIcon", in: .module, compatibleWith: nil)! 12 | public static let close = UIImage(named: "CloseIcon", in: .module, with: nil)! 13 | public static let crownOutlined = UIImage(named: "CrownOutlinedIcon", in: .module, compatibleWith: nil)! 14 | public static let heart = UIImage(named: "HeartIcon", in: .module, compatibleWith: nil)! 15 | public static let heartOutlined = UIImage(named: "HeartOutlinedIcon", in: Bundle.module, compatibleWith: nil)! 16 | public static let house = UIImage(named: "HouseIcon", in: .module, compatibleWith: nil)! 17 | public static let houseOutlined = UIImage(named: "HouseOutlinedIcon", in: .module, compatibleWith: nil)! 18 | public static let imageOutlined = UIImage(named: "ImageOutlinedIcon", in: .module, compatibleWith: nil)! 19 | public static let infoOutlined = UIImage(named: "InfoOutlinedIcon", in: .module, compatibleWith: nil)! 20 | public static let search = UIImage(named: "SearchIcon", in: .module, compatibleWith: nil)! 21 | public static let starOutlined = UIImage(named: "StarOutlinedIcon", in: .module, compatibleWith: nil)! 22 | public static let trendingUp = UIImage(named: "TrendingUpIcon", in: .module, compatibleWith: nil)! 23 | public static let user = UIImage(named: "UserIcon", in: .module, compatibleWith: nil)! 24 | public static let userOutlined = UIImage(named: "UserOutlinedIcon", in: .module, compatibleWith: nil)! 25 | public static let usersOutlined = UIImage(named: "UsersOutlinedIcon", in: .module, compatibleWith: nil)! 26 | public static let wifiSlash = "wifi.slash" 27 | } 28 | 29 | public struct IconSize { 30 | /// 16.0 31 | public static let small = 16.0 32 | /// 24.0 33 | public static let medium = 24.0 34 | /// 32.0 35 | public static let large = 32.0 36 | } 37 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/Theme/Shape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shape.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | public struct Shape { 9 | /// 4.0 10 | public static let extraSmall = 4.0 11 | /// 8.0 12 | public static let small = 8.0 13 | } 14 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/Theme/Space.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Space.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | public struct Space { 9 | /// 0.0 10 | public static let none = 0.0 11 | /// 4.0 12 | public static let tiny = 4.0 13 | /// 8.0 14 | public static let small = 8.0 15 | /// 16.0 16 | public static let medium = 16.0 17 | /// 24.0 18 | public static let large = 24.0 19 | } 20 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/AnimeCardItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeCardItem.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 29/12/22. 6 | // 7 | 8 | import Anime 9 | import Core 10 | import SwiftUI 11 | import SDWebImageSwiftUI 12 | 13 | public struct AnimeCardItem: View { 14 | @State var anime: AnimeDomainModel 15 | 16 | public init(anime: AnimeDomainModel) { 17 | self.anime = anime 18 | } 19 | 20 | public var body: some View { 21 | HStack(spacing: Space.small) { 22 | mainPicture 23 | content 24 | Spacer() 25 | } 26 | .frame(height: 150) 27 | .background(YumeColor.surface) 28 | .overlay( 29 | RoundedRectangle(cornerRadius: 8) 30 | .stroke(YumeColor.outline, lineWidth: 1) 31 | ) 32 | } 33 | 34 | } 35 | 36 | extension AnimeCardItem { 37 | 38 | var mainPicture: some View { 39 | WebImage(url: URL(string: anime.mainPicture)) 40 | .resizable() 41 | .placeholder { 42 | ImagePlaceholder() 43 | } 44 | .indicator(.activity) 45 | .transition(.fade(duration: 0.5)) 46 | .scaledToFill() 47 | .frame(width: 100, height: 150) 48 | .cornerRadius(Shape.small) 49 | } 50 | 51 | var content: some View { 52 | VStack(alignment: .leading, spacing: Space.small) { 53 | overview 54 | Spacer() 55 | tags 56 | }.padding( 57 | EdgeInsets( 58 | top: Space.small, 59 | leading: Space.none, 60 | bottom: Space.small, 61 | trailing: Space.medium 62 | ) 63 | ) 64 | } 65 | 66 | var overview: some View { 67 | VStack(alignment: .leading, spacing: Space.tiny) { 68 | Text("\(anime.mediaType)" 69 | + " · \(anime.startSeason) \(anime.startSeasonYear)" 70 | + " · \(anime.status)" 71 | ).typography(.caption(color: YumeColor.onSurfaceVariant)) 72 | 73 | Text(anime.alternativeTitleEnglish.isEmpty 74 | ? anime.title : anime.alternativeTitleEnglish) 75 | .typography(.body(color: YumeColor.onSurface)) 76 | .lineLimit(2) 77 | } 78 | } 79 | 80 | var tags: some View { 81 | VStack(alignment: .leading, spacing: Space.tiny) { 82 | stats 83 | Text(Array(anime.genre.prefix(3)) 84 | .joined(separator: " · ")) 85 | .lineLimit(1) 86 | }.typography(.caption(color: YumeColor.onSurfaceVariant)) 87 | } 88 | 89 | var stats: some View { 90 | HStack(spacing: Space.small) { 91 | HStack(spacing: Space.tiny) { 92 | IconView( 93 | icon: Icons.starOutlined, 94 | color: .yellow, 95 | size: IconSize.small 96 | ) 97 | Text(anime.rating.description) 98 | .typography(.caption(color: YumeColor.onSurfaceVariant)) 99 | } 100 | HStack(spacing: Space.tiny) { 101 | IconView( 102 | icon: Icons.crownOutlined, 103 | color: .orange, 104 | size: IconSize.small 105 | ) 106 | Text("#\(anime.rank.formatNumber())") 107 | .typography(.caption(color: YumeColor.onSurfaceVariant)) 108 | } 109 | HStack(spacing: Space.tiny) { 110 | IconView( 111 | icon: Icons.trendingUp, 112 | color: .green, 113 | size: IconSize.small 114 | ) 115 | Text("#\(anime.popularity.formatNumber())") 116 | .typography(.caption(color: YumeColor.onSurfaceVariant)) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/AnimeItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeItem.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | import Anime 9 | import Core 10 | import SwiftUI 11 | import SDWebImageSwiftUI 12 | 13 | public struct AnimeItem: View { 14 | @State var anime: AnimeDomainModel 15 | 16 | public init(anime: AnimeDomainModel) { 17 | self.anime = anime 18 | } 19 | 20 | public var body: some View { 21 | VStack(alignment: .leading, spacing: Space.small) { 22 | mainPicture 23 | content 24 | } 25 | .frame(width: 100) 26 | } 27 | } 28 | 29 | extension AnimeItem { 30 | 31 | var mainPicture: some View { 32 | WebImage(url: URL(string: anime.mainPicture)) 33 | .resizable() 34 | .placeholder { 35 | ImagePlaceholder() 36 | } 37 | .indicator(.activity) 38 | .transition(.fade(duration: 0.5)) 39 | .scaledToFill() 40 | .frame(width: 100, height: 150) 41 | .cornerRadius(Shape.small) 42 | } 43 | 44 | var content: some View { 45 | Text(anime.alternativeTitleEnglish.isEmpty 46 | ? anime.title : anime.alternativeTitleEnglish) 47 | .typography(.caption(color: YumeColor.onBackground)) 48 | .lineLimit(2, reservesSpace: true) 49 | .padding( 50 | EdgeInsets( 51 | top: Space.none, 52 | leading: Space.tiny, 53 | bottom: Space.tiny, 54 | trailing: Space.tiny) 55 | ) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/AnimeRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimeRow.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | import Anime 9 | import SwiftUI 10 | 11 | public struct AnimeRow: View { 12 | @State var animes: [AnimeDomainModel] 13 | 14 | public var body: some View { 15 | ScrollView(.horizontal, showsIndicators: false) { 16 | LazyHStack(spacing: Space.small) { 17 | ForEach(animes) { anime in 18 | AnimeItem(anime: anime) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/AppBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppBar.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct AppBar: View { 12 | var scrollOffset: CGFloat 13 | let label: String 14 | let alwaysShowLabel: Bool 15 | let leading: () -> Content? 16 | let trailing: () -> Content? 17 | 18 | public init( 19 | scrollOffset: CGFloat, 20 | label: String, 21 | alwaysShowLabel: Bool = false, 22 | @ViewBuilder leading: @escaping () -> Content? = { Text("") }, 23 | @ViewBuilder trailing: @escaping () -> Content? = { Text("") } 24 | ) { 25 | self.scrollOffset = scrollOffset 26 | self.label = label 27 | self.alwaysShowLabel = alwaysShowLabel 28 | self.leading = leading 29 | self.trailing = trailing 30 | } 31 | 32 | public var body: some View { 33 | GeometryReader { geo in 34 | VStack(spacing: Space.small) { 35 | HStack(spacing: Space.small) { 36 | leading() 37 | .frame(maxWidth: geo.size.width / 4, alignment: .leading) 38 | Text(label) 39 | .typography(.title3(weight: .bold, color: .black)) 40 | .lineLimit(1) 41 | .opacity(alwaysShowLabel ? 1 : scrollOffset / 100) 42 | .frame(maxWidth: geo.size.width / 2, alignment: .center) 43 | trailing() 44 | .frame(maxWidth: geo.size.width / 4, alignment: .trailing) 45 | } 46 | } 47 | .padding( 48 | EdgeInsets( 49 | top: Space.none, 50 | leading: Space.medium, 51 | bottom: Space.small, 52 | trailing: Space.medium) 53 | ) 54 | .frame(width: geo.size.width) 55 | .background(scrollOffset > 1 ? YumeColor.surface2 : Color.black.opacity(0)) 56 | } 57 | } 58 | } 59 | 60 | public struct BackAppBar: View { 61 | @Environment(\.presentationMode) var presentationMode: Binding 62 | var scrollOffset: CGFloat 63 | let label: String 64 | let alwaysShowLabel: Bool 65 | let trailing: () -> Content? 66 | 67 | public init( 68 | scrollOffset: CGFloat, 69 | label: String, 70 | alwaysShowLabel: Bool = false, 71 | @ViewBuilder trailing: @escaping () -> Content? = { Text("") } 72 | ) { 73 | self.scrollOffset = scrollOffset 74 | self.label = label 75 | self.alwaysShowLabel = alwaysShowLabel 76 | self.trailing = trailing 77 | } 78 | 79 | public var body: some View { 80 | GeometryReader { geo in 81 | VStack(spacing: Space.small) { 82 | HStack(spacing: Space.small) { 83 | Button { 84 | presentationMode.wrappedValue.dismiss() 85 | } label: { 86 | IconView(icon: Icons.caretLeft) 87 | }.frame(maxWidth: geo.size.width / 4, alignment: .leading) 88 | Text(label) 89 | .typography(.title3(weight: .bold, color: .black)) 90 | .lineLimit(1) 91 | .opacity(alwaysShowLabel ? 1 : scrollOffset / 100) 92 | .frame(maxWidth: geo.size.width / 2, alignment: .center) 93 | trailing() 94 | .frame(maxWidth: geo.size.width / 4, alignment: .trailing) 95 | } 96 | } 97 | .padding( 98 | EdgeInsets( 99 | top: Space.none, 100 | leading: Space.medium, 101 | bottom: Space.small, 102 | trailing: Space.medium) 103 | ) 104 | .frame(width: geo.size.width) 105 | .background(scrollOffset > 1 ? YumeColor.surface2 : Color.black.opacity(0)) 106 | } 107 | } 108 | } 109 | 110 | struct AppBar_Previews: PreviewProvider { 111 | static var previews: some View { 112 | Group { 113 | AppBar(scrollOffset: 500, label: "Title") 114 | .previewDisplayName("App Bar") 115 | 116 | BackAppBar(scrollOffset: 500, label: "Title") 117 | .previewDisplayName("App Bar with Back") 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Bryan on 03/02/23. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | struct FilledButton: ButtonStyle { 12 | @State private var pressed = false 13 | 14 | func makeBody(configuration: Configuration) -> some View { 15 | configuration.label 16 | .padding( 17 | EdgeInsets( 18 | top: Space.small, 19 | leading: Space.large, 20 | bottom: Space.small, 21 | trailing: Space.large) 22 | ) 23 | .background(YumeColor.primary) 24 | .compositingGroup() 25 | .opacity(configuration.isPressed ? 0.5 : 1.0) 26 | .clipShape(Capsule()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/CircleImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleImage.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 31/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct CircleImage: View { 11 | let image: Image 12 | 13 | public init(image: Image) { 14 | self.image = image 15 | } 16 | 17 | public var body: some View { 18 | image 19 | .resizable() 20 | .aspectRatio(1, contentMode: .fit) 21 | .clipShape(Circle()) 22 | } 23 | } 24 | 25 | struct CircleImage_Previews: PreviewProvider { 26 | static var previews: some View { 27 | CircleImage(image: Image("ProfilePicture")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/CustomEmptyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomEmptyView.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct CustomEmptyView: View { 12 | let image: UIImage? 13 | let icon: String? 14 | let label: String 15 | let imageWidth: CGFloat? 16 | 17 | public init( 18 | image: UIImage? = nil, 19 | icon: String? = nil, 20 | label: String, 21 | imageWidth: CGFloat? = nil 22 | ) { 23 | self.image = image 24 | self.icon = icon 25 | self.label = label 26 | self.imageWidth = imageWidth 27 | } 28 | 29 | public var body: some View { 30 | VStack(spacing: Space.large) { 31 | if image != nil { 32 | Image(uiImage: image!) 33 | .resizable() 34 | .scaledToFit() 35 | .frame(width: imageWidth ?? 240) 36 | .foregroundColor(YumeColor.onSurfaceVariant) 37 | } 38 | 39 | if icon != nil { 40 | Image(systemName: icon!) 41 | .resizable() 42 | .scaledToFit() 43 | .frame(width: imageWidth ?? 80) 44 | .foregroundColor(YumeColor.onSurfaceVariant) 45 | } 46 | 47 | Text(label) 48 | .typography(.body(color: YumeColor.onSurfaceVariant)) 49 | } 50 | .frame(maxWidth: .infinity, maxHeight: .infinity) 51 | .background(YumeColor.background) 52 | } 53 | } 54 | 55 | struct CustomEmptyView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | CustomEmptyView(label: "No anime found") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/IconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconView.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 29/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct IconView: View { 11 | let icon: UIImage 12 | let color: Color 13 | let size: CGFloat 14 | 15 | public init( 16 | icon: UIImage, 17 | color: Color = .black, 18 | size: CGFloat = IconSize.medium 19 | ) { 20 | self.icon = icon 21 | self.color = color 22 | self.size = size 23 | } 24 | 25 | public var body: some View { 26 | Image(uiImage: icon) 27 | .resizable() 28 | .foregroundColor(color) 29 | .frame(width: size, height: size) 30 | } 31 | } 32 | 33 | struct IconView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | IconView(icon: Icons.trendingUp, color: .green, size: IconSize.medium) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/ImagePlaceholder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePlaceholder.swift 3 | // 4 | // 5 | // Created by Bryan on 04/02/23. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct ImagePlaceholder: View { 12 | 13 | public init() {} 14 | 15 | public var body: some View { 16 | VStack { 17 | IconView( 18 | icon: Icons.imageOutlined, 19 | color: YumeColor.onSurfaceVariant) 20 | } 21 | .frame(maxWidth: .infinity, maxHeight: .infinity) 22 | .background(YumeColor.surfaceVariant) 23 | } 24 | } 25 | 26 | struct ImagePlaceholder_Previews: PreviewProvider { 27 | static var previews: some View { 28 | ImagePlaceholder() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/NoInternetView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoConnectionView.swift 3 | // 4 | // 5 | // Created by Bryan on 03/02/23. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct NoInternetView: View { 12 | let onRetry: () -> Void 13 | 14 | public init( 15 | onRetry: @escaping () -> Void 16 | ) { 17 | self.onRetry = onRetry 18 | } 19 | 20 | public var body: some View { 21 | VStack(spacing: Space.large) { 22 | Image(systemName: Icons.wifiSlash) 23 | .resizable() 24 | .scaledToFit() 25 | .frame(width: 80) 26 | .foregroundColor(YumeColor.onSurfaceVariant) 27 | 28 | Text("no_internet_label".localized(bundle: .module)) 29 | .typography(.body(color: YumeColor.onSurfaceVariant)) 30 | 31 | Button { 32 | onRetry() 33 | } label: { 34 | Text("retry_label".localized(bundle: .module)) 35 | .typography(.body(weight: .bold, color: YumeColor.onPrimary)) 36 | }.buttonStyle(FilledButton()) 37 | } 38 | .frame(maxWidth: .infinity, maxHeight: .infinity) 39 | .background(YumeColor.background) 40 | } 41 | } 42 | 43 | struct NoConnectionView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | NoInternetView(onRetry: {}) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/ObservableScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableScrollView.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 29/12/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Credit: https://swiftuirecipes.com/blog/swiftui-scrollview-scroll-offset 11 | // Simple preference that observes a CGFloat. 12 | struct ScrollViewOffsetPreferenceKey: PreferenceKey { 13 | static var defaultValue = CGFloat.zero 14 | 15 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 16 | value += nextValue() 17 | } 18 | } 19 | 20 | // A ScrollView wrapper that tracks scroll offset changes. 21 | public struct ObservableScrollView: View where Content: View { 22 | @Namespace var scrollSpace 23 | 24 | @Binding var scrollOffset: CGFloat 25 | let showsIndicators: Bool 26 | let content: (ScrollViewProxy) -> Content 27 | 28 | public init( 29 | scrollOffset: Binding, 30 | showsIndicators: Bool = true, 31 | @ViewBuilder content: @escaping (ScrollViewProxy) -> Content 32 | ) { 33 | _scrollOffset = scrollOffset 34 | self.showsIndicators = showsIndicators 35 | self.content = content 36 | } 37 | 38 | public var body: some View { 39 | ScrollView(showsIndicators: showsIndicators) { 40 | ScrollViewReader { proxy in 41 | content(proxy) 42 | .background(GeometryReader { geo in 43 | let offset = -geo.frame(in: .named(scrollSpace)).minY 44 | Color.clear 45 | .preference( 46 | key: ScrollViewOffsetPreferenceKey.self, 47 | value: offset 48 | ) 49 | }) 50 | } 51 | } 52 | .coordinateSpace(name: scrollSpace) 53 | .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in 54 | scrollOffset = value 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/ProgressIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressIndicator.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct ProgressIndicator: View { 12 | private let _label: String 13 | 14 | public init( 15 | label: String = "loading_label" 16 | ) { 17 | _label = label 18 | } 19 | 20 | public var body: some View { 21 | GeometryReader { geo in 22 | VStack(spacing: Space.medium) { 23 | ProgressView() 24 | .tint(YumeColor.onSurfaceVariant) 25 | Text(_label.localized(bundle: .common)) 26 | .typography(.body(color: YumeColor.onSurfaceVariant)) 27 | } 28 | .frame(width: geo.size.width, height: geo.size.height) 29 | } 30 | } 31 | } 32 | 33 | struct ProgressIndicator_Previews: PreviewProvider { 34 | static var previews: some View { 35 | ProgressIndicator(label: "Searching anime") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/RefreshableScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshableScrollView.swift 3 | // 4 | // 5 | // Created by Bryan on 01/02/23. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | // A ScrollView wrapper that tracks scroll offset changes. 12 | public struct RefreshableScrollView: View where Content: View { 13 | 14 | @Binding var scrollOffset: CGFloat 15 | @Binding var isRefreshing: Bool 16 | let refreshLabel: String 17 | let onRefresh: () -> Void 18 | let showsIndicators: Bool 19 | let content: (ScrollViewProxy) -> Content 20 | 21 | public init( 22 | scrollOffset: Binding, 23 | showsIndicators: Bool = true, 24 | refreshLabel: String = "", 25 | isRefreshing: Binding, 26 | onRefresh: @escaping () -> Void, 27 | @ViewBuilder content: @escaping (ScrollViewProxy) -> Content 28 | ) { 29 | _scrollOffset = scrollOffset 30 | self.showsIndicators = showsIndicators 31 | self.refreshLabel = refreshLabel 32 | _isRefreshing = isRefreshing 33 | self.onRefresh = onRefresh 34 | self.content = content 35 | } 36 | 37 | public var body: some View { 38 | ObservableScrollView( 39 | scrollOffset: $scrollOffset, 40 | showsIndicators: showsIndicators, 41 | content: content 42 | ) 43 | .onAppear { 44 | UIRefreshControl.appearance().tintColor = UIColor(YumeColor.onSurfaceVariant) 45 | UIRefreshControl.appearance().attributedTitle = NSAttributedString(string: refreshLabel) 46 | } 47 | .refreshable { 48 | onRefresh() 49 | await refreshing() 50 | } 51 | } 52 | } 53 | 54 | extension RefreshableScrollView { 55 | func refreshing() async { 56 | while isRefreshing {} 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct SearchBar: View { 12 | let placeholder: String 13 | @Binding var searchText: String 14 | 15 | public init( 16 | placeholder: String, 17 | searchText: Binding 18 | ) { 19 | self.placeholder = placeholder 20 | self._searchText = searchText 21 | } 22 | 23 | public var body: some View { 24 | HStack { 25 | IconView( 26 | icon: Icons.search, 27 | color: searchText.isEmpty ? YumeColor.onSurfaceVariant : YumeColor.onSurface, 28 | size: IconSize.small 29 | ) 30 | 31 | ZStack(alignment: .leading) { 32 | if searchText.isEmpty { 33 | Text(placeholder) 34 | .typography( 35 | .body( 36 | color: YumeColor.onSurfaceVariant 37 | ) 38 | ) 39 | } 40 | TextField("", text: $searchText) 41 | .typography( 42 | .body( 43 | color: searchText.isEmpty ? YumeColor.onSurfaceVariant : YumeColor.onSurface 44 | ) 45 | ) 46 | .tint(YumeColor.primary) 47 | .autocorrectionDisabled(true) 48 | } 49 | } 50 | .padding( 51 | EdgeInsets( 52 | top: Space.small, 53 | leading: Space.medium, 54 | bottom: Space.small, 55 | trailing: Space.medium) 56 | ) 57 | .background(YumeColor.surfaceVariant) 58 | .cornerRadius(Shape.small) 59 | } 60 | } 61 | 62 | struct SearchBar_Previews: PreviewProvider { 63 | static var previews: some View { 64 | SearchBar(placeholder: "Search anime", searchText: .constant("")) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/Snackbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Snackbar.swift 3 | // 4 | // 5 | // Created by Bryan on 03/02/23. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct SnackbarModifier: ViewModifier { 12 | @State private var animate = false 13 | @State private var timer: Timer? 14 | 15 | @Binding var show: Bool 16 | @Binding var restart: Bool 17 | let message: String 18 | var withCloseIcon: Bool 19 | 20 | public func body(content: Content) -> some View { 21 | ZStack { 22 | content 23 | 24 | if show { 25 | VStack { 26 | Spacer() 27 | 28 | HStack(spacing: Space.small) { 29 | Text(message) 30 | .typography(.body(color: YumeColor.inverseOnSurface)) 31 | 32 | Spacer() 33 | 34 | if withCloseIcon { 35 | Button { 36 | hideSnackbar() 37 | } label: { 38 | IconView( 39 | icon: Icons.close, 40 | color: YumeColor.inverseOnSurface) 41 | } 42 | } 43 | } 44 | .padding(Space.medium) 45 | .background(YumeColor.inverseSurface) 46 | .cornerRadius(Shape.extraSmall) 47 | } 48 | .padding( 49 | EdgeInsets( 50 | top: Space.small, 51 | leading: Space.medium, 52 | bottom: Space.small, 53 | trailing: Space.medium) 54 | ) 55 | .animation(.easeInOut, value: animate) 56 | .transition(.move(edge: .bottom).combined(with: .opacity)) 57 | .onAppear { 58 | animate = true 59 | startTimer() 60 | } 61 | .onDisappear { 62 | if restart { 63 | restart = false 64 | showSnackbar() 65 | } 66 | } 67 | .onChange(of: restart) { newRestart in 68 | if newRestart { 69 | hideSnackbar() 70 | } 71 | } 72 | .zIndex(100) 73 | } 74 | } 75 | 76 | } 77 | } 78 | 79 | extension SnackbarModifier { 80 | private func startTimer() { 81 | stopTimer() 82 | guard timer == nil else { return } 83 | timer = Timer.scheduledTimer(withTimeInterval: 4.0, repeats: false) { _ in 84 | hideSnackbar() 85 | } 86 | } 87 | 88 | private func stopTimer() { 89 | guard timer != nil else { return } 90 | timer?.invalidate() 91 | timer = nil 92 | } 93 | 94 | private func showSnackbar() { 95 | withAnimation(.easeInOut) { 96 | show = true 97 | } 98 | startTimer() 99 | } 100 | 101 | private func hideSnackbar() { 102 | withAnimation(.easeInOut) { 103 | show = false 104 | } 105 | stopTimer() 106 | } 107 | } 108 | 109 | extension View { 110 | public func snackbar( 111 | message: String, 112 | withCloseIcon: Bool = false, 113 | isShowing: Binding, 114 | restart: Binding) -> some View { 115 | self.modifier(SnackbarModifier( 116 | show: isShowing, 117 | restart: restart, 118 | message: message, 119 | withCloseIcon: withCloseIcon)) 120 | } 121 | } 122 | 123 | private struct Snackbar: View { 124 | @State var showSnackbar = false 125 | @State var restartSnackbar = false 126 | 127 | var body: some View { 128 | VStack { 129 | Button("Show snackbar") { 130 | withAnimation(.easeInOut) { 131 | showSnackbar.toggle() 132 | } 133 | }.buttonStyle(.borderedProminent) 134 | } 135 | .snackbar( 136 | message: "Snackbar", 137 | withCloseIcon: true, 138 | isShowing: $showSnackbar, 139 | restart: $restartSnackbar) 140 | } 141 | } 142 | 143 | struct Snackbar_Previews: PreviewProvider { 144 | static var previews: some View { 145 | Snackbar() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/TabBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBar.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 29/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct TabBar: View { 12 | @Binding var selection: Tab 13 | 14 | public init(selection: Binding) { 15 | self._selection = selection 16 | } 17 | 18 | public var body: some View { 19 | HStack(alignment: .center) { 20 | TabItem( 21 | icon: selection == .home ? Icons.house : Icons.houseOutlined, 22 | label: "home_title", 23 | isActive: selection == .home 24 | ) { 25 | selection = .home 26 | } 27 | 28 | TabItem( 29 | icon: Icons.search, 30 | label: "search_title", 31 | isActive: selection == .search 32 | ) { 33 | selection = .search 34 | } 35 | 36 | TabItem( 37 | icon: selection == .favorite ? Icons.heart : Icons.heartOutlined, 38 | label: "favorite_title", 39 | isActive: selection == .favorite 40 | ) { 41 | selection = .favorite 42 | } 43 | 44 | TabItem( 45 | icon: selection == .profile ? Icons.user : Icons.userOutlined, 46 | label: "profile_title", 47 | isActive: selection == .profile 48 | ) { 49 | selection = .profile 50 | } 51 | } 52 | .frame(height: 56.0) 53 | .padding( 54 | EdgeInsets( 55 | top: Space.tiny, 56 | leading: Space.medium, 57 | bottom: Space.none, 58 | trailing: Space.medium 59 | ) 60 | ) 61 | .background(YumeColor.surface2) 62 | } 63 | } 64 | 65 | public enum Tab { 66 | case home, search, favorite, profile 67 | } 68 | 69 | struct TabBar_Previews: PreviewProvider { 70 | static var previews: some View { 71 | TabBar(selection: .constant(.home)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/TabItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabItem.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 29/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct TabItem: View { 12 | let icon: UIImage 13 | let label: String 14 | let isActive: Bool 15 | let onTap: () -> Void 16 | 17 | public var body: some View { 18 | let color = isActive ? YumeColor.onSurface : YumeColor.onSurfaceVariant 19 | GeometryReader { geo in 20 | VStack(alignment: .center, spacing: Space.tiny) { 21 | IconView(icon: icon, color: color) 22 | Text(label.localized(bundle: .common)) 23 | .typography(.caption(color: color)) 24 | } 25 | .frame(width: geo.size.width, height: geo.size.height) 26 | .onTapGesture { 27 | withAnimation(.easeIn(duration: 0.1)) { 28 | onTap() 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | struct TabItem_Previews: PreviewProvider { 36 | static var previews: some View { 37 | TabItem(icon: Icons.houseOutlined, label: "Home", isActive: true) {} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Modules/Common/Sources/Common/Utils/View/YumeDivider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YumeDivider.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import Core 9 | import SwiftUI 10 | 11 | public struct YumeDivider: View { 12 | 13 | public init() {} 14 | 15 | public var body: some View { 16 | Divider() 17 | .overlay(YumeColor.outlineVariant) 18 | } 19 | } 20 | 21 | struct YumeDivider_Previews: PreviewProvider { 22 | static var previews: some View { 23 | YumeDivider() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Modules/Common/Tests/CommonTests/CommonTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Common 3 | 4 | final class CommonTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Modules/Favorite/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/Favorite/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/Favorite/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Favorite", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "Favorite", 14 | targets: ["Favorite"]) 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 19 | .package(path: "../Anime"), 20 | .package(path: "../Common") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Favorite", 27 | dependencies: [ 28 | .product(name: "Core", package: "Yume-Core-Module"), 29 | "Anime", 30 | "Common" 31 | ]), 32 | .testTarget( 33 | name: "FavoriteTests", 34 | dependencies: ["Favorite"]) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Modules/Favorite/README.md: -------------------------------------------------------------------------------- 1 | # Favorite 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/Favorite/Sources/Favorite/Presentation/View/FavoriteView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteView.swift 3 | // 4 | // 5 | // Created by Bryan on 08/01/23. 6 | // 7 | 8 | import Anime 9 | import Common 10 | import Core 11 | import SwiftUI 12 | 13 | public struct FavoriteView: View { 14 | @ObservedObject var presenter: GetListPresenter< 15 | Int, 16 | AnimeDomainModel, 17 | Interactor< 18 | Int, 19 | [AnimeDomainModel], 20 | GetFavoriteAnimesRepository< 21 | GetFavoriteAnimeLocaleDataSource, 22 | AnimesTransformer>>> 23 | @State var scrollOffset: CGFloat 24 | let detailDestination: ((_ anime: AnimeDomainModel) -> DetailDestination) 25 | 26 | public init( 27 | presenter: GetListPresenter< 28 | Int, 29 | AnimeDomainModel, 30 | Interactor< 31 | Int, 32 | [AnimeDomainModel], 33 | GetFavoriteAnimesRepository< 34 | GetFavoriteAnimeLocaleDataSource, 35 | AnimesTransformer>>>, 36 | scrollOffset: CGFloat = CGFloat.zero, 37 | detailDestination: @escaping (_ anime: AnimeDomainModel) -> DetailDestination) { 38 | self.presenter = presenter 39 | self.scrollOffset = scrollOffset 40 | self.detailDestination = detailDestination 41 | } 42 | 43 | public var body: some View { 44 | ZStack { 45 | if presenter.isLoading { 46 | ProgressIndicator() 47 | .background(YumeColor.background) 48 | } else if presenter.isError { 49 | Text(presenter.errorMessage) 50 | .background(YumeColor.background) 51 | } else if presenter.list.isEmpty { 52 | empty 53 | } else { 54 | content 55 | } 56 | }.onAppear { 57 | presenter.getList(request: nil) 58 | } 59 | } 60 | } 61 | 62 | extension FavoriteView { 63 | var empty: some View { 64 | VStack(alignment: .leading) { 65 | Text("favorite_title".localized(bundle: .common)) 66 | .typography(.largeTitle(weight: .bold)) 67 | CustomEmptyView(label: "no_favorite_anime_label".localized(bundle: .module)) 68 | } 69 | .padding( 70 | EdgeInsets( 71 | top: 40, 72 | leading: Space.medium, 73 | bottom: Space.medium, 74 | trailing: Space.medium) 75 | ) 76 | .background(YumeColor.background) 77 | } 78 | 79 | var content: some View { 80 | ZStack(alignment: .top) { 81 | ObservableScrollView(scrollOffset: $scrollOffset, showsIndicators: false) { _ in 82 | LazyVStack(alignment: .leading, spacing: Space.small) { 83 | Text("favorite_title".localized(bundle: .common)) 84 | .typography(.largeTitle(weight: .bold)) 85 | ForEach(presenter.list) { anime in 86 | NavigationLink(destination: detailDestination(anime)) { 87 | AnimeCardItem(anime: anime) 88 | }.buttonStyle(.plain) 89 | } 90 | }.padding( 91 | EdgeInsets( 92 | top: 40, 93 | leading: Space.medium, 94 | bottom: Space.medium, 95 | trailing: Space.medium) 96 | ) 97 | } 98 | .background(YumeColor.background) 99 | 100 | AppBar(scrollOffset: scrollOffset, label: "favorite_title".localized(bundle: .common)) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Modules/Favorite/Sources/Favorite/Supporting Files/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "no_favorite_anime_label" = "No favorite anime"; 10 | -------------------------------------------------------------------------------- /Modules/Favorite/Sources/Favorite/Supporting Files/Localization/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "no_favorite_anime_label" = "Tidak ada anime favorit"; 10 | -------------------------------------------------------------------------------- /Modules/Favorite/Tests/FavoriteTests/FavoriteTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Favorite 3 | 4 | final class FavoriteTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(Favorite().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modules/Home/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/Home/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/Home/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Home", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "Home", 14 | targets: ["Home"]) 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 19 | .package(path: "../Anime"), 20 | .package(path: "../Common") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Home", 27 | dependencies: [ 28 | .product(name: "Core", package: "Yume-Core-Module"), 29 | "Anime", 30 | "Common" 31 | ]), 32 | .testTarget( 33 | name: "HomeTests", 34 | dependencies: ["Home"]) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Modules/Home/README.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/Home/Sources/Home/Supporting Files/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "see_all_label" = "See All"; 10 | -------------------------------------------------------------------------------- /Modules/Home/Sources/Home/Supporting Files/Localization/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "see_all_label" = "Lihat Semua"; 10 | -------------------------------------------------------------------------------- /Modules/Home/Tests/HomeTests/HomeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Home 3 | 4 | final class HomeTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(Home().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modules/Profile/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/Profile/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/Profile/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Profile", 8 | platforms: [.iOS(.v16)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "Profile", 13 | targets: ["Profile"]) 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 18 | .package(path: "Common") 19 | ], 20 | targets: [ 21 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 22 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 23 | .target( 24 | name: "Profile", 25 | dependencies: [ 26 | .product(name: "Core", package: "Yume-Core-Module"), 27 | "Common" 28 | ]), 29 | .testTarget( 30 | name: "ProfileTests", 31 | dependencies: ["Profile"]) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Modules/Profile/README.md: -------------------------------------------------------------------------------- 1 | # Profile 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/Profile/Sources/Profile/Presentation/View/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // 4 | // 5 | // Created by Bryan on 08/01/23. 6 | // 7 | 8 | import Common 9 | import Core 10 | import SwiftUI 11 | 12 | public struct ProfileView: View { 13 | @State var scrollOffset: CGFloat 14 | 15 | public init(scrollOffset: CGFloat = CGFloat.zero) { 16 | self.scrollOffset = scrollOffset 17 | } 18 | 19 | public var body: some View { 20 | ZStack(alignment: .top) { 21 | ObservableScrollView(scrollOffset: $scrollOffset, showsIndicators: false) { _ in 22 | VStack(spacing: Space.large) { 23 | profile 24 | } 25 | .padding( 26 | EdgeInsets( 27 | top: 0, 28 | leading: Space.none, 29 | bottom: Space.medium, 30 | trailing: Space.none) 31 | ) 32 | }.background(YumeColor.background) 33 | 34 | AppBar(scrollOffset: scrollOffset, label: "profile_title".localized(bundle: .common)) 35 | } 36 | } 37 | } 38 | 39 | extension ProfileView { 40 | var profile: some View { 41 | VStack(spacing: Space.none) { 42 | profileBackground 43 | HStack { 44 | VStack(alignment: .leading, spacing: Space.small) { 45 | profilePicture 46 | 47 | VStack(alignment: .leading, spacing: Space.tiny) { 48 | Text("Bryan") 49 | .typography(.title2(weight: .bold)) 50 | Text(verbatim: "bryan001@student.ciputra.ac.id") 51 | .typography(.caption(color: YumeColor.onSurfaceVariant)) 52 | } } 53 | Spacer() 54 | } 55 | .padding(.horizontal, 16) 56 | } 57 | } 58 | 59 | var profileBackground: some View { 60 | Rectangle() 61 | .fill(Formatter.rgbToColor(red: 201, green: 203, blue: 202)) 62 | .frame(height: 100) 63 | } 64 | 65 | var profilePicture: some View { 66 | CircleImage(image: Image("ProfilePicture", bundle: .module)) 67 | .frame(width: 80, height: 80) 68 | .overlay { 69 | Circle().stroke(.white, lineWidth: 2) 70 | } 71 | .offset(y: -40) 72 | .padding(.bottom, -40) 73 | } 74 | } 75 | 76 | struct ProfileView_Previews: PreviewProvider { 77 | static var previews: some View { 78 | ProfileView() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "profile.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "profile@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "profile@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/profile.png -------------------------------------------------------------------------------- /Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/profile@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/profile@2x.png -------------------------------------------------------------------------------- /Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/profile@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Modules/Profile/Sources/Profile/Supporting Files/Assets.xcassets/ProfilePicture.imageset/profile@3x.png -------------------------------------------------------------------------------- /Modules/Profile/Tests/ProfileTests/ProfileTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Profile 3 | 4 | final class ProfileTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(Profile().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modules/Search/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/Search/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/Search/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Search", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "Search", 14 | targets: ["Search"]) 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 19 | .package(path: "../Anime"), 20 | .package(path: "../Common") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "Search", 27 | dependencies: [ 28 | .product(name: "Core", package: "Yume-Core-Module"), 29 | "Anime", 30 | "Common" 31 | ]), 32 | .testTarget( 33 | name: "SearchTests", 34 | dependencies: ["Search"]) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Modules/Search/README.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/Search/Sources/Search/Supporting Files/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "searching_anime_label" = "Searching anime"; 10 | "search_placeholder" = "Search anime"; 11 | -------------------------------------------------------------------------------- /Modules/Search/Sources/Search/Supporting Files/Localization/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | 9 | "searching_anime_label" = "Mencari anime"; 10 | "search_placeholder" = "Cari anime"; 11 | -------------------------------------------------------------------------------- /Modules/Search/Tests/SearchTests/SearchTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Search 3 | 4 | final class SearchTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(Search().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Modules/SeeAllAnime/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Modules/SeeAllAnime/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Modules/SeeAllAnime/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SeeAllAnime", 8 | platforms: [.iOS(.v16)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "SeeAllAnime", 13 | targets: ["SeeAllAnime"]) 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | .package(url: "https://github.com/bryanless/Yume-Core-Module.git", .upToNextMajor(from: "1.0.0")), 18 | .package(path: "../Anime"), 19 | .package(path: "../Common") 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "SeeAllAnime", 26 | dependencies: [ 27 | .product(name: "Core", package: "Yume-Core-Module"), 28 | "Anime", 29 | "Common" 30 | ]), 31 | .testTarget( 32 | name: "SeeAllAnimeTests", 33 | dependencies: ["SeeAllAnime"]) 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Modules/SeeAllAnime/README.md: -------------------------------------------------------------------------------- 1 | # SeeAllAnime 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Modules/SeeAllAnime/Sources/SeeAllAnime/Presentation/View/SeeAllAnimeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeeAllAnimeView.swift 3 | // 4 | // 5 | // Created by Bryan on 08/01/23. 6 | // 7 | 8 | import Anime 9 | import Common 10 | import Core 11 | import SwiftUI 12 | 13 | public struct SeeAllAnimeView: View { 14 | 15 | @ObservedObject var presenter: GetListPresenter< 16 | AnimeRankingRequest, 17 | AnimeDomainModel, 18 | Interactor< 19 | AnimeRankingRequest, 20 | [AnimeDomainModel], 21 | GetAnimeRankingRepository< 22 | GetAnimeRankingLocaleDataSource, 23 | GetAnimeRankingRemoteDataSource, 24 | AnimesTransformer>>> 25 | @State var scrollOffset: CGFloat 26 | let rankingType: RankingTypeRequest 27 | let navigationTitle: String 28 | let detailDestination: ((_ anime: AnimeDomainModel) -> DetailDestination) 29 | 30 | public init( 31 | presenter: GetListPresenter< 32 | AnimeRankingRequest, 33 | AnimeDomainModel, 34 | Interactor< 35 | AnimeRankingRequest, 36 | [AnimeDomainModel], 37 | GetAnimeRankingRepository< 38 | GetAnimeRankingLocaleDataSource, 39 | GetAnimeRankingRemoteDataSource, 40 | AnimesTransformer>>>, 41 | scrollOffset: CGFloat = CGFloat.zero, 42 | rankingType: RankingTypeRequest, 43 | navigationTitle: String, 44 | detailDestination: @escaping (_ anime: AnimeDomainModel) -> DetailDestination 45 | ) { 46 | self.presenter = presenter 47 | self.scrollOffset = scrollOffset 48 | self.rankingType = rankingType 49 | self.navigationTitle = navigationTitle 50 | self.detailDestination = detailDestination 51 | } 52 | 53 | public var body: some View { 54 | ZStack { 55 | if presenter.isLoading { 56 | ProgressIndicator() 57 | .background(YumeColor.background) 58 | } else if presenter.isError { 59 | CustomEmptyView(label: presenter.errorMessage) 60 | } else { 61 | content 62 | } 63 | } 64 | .toolbar(.hidden) 65 | .onAppear { 66 | if presenter.list.isEmpty { 67 | presenter.getList(request: AnimeRankingRequest(type: rankingType)) 68 | } 69 | } 70 | } 71 | } 72 | 73 | extension SeeAllAnimeView { 74 | var content: some View { 75 | ZStack(alignment: .top) { 76 | ObservableScrollView(scrollOffset: $scrollOffset, showsIndicators: false) { _ in 77 | LazyVStack(spacing: Space.small) { 78 | ForEach(presenter.list.prefix(20)) { anime in 79 | NavigationLink(destination: detailDestination(anime)) { 80 | AnimeCardItem(anime: anime) 81 | }.buttonStyle(.plain) 82 | } 83 | } 84 | .padding( 85 | EdgeInsets( 86 | top: 56, 87 | leading: Space.medium, 88 | bottom: Space.medium, 89 | trailing: Space.medium) 90 | ) 91 | }.background(YumeColor.background) 92 | 93 | BackAppBar(scrollOffset: scrollOffset, label: navigationTitle.localized(bundle: .common), alwaysShowLabel: true) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Modules/SeeAllAnime/Tests/SeeAllAnimeTests/SeeAllAnimeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SeeAllAnime 3 | 4 | final class SeeAllAnimeTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(SeeAllAnime().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Yume](readme/feature-graphic.jpg "Yume") 2 | 3 | # Yume 4 | Discover anime anytime, anywhere 5 | 6 | Get to know various information about anime such as season, score, rank, adaptation source, synopsis, and more. Find any anime by title using either English or Rōmaji. Create your own favorite list for easy access. 7 | 8 | Yume is available in English and Bahasa Indonesia. 9 | > Support for Bahasa Indonesia is limited to some features and can only be used by changing device's system language 10 | 11 | ## Features 12 |

13 | 14 | 15 | 16 | 17 | 18 |

19 | 20 | ### Home 21 | - Top airing anime 22 | - Top upcoming anime 23 | - Most popular anime 24 | - Top rated anime 25 | 26 | ### Search 27 | - Top favorite anime 28 | - Search anime by title using either English or Rōmaji 29 | 30 | ### Favorite 31 | - List of anime added to favorite 32 | 33 | ### Profile 34 | 35 | ## Dependency Diagram 36 | ![Yume Dependency Diagram](readme/dependency-diagram.png "Yume Dependency Diagram") 37 | 38 | ## Getting Started 39 | ### Prerequisites 40 | #### OS & Software 41 | > Requirements might be lower, the app is developed using the system listed below 42 | * macOS Ventura 13.1 43 | * Xcode 14.2 44 | * iOS 16.2 45 | 46 | #### App 47 | * Client ID (API token) from [MyAnimeList](https://myanimelist.net/apiconfig) 48 | 49 | ### Installation 50 | 1. Download the repository 51 | 2. Open the project by using Xcode 52 | 3. Build the project and a `Keys.plist` file should be created automatically at `Yume/Supporting Files/` 53 | > If it isn't created automatically, copy `Keys-Example.plist` at `Yume/Supporting Files/` and paste it as `Keys.plist` at `Yume/Supporting Files/` 54 | 4. Replace the value of key `API_KEY` with your Client ID (API token) at Keys.plist 55 | 56 | ## License 57 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/bryanless/Yume-Swift/blob/main/LICENSE) file for details 58 | 59 | ## Acknowledgments 60 | * [MyAnimeList API](https://myanimelist.net/apiconfig/references/api/v2) by [MyAnimeList](https://myanimelist.net) 61 | -------------------------------------------------------------------------------- /Yume.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Yume.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Yume.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "alamofire", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Alamofire/Alamofire", 7 | "state" : { 8 | "revision" : "78424be314842833c04bc3bef5b72e85fff99204", 9 | "version" : "5.6.4" 10 | } 11 | }, 12 | { 13 | "identity" : "realm-cocoa", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/realm/realm-cocoa", 16 | "state" : { 17 | "revision" : "b1072f3ef733108dfcc62c2001781e5d65605fbf", 18 | "version" : "10.34.1" 19 | } 20 | }, 21 | { 22 | "identity" : "realm-core", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/realm/realm-core.git", 25 | "state" : { 26 | "revision" : "b77443ca7fa25407869ca537bf3ae912b1427bff", 27 | "version" : "12.13.0" 28 | } 29 | }, 30 | { 31 | "identity" : "sdwebimage", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/SDWebImage/SDWebImage.git", 34 | "state" : { 35 | "revision" : "6c6b951845a520fa7e8356e28adb5339c0f008d3", 36 | "version" : "5.15.0" 37 | } 38 | }, 39 | { 40 | "identity" : "sdwebimageswiftui", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/SDWebImage/SDWebImageSwiftUI", 43 | "state" : { 44 | "revision" : "61fefe9c284fd41ddef77d02749e88f00c305196", 45 | "version" : "2.2.2" 46 | } 47 | }, 48 | { 49 | "identity" : "yume-core-module", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/bryanless/Yume-Core-Module", 52 | "state" : { 53 | "revision" : "2f56655a87143cc20add17c293f7503c0a3d6728", 54 | "version" : "1.3.6" 55 | } 56 | } 57 | ], 58 | "version" : 2 59 | } 60 | -------------------------------------------------------------------------------- /Yume.xcodeproj/xcshareddata/xcschemes/Yume - ID.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Yume.xcodeproj/xcshareddata/xcschemes/Yume.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 45 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 82 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Yume/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/.DS_Store -------------------------------------------------------------------------------- /Yume/App/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 25/12/22. 6 | // 7 | 8 | import Anime 9 | import AnimeDetail 10 | import Core 11 | import Common 12 | import Favorite 13 | import Home 14 | import Profile 15 | import Search 16 | import SeeAllAnime 17 | import SwiftUI 18 | 19 | struct ContentView: View { 20 | @EnvironmentObject var homePresenter: HomePresenter< 21 | Interactor< 22 | AnimeRankingRequest, 23 | [AnimeDomainModel], 24 | GetAnimeRankingRepository< 25 | GetAnimeRankingLocaleDataSource, 26 | GetAnimeRankingRemoteDataSource, 27 | AnimesTransformer>>, 28 | Interactor< 29 | AnimeRankingRequest, 30 | [AnimeDomainModel], 31 | GetAnimeRankingRepository< 32 | GetAnimeRankingLocaleDataSource, 33 | GetAnimeRankingRemoteDataSource, 34 | AnimesTransformer>>, 35 | Interactor< 36 | AnimeRankingRequest, 37 | [AnimeDomainModel], 38 | GetAnimeRankingRepository< 39 | GetAnimeRankingLocaleDataSource, 40 | GetAnimeRankingRemoteDataSource, 41 | AnimesTransformer>>, 42 | Interactor< 43 | AnimeRankingRequest, 44 | [AnimeDomainModel], 45 | GetAnimeRankingRepository< 46 | GetAnimeRankingLocaleDataSource, 47 | GetAnimeRankingRemoteDataSource, 48 | AnimesTransformer>>> 49 | @EnvironmentObject var searchPresenter: SearchPresenter< 50 | Interactor< 51 | AnimeListRequest, 52 | [AnimeDomainModel], 53 | SearchAnimeRepository< 54 | GetAnimeListLocaleDataSource, 55 | GetAnimeListRemoteDataSource, 56 | AnimesTransformer>>, 57 | Interactor< 58 | AnimeRankingRequest, 59 | [AnimeDomainModel], 60 | GetAnimeRankingRepository< 61 | GetAnimeRankingLocaleDataSource, 62 | GetAnimeRankingRemoteDataSource, 63 | AnimesTransformer>>> 64 | @EnvironmentObject var favoritePresenter: GetListPresenter< 65 | Int, 66 | AnimeDomainModel, 67 | Interactor< 68 | Int, 69 | [AnimeDomainModel], 70 | GetFavoriteAnimesRepository< 71 | GetFavoriteAnimeLocaleDataSource, 72 | AnimesTransformer>>> 73 | @State private var selection: Tab = .home 74 | 75 | init() { 76 | NetworkMonitor.shared.startMonitoring() 77 | 78 | UITabBar.appearance().isHidden = true 79 | } 80 | 81 | var body: some View { 82 | VStack(spacing: 0) { 83 | TabView(selection: $selection) { 84 | NavigationStack { 85 | HomeView< 86 | SeeAllAnimeView, 87 | AnimeDetailView>(presenter: homePresenter) { rankingType in 88 | Router().makeSeeAllAnimeView(for: rankingType) { anime in 89 | Router().makeAnimeDetailView(for: anime) 90 | } 91 | } detailDestination: { anime in 92 | Router().makeAnimeDetailView(for: anime) 93 | } 94 | }.tag(Tab.home) 95 | NavigationStack { 96 | SearchView(presenter: searchPresenter) { anime in 97 | Router().makeAnimeDetailView(for: anime) 98 | } 99 | }.tag(Tab.search) 100 | NavigationStack { 101 | FavoriteView(presenter: favoritePresenter) { anime in 102 | Router().makeAnimeDetailView(for: anime) 103 | } 104 | }.tag(Tab.favorite) 105 | NavigationStack { 106 | ProfileView() 107 | }.tag(Tab.profile) 108 | } 109 | TabBar(selection: $selection) 110 | }.ignoresSafeArea(.keyboard) 111 | } 112 | 113 | } 114 | 115 | struct ContentView_Previews: PreviewProvider { 116 | static var previews: some View { 117 | ContentView() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Yume/App/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 08/01/23. 6 | // 7 | 8 | import Anime 9 | import AnimeDetail 10 | import Core 11 | import SeeAllAnime 12 | import SwiftUI 13 | 14 | class Router { 15 | func makeAnimeDetailView(for anime: AnimeDomainModel) -> AnimeDetailView { 16 | 17 | let animeUseCase: Interactor< 18 | AnimeRequest, 19 | AnimeDomainModel, 20 | GetAnimeRepository< 21 | GetAnimeLocaleDataSource, 22 | GetAnimeRemoteDataSource, 23 | AnimeTransformer>> = Injection.init().provideAnime() 24 | 25 | let favoriteUseCase: Interactor< 26 | Int, 27 | AnimeDomainModel, 28 | UpdateFavoriteAnimeRepository< 29 | GetFavoriteAnimeLocaleDataSource, 30 | AnimeDataTransformer>> = Injection.init().provideUpdateFavoriteAnime() 31 | 32 | let presenter = AnimePresenter(animeUseCase: animeUseCase, favoriteUseCase: favoriteUseCase) 33 | 34 | return AnimeDetailView(presenter: presenter, anime: anime) 35 | 36 | } 37 | 38 | func makeSeeAllAnimeView( 39 | for rankingType: RankingTypeRequest, 40 | detailDestination: @escaping ((_ anime: AnimeDomainModel) -> AnimeDetailView)) -> SeeAllAnimeView { 41 | var navigationTitle: String 42 | 43 | switch rankingType { 44 | case .airing: 45 | navigationTitle = "now_airing_title" 46 | case .upcoming: 47 | navigationTitle = "upcoming_title" 48 | case .byPopularity: 49 | navigationTitle = "most_popular_title" 50 | default: 51 | // All 52 | navigationTitle = "top_rated_title" 53 | } 54 | 55 | let seeAllAnimeUseCase: Interactor< 56 | AnimeRankingRequest, 57 | [AnimeDomainModel], 58 | GetAnimeRankingRepository< 59 | GetAnimeRankingLocaleDataSource, 60 | GetAnimeRankingRemoteDataSource, 61 | AnimesTransformer>> = Injection.init().provideAnimeRanking() 62 | 63 | let presenter = GetListPresenter(useCase: seeAllAnimeUseCase) 64 | 65 | return SeeAllAnimeView( 66 | presenter: presenter, 67 | rankingType: rankingType, 68 | navigationTitle: navigationTitle, 69 | detailDestination: detailDestination 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Yume/App/YumeApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YumeApp.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 25/12/22. 6 | // 7 | 8 | import Anime 9 | import Core 10 | import Home 11 | import Search 12 | import SwiftUI 13 | 14 | let injection = Injection.init() 15 | 16 | // Home 17 | let topAiringAnimeUseCase: Interactor< 18 | AnimeRankingRequest, 19 | [AnimeDomainModel], 20 | GetAnimeRankingRepository< 21 | GetAnimeRankingLocaleDataSource, 22 | GetAnimeRankingRemoteDataSource, 23 | AnimesTransformer>> = injection.provideAnimeRanking() 24 | let topUpcomingAnimeUseCase: Interactor< 25 | AnimeRankingRequest, 26 | [AnimeDomainModel], 27 | GetAnimeRankingRepository< 28 | GetAnimeRankingLocaleDataSource, 29 | GetAnimeRankingRemoteDataSource, 30 | AnimesTransformer>> = injection.provideAnimeRanking() 31 | let popularAnimeUseCase: Interactor< 32 | AnimeRankingRequest, 33 | [AnimeDomainModel], 34 | GetAnimeRankingRepository< 35 | GetAnimeRankingLocaleDataSource, 36 | GetAnimeRankingRemoteDataSource, 37 | AnimesTransformer>> = injection.provideAnimeRanking() 38 | let topAllAnimeUseCase: Interactor< 39 | AnimeRankingRequest, 40 | [AnimeDomainModel], 41 | GetAnimeRankingRepository< 42 | GetAnimeRankingLocaleDataSource, 43 | GetAnimeRankingRemoteDataSource, 44 | AnimesTransformer>> = injection.provideAnimeRanking() 45 | 46 | // Search 47 | let searchAnimeUseCase: Interactor< 48 | AnimeListRequest, 49 | [AnimeDomainModel], 50 | SearchAnimeRepository< 51 | GetAnimeListLocaleDataSource, 52 | GetAnimeListRemoteDataSource, 53 | AnimesTransformer>> = injection.provideSearchAnime() 54 | let topFavoriteAnimeUseCase: Interactor< 55 | AnimeRankingRequest, 56 | [AnimeDomainModel], 57 | GetAnimeRankingRepository< 58 | GetAnimeRankingLocaleDataSource, 59 | GetAnimeRankingRemoteDataSource, 60 | AnimesTransformer>> = injection.provideAnimeRanking() 61 | 62 | // Favorite 63 | let favoriteAnimeUseCase: Interactor< 64 | Int, 65 | [AnimeDomainModel], 66 | GetFavoriteAnimesRepository< 67 | GetFavoriteAnimeLocaleDataSource, 68 | AnimesTransformer>> = injection.provideFavoriteAnime() 69 | 70 | @main 71 | struct YumeApp: App { 72 | 73 | let homePresenter = HomePresenter( 74 | topAiringAnimeUseCase: topAiringAnimeUseCase, 75 | topUpcomingAnimeUseCase: topUpcomingAnimeUseCase, 76 | popularAnimeUseCase: popularAnimeUseCase, 77 | topAllAnimeUseCase: topAllAnimeUseCase) 78 | let searchPresenter = SearchPresenter( 79 | searchAnimeUseCase: searchAnimeUseCase, 80 | topFavoriteAnimeUseCase: topFavoriteAnimeUseCase) 81 | let favoritePresenter = GetListPresenter(useCase: favoriteAnimeUseCase) 82 | 83 | var body: some Scene { 84 | WindowGroup { 85 | ContentView() 86 | .environmentObject(homePresenter) 87 | .environmentObject(searchPresenter) 88 | .environmentObject(favoritePresenter) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Yume/Core/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Core/.DS_Store -------------------------------------------------------------------------------- /Yume/Core/DI/Injection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Injection.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | import Anime 9 | import Core 10 | 11 | import Foundation 12 | import RealmSwift 13 | 14 | final class Injection: NSObject { 15 | 16 | private let realm = try? Realm() 17 | 18 | func provideAnimeRanking() -> U 19 | where 20 | U.Request == AnimeRankingRequest, 21 | U.Response == [AnimeDomainModel] { 22 | let locale = GetAnimeRankingLocaleDataSource(realm: realm!) 23 | 24 | let remote = GetAnimeRankingRemoteDataSource( 25 | endpoint: Endpoints.Gets.ranking.url, 26 | encoder: API.encoder, 27 | headers: API.headers 28 | ) 29 | 30 | let mapper = AnimesTransformer() 31 | 32 | let repository = GetAnimeRankingRepository( 33 | localeDataSource: locale, 34 | remoteDataSource: remote, 35 | mapper: mapper) 36 | 37 | return Interactor(repository: repository) as! U 38 | } 39 | 40 | func provideSearchAnime() -> U 41 | where 42 | U.Request == AnimeListRequest, 43 | U.Response == [AnimeDomainModel] { 44 | let locale = GetAnimeListLocaleDataSource(realm: realm!) 45 | 46 | let remote = GetAnimeListRemoteDataSource( 47 | endpoint: Endpoints.Gets.search.url, 48 | encoder: API.encoder, 49 | headers: API.headers 50 | ) 51 | 52 | let mapper = AnimesTransformer() 53 | 54 | let repository = SearchAnimeRepository( 55 | localeDataSource: locale, 56 | remoteDataSource: remote, 57 | mapper: mapper) 58 | 59 | return Interactor(repository: repository) as! U 60 | } 61 | 62 | func provideAnime() -> U 63 | where 64 | U.Request == AnimeRequest, 65 | U.Response == AnimeDomainModel { 66 | let locale = GetAnimeLocaleDataSource(realm: realm!) 67 | 68 | let remote = GetAnimeRemoteDataSource( 69 | endpoint: Endpoints.Gets.detail.url, 70 | encoder: API.encoder, 71 | headers: API.headers 72 | ) 73 | 74 | let mapper = AnimeTransformer() 75 | 76 | let repository = GetAnimeRepository( 77 | localeDataSource: locale, 78 | remoteDataSource: remote, 79 | mapper: mapper 80 | ) 81 | 82 | return Interactor(repository: repository) as! U 83 | } 84 | 85 | func provideUpdateFavoriteAnime() -> U 86 | where 87 | U.Request == Int, 88 | U.Response == AnimeDomainModel { 89 | let locale = GetFavoriteAnimeLocaleDataSource(realm: realm!) 90 | 91 | let mapper = AnimeDataTransformer() 92 | 93 | let repository = UpdateFavoriteAnimeRepository( 94 | localeDataSource: locale, 95 | mapper: mapper 96 | ) 97 | 98 | return Interactor(repository: repository) as! U 99 | } 100 | 101 | func provideFavoriteAnime() -> U 102 | where 103 | U.Request == Int, 104 | U.Response == [AnimeDomainModel] { 105 | let locale = GetFavoriteAnimeLocaleDataSource(realm: realm!) 106 | 107 | let mapper = AnimesTransformer() 108 | 109 | let repository = GetFavoriteAnimesRepository( 110 | localeDataSource: locale, 111 | mapper: mapper 112 | ) 113 | 114 | return Interactor(repository: repository) as! U 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /Yume/Core/Utils/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Core/Utils/.DS_Store -------------------------------------------------------------------------------- /Yume/Core/Utils/Extensions/File+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File+Ext.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 06/02/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct File { 11 | static func loadFile( 12 | forResource resource: String, 13 | ofType type: String) -> String { 14 | guard let filePath = Bundle.main.path(forResource: resource, ofType: type) else { 15 | fatalError("Couldn't find file '\(resource).\(type)'.") 16 | } 17 | 18 | return filePath 19 | } 20 | 21 | static func loadDictionaryKey( 22 | forKey key: String, 23 | forResource resource: String, 24 | ofType type: String) -> Any { 25 | let filePath = loadFile(forResource: resource, ofType: type) 26 | 27 | let plist = NSDictionary(contentsOfFile: filePath) 28 | guard let value = plist?.object(forKey: "API_KEY") else { 29 | fatalError("Couldn't find key '\(key)' in '\(resource).\(type)'.") 30 | } 31 | 32 | return value 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Yume/Core/Utils/Extensions/UINavigationController+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+Ext.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 30/12/22. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UINavigationController { 11 | open override func viewDidLoad() { 12 | super.viewDidLoad() 13 | interactivePopGestureRecognizer?.delegate = nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Yume/Core/Utils/Network/APICall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APICall.swift 3 | // Yume 4 | // 5 | // Created by Bryan on 28/12/22. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | 11 | struct API { 12 | 13 | static let baseUrl = "https://api.myanimelist.net/v2/" 14 | static let encoder = URLEncodedFormParameterEncoder(encoder: URLEncodedFormEncoder(keyEncoding: .convertToSnakeCase)) 15 | static var headers: HTTPHeaders { 16 | guard let apiKey = File.loadDictionaryKey(forKey: "API_KEY", forResource: "Keys", ofType: "plist") as? String else { 17 | fatalError("Value of 'API_KEY' in 'Keys.plist' is not a String") 18 | } 19 | 20 | if apiKey.starts(with: "_") { 21 | debugPrint("Create a MyAnimeList account and get a Client ID (API key) at https://myanimelist.net/apiconfig.") 22 | } 23 | 24 | return ["X-MAL-CLIENT-ID": apiKey] 25 | } 26 | 27 | } 28 | 29 | protocol Endpoint { 30 | 31 | var url: String { get } 32 | 33 | } 34 | 35 | enum Endpoints { 36 | 37 | enum Gets: Endpoint { 38 | case detail 39 | case ranking 40 | case search 41 | 42 | public var url: String { 43 | switch self { 44 | case .detail: return "\(API.baseUrl)anime" 45 | case .ranking: return "\(API.baseUrl)anime/ranking" 46 | case .search: return "\(API.baseUrl)anime" 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Yume/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Yume/Preview Content/PreviewData copy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewData.swift 3 | // Rawg 4 | // 5 | // Created by Bryan on 21/10/22. 6 | // 7 | 8 | import Foundation 9 | 10 | class PreviewData { 11 | static func load(_ file: String) -> T { 12 | guard let path = Bundle.main.path(forResource: file, ofType: "json") else { 13 | fatalError("Failed to locate \(file) in bundle.") 14 | } 15 | 16 | guard let data = try? Data(contentsOf: URL(filePath: path)) else { 17 | fatalError("Failed to load \(file) from bundle.") 18 | } 19 | 20 | let decoder = JSONDecoder() 21 | 22 | guard let result = try? decoder.decode(T.self, from: data) else { 23 | fatalError("Failed to decode \(file) from bundle.") 24 | } 25 | 26 | return result as T 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Yume/Supporting Files/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/.DS_Store -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /Yume/Supporting Files/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 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/66.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Assets.xcassets/AppIcon.appiconset/92.png -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFB", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorInverseOnSurface.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF4", 9 | "green" : "0xEF", 10 | "red" : "0xF3" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorInversePrimary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xC1", 10 | "red" : "0xC1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorInverseSurface.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x34", 9 | "green" : "0x30", 10 | "red" : "0x31" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOnBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1F", 9 | "green" : "0x1B", 10 | "red" : "0x1C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOnPrimary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOnSecondaryContainer.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x2C", 9 | "green" : "0x1A", 10 | "red" : "0x1A" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOnSurface.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x1F", 9 | "green" : "0x1B", 10 | "red" : "0x1C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOnSurfaceVariant.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4F", 9 | "green" : "0x46", 10 | "red" : "0x47" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOutline.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x80", 9 | "green" : "0x76", 10 | "red" : "0x77" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorOutlineVariant.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD0", 9 | "green" : "0xC5", 10 | "red" : "0xC8" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorPrimary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xC2", 9 | "green" : "0x50", 10 | "red" : "0x4F" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorSecondaryContainer.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xF9", 9 | "green" : "0xE0", 10 | "red" : "0xE2" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorSurface.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFB", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorSurface2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFA", 9 | "green" : "0xED", 10 | "red" : "0xF1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/ColorSurfaceVariant.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEC", 9 | "green" : "0xE1", 10 | "red" : "0xE4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-Black.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-BlackItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-Bold.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-BoldItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-ExtraBold.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-ExtraLight.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-Italic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-Light.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-LightItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-Medium.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-MediumItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-Regular.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-SemiBold.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Font/Nunito-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/Yume/Supporting Files/Font/Nunito-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /Yume/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIAppFonts 6 | 7 | Nunito-Regular.ttf 8 | Nunito-SemiBold.ttf 9 | 10 | UIApplicationSceneManifest 11 | 12 | UIApplicationSupportsMultipleScenes 13 | 14 | UISceneConfigurations 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Yume/Supporting Files/Keys-Example.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_KEY 6 | _MyAnimeList_Client_ID 7 | 8 | 9 | -------------------------------------------------------------------------------- /Yume/Supporting Files/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Yume 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | -------------------------------------------------------------------------------- /Yume/Supporting Files/id.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | Yume 4 | 5 | Created by Bryan on 09/01/23. 6 | 7 | */ 8 | -------------------------------------------------------------------------------- /YumeTests/YumeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YumeTests.swift 3 | // YumeTests 4 | // 5 | // Created by Bryan on 25/12/22. 6 | // 7 | 8 | import XCTest 9 | @testable import Yume 10 | 11 | final class YumeTests: 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 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. 27 | // 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 | -------------------------------------------------------------------------------- /YumeUITests/YumeUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YumeUITests.swift 3 | // YumeUITests 4 | // 5 | // Created by Bryan on 25/12/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class YumeUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required 19 | // 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 | -------------------------------------------------------------------------------- /YumeUITests/YumeUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // YumeUITestsLaunchTests.swift 3 | // YumeUITests 4 | // 5 | // Created by Bryan on 25/12/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class YumeUITestsLaunchTests: 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 | -------------------------------------------------------------------------------- /codemagic.yaml: -------------------------------------------------------------------------------- 1 | workflows: 2 | ios-project-debug: # workflow ID 3 | name: iOS debug # workflow name 4 | environment: 5 | xcode: latest 6 | cocoapods: default 7 | vars: 8 | XCODE_PROJECT: "Yume.xcodeproj" # Project name 9 | XCODE_SCHEME: "Yume" # Scheme name 10 | scripts: 11 | - name: Run tests 12 | script: | 13 | xcodebuild \ 14 | -project "$XCODE_PROJECT" \ 15 | -scheme "$XCODE_SCHEME" \ 16 | -sdk iphonesimulator \ 17 | -destination 'platform=iOS Simulator,name=iPhone 14 Pro,OS=16.4' \ 18 | clean build test CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO 19 | - name: Build debug app 20 | script: | 21 | xcodebuild build -project "$XCODE_PROJECT" \ 22 | -scheme "$XCODE_SCHEME" \ 23 | CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO 24 | artifacts: 25 | - $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.app 26 | publishing: 27 | email: 28 | recipients: 29 | - bryanmuliawan@yahoo.co.id # Notification email 30 | -------------------------------------------------------------------------------- /readme/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/.DS_Store -------------------------------------------------------------------------------- /readme/dependency-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/dependency-diagram.png -------------------------------------------------------------------------------- /readme/feature-graphic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/feature-graphic.jpg -------------------------------------------------------------------------------- /readme/screen-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/screen-1.jpg -------------------------------------------------------------------------------- /readme/screen-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/screen-2.jpg -------------------------------------------------------------------------------- /readme/screen-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/screen-3.jpg -------------------------------------------------------------------------------- /readme/screen-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/screen-4.jpg -------------------------------------------------------------------------------- /readme/screen-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryanless/Yume-Swift/fdce1064d8107a8f1001d223492d5c7f367145e1/readme/screen-5.jpg --------------------------------------------------------------------------------