├── Packages ├── KinoPubUI │ ├── README.md │ ├── Sources │ │ └── KinoPubUI │ │ │ ├── KinoPubUI.swift │ │ │ ├── Media.xcassets │ │ │ ├── Contents.json │ │ │ ├── Colors │ │ │ │ ├── Contents.json │ │ │ │ ├── text_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── accent_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── accent_red_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── skeleton_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── accent_blue_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── background_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── dark_background_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── subtitle_text_color.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── selection_background_color.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── imdb.imageset │ │ │ │ ├── imdb@2x.png │ │ │ │ └── Contents.json │ │ │ └── kinopoisk.imageset │ │ │ │ ├── kinopoisk@2x.png │ │ │ │ └── Contents.json │ │ │ ├── Extensions │ │ │ ├── Double+Formatting.swift │ │ │ └── View+Skeleton.swift │ │ │ ├── Components │ │ │ ├── Image │ │ │ │ └── Image+CenterCropped.swift │ │ │ ├── Button │ │ │ │ ├── KinoPubButtonTextStyle.swift │ │ │ │ ├── KinoPubButtonStyle.swift │ │ │ │ ├── KinoPubButton.swift │ │ │ │ └── ProgressButton.swift │ │ │ ├── Toast │ │ │ │ └── ToastContentView.swift │ │ │ └── Content │ │ │ │ ├── ContentItemRatingView.swift │ │ │ │ ├── Modifiers │ │ │ │ └── PosterStyle.swift │ │ │ │ └── ContentItemView.swift │ │ │ ├── Font │ │ │ └── Font+Extension.swift │ │ │ └── Colors │ │ │ └── Colors+Extension.swift │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Tests │ │ └── KinoPubUITests │ │ │ └── KinoPubUITests.swift │ └── Package.swift ├── KinoPubBackend │ ├── README.md │ ├── Sources │ │ └── KinoPubBackend │ │ │ ├── KinoPubBackend.swift │ │ │ ├── Models │ │ │ ├── SeasonWatching.swift │ │ │ ├── EpisodeWatching.swift │ │ │ ├── Pagination.swift │ │ │ ├── Trailer.swift │ │ │ ├── Author.swift │ │ │ ├── MediaGenre.swift │ │ │ ├── Posters.swift │ │ │ ├── Subtitle.swift │ │ │ ├── URLInfo.swift │ │ │ ├── Country.swift │ │ │ ├── PlayableItem.swift │ │ │ ├── EpisodeAudio.swift │ │ │ ├── TypeClass.swift │ │ │ ├── WatchingMetadata.swift │ │ │ ├── MediaShortcut.swift │ │ │ ├── Video.swift │ │ │ ├── Duration.swift │ │ │ ├── VideoAudio.swift │ │ │ ├── BackendError.swift │ │ │ ├── DownloadableMediaItem.swift │ │ │ ├── Directory.swift │ │ │ ├── MediaType.swift │ │ │ ├── FileInfo.swift │ │ │ ├── Bookmark.swift │ │ │ ├── WatchData.swift │ │ │ ├── Season.swift │ │ │ ├── Episode.swift │ │ │ └── UserData.swift │ │ │ ├── Responses │ │ │ ├── EmptyResponseData.swift │ │ │ ├── UserDataResponse.swift │ │ │ ├── ArrayData.swift │ │ │ ├── SingleItemData.swift │ │ │ ├── PaginatedData.swift │ │ │ ├── AccessToken.swift │ │ │ └── VerificationResponse.swift │ │ │ ├── Client │ │ │ ├── APIClientError.swift │ │ │ ├── Plugins │ │ │ │ ├── APIClientPlugin.swift │ │ │ │ ├── ResponseLoggingPlugin.swift │ │ │ │ └── CURLLoggingPlugin.swift │ │ │ ├── Endpoint.swift │ │ │ ├── URLSessionProtocol.swift │ │ │ ├── RequestBuilder.swift │ │ │ └── APIClient.swift │ │ │ └── Requests │ │ │ ├── GenresRequest.swift │ │ │ ├── CountriesRequest.swift │ │ │ ├── UserDataRequest.swift │ │ │ ├── BookmarksRequest.swift │ │ │ ├── BookmarkItemsRequest.swift │ │ │ ├── ItemDetailsRequest.swift │ │ │ ├── ToggleWatchingRequest.swift │ │ │ ├── GetWatchingDataRequest.swift │ │ │ ├── MarkTimeRequest.swift │ │ │ ├── RefreshTokenRequest.swift │ │ │ ├── ShortcutItemsRequest.swift │ │ │ ├── SearchItemsRequest.swift │ │ │ └── DeviceCodeRequest.swift │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ ├── package.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── KinoPubBackendTests.xcscheme │ ├── Tests │ │ └── KinoPubBackendTests │ │ │ ├── Mocks │ │ │ └── URLSessionMock.swift │ │ │ ├── RequestBuilderTests.swift │ │ │ └── APIClientTests.swift │ └── Package.swift ├── KinoPubKit │ ├── Sources │ │ └── KinoPubKit │ │ │ ├── KinoPubKit.swift │ │ │ └── Downloading │ │ │ ├── FileSaver.swift │ │ │ ├── DownloadedFileInfo.swift │ │ │ ├── DownloadedFilesDatabase.swift │ │ │ └── Download.swift │ ├── Tests │ │ └── KinoPubKitTests │ │ │ ├── KinoPubKitTests.swift │ │ │ ├── Mocks │ │ │ ├── FileManagerMock.swift │ │ │ └── FileSaverMock.swift │ │ │ └── FileSaverTests.swift │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ ├── package.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── KinoPubKitTests.xcscheme │ │ │ └── KinoPubKit.xcscheme │ └── Package.swift └── KinoPubLogging │ ├── Sources │ └── KinoPubLogging │ │ ├── KinoPubLogging.swift │ │ └── Logger+Extension.swift │ ├── .gitignore │ ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── Tests │ └── KinoPubLoggingTests │ │ └── KinoPubLoggingTests.swift │ └── Package.swift ├── KinoPubAppleClient ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 16.png │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 32.png │ │ │ ├── 40.png │ │ │ ├── 48.png │ │ │ ├── 50.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 │ │ │ ├── 100.png │ │ │ ├── 1024.png │ │ │ ├── 114.png │ │ │ ├── 120.png │ │ │ ├── 128.png │ │ │ ├── 144.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 172.png │ │ │ ├── 180.png │ │ │ ├── 196.png │ │ │ ├── 216.png │ │ │ ├── 256 1.png │ │ │ ├── 256.png │ │ │ ├── 512 1.png │ │ │ ├── 512.png │ │ │ └── 1024 1.png │ │ └── kinopub_icon.imageset │ │ │ ├── playstore.png │ │ │ └── Contents.json │ ├── KinoPubAppleClient.entitlements │ └── GoogleService-Info.plist ├── Custom │ ├── String+Localization.swift │ ├── Locale+Extensions.swift │ ├── Bundle+Data.swift │ ├── WindowSettings.swift │ ├── APIClientError+Description.swift │ ├── PlayerTimeObserver.swift │ └── BestVideoQualityFinder.swift ├── States │ ├── Navigation │ │ ├── NavigationTabs.swift │ │ ├── NavigationState.swift │ │ ├── Routes.swift │ │ └── NavigationLinkProvider.swift │ ├── Error │ │ ├── View+ErrorState.swift │ │ └── ErrorHandler.swift │ └── Auth │ │ └── AuthState.swift ├── Info.plist ├── Services │ ├── Configuration │ │ ├── Configuration.swift │ │ └── BundleConfiguration.swift │ ├── Filtering │ │ ├── FilterDataService.swift │ │ └── FilteDataServiceImpl.swift │ ├── User │ │ ├── UserService.swift │ │ └── UserServiceImpl.swift │ ├── Download │ │ └── Providers.swift │ ├── KeychainStorage │ │ ├── KeychainStorage.swift │ │ └── KeychainStorageImpl.swift │ ├── AccessToken │ │ ├── AccessTokenServiceImpl.swift │ │ ├── AccessTokenService.swift │ │ └── AccessTokenPlugin.swift │ ├── UserActions │ │ ├── UserActionsService.swift │ │ └── UserActionsServiceImpl.swift │ ├── Authorization │ │ ├── AuthorizationService.swift │ │ └── AuthorizationServiceImpl.swift │ └── VideoContent │ │ ├── VideoContentService.swift │ │ └── VideoContentServiceImpl.swift ├── Views │ ├── MediaItem │ │ ├── Seasons │ │ │ ├── SeasonsModel.swift │ │ │ └── SeasonsView.swift │ │ ├── Season │ │ │ ├── SeasonModel.swift │ │ │ ├── SeasonView.swift │ │ │ └── SeasonItemView.swift │ │ ├── MediaItemModel.swift │ │ └── Subviews │ │ │ ├── MediaItemHeaderView.swift │ │ │ └── MediaItemFieldsCard.swift │ ├── macOS │ │ ├── Settings │ │ │ └── SettingsView.swift │ │ └── Sidebar │ │ │ ├── SidebarView.swift │ │ │ ├── SidebarNavigationDetail.swift │ │ │ └── Sidebar.swift │ ├── Root │ │ └── RootView.swift │ ├── Main │ │ ├── Filter │ │ │ ├── FilterModel.swift │ │ │ └── FilterView.swift │ │ └── Shortcut │ │ │ └── ShortcutView.swift │ ├── Downloads │ │ ├── Models │ │ │ └── DownloadMeta.swift │ │ ├── DownloadsCatalog.swift │ │ └── DownloadedItemView.swift │ ├── Bookmarks │ │ ├── Item │ │ │ ├── BookmarkModel.swift │ │ │ └── BookmarkView.swift │ │ └── List │ │ │ └── BookmarksCatalog.swift │ ├── Player │ │ └── PlayerContinueWatchingView.swift │ ├── Profile │ │ └── ProfileModel.swift │ └── Auth │ │ ├── AuthView.swift │ │ └── AuthModel.swift └── App │ ├── AppDelegate.swift │ └── KinoPubAppleClientApp.swift ├── KinoPubAppleClient.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── kirillkunst.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── kirillkunst.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── README.md /Packages/KinoPubUI/README.md: -------------------------------------------------------------------------------- 1 | # KinoPubUI 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/README.md: -------------------------------------------------------------------------------- 1 | # KinoPubBackend 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/KinoPubUI.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Sources/KinoPubKit/KinoPubKit.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/KinoPubBackend.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | -------------------------------------------------------------------------------- /Packages/KinoPubLogging/Sources/KinoPubLogging/KinoPubLogging.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/66.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/92.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/256 1.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/512 1.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/1024 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/AppIcon.appiconset/1024 1.png -------------------------------------------------------------------------------- /Packages/KinoPubKit/Tests/KinoPubKitTests/KinoPubKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import KinoPubKit 3 | 4 | final class KinoPubKitTests: XCTestCase { 5 | func testExample() throws { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/imdb.imageset/imdb@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/imdb.imageset/imdb@2x.png -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/kinopub_icon.imageset/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient/Resources/Assets.xcassets/kinopub_icon.imageset/playstore.png -------------------------------------------------------------------------------- /Packages/KinoPubKit/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/KinoPubLogging/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /KinoPubAppleClient.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/kinopoisk.imageset/kinopoisk@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/kinopoisk.imageset/kinopoisk@2x.png -------------------------------------------------------------------------------- /Packages/KinoPubUI/.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 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/.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 | -------------------------------------------------------------------------------- /KinoPubAppleClient.xcodeproj/project.xcworkspace/xcuserdata/kirillkunst.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/kinopub-apple-client/HEAD/KinoPubAppleClient.xcodeproj/project.xcworkspace/xcuserdata/kirillkunst.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/SeasonWatching.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonWatching.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SeasonWatching: Codable, Hashable { 11 | public let status: Int 12 | } 13 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/EmptyResponseData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyResponseData.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EmptyResponseData: Codable { 11 | public var status: Int 12 | } 13 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/UserDataResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDataResponse.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 9.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct UserDataResponse: Codable { 11 | public let user: UserData 12 | } 13 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/String+Localization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Localization.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 2.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var localized: String { 12 | NSLocalizedString(self, comment: "") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Navigation/NavigationTabs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationTabs.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NavigationTabs { 11 | case main 12 | case bookmarks 13 | case downloads 14 | case profile 15 | } 16 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Extensions/Double+Formatting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Formatting.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | var scoreFormatted: String { 12 | String(format: "%.1f", self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/EpisodeWatching.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EpisodeWatching.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EpisodeWatching: Codable, Hashable { 11 | public let status: Int 12 | public let time: Int 13 | } 14 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Pagination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pagination.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Pagination: Codable { 11 | public let total: Int 12 | public let current: Int 13 | public let perpage: Int 14 | } 15 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Trailer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Trailer.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Trailer: Codable, Hashable { 11 | public let id: Int 12 | public let file: String 13 | public let url: String? 14 | } 15 | -------------------------------------------------------------------------------- /KinoPubAppleClient.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Author.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Author.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Author: Codable, Hashable { 11 | public let id: Int 12 | public let title: String? 13 | public let shortTitle: String? 14 | } 15 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/MediaGenre.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaGenre.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MediaGenre: Codable { 11 | public let id: String 12 | public let title: String 13 | public let type: MediaType 14 | } 15 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Packages/KinoPubLogging/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/Locale+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Locale+Extensions.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Patrikas Karpickas on 06/02/2025. 6 | // 7 | import Foundation 8 | 9 | extension Locale { 10 | static var currentLanguageCode: String { 11 | self.current.language.languageCode?.identifier ?? "en" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/APIClientError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientError.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum APIClientError: Error { 11 | case urlError 12 | case invalidUrlParams 13 | case networkError(Error) 14 | case decodingError(Error) 15 | } 16 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Posters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Posters.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Posters: Codable, Hashable { 11 | public let small: String 12 | public let medium: String 13 | public let big: String 14 | public let wide: String? 15 | } 16 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Subtitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subtitle.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Subtitle: Codable, Hashable { 11 | public let lang: String 12 | public let shift: Int 13 | public let embed: Bool 14 | public let url: String 15 | } 16 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/URLInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLInfo.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct URLInfo: Codable, Hashable { 11 | public let http: String 12 | public let hls: String 13 | public let hls4: String 14 | public let hls2: String 15 | } 16 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/ArrayData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayData.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ArrayData: Codable { 11 | 12 | public var items: [T] 13 | 14 | public static func mock(data: [T]) -> ArrayData { 15 | return ArrayData(items: data) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/Plugins/APIClientPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientPlugin.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol APIClientPlugin { 11 | func prepare(_ request: URLRequest) -> URLRequest 12 | func willSend(_ request: URLRequest) 13 | func didReceive(_ response: URLResponse, data: Data?) 14 | } 15 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Country.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Country.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Country: Codable, Hashable { 11 | public let id: Int 12 | public let title: String 13 | 14 | private enum CodingKeys: String, CodingKey { 15 | case id = "id" 16 | case title = "title" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/PlayableItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayableItem.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 9.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol PlayableItem: Identifiable, Hashable, Equatable { 11 | var id: Int { get } 12 | var files: [FileInfo] { get } 13 | var trailer: Trailer? { get } 14 | var metadata: WatchingMetadata { get } 15 | } 16 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/SingleItemData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleItemData.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SingleItemData: Codable { 11 | 12 | public var item: T 13 | 14 | public static func mock(data: T) -> SingleItemData { 15 | return SingleItemData(item: data) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BaseURL 6 | https://api.service-kp.com 7 | ClientID 8 | xbmc 9 | ClientSecret 10 | cgg3gtifu46urtfp2zp1nqtba0k2ezxh 11 | 12 | 13 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Tests/KinoPubUITests/KinoPubUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import KinoPubUI 3 | 4 | final class KinoPubUITests: 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(KinoPubUI().text, "Hello, World!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Configuration { 11 | var clientID: String { get } 12 | var clientSecret: String { get } 13 | var baseURL: String { get } 14 | } 15 | 16 | protocol ConfigurationProvider { 17 | var configuration: Configuration { get set } 18 | } 19 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/Assets.xcassets/kinopub_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "playstore.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/imdb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "imdb@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/EpisodeAudio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EpisodeAudio.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct EpisodeAudio: Codable, Hashable { 11 | public let id: Int 12 | public let index: Int 13 | public let codec: String 14 | public let channels: Int 15 | public let lang: String 16 | public let type: TypeClass? 17 | public let author: Author? 18 | } 19 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/Bundle+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Data.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 10.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | public var appBuild: String { getInfo("CFBundleVersion") } 12 | public var appVersionLong: String { getInfo("CFBundleShortVersionString") } 13 | fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } 14 | } 15 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/kinopoisk.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "kinopoisk@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/KinoPubLogging/Tests/KinoPubLoggingTests/KinoPubLoggingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import KinoPubLogging 3 | 4 | final class KinoPubLoggingTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Filtering/FilterDataService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterDataService.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 23.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | protocol FilterDataService { 12 | func fetchGenres() async throws -> [MediaGenre] 13 | func fetchCountries() async throws -> [Country] 14 | } 15 | 16 | protocol FilterDataServiceProvider { 17 | var filterDataService: FilterDataService { get set } 18 | } 19 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/TypeClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeClass.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TypeClass: Codable, Hashable { 11 | public let id: Int 12 | public let title: String? 13 | public let shortTitle: String? 14 | 15 | private enum CodingKeys: String, CodingKey { 16 | case id = "id" 17 | case title = "title" 18 | case shortTitle = "short_title" 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/WatchingMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WatchingMetadata.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 13.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct WatchingMetadata: Codable, Hashable { 11 | public let id: Int 12 | public let video: Int? 13 | public let season: Int? 14 | 15 | public init(id: Int, video: Int? = nil, season: Int? = nil) { 16 | self.id = id 17 | self.video = video 18 | self.season = season 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Image/Image+CenterCropped.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image+CenterCropped.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public extension Image { 12 | func centerCropped() -> some View { 13 | GeometryReader { geo in 14 | self 15 | .resizable() 16 | .scaledToFill() 17 | .frame(width: geo.size.width, height: geo.size.height) 18 | .clipped() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/PaginatedData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaginatedItemsResponse.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct PaginatedData: Codable { 11 | 12 | public var items: [T] 13 | public var pagination: Pagination 14 | 15 | public static func mock(data: [T]) -> PaginatedData { 16 | return PaginatedData(items: data, pagination: Pagination(total: 0, current: 0, perpage: 0)) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Endpoint { 11 | var path: String { get } 12 | var method: String { get } 13 | var headers: [String: String]? { get } 14 | var parameters: [String: Any]? { get } 15 | var forceSendAsGetParams: Bool { get } 16 | } 17 | 18 | extension Endpoint { 19 | var forceSendAsGetParams: Bool { 20 | return false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/Seasons/SeasonsModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonsModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 4.11.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | class SeasonsModel: ObservableObject { 12 | 13 | public var seasons: [Season] 14 | public var linkProvider: NavigationLinkProvider 15 | 16 | init(seasons: [Season], linkProvider: NavigationLinkProvider) { 17 | self.seasons = seasons 18 | self.linkProvider = linkProvider 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/AccessToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenResponse.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 17.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AccessToken: Codable { 11 | public let accessToken: String 12 | public let refreshToken: String 13 | public let expiresIn: Int 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case accessToken = "access_token" 17 | case expiresIn = "expires_in" 18 | case refreshToken = "refresh_token" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/User/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserService.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 9.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | protocol UserService { 12 | func fetchUserData() async throws -> UserData 13 | } 14 | 15 | protocol UserServiceProvider { 16 | var userService: UserService { get set } 17 | } 18 | 19 | struct UserServiceMock: UserService { 20 | func fetchUserData() async throws -> UserData { 21 | return UserData.mock() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Font/Font+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font+Extension.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public extension Font { 12 | 13 | struct KinoPub { 14 | public static var header: Font { 15 | .system(size: 22.0) 16 | } 17 | 18 | public static var subheader: Font { 19 | .system(size: 17.0) 20 | } 21 | 22 | public static var small: Font { 23 | .system(size: 14.0) 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Button/KinoPubButtonTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KinoPubButtonTextStyle.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 4.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public struct KinoPubButtonTextStyle: ViewModifier { 12 | 13 | public init() {} 14 | 15 | public func body(content: Content) -> some View { 16 | content 17 | .padding(.horizontal, 8) 18 | .frame(maxWidth: .infinity, maxHeight: 40) 19 | .font(.system(size: 16, weight: .semibold)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Download/Providers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Providers.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 8.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubKit 10 | import KinoPubBackend 11 | 12 | protocol DownloadedFilesDatabaseProvider { 13 | var downloadedFilesDatabase: DownloadedFilesDatabase { get set } 14 | } 15 | 16 | protocol DownloadManagerProvider { 17 | var downloadManager: DownloadManager { get set } 18 | } 19 | 20 | protocol FileSaverProvider { 21 | var fileSaver: FileSaving { get set } 22 | } 23 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Navigation/NavigationState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationState.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | class NavigationState: ObservableObject { 12 | @Published var columnVisibility = NavigationSplitViewVisibility.automatic 13 | @Published var selectedTab: NavigationTabs = .main 14 | @Published var mainRoutes: [MainRoutes] = [] 15 | @Published var bookmarksRoutes: [BookmarksRoutes] = [] 16 | @Published var downloadsRoutes: [DownloadsRoutes] = [] 17 | } 18 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/MediaShortcut.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaShortcut.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum MediaShortcut: String, Codable, CaseIterable, Identifiable { 11 | case hot 12 | case fresh 13 | case popular 14 | 15 | public var id: Self { 16 | return self 17 | } 18 | 19 | public var title: String { 20 | switch self { 21 | case .hot: return "Hot" 22 | case .fresh: return "Fresh" 23 | case .popular: return "Popular" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/KinoPubAppleClient.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.downloads.read-write 8 | 9 | com.apple.security.files.user-selected.read-only 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Tests/KinoPubBackendTests/Mocks/URLSessionMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | @testable import KinoPubBackend 10 | 11 | public class URLSessionMock: URLSessionProtocol { 12 | var data: Data? 13 | var response: URLResponse? 14 | var error: Error? 15 | 16 | public func data(for request: URLRequest) async throws -> (Data, URLResponse) { 17 | if let error = error { 18 | throw error 19 | } 20 | 21 | return (data ?? Data(), response ?? URLResponse()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/macOS/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 28.10.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | #if os(macOS) 12 | struct SettingsView: View { 13 | @EnvironmentObject var windowSettings: WindowSettings 14 | @AppStorage("alwaysOnTop") var alwaysOnTop: Bool = false 15 | 16 | var body: some View { 17 | Form { 18 | Toggle("AlwaysOnTop", isOn: $windowSettings.alwaysOnTop) 19 | } 20 | .padding() 21 | .frame(width: 300, height: 200) 22 | } 23 | } 24 | #endif 25 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/GenresRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 14.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GenresRequest: Endpoint { 11 | 12 | public init() {} 13 | 14 | public var path: String { 15 | "/v1/countries" 16 | } 17 | 18 | public var method: String { 19 | "GET" 20 | } 21 | 22 | public var parameters: [String: Any]? { 23 | nil 24 | } 25 | 26 | public var headers: [String: String]? { 27 | nil 28 | } 29 | 30 | public var forceSendAsGetParams: Bool { false } 31 | } 32 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/CountriesRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 14.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct CountriesRequest: Endpoint { 11 | 12 | public init() {} 13 | 14 | public var path: String { 15 | "/v1/countries" 16 | } 17 | 18 | public var method: String { 19 | "GET" 20 | } 21 | 22 | public var parameters: [String: Any]? { 23 | nil 24 | } 25 | 26 | public var headers: [String: String]? { 27 | nil 28 | } 29 | 30 | public var forceSendAsGetParams: Bool { false } 31 | } 32 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Video.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Video.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Video: Codable, Hashable { 11 | public let id: Int 12 | public let title: String 13 | public let thumbnail: String 14 | public let duration: Int 15 | public let tracks: Int 16 | public let number: Int 17 | public let ac3: Int 18 | public let audios: [VideoAudio] 19 | public let watched: Int 20 | public let watching: EpisodeWatching 21 | public let subtitles: [Subtitle] 22 | public let files: [FileInfo] 23 | } 24 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/UserDataRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDataRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 9.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct UserDataRequest: Endpoint { 11 | 12 | public init() {} 13 | 14 | public var path: String { 15 | "/v1/user" 16 | } 17 | 18 | public var method: String { 19 | "GET" 20 | } 21 | 22 | public var parameters: [String: Any]? { 23 | nil 24 | } 25 | 26 | public var headers: [String: String]? { 27 | nil 28 | } 29 | 30 | public var forceSendAsGetParams: Bool { false } 31 | } 32 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/BookmarksRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarksRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BookmarksRequest: Endpoint { 11 | 12 | 13 | public init() {} 14 | 15 | public var path: String { 16 | "/v1/bookmarks" 17 | } 18 | 19 | public var method: String { 20 | "GET" 21 | } 22 | 23 | public var parameters: [String: Any]? { 24 | nil 25 | } 26 | 27 | public var headers: [String: String]? { 28 | nil 29 | } 30 | 31 | public var forceSendAsGetParams: Bool { false } 32 | } 33 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Configuration/BundleConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleConfiguration.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | final class BundleConfiguration: Configuration { 11 | 12 | var clientID: String { 13 | value(for: "ClientID") 14 | } 15 | 16 | var clientSecret: String { 17 | value(for: "ClientSecret") 18 | } 19 | 20 | var baseURL: String { 21 | value(for: "BaseURL") 22 | } 23 | 24 | private func value(for key: String) -> String { 25 | Bundle.main.object(forInfoDictionaryKey: key) as! String 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Duration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Duration.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Duration: Codable, Hashable { 11 | public let average: Double 12 | public let total: TimeInterval 13 | } 14 | 15 | public extension Duration { 16 | var totalFormatted: String { 17 | let formatter = DateComponentsFormatter() 18 | #if os(iOS) 19 | formatter.allowedUnits = [.hour, .minute, .second, .nanosecond] 20 | #endif 21 | formatter.unitsStyle = .positional 22 | return formatter.string(from: self.total) ?? "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Packages/KinoPubLogging/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 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: "KinoPubLogging", 8 | platforms: [.macOS(.v13), .iOS(.v16)], 9 | products: [ 10 | .library( 11 | name: "KinoPubLogging", 12 | targets: ["KinoPubLogging"]) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "KinoPubLogging"), 17 | .testTarget( 18 | name: "KinoPubLoggingTests", 19 | dependencies: ["KinoPubLogging"]) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Responses/VerificationResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerificationResponse.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 17.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct VerificationResponse: Codable { 11 | public let code: String 12 | public let userCode: String 13 | public let verificationUri: String 14 | public let expiresIn: Int 15 | public let interval: Int 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case code 19 | case userCode = "user_code" 20 | case verificationUri = "verification_uri" 21 | case expiresIn = "expires_in" 22 | case interval 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Root/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 17.07.2023. 6 | // 7 | 8 | import SwiftUI 9 | import KinoPubUI 10 | import KinoPubBackend 11 | 12 | struct RootView: View { 13 | 14 | var placement: ToolbarPlacement { 15 | #if os(iOS) 16 | .tabBar 17 | #elseif os(macOS) 18 | .windowToolbar 19 | #endif 20 | } 21 | 22 | var body: some View { 23 | #if os(iOS) 24 | TabsNavigationView() 25 | #elseif os(macOS) 26 | SidebarView() 27 | #endif 28 | } 29 | } 30 | 31 | struct RootView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | RootView() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Button/KinoPubButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KinoPubButtonStyle.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public struct KinoPubButtonStyle: ButtonStyle { 12 | 13 | var buttonColor: KinoPubButton.ButtonColor 14 | 15 | public init(buttonColor: KinoPubButton.ButtonColor) { 16 | self.buttonColor = buttonColor 17 | } 18 | 19 | public func makeBody(configuration: Self.Configuration) -> some View { 20 | configuration.label 21 | .foregroundColor(.white) 22 | .background(buttonColor.color) 23 | .cornerRadius(6.0) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Toast/ToastContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastContentView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public struct ToastContentView: View { 12 | 13 | public var text: String 14 | 15 | public init(text: String) { 16 | self.text = text 17 | } 18 | 19 | public var body: some View { 20 | Text(text) 21 | .padding(.horizontal, 16) 22 | .padding(.vertical, 16) 23 | .foregroundColor(Color.white) 24 | .background(Color.KinoPub.accentRed) 25 | .cornerRadius(16) 26 | .frame(minHeight: 60) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/BookmarkItemsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BookmarkItemsRequest: Endpoint { 11 | 12 | private var id: String 13 | 14 | public init(id: String) { 15 | self.id = id 16 | } 17 | 18 | public var path: String { 19 | "/v1/bookmarks/\(id)" 20 | } 21 | 22 | public var method: String { 23 | "GET" 24 | } 25 | 26 | public var parameters: [String: Any]? { 27 | nil 28 | } 29 | 30 | public var headers: [String: String]? { 31 | nil 32 | } 33 | 34 | public var forceSendAsGetParams: Bool { false } 35 | } 36 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/ItemDetailsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemDetailsRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ItemDetailsRequest: Endpoint { 11 | 12 | private var id: String 13 | 14 | public init(id: String) { 15 | self.id = id 16 | } 17 | 18 | public var path: String { 19 | "/v1/items/\(id)" 20 | } 21 | 22 | public var method: String { 23 | "GET" 24 | } 25 | 26 | public var parameters: [String: Any]? { 27 | return nil 28 | } 29 | 30 | public var headers: [String: String]? { 31 | nil 32 | } 33 | 34 | public var forceSendAsGetParams: Bool { false } 35 | } 36 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Main/Filter/FilterModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 4.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | 12 | class FilterModel: ObservableObject { 13 | 14 | @Published var mediaType: MediaType = .movie 15 | 16 | @Published var yearFilterEnabled: Bool = false 17 | @Published var yearMin: Int = 1920 18 | @Published var yearMax: Int = 2023 19 | 20 | @Published var imdbFilterEnabled: Bool = false 21 | @Published var imdbMin: Int = 0 22 | @Published var imdbMax: Int = 0 23 | 24 | @Published var selectedGenre: MediaGenre? 25 | @Published var selectedCountry: Country? 26 | 27 | } 28 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/Season/SeasonModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 4.11.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | class SeasonModel: ObservableObject { 12 | 13 | public var season: Season 14 | public var linkProvider: NavigationLinkProvider 15 | 16 | init(season: Season, linkProvider: NavigationLinkProvider) { 17 | self.season = season 18 | self.linkProvider = linkProvider 19 | } 20 | 21 | func filledEpisode(_ episode: Episode) -> Episode { 22 | let episode = episode 23 | episode.seasonNumber = season.number 24 | episode.mediaId = season.mediaId 25 | return episode 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/VideoAudio.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoAudio.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct VideoAudio: Codable, Hashable { 11 | public let id: Int 12 | public let index: Int 13 | public let codec: String 14 | public let channels: Int 15 | public let lang: String 16 | public let type: TypeClass? 17 | public let author: TypeClass? 18 | 19 | private enum CodingKeys: String, CodingKey { 20 | case id = "id" 21 | case index = "index" 22 | case codec = "codec" 23 | case channels = "channels" 24 | case lang = "lang" 25 | case type = "type" 26 | case author = "author" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/BackendError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackendError.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum BackendErrorCode: String, Codable { 11 | case authorizationPending = "authorization_pending" 12 | case invalidClient = "invalid_client" 13 | case unauthorized = "unauthorized" 14 | } 15 | 16 | public struct BackendError: Error, Codable { 17 | public var status: Int 18 | public var errorCode: BackendErrorCode 19 | public var errorDescription: String? 20 | 21 | private enum CodingKeys: String, CodingKey { 22 | case status 23 | case errorCode = "error" 24 | case errorDescription = "error_description" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/DownloadableMediaItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadableMediaItem.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 10.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DownloadableMediaItem: Hashable, Identifiable, Codable { 11 | public var name: String 12 | public var id: String { name } 13 | public var files: [FileInfo] 14 | public var mediaItem: MediaItem 15 | public var watchingMetadata: WatchingMetadata 16 | 17 | public init(name: String, files: [FileInfo], mediaItem: MediaItem, watchingMetadata: WatchingMetadata) { 18 | self.name = name 19 | self.files = files 20 | self.mediaItem = mediaItem 21 | self.watchingMetadata = watchingMetadata 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/URLSessionProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol URLSessionProtocol { 11 | func data(for request: URLRequest) async throws -> (Data, URLResponse) 12 | } 13 | 14 | public class URLSessionImpl: URLSessionProtocol { 15 | let session: URLSession 16 | 17 | public init(session: URLSession) { 18 | self.session = session 19 | } 20 | 21 | public func data(for request: URLRequest) async throws -> (Data, URLResponse) { 22 | do { 23 | return try await session.data(for: request) 24 | } catch { 25 | throw APIClientError.networkError(error) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/KeychainStorage/KeychainStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainStorage.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol KeyRepresentable { 11 | var rawValue: String { get } 12 | } 13 | 14 | struct Key: KeyRepresentable { 15 | public let rawValue: String 16 | public init(rawValue: String) { 17 | self.rawValue = rawValue 18 | } 19 | } 20 | 21 | protocol KeychainStorage { 22 | func object(for key: Key) -> Value? where Value: Codable 23 | func setObject(_ object: Value?, for key: Key) where Value: Codable 24 | func clear() 25 | } 26 | 27 | protocol KeychainStorageProvider { 28 | var keychainStorage: KeychainStorage { get set } 29 | } 30 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 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: "KinoPubKit", 8 | platforms: [.macOS(.v13), .iOS(.v16)], 9 | products: [ 10 | .library( 11 | name: "KinoPubKit", 12 | targets: ["KinoPubKit"]) 13 | ], 14 | dependencies: [ 15 | .package(name: "KinoPubLogging", path: "../KinoPubLogging") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "KinoPubKit", 20 | dependencies: [ 21 | .product(name: "KinoPubLogging", package: "KinoPubLogging") 22 | ]), 23 | .testTarget( 24 | name: "KinoPubKitTests", 25 | dependencies: ["KinoPubKit"]) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/Plugins/ResponseLoggingPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseLoggingPlugin.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import KinoPubLogging 11 | 12 | public class ResponseLoggingPlugin: APIClientPlugin { 13 | 14 | public init() {} 15 | 16 | public func prepare(_ request: URLRequest) -> URLRequest { 17 | return request 18 | } 19 | 20 | public func willSend(_ request: URLRequest) {} 21 | 22 | public func didReceive(_ response: URLResponse, data: Data?) { 23 | Logger.backend.debug("Response: \(response)") 24 | if let data = data { 25 | let str = String(data: data, encoding: .utf8) 26 | Logger.backend.debug("Data: \(str ?? "")") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Tests/KinoPubKitTests/Mocks/FileManagerMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManagerMock.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | class FileManagerMock: FileManager { 11 | var shouldThrowError = false 12 | var didRemoveItem = false 13 | var didMoveItem = false 14 | 15 | override func removeItem(at URL: URL) throws { 16 | didRemoveItem = true 17 | if shouldThrowError { 18 | throw NSError(domain: "FileManagerMockErrorDomain", code: 123, userInfo: nil) 19 | } 20 | } 21 | 22 | override func moveItem(at srcURL: URL, to dstURL: URL) throws { 23 | didMoveItem = true 24 | if shouldThrowError { 25 | throw NSError(domain: "FileManagerMockErrorDomain", code: 456, userInfo: nil) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 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: "KinoPubBackend", 8 | platforms: [.macOS(.v13), .iOS(.v16)], 9 | products: [ 10 | .library( 11 | name: "KinoPubBackend", 12 | targets: ["KinoPubBackend"]) 13 | ], 14 | dependencies: [ 15 | .package(name: "KinoPubLogging", path: "../KinoPubLogging") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "KinoPubBackend", 20 | dependencies: [ 21 | .product(name: "KinoPubLogging", package: "KinoPubLogging") 22 | ]), 23 | .testTarget( 24 | name: "KinoPubBackendTests", 25 | dependencies: ["KinoPubBackend"]) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/AccessToken/AccessTokenServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTokenServiceImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Key where Value: Token { 11 | static var token: Key { .init(rawValue: "com.kunst.kinopub.token") } 12 | } 13 | 14 | public final class AccessTokenServiceImpl: AccessTokenService { 15 | 16 | private let storage: KeychainStorage 17 | 18 | init(storage: KeychainStorage) { 19 | self.storage = storage 20 | } 21 | 22 | func set(token: T) where T: Token { 23 | storage.setObject(token, for: .token) 24 | } 25 | 26 | func token() -> T? where T: Token { 27 | storage.object(for: .token) 28 | } 29 | 30 | func clear() { 31 | storage.clear() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Directory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Directory.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Directory: Codable, Hashable { 11 | public let id: Int 12 | public let title: String 13 | public let views: Int 14 | public let created: Int 15 | public let updated: Int 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case id = "id" 19 | case title = "title" 20 | case views = "views" 21 | case created = "created" 22 | case updated = "updated" 23 | } 24 | 25 | public init(id: Int, title: String, views: Int, created: Int, updated: Int) { 26 | self.id = id 27 | self.title = title 28 | self.views = views 29 | self.created = created 30 | self.updated = updated 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/MediaType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaType.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum MediaType: String, Codable, CaseIterable, Identifiable { 11 | case movie 12 | case serial 13 | case threeD = "3D" 14 | case concert 15 | case documovie 16 | case docuserial 17 | case tvshow 18 | 19 | public var id: Self { 20 | return self 21 | } 22 | 23 | public var title: String { 24 | switch self { 25 | case .movie: return "Movie" 26 | case .concert: return "Concert" 27 | case .documovie: return "Documental" 28 | case .docuserial: return "Documental Series" 29 | case .serial: return "Serial" 30 | case .threeD: return "3D" 31 | case .tvshow: return "TV Show" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/AccessToken/AccessTokenService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTokenService.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Token: Codable { 11 | var accessToken: String { get } 12 | var refreshToken: String { get } 13 | var expiresIn: Int { get } 14 | } 15 | 16 | protocol AccessTokenService { 17 | func set(token: T) where T: Token 18 | func token() -> T? where T: Token 19 | func clear() 20 | } 21 | 22 | protocol AccessTokenServiceProvider { 23 | var accessTokenService: AccessTokenService { get set } 24 | } 25 | 26 | struct AccessTokenServiceMock: AccessTokenService { 27 | 28 | func set(token: T) where T: Token { 29 | 30 | } 31 | 32 | func token() -> T? where T: Token { 33 | nil 34 | } 35 | 36 | func clear() { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/FileInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileInfo.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct FileInfo: Codable, Hashable { 11 | public let codec: String 12 | public let w: Int 13 | public let h: Int 14 | public let quality: String 15 | public let qualityID: Int 16 | public let url: URLInfo 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case codec = "codec" 20 | case w = "w" 21 | case h = "h" 22 | case quality = "quality" 23 | case qualityID = "quality_id" 24 | case url = "url" 25 | } 26 | } 27 | 28 | public extension FileInfo { 29 | var resolution: Int { 30 | Int(quality.dropLast()) ?? 0 31 | } 32 | } 33 | 34 | extension FileInfo: Identifiable { 35 | public var id: Int { 36 | url.hashValue 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Bookmark.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bookmark.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Bookmark: Codable { 11 | public let id: Int 12 | public let title: String 13 | public let views: Int 14 | public let count: String 15 | public let created: Int 16 | public let updated: Int 17 | public var skeleton: Bool? 18 | } 19 | 20 | public extension Bookmark { 21 | static func skeletonMock() -> [Bookmark] { 22 | (0..<4).map { id in 23 | mock(id: id, skeleton: true) 24 | } 25 | } 26 | 27 | static func mock(id: Int = 1, skeleton: Bool = false) -> Bookmark { 28 | Bookmark(id: id, title: "", views: 0, count: "", created: 0, updated: 0, skeleton: skeleton) 29 | } 30 | } 31 | 32 | extension Bookmark: Identifiable { } 33 | extension Bookmark: Hashable { } 34 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/Seasons/SeasonsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonsView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 4.11.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubUI 11 | import KinoPubBackend 12 | 13 | struct SeasonsView: View { 14 | @StateObject private var model: SeasonsModel 15 | 16 | init(model: @autoclosure @escaping () -> SeasonsModel) { 17 | _model = StateObject(wrappedValue: model()) 18 | } 19 | 20 | var body: some View { 21 | VStack { 22 | listView 23 | } 24 | .navigationTitle("Seasons") 25 | .background(Color.KinoPub.background) 26 | } 27 | 28 | var listView: some View { 29 | List(model.seasons) { season in 30 | NavigationLink(value: model.linkProvider.season(for: season)) { 31 | Text(season.fixedTitle) 32 | } 33 | } 34 | .scrollContentBackground(.hidden) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Colors/Colors+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Extension.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Color { 12 | public struct KinoPub { 13 | public static let accent = Color("accent_color", bundle: .module) 14 | public static let accentRed = Color("accent_red_color", bundle: .module) 15 | public static let accentBlue = Color("accent_blue_color", bundle: .module) 16 | public static let background = Color("background_color", bundle: .module) 17 | public static let text = Color("text_color", bundle: .module) 18 | public static let subtitle = Color("subtitle_text_color", bundle: .module) 19 | public static let selectionBackground = Color("selection_background_color", bundle: .module) 20 | public static let skeleton = Color("skeleton_color", bundle: .module) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/text_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x00", 9 | "green" : "0x00", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xB5", 27 | "green" : "0xB1", 28 | "red" : "0xB0" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 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: "KinoPubUI", 8 | platforms: [.macOS(.v13), .iOS(.v16)], 9 | products: [ 10 | .library( 11 | name: "KinoPubUI", 12 | targets: ["KinoPubUI"]) 13 | ], 14 | dependencies: [ 15 | .package(name: "KinoPubBackend", path: "../KinoPubBackend"), 16 | .package(url: "https://github.com/CSolanaM/SkeletonUI.git", branch: "master") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "KinoPubUI", 21 | dependencies: [ 22 | .product(name: "KinoPubBackend", package: "KinoPubBackend"), 23 | .product(name: "SkeletonUI", package: "SkeletonUI") 24 | ]), 25 | .testTarget( 26 | name: "KinoPubUITests", 27 | dependencies: ["KinoPubUI"]) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/accent_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x88", 9 | "green" : "0xC7", 10 | "red" : "0x6B" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x88", 27 | "green" : "0xC7", 28 | "red" : "0x6B" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/accent_red_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4F", 9 | "green" : "0x53", 10 | "red" : "0xD9" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x4F", 27 | "green" : "0x53", 28 | "red" : "0xD9" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/skeleton_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xBD", 9 | "green" : "0xBD", 10 | "red" : "0xBD" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xBD", 27 | "green" : "0xBD", 28 | "red" : "0xBD" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/accent_blue_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xD8", 9 | "green" : "0x73", 10 | "red" : "0x04" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xD8", 27 | "green" : "0x73", 28 | "red" : "0x04" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/background_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x2B", 27 | "green" : "0x20", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/dark_background_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x2B", 9 | "green" : "0x20", 10 | "red" : "0x1C" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x2B", 27 | "green" : "0x20", 28 | "red" : "0x1C" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/subtitle_text_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x7E", 9 | "green" : "0x77", 10 | "red" : "0x75" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x7E", 27 | "green" : "0x77", 28 | "red" : "0x75" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Media.xcassets/Colors/selection_background_color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x39", 9 | "green" : "0x39", 10 | "red" : "0x39" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x39", 27 | "green" : "0x39", 28 | "red" : "0x39" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/ToggleWatchingRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleWatchingRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ToggleWatchingRequest: Endpoint { 11 | 12 | public var id: Int 13 | public var video: Int 14 | public var season: Int = -1 15 | 16 | init(id: Int, video: Int, season: Int? = nil) { 17 | self.id = id 18 | self.video = video 19 | self.season = season ?? -1 20 | } 21 | 22 | public var path: String { 23 | "/v1/watching/toggle" 24 | } 25 | 26 | public var method: String { 27 | "GET" 28 | } 29 | 30 | public var parameters: [String: Any]? { 31 | [ 32 | "id": id, 33 | "video": video, 34 | "season": season 35 | ].filter({ $0.value != -1 }) 36 | } 37 | 38 | public var headers: [String: String]? { 39 | nil 40 | } 41 | 42 | public var forceSendAsGetParams: Bool { true } 43 | } 44 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Error/View+ErrorState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ErrorState.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import PopupView 11 | import KinoPubUI 12 | 13 | /// Extension for the View protocol to handle error states. 14 | extension View { 15 | 16 | /// Displays a popup with an error message when the error state is true. 17 | /// - Parameter state: A binding to the error state. 18 | /// - Returns: A modified view with error handling. 19 | func handleError(state: Binding) -> some View { 20 | self.popup(isPresented: state.showError) { 21 | ToastContentView(text: state.error.wrappedValue ?? "") 22 | .padding() 23 | } customize: { 24 | $0 25 | .type(.floater()) 26 | .position(.bottom) 27 | .animation(.spring()) 28 | .closeOnTapOutside(true) 29 | .autohideIn(5.0) 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Tests/KinoPubKitTests/Mocks/FileSaverMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSaverMock.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | @testable import KinoPubKit 10 | 11 | class FileSaverMock: FileSaving { 12 | 13 | var shouldThrowError = false 14 | var didSaveFileCalled = false 15 | var savedFileSourceURL: URL? 16 | var savedFileDestinationURL: URL? 17 | 18 | func saveFile(from sourceURL: URL, to destinationURL: URL) throws { 19 | didSaveFileCalled = true 20 | savedFileSourceURL = sourceURL 21 | savedFileDestinationURL = destinationURL 22 | 23 | if shouldThrowError { 24 | throw NSError(domain: "FileSaverMockErrorDomain", code: 123, userInfo: nil) 25 | } 26 | } 27 | 28 | func getDocumentsDirectoryURL(forFilename filename: String) -> URL { 29 | // Provide a mock URL for testing purposes 30 | return URL(string: "file:///path/to/documents/")!.appendingPathComponent(filename) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/AccessToken/AccessTokenPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTokenPlugin.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | extension AccessToken: Token {} 12 | 13 | struct AccessTokenPlugin: APIClientPlugin { 14 | 15 | private let accessTokenService: AccessTokenService 16 | 17 | init(accessTokenService: AccessTokenService) { 18 | self.accessTokenService = accessTokenService 19 | } 20 | 21 | func prepare(_ request: URLRequest) -> URLRequest { 22 | var request = request 23 | 24 | if let token: AccessToken = accessTokenService.token() { 25 | let authValue = "Bearer " + token.accessToken 26 | request.addValue(authValue, forHTTPHeaderField: "Authorization") 27 | } 28 | 29 | return request 30 | } 31 | 32 | func willSend(_ request: URLRequest) { 33 | 34 | } 35 | 36 | func didReceive(_ response: URLResponse, data: Data?) { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/GetWatchingDataRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GetWatchingDataRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GetWatchingDataRequest: Endpoint { 11 | 12 | public var id: Int 13 | public var video: Int = -1 14 | public var season: Int = -1 15 | 16 | public init(id: Int, video: Int? = nil, season: Int? = nil) { 17 | self.id = id 18 | self.video = video ?? -1 19 | self.season = season ?? -1 20 | } 21 | 22 | 23 | public var path: String { 24 | "/v1/watching" 25 | } 26 | 27 | public var method: String { 28 | "GET" 29 | } 30 | 31 | public var parameters: [String: Any]? { 32 | [ 33 | "id": id, 34 | "video": video, 35 | "season": season 36 | ].filter({ $0.value != -1 }) 37 | } 38 | 39 | public var headers: [String: String]? { 40 | nil 41 | } 42 | 43 | public var forceSendAsGetParams: Bool { true } 44 | } 45 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/UserActions/UserActionsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserActionsService.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | protocol UserActionsService { 12 | func markWatch(id: Int, time: Int, video: Int?, season: Int?) async throws 13 | func toggleWatching(id: Int, video: Int?, season: Int?) async throws 14 | func fetchWatchMark(id: Int, video: Int?, season: Int?) async throws -> WatchData 15 | } 16 | 17 | protocol UserActionsServiceProvider { 18 | var actionsService: UserActionsService { get set } 19 | } 20 | 21 | struct UserActionsServiceMock: UserActionsService { 22 | func markWatch(id: Int, time: Int, video: Int?, season: Int?) async throws { 23 | 24 | } 25 | 26 | func toggleWatching(id: Int, video: Int?, season: Int?) async throws { 27 | 28 | } 29 | 30 | func fetchWatchMark(id: Int, video: Int?, season: Int?) async throws -> WatchData { 31 | WatchData.mock 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/WindowSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowSettings.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 28.10.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | #if os(macOS) 12 | 13 | /// A class that manages the window settings, including the "always on top" feature. 14 | class WindowSettings: ObservableObject { 15 | @Published var alwaysOnTop = UserDefaults.standard.bool(forKey: "alwaysOnTop") { 16 | didSet { 17 | UserDefaults.standard.set(alwaysOnTop, forKey: "alwaysOnTop") 18 | updateWindowLevel() 19 | } 20 | } 21 | 22 | /// Updates the window level based on the "always on top" setting. 23 | func updateWindowLevel() { 24 | if alwaysOnTop { 25 | NSApp.windows.forEach { window in 26 | window.level = .floating 27 | } 28 | } else { 29 | NSApp.windows.forEach { window in 30 | window.level = .normal 31 | } 32 | } 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Filtering/FilteDataServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilteDataServiceImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 23.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | final class FilteDataServiceImpl: FilterDataService { 12 | 13 | private var apiClient: APIClient 14 | 15 | init(apiClient: APIClient) { 16 | self.apiClient = apiClient 17 | } 18 | 19 | func fetchGenres() async throws -> [MediaGenre] { 20 | let request = GenresRequest() 21 | let response = try await apiClient.performRequest(with: request, 22 | decodingType: ArrayData.self) 23 | return response.items 24 | } 25 | 26 | func fetchCountries() async throws -> [Country] { 27 | let request = CountriesRequest() 28 | let response = try await apiClient.performRequest(with: request, 29 | decodingType: ArrayData.self) 30 | return response.items 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Error/ErrorHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorHandler.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /// ErrorHandler is a class that handles error states and provides methods to set and reset errors. 12 | final class ErrorHandler: ObservableObject { 13 | 14 | /// State is a struct that holds the error message and a flag to indicate whether to show the error. 15 | struct State { 16 | var error: String? 17 | var showError: Bool = false 18 | } 19 | 20 | /// The current state of the ErrorHandler. 21 | @Published var state: State = State(error: nil, showError: false) 22 | 23 | /// Sets the error state with the provided error. 24 | /// - Parameter error: The error to be set. 25 | func setError(_ error: Error) { 26 | self.state = State(error: error.localizedDescription, showError: true) 27 | } 28 | 29 | /// Resets the error state. 30 | func reset() { 31 | self.state = State(error: nil, showError: false) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/MarkTimeRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkTimeRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct MarkTimeRequest: Endpoint { 11 | 12 | public var id: Int 13 | public var time: Int 14 | public var video: Int = -1 15 | public var season: Int = -1 16 | 17 | public init(id: Int, time: Int, video: Int? = nil, season: Int? = nil) { 18 | self.id = id 19 | self.time = time 20 | self.video = video ?? -1 21 | self.season = season ?? -1 22 | } 23 | 24 | 25 | public var path: String { 26 | "/v1/watching/marktime" 27 | } 28 | 29 | public var method: String { 30 | "GET" 31 | } 32 | 33 | public var parameters: [String: Any]? { 34 | [ 35 | "id": id, 36 | "time": time, 37 | "video": video, 38 | "season": season 39 | ].filter({ $0.value != -1 }) 40 | } 41 | 42 | public var headers: [String: String]? { 43 | nil 44 | } 45 | 46 | public var forceSendAsGetParams: Bool { true } 47 | } 48 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Authorization/AuthorizationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationService.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | enum MockError: Error { 12 | case mock 13 | } 14 | 15 | protocol AuthorizationService { 16 | func fetchDeviceCode() async throws -> VerificationResponse 17 | func fetchToken(by verification: VerificationResponse) async throws 18 | func refreshToken() async throws 19 | func logout() 20 | } 21 | 22 | protocol AuthorizationServiceProvider { 23 | var authService: AuthorizationService { get set } 24 | } 25 | 26 | struct AuthorizationServiceMock: AuthorizationService { 27 | 28 | func fetchDeviceCode() async throws -> VerificationResponse { 29 | throw MockError.mock 30 | } 31 | 32 | func fetchToken(by verification: VerificationResponse) async throws { 33 | throw MockError.mock 34 | } 35 | 36 | func refreshToken() async throws { 37 | throw MockError.mock 38 | } 39 | 40 | func logout() { 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/WatchData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WatchData.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct WatchData: Codable, Hashable { 11 | 12 | public struct Season: Codable, Hashable { 13 | public var id: Int 14 | public var number: Int 15 | public var status: Int 16 | public var episodes: [WatchDataVideoItem] 17 | } 18 | 19 | public struct WatchDataItem: Codable, Hashable { 20 | public var seasons: [Season]? 21 | public var videos: [WatchDataVideoItem]? 22 | } 23 | 24 | public struct WatchDataVideoItem: Codable, Hashable { 25 | public var id: Int 26 | public var number: Int 27 | public var title: String 28 | public var time: TimeInterval 29 | public var status: Int 30 | } 31 | 32 | public var item: WatchDataItem 33 | 34 | init(item: WatchDataItem) { 35 | self.item = item 36 | } 37 | } 38 | 39 | public extension WatchData { 40 | static var mock: WatchData { 41 | WatchData(item: WatchDataItem()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/RefreshTokenRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshTokenRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct RefreshTokenRequest: Endpoint { 11 | 12 | public var clientID: String 13 | public var clientSecret: String 14 | public var refreshToken: String 15 | 16 | public init(clientID: String, clientSecret: String, refreshToken: String) { 17 | self.clientID = clientID 18 | self.clientSecret = clientSecret 19 | self.refreshToken = refreshToken 20 | } 21 | 22 | public var path: String { 23 | "/oauth2/token" 24 | } 25 | 26 | public var method: String { 27 | "POST" 28 | } 29 | 30 | public var parameters: [String: Any]? { 31 | [ 32 | "grant_type": "refresh_token", 33 | "client_id": clientID, 34 | "client_secret": clientSecret, 35 | "refresh_token": refreshToken 36 | ] 37 | } 38 | 39 | public var headers: [String: String]? { 40 | nil 41 | } 42 | 43 | public var forceSendAsGetParams: Bool { false } 44 | } 45 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Extensions/View+Skeleton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Skeleton.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 2.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import SkeletonUI 11 | 12 | public extension View { 13 | func skeleton(enabled: Bool, size: CGSize? = nil) -> some View { 14 | self.skeleton(with: enabled, size: size) 15 | .appearance(type: .gradient(.linear, color: Color.KinoPub.skeleton, background: Color.KinoPub.skeleton.opacity(0.8), radius: 2.0, angle: 2.0)) 16 | .animation(type: .linear()) 17 | .shape(type: .rounded(.radius(6, style: .continuous))) 18 | } 19 | 20 | func multilineSkeleton(enabled: Bool, size: CGSize? = nil) -> some View { 21 | self.skeleton(with: enabled) 22 | .appearance(type: .gradient(.linear, color: Color.KinoPub.skeleton, background: Color.KinoPub.skeleton.opacity(0.8), radius: 2.0, angle: 2.0)) 23 | .animation(type: .linear()) 24 | .shape(type: .rounded(.radius(6, style: .continuous))) 25 | .multiline(lines: 3, scales: [1: 0.8, 2: 0.5]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/ShortcutItemsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutItemsRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ShortcutItemsRequest: Endpoint { 11 | 12 | private var shortcut: MediaShortcut 13 | private var contentType: MediaType 14 | private var page: Int? 15 | 16 | public init(shortcut: MediaShortcut, contentType: MediaType, page: Int? = nil) { 17 | self.shortcut = shortcut 18 | self.contentType = contentType 19 | self.page = page 20 | } 21 | 22 | public var path: String { 23 | "/v1/items/\(shortcut.rawValue)" 24 | } 25 | 26 | public var method: String { 27 | "GET" 28 | } 29 | 30 | public var parameters: [String: Any]? { 31 | var params = [ 32 | "type": contentType.rawValue 33 | ] 34 | 35 | if let page = page { 36 | params["page"] = "\(page)" 37 | } 38 | 39 | return params 40 | } 41 | 42 | public var headers: [String: String]? { 43 | nil 44 | } 45 | 46 | public var forceSendAsGetParams: Bool { false } 47 | } 48 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/UserActions/UserActionsServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserActionsServiceImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 11.11.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | final class UserActionsServiceImpl: UserActionsService { 12 | 13 | private var apiClient: APIClient 14 | 15 | init(apiClient: APIClient) { 16 | self.apiClient = apiClient 17 | } 18 | 19 | func markWatch(id: Int, time: Int, video: Int?, season: Int?) async throws { 20 | let request = MarkTimeRequest(id: id, time: time, video: video, season: season) 21 | _ = try await apiClient.performRequest(with: request, decodingType: EmptyResponseData.self) 22 | } 23 | 24 | func fetchWatchMark(id: Int, video: Int?, season: Int?) async throws -> WatchData { 25 | let request = GetWatchingDataRequest(id: id, video: video, season: season) 26 | return try await apiClient.performRequest(with: request, decodingType: WatchData.self) 27 | } 28 | 29 | func toggleWatching(id: Int, video: Int?, season: Int?) async throws { 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/APIClientError+Description.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClientError+Description.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | extension APIClientError: CustomStringConvertible { 12 | public var description: String { 13 | switch self { 14 | case .urlError: 15 | return "Wrong URL" 16 | case .invalidUrlParams: 17 | return "Invalid URL params" 18 | case .decodingError(let error): 19 | return "Decoding issue: \(error)" 20 | case .networkError(let error): 21 | if let error = error as? BackendError { 22 | return error.errorDescription ?? error.localizedDescription 23 | } 24 | return "Networking issue: \(error)" 25 | } 26 | } 27 | 28 | var isAuthorizationPending: Bool { 29 | switch self { 30 | case .networkError(let error): 31 | if let backendError = error as? BackendError, backendError.errorCode == .authorizationPending { 32 | return true 33 | } 34 | break 35 | default: return false 36 | } 37 | return false 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/User/UserServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserServiceImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 9.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | final class UserServiceImpl: UserService { 12 | 13 | private var apiClient: APIClient 14 | 15 | init(apiClient: APIClient) { 16 | self.apiClient = apiClient 17 | } 18 | 19 | func fetch(shortcut: MediaShortcut, contentType: MediaType, page: Int?) async throws -> PaginatedData { 20 | let request = ShortcutItemsRequest(shortcut: shortcut, contentType: contentType, page: page) 21 | let response = try await apiClient.performRequest(with: request, 22 | decodingType: PaginatedData.self) 23 | return response 24 | } 25 | 26 | func fetchUserData() async throws -> UserData { 27 | let request = UserDataRequest() 28 | let response = try await apiClient.performRequest(with: request, 29 | decodingType: UserDataResponse.self) 30 | return response.user 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/KeychainStorage/KeychainStorageImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainStorageImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KeychainAccess 10 | 11 | final class KeychainStorageImpl: KeychainStorage { 12 | 13 | private lazy var keychain: Keychain = { 14 | return Keychain(service: "com.kunst.kinopub") 15 | }() 16 | 17 | public func object(for key: Key) -> Value? where Value: Decodable, Value: Encodable { 18 | do { 19 | guard let data = try keychain.getData(key.rawValue) else { return nil } 20 | return try JSONDecoder().decode(Value.self, from: data) 21 | } catch { 22 | print(error) 23 | return nil 24 | } 25 | } 26 | 27 | public func setObject(_ object: Value?, for key: Key) where Value: Decodable, Value: Encodable { 28 | do { 29 | let data = try JSONEncoder().encode(object) 30 | try keychain.set(data, key: key.rawValue) 31 | } catch { 32 | print(error) 33 | } 34 | } 35 | 36 | func clear() { 37 | do { 38 | try keychain.removeAll() 39 | } catch { 40 | print(error) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /KinoPubAppleClient/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 24.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import FirebaseCore 11 | 12 | #if os(iOS) 13 | class AppDelegate: NSObject, UIApplicationDelegate { 14 | 15 | // This flag is used to lock orientation on the player view 16 | static var orientationLock = UIInterfaceOrientationMask.all 17 | 18 | func application(_ application: UIApplication, 19 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { 20 | FirebaseApp.configure() 21 | UIDevice.current.beginGeneratingDeviceOrientationNotifications() 22 | return true 23 | } 24 | 25 | func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { 26 | return AppDelegate.orientationLock 27 | } 28 | } 29 | #endif 30 | 31 | #if os(macOS) 32 | class AppDelegate: NSObject, NSApplicationDelegate { 33 | 34 | var window: NSWindow? 35 | 36 | func applicationDidFinishLaunching(_ notification: Notification) { 37 | FirebaseApp.configure() 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/SearchItemsRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchItemsRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SearchItemsRequest: Endpoint { 11 | 12 | private var contentType: MediaType? 13 | private var page: Int? 14 | private var query: String? 15 | 16 | public init(contentType: MediaType?, page: Int? = nil, query: String? = nil) { 17 | self.contentType = contentType 18 | self.page = page 19 | self.query = query 20 | } 21 | 22 | public var path: String { 23 | "/v1/items/search" 24 | } 25 | 26 | public var method: String { 27 | "GET" 28 | } 29 | 30 | public var parameters: [String: Any]? { 31 | var params = [String: Any]() 32 | 33 | if let contentType = contentType { 34 | params["type"] = contentType.rawValue 35 | } 36 | 37 | if let page = page { 38 | params["page"] = "\(page)" 39 | } 40 | 41 | if let query = query { 42 | params["q"] = query 43 | } 44 | 45 | return params 46 | } 47 | 48 | public var headers: [String: String]? { 49 | nil 50 | } 51 | 52 | public var forceSendAsGetParams: Bool { false } 53 | } 54 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Sources/KinoPubKit/Downloading/FileSaver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSaver.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol FileSaving { 11 | func saveFile(from sourceURL: URL, to destinationURL: URL) throws 12 | func removeFile(at sourceURL: URL) throws 13 | func getDocumentsDirectoryURL(forFilename filename: String) -> URL 14 | } 15 | 16 | public class FileSaver: FileSaving { 17 | private let fileManager: FileManager 18 | 19 | public init(fileManager: FileManager = .default) { 20 | self.fileManager = fileManager 21 | } 22 | 23 | public func saveFile(from sourceURL: URL, to destinationURL: URL) throws { 24 | try? fileManager.removeItem(at: destinationURL) 25 | try fileManager.moveItem(at: sourceURL, to: destinationURL) 26 | } 27 | 28 | public func removeFile(at sourceURL: URL) throws { 29 | try fileManager.removeItem(at: sourceURL) 30 | } 31 | 32 | public func getDocumentsDirectoryURL(forFilename filename: String) -> URL { 33 | let documentsDirectoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! 34 | return documentsDirectoryURL.appendingPathComponent(filename) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Downloads/Models/DownloadMeta.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadMeta.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 10.11.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | public struct DownloadMeta: PlayableItem, Codable, Equatable { 12 | public var id: Int 13 | public var files: [FileInfo] 14 | public var trailer: Trailer? { nil } 15 | public var originalTitle: String 16 | public var localizedTitle: String 17 | public var imageUrl: String 18 | public var metadata: WatchingMetadata 19 | } 20 | 21 | extension DownloadMeta { 22 | static func make(from item: DownloadableMediaItem) -> DownloadMeta { 23 | let originalTitle = item.mediaItem.isSeries ? "\(item.mediaItem.originalTitle) \(item.name)" : item.mediaItem.originalTitle 24 | let localizedTitle = item.mediaItem.isSeries ? "\(item.mediaItem.localizedTitle) \(item.name)" : item.mediaItem.localizedTitle 25 | return DownloadMeta(id: item.mediaItem.id, 26 | files: item.files, 27 | originalTitle: originalTitle, 28 | localizedTitle: localizedTitle, 29 | imageUrl: item.mediaItem.posters.small, metadata: item.watchingMetadata) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Bookmarks/Item/BookmarkModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | import OSLog 11 | import KinoPubLogging 12 | 13 | @MainActor 14 | class BookmarkModel: ObservableObject { 15 | 16 | private var contentService: VideoContentService 17 | private var errorHandler: ErrorHandler 18 | 19 | public var bookmark: Bookmark 20 | @Published public var items: [MediaItem] = MediaItem.skeletonMock() 21 | 22 | init(bookmark: Bookmark, itemsService: VideoContentService, errorHandler: ErrorHandler) { 23 | self.contentService = itemsService 24 | self.bookmark = bookmark 25 | self.errorHandler = errorHandler 26 | } 27 | 28 | func fetchItems() async { 29 | do { 30 | items = try await contentService.fetchBookmarkItems(id: "\(bookmark.id)").items 31 | } catch { 32 | Logger.app.debug("fetch bookmark items error: \(error)") 33 | errorHandler.setError(error) 34 | } 35 | } 36 | 37 | @MainActor 38 | func refresh() { 39 | items = MediaItem.skeletonMock() 40 | Task { 41 | Logger.app.debug("refetch bookmark items") 42 | await fetchItems() 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Resources/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 754106153209-ovdgog4q5nq9dma8gghcq1c7a4j25uu0.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.754106153209-ovdgog4q5nq9dma8gghcq1c7a4j25uu0 9 | API_KEY 10 | AIzaSyC7_2CZUOVhauKhaxRt2FDSq0wy0orDxtk 11 | GCM_SENDER_ID 12 | 754106153209 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.kunst.kinopub 17 | PROJECT_ID 18 | kinopub-apple-client 19 | STORAGE_BUCKET 20 | kinopub-apple-client.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:754106153209:ios:840acdc135b127eafb4a28 33 | 34 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Player/PlayerContinueWatchingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerContinueWatchingView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 13.11.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubUI 11 | 12 | struct PlayerContinueWatchingView: View { 13 | 14 | var time: TimeInterval 15 | var onContinueWatching: () -> Void 16 | var onCancelContinueWatching: () -> Void 17 | 18 | var body: some View { 19 | VStack { 20 | Text("Continue Watching") 21 | .font(Font.KinoPub.small) 22 | Text(formatTimeInterval(time)) 23 | .font(Font.KinoPub.small) 24 | HStack { 25 | KinoPubButton(title: "Yes".localized, color: .blue) { 26 | onContinueWatching() 27 | }.frame(width: 60, height: 30) 28 | KinoPubButton(title: "\("No".localized)", color: .green) { 29 | onCancelContinueWatching() 30 | }.frame(width: 60, height: 30) 31 | } 32 | } 33 | } 34 | 35 | func formatTimeInterval(_ interval: TimeInterval) -> String { 36 | let formatter = DateComponentsFormatter() 37 | formatter.allowedUnits = [.hour, .minute, .second] 38 | formatter.unitsStyle = .positional 39 | formatter.zeroFormattingBehavior = .pad 40 | return formatter.string(from: interval) ?? "00:00:00" 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/Season/SeasonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 4.11.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubUI 11 | import KinoPubBackend 12 | 13 | struct SeasonView: View { 14 | @StateObject private var model: SeasonModel 15 | 16 | init(model: @autoclosure @escaping () -> SeasonModel) { 17 | _model = StateObject(wrappedValue: model()) 18 | } 19 | 20 | var cellSize: Double { 140 } 21 | 22 | var body: some View { 23 | VStack { 24 | listView 25 | } 26 | .navigationTitle(model.season.fixedTitle) 27 | .background(Color.KinoPub.background) 28 | } 29 | 30 | var gridLayout: [GridItem] { 31 | [GridItem(.adaptive(minimum: cellSize), spacing: 16, alignment: .top)] 32 | } 33 | 34 | var listView: some View { 35 | ScrollView { 36 | LazyVGrid(columns: gridLayout, content: { 37 | ForEach(model.season.episodes, id: \.id) { item in 38 | NavigationLink(value: model.linkProvider.player(for: model.filledEpisode(item))) { 39 | SeasonItemView(episode: item) 40 | .padding(.bottom, 16) 41 | } 42 | #if os(macOS) 43 | .buttonStyle(PlainButtonStyle()) 44 | #endif 45 | } 46 | }) 47 | .padding(.horizontal, 16) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/PlayerTimeObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerTimeObserver.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 13.11.2023. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | class PlayerTimeObserver { 12 | private weak var player: AVPlayer? 13 | private var timeObserverToken: Any? 14 | private var timeUpdateHandler: ((TimeInterval) -> Void)? 15 | private var period: TimeInterval 16 | 17 | init(player: AVPlayer, period: TimeInterval, timeUpdateHandler: @escaping (Double) -> Void) { 18 | self.player = player 19 | self.period = period 20 | self.timeUpdateHandler = timeUpdateHandler 21 | addPeriodicTimeObserver() 22 | } 23 | 24 | private func addPeriodicTimeObserver() { 25 | guard let player = player else { return } 26 | 27 | let timeScale = CMTimeScale(NSEC_PER_SEC) 28 | let time = CMTime(seconds: period, preferredTimescale: timeScale) 29 | 30 | timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .global(qos: .userInteractive)) { [weak self] time in 31 | if time.seconds > 60.0 { 32 | self?.timeUpdateHandler?(time.seconds) 33 | } 34 | } 35 | } 36 | 37 | deinit { 38 | if let timeObserverToken = timeObserverToken, let player = player { 39 | player.removeTimeObserver(timeObserverToken) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Season.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Season.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Season: Codable, Hashable, Identifiable { 11 | public static func == (lhs: Season, rhs: Season) -> Bool { 12 | lhs.id == rhs.id 13 | } 14 | 15 | public func hash(into hasher: inout Hasher) { 16 | hasher.combine(id) 17 | } 18 | 19 | public let id: Int 20 | public let title: String 21 | public let number: Int 22 | public let watching: SeasonWatching 23 | public let episodes: [Episode] 24 | public var mediaId: Int? = nil 25 | 26 | public init(id: Int, 27 | title: String, 28 | number: Int, 29 | watching: SeasonWatching, 30 | episodes: [Episode]) { 31 | self.id = id 32 | self.title = title 33 | self.number = number 34 | self.watching = watching 35 | self.episodes = episodes 36 | } 37 | 38 | private enum CodingKeys: String, CodingKey { 39 | case id = "id" 40 | case title = "title" 41 | case number = "number" 42 | case watching = "watching" 43 | case episodes = "episodes" 44 | case mediaId = "mediaId" 45 | } 46 | 47 | public var fixedTitle: String { 48 | if title.isEmpty { 49 | return "Сезон \(number)" 50 | } 51 | return title 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Sources/KinoPubKit/Downloading/DownloadedFileInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadedFileInfo.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | /// DownloadedFileInfo is a struct that contains information about a downloaded file. 11 | /// 12 | /// It has the following properties: 13 | /// 14 | /// - originalURL: The original URL of the file that was downloaded. 15 | /// 16 | /// - localFilename: The filename of the downloaded file saved locally. 17 | /// 18 | /// - downloadDate: The date when the file was downloaded. 19 | /// 20 | /// - metadata: Generic metadata associated with the file. Metadata must conform to Codable and Equatable. 21 | /// 22 | /// DownloadedFileInfo conforms to Codable so it can be encoded/decoded. 23 | /// The Meta generic type allows custom metadata types to be stored for each downloaded file. 24 | public struct DownloadedFileInfo: Codable { 25 | public let originalURL: URL 26 | public let localFilename: String 27 | public let downloadDate: Date 28 | public let metadata: Meta 29 | } 30 | 31 | public extension DownloadedFileInfo { 32 | var localFileURL: URL { 33 | let basePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 34 | return basePath.appending(path: localFilename) 35 | } 36 | } 37 | 38 | extension DownloadedFileInfo: Equatable {} 39 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Requests/DeviceCodeRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceCodeRequest.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum DeviceCodeGrantType: String { 11 | case deviceCode = "device_code" 12 | case deviceToken = "device_token" 13 | } 14 | 15 | public struct DeviceCodeRequest: Endpoint { 16 | 17 | public var grantType: DeviceCodeGrantType 18 | public var clientID: String 19 | public var clientSecret: String 20 | public var code: String? 21 | 22 | public init(grantType: DeviceCodeGrantType, clientID: String, clientSecret: String, code: String? = nil) { 23 | self.grantType = grantType 24 | self.clientID = clientID 25 | self.clientSecret = clientSecret 26 | self.code = code 27 | } 28 | 29 | public var path: String { 30 | "/oauth2/device" 31 | } 32 | 33 | public var method: String { 34 | "POST" 35 | } 36 | 37 | public var parameters: [String: Any]? { 38 | var params = [ 39 | "grant_type": grantType.rawValue, 40 | "client_id": clientID, 41 | "client_secret": clientSecret 42 | ] 43 | 44 | if let code = code { 45 | params["code"] = code 46 | } 47 | 48 | return params 49 | } 50 | 51 | public var headers: [String: String]? { 52 | nil 53 | } 54 | 55 | public var forceSendAsGetParams: Bool { 56 | return true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/macOS/Sidebar/SidebarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 11.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubUI 11 | import KinoPubBackend 12 | 13 | #if os(macOS) 14 | struct SidebarView: View { 15 | 16 | @Environment(\.appContext) var appContext 17 | @EnvironmentObject var navigationState: NavigationState 18 | @EnvironmentObject var errorHandler: ErrorHandler 19 | @EnvironmentObject var authState: AuthState 20 | 21 | var body: some View { 22 | NavigationSplitView(columnVisibility: $navigationState.columnVisibility) { 23 | Sidebar(selection: $navigationState.selectedTab) 24 | } detail: { 25 | SidebarNavigationDetail(selection: $navigationState.selectedTab) 26 | } 27 | .accentColor(Color.KinoPub.accent) 28 | .sheet(isPresented: $authState.shouldShowAuthentication, content: { 29 | AuthView(model: AuthModel(authService: appContext.authService, 30 | authState: authState, 31 | errorHandler: errorHandler)) 32 | .frame(width: 600, height: 600) 33 | }) 34 | .environmentObject(navigationState) 35 | .environmentObject(errorHandler) 36 | .task { 37 | await authState.check() 38 | } 39 | } 40 | 41 | } 42 | 43 | struct SideBarView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | SidebarView() 46 | } 47 | } 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Content/ContentItemRatingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentItemRatingView.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 24.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import SkeletonUI 11 | public struct ContentItemRatingView: View { 12 | 13 | var imdbScore: Double? 14 | var kinopoiskScore: Double? 15 | 16 | public var body: some View { 17 | HStack { 18 | imdbImage 19 | imdbRating 20 | kpImage 21 | kpRating 22 | } 23 | .padding(.horizontal, 15) 24 | .padding(.vertical, 4) 25 | .background(Color.KinoPub.selectionBackground) 26 | .cornerRadius(8) 27 | .opacity(isEmpty ? 0 : 1) 28 | } 29 | 30 | var isEmpty: Bool { 31 | imdbScore == nil && kinopoiskScore == nil 32 | } 33 | 34 | var imdbImage: some View { 35 | Image("imdb", bundle: .module) 36 | .resizable() 37 | .tint(.white) 38 | .colorInvert() 39 | .frame(width: 24, height: 24) 40 | } 41 | 42 | var kpImage: some View { 43 | Image("kinopoisk", bundle: .module) 44 | .resizable() 45 | .frame(width: 20, height: 20) 46 | } 47 | 48 | var imdbRating: some View { 49 | Text("\(imdbScore?.scoreFormatted ?? "0.0")") 50 | .foregroundColor(.white) 51 | } 52 | 53 | var kpRating: some View { 54 | Text("\(kinopoiskScore?.scoreFormatted ?? "0.0")") 55 | .foregroundColor(.white) 56 | } 57 | 58 | } 59 | 60 | #Preview { 61 | ContentItemRatingView(imdbScore: 5.0, kinopoiskScore: 5.0) 62 | } 63 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Button/KinoPubButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public struct KinoPubButton: View { 12 | 13 | public enum ButtonColor { 14 | case green 15 | case gray 16 | case red 17 | case blue 18 | 19 | internal var color: Color { 20 | switch self { 21 | case .green: 22 | return Color.KinoPub.accent 23 | case .red: 24 | return Color.KinoPub.accentRed 25 | case .gray: 26 | return Color.KinoPub.selectionBackground 27 | case .blue: 28 | return Color.KinoPub.accentBlue 29 | } 30 | } 31 | } 32 | 33 | public var title: String 34 | public var color: ButtonColor 35 | public var action: () -> Void 36 | 37 | public init(title: String, color: ButtonColor, action: @escaping () -> Void) { 38 | self.title = title 39 | self.action = action 40 | self.color = color 41 | } 42 | 43 | public var body: some View { 44 | Button(action: action) { 45 | Text(title) 46 | .padding(.horizontal, 8) 47 | .frame(maxWidth: .infinity, maxHeight: 40) 48 | .font(.system(size: 16, weight: .semibold)) 49 | } 50 | .buttonStyle(KinoPubButtonStyle(buttonColor: color)) 51 | 52 | } 53 | } 54 | 55 | struct KinoPubButton_Previews: PreviewProvider { 56 | static var previews: some View { 57 | KinoPubButton(title: "Watch", color: .green, action: { 58 | 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/Season/SeasonItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonItemView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 10.11.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | import KinoPubUI 12 | 13 | public struct SeasonItemView: View { 14 | 15 | private var episode: Episode 16 | 17 | init(episode: Episode) { 18 | self.episode = episode 19 | } 20 | 21 | public var body: some View { 22 | VStack(alignment: .center) { 23 | ZStack { 24 | image 25 | VStack { 26 | Spacer() 27 | title 28 | .padding(.horizontal, 4) 29 | .padding(.bottom, 4) 30 | } 31 | } 32 | } 33 | .background(Color.clear) 34 | } 35 | 36 | var image: some View { 37 | AsyncImage(url: URL(string: episode.thumbnail)) { image in 38 | image.resizable() 39 | .renderingMode(.original) 40 | .posterStyle(size: .regular, orientation: .horizontal) 41 | } placeholder: { 42 | Color.KinoPub.skeleton 43 | .frame(width: PosterStyle.Size.regular.height, 44 | height: PosterStyle.Size.regular.width) 45 | } 46 | .cornerRadius(8) 47 | } 48 | 49 | var title: some View { 50 | Text(episode.fixedTitle) 51 | .padding(.vertical, 3) 52 | .padding(.horizontal, 6) 53 | .font(.system(size: 14.0, weight: .medium)) 54 | .foregroundStyle(Color.KinoPub.text) 55 | .background(Color.black.opacity(0.7)) 56 | 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /KinoPubAppleClient/App/KinoPubAppleClientApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KinoPubAppleClientApp.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 17.07.2023. 6 | // 7 | 8 | import SwiftUI 9 | import FirebaseCore 10 | 11 | enum WindowSize { 12 | static let macos = CGSize(width: 1280, height: 720) 13 | } 14 | 15 | @main 16 | struct KinoPubAppleClientApp: App { 17 | 18 | @StateObject var navigationState = NavigationState() 19 | @StateObject var errorHandler = ErrorHandler() 20 | @StateObject var authState = AuthState(authService: AppContext.shared.authService, 21 | accessTokenService: AppContext.shared.accessTokenService) 22 | 23 | #if os(macOS) 24 | @StateObject var windowSettings = WindowSettings() 25 | #endif 26 | 27 | #if os(iOS) 28 | @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate 29 | #endif 30 | 31 | #if os(macOS) 32 | @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate 33 | #endif 34 | 35 | var body: some Scene { 36 | WindowGroup { 37 | RootView() 38 | .environment(\.appContext, AppContext.shared) 39 | .environmentObject(navigationState) 40 | .environmentObject(authState) 41 | .environmentObject(errorHandler) 42 | #if os(macOS) 43 | .frame(minWidth: WindowSize.macos.width, minHeight: WindowSize.macos.height) 44 | #endif 45 | } 46 | #if os(macOS) 47 | .windowResizability(.contentSize) 48 | #endif 49 | 50 | #if os(macOS) 51 | Settings { 52 | SettingsView() 53 | .environmentObject(windowSettings) 54 | } 55 | #endif 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/Plugins/CURLLoggingPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CURLLoggingPlugin.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | import KinoPubLogging 11 | 12 | public class CURLLoggingPlugin: APIClientPlugin { 13 | 14 | public init() {} 15 | 16 | public func prepare(_ request: URLRequest) -> URLRequest { 17 | return request 18 | } 19 | 20 | public func willSend(_ request: URLRequest) { 21 | Logger.backend.debug("\(request.cURL(pretty: true))") 22 | } 23 | 24 | public func didReceive(_ response: URLResponse, data: Data?) { 25 | // Do nothing 26 | } 27 | } 28 | 29 | extension URLRequest { 30 | public func cURL(pretty: Bool = false) -> String { 31 | let newLine = pretty ? "\\\n" : "" 32 | let method = (pretty ? "--request " : "-X ") + "\(self.httpMethod ?? "GET") \(newLine)" 33 | let url: String = (pretty ? "--url " : "") + "\'\(self.url?.absoluteString ?? "")\' \(newLine)" 34 | 35 | var cURL = "curl " 36 | var header = "" 37 | var data: String = "" 38 | 39 | if let httpHeaders = self.allHTTPHeaderFields, httpHeaders.keys.count > 0 { 40 | for (key, value) in httpHeaders { 41 | header += (pretty ? "--header " : "-H ") + "\'\(key): \(value)\' \(newLine)" 42 | } 43 | } 44 | 45 | if let bodyData = self.httpBody, let bodyString = String(data: bodyData, encoding: .utf8), !bodyString.isEmpty { 46 | data = "--data '\(bodyString)'" 47 | } 48 | 49 | cURL += method + url + header + data 50 | 51 | return cURL 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/VideoContent/VideoContentService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoContentService.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | protocol VideoContentService { 12 | func fetch(shortcut: MediaShortcut, contentType: MediaType, page: Int?) async throws -> PaginatedData 13 | func search(query: String?, page: Int?) async throws -> PaginatedData 14 | func fetchDetails(for id: String) async throws -> SingleItemData 15 | func fetchBookmarks() async throws -> ArrayData 16 | func fetchBookmarkItems(id: String) async throws -> ArrayData 17 | } 18 | 19 | protocol VideoContentServiceProvider { 20 | var contentService: VideoContentService { get set } 21 | } 22 | 23 | struct VideoContentServiceMock: VideoContentService { 24 | 25 | func fetch(shortcut: MediaShortcut, contentType: MediaType, page: Int?) async throws -> PaginatedData { 26 | return PaginatedData.mock(data: []) 27 | } 28 | 29 | func search(query: String?, page: Int?) async throws -> PaginatedData { 30 | return PaginatedData.mock(data: []) 31 | } 32 | 33 | func fetchDetails(for id: String) async throws -> SingleItemData { 34 | return SingleItemData.mock(data: MediaItem.mock()) 35 | } 36 | 37 | func fetchBookmarks() async throws -> ArrayData { 38 | return ArrayData.mock(data: []) 39 | } 40 | 41 | func fetchBookmarkItems(id: String) async throws -> ArrayData { 42 | return ArrayData.mock(data: []) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Bookmarks/List/BookmarksCatalog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarksCatalog.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | import OSLog 11 | import KinoPubLogging 12 | import Combine 13 | 14 | @MainActor 15 | class BookmarksCatalog: ObservableObject { 16 | 17 | private var authState: AuthState 18 | private var contentService: VideoContentService 19 | private var errorHandler: ErrorHandler 20 | private var bag = Set() 21 | 22 | @Published public var items: [Bookmark] = Bookmark.skeletonMock() 23 | 24 | init(itemsService: VideoContentService, authState: AuthState, errorHandler: ErrorHandler) { 25 | self.contentService = itemsService 26 | self.authState = authState 27 | self.errorHandler = errorHandler 28 | } 29 | 30 | func fetchItems() async { 31 | guard authState.userState == .authorized else { 32 | subscribeForAuth() 33 | return 34 | } 35 | 36 | do { 37 | items = try await contentService.fetchBookmarks().items 38 | } catch { 39 | Logger.app.debug("fetch bookmarks error: \(error)") 40 | errorHandler.setError(error) 41 | } 42 | } 43 | 44 | 45 | 46 | @Sendable @MainActor 47 | func refresh() async { 48 | items = Bookmark.skeletonMock() 49 | Logger.app.debug("refetch bookmarks") 50 | await fetchItems() 51 | } 52 | 53 | private func subscribeForAuth() { 54 | authState.$userState.filter({ $0 == .authorized }).first().sink { [weak self] _ in 55 | Task { 56 | await self?.refresh() 57 | } 58 | }.store(in: &bag) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Profile/ProfileModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 9.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | import KinoPubLogging 11 | import OSLog 12 | 13 | @MainActor 14 | class ProfileModel: ObservableObject { 15 | 16 | private var userService: UserService 17 | private var errorHandler: ErrorHandler 18 | private var authState: AuthState 19 | 20 | @Published public var userData: UserData = UserData.mock() 21 | @Published var selectedLanguage: String 22 | @Published var shouldShowExitAlert: Bool = false 23 | 24 | let availableLanguages = ["en": "English", "lt": "Lietuvių", "ru": "Русский"] 25 | 26 | init(userService: UserService, 27 | errorHandler: ErrorHandler, 28 | authState: AuthState) { 29 | self.userService = userService 30 | self.errorHandler = errorHandler 31 | self.authState = authState 32 | self.selectedLanguage = UserDefaults.standard.string(forKey: "selectedLanguage") ?? (Locale.current.language.languageCode?.identifier ?? "en") 33 | } 34 | func fetch() { 35 | Task { 36 | do { 37 | self.userData = try await userService.fetchUserData() 38 | } catch { 39 | errorHandler.setError(error) 40 | } 41 | } 42 | } 43 | 44 | func logout() { 45 | authState.logout() 46 | } 47 | 48 | func changeLanguage(to language: String) { 49 | UserDefaults.standard.setValue([language], forKey: "AppleLanguages") 50 | UserDefaults.standard.synchronize() 51 | shouldShowExitAlert = true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Button/ProgressButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 8.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public struct ProgressButton: View { 12 | 13 | public enum ProgressState { 14 | case pause 15 | case resume 16 | } 17 | 18 | private var action: (ProgressState) -> Void 19 | var progress: Float 20 | 21 | public init(progress: Float, action: @escaping (ProgressState) -> Void) { 22 | self.action = action 23 | self.progress = progress 24 | } 25 | 26 | @State private var progressState = ProgressState.resume 27 | 28 | public var body: some View { 29 | ZStack { 30 | // Progress Ring 31 | 32 | Circle() 33 | .trim(from: 0.0, to: 1.0) 34 | .stroke(Color.KinoPub.accent.opacity(0.2), lineWidth: 3) 35 | .background(Color.clear) 36 | .frame(width: 35, height: 35) 37 | .rotationEffect(.degrees(-90)) 38 | 39 | Circle() 40 | .trim(from: 0.0, to: CGFloat(progress)) 41 | .stroke(Color.KinoPub.accent, lineWidth: 3) 42 | .background(Color.clear) 43 | .frame(width: 40, height: 40) 44 | .rotationEffect(.degrees(-90)) 45 | 46 | Button(action: { 47 | progressState = progressState == .pause ? .resume : .pause 48 | action(progressState) 49 | }) { 50 | Image(systemName: progressState == .resume ? "pause.fill" : "play.fill") 51 | .foregroundColor(Color.KinoPub.accent) 52 | .font(.headline) 53 | .frame(width: 20, height: 20) 54 | .background(Color.clear) 55 | .clipShape(Circle()) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /KinoPubAppleClient.xcodeproj/xcuserdata/kirillkunst.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | KinoPubAppleClient.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 4 11 | 12 | Promises (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 12 18 | 19 | Promises (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 13 25 | 26 | Promises (Playground) 3.xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 14 32 | 33 | Promises (Playground) 4.xcscheme 34 | 35 | isShown 36 | 37 | orderHint 38 | 15 39 | 40 | Promises (Playground) 5.xcscheme 41 | 42 | isShown 43 | 44 | orderHint 45 | 16 46 | 47 | Promises (Playground).xcscheme 48 | 49 | isShown 50 | 51 | orderHint 52 | 11 53 | 54 | 55 | SuppressBuildableAutocreation 56 | 57 | CC88E1E02A6586E600E9387F 58 | 59 | primary 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Custom/BestVideoQualityFinder.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // BestVideoQualityFinder.swift 4 | // KinoPubAppleClient 5 | // 6 | // Created by Kirill Kunst on 11.08.2023. 7 | // 8 | 9 | import Foundation 10 | #if os(iOS) 11 | import UIKit 12 | #endif 13 | import SystemConfiguration 14 | import Reachability 15 | import KinoPubBackend 16 | 17 | struct BestVideoQualityFinder { 18 | 19 | #if os(iOS) 20 | private static var deviceCapabilitySize: CGFloat { 21 | UIApplication.shared.statusBarOrientation.isLandscape ? UIScreen.main.bounds.width : UIScreen.main.bounds.height 22 | } 23 | #endif 24 | 25 | private static func currentNetworkStatus() -> Reachability.Connection { 26 | guard let reachability = try? Reachability() else { return .unavailable } 27 | return reachability.connection 28 | } 29 | 30 | private static func isConnectionGood() -> Bool { 31 | currentNetworkStatus() == .wifi 32 | } 33 | 34 | static func findBestURL(for files: [FileInfo]) -> String { 35 | var bestURL: String = files.last?.url.hls4 ?? "" 36 | var closestResolutionDifference = Int.max 37 | 38 | #if os(macOS) 39 | bestURL = files.first?.url.hls4 ?? "" 40 | #endif 41 | 42 | #if os(iOS) 43 | guard isConnectionGood() else { 44 | return bestURL 45 | } 46 | 47 | for fileInfo in files { 48 | let resolutionDifference = abs(fileInfo.resolution - Int(deviceCapabilitySize)) 49 | 50 | if fileInfo.resolution <= Int(deviceCapabilitySize) && resolutionDifference < closestResolutionDifference { 51 | bestURL = fileInfo.url.hls4 52 | closestResolutionDifference = resolutionDifference 53 | } 54 | } 55 | #endif 56 | 57 | return bestURL 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Bookmarks/Item/BookmarkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BookmarkView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 6.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubUI 11 | import KinoPubBackend 12 | 13 | struct BookmarkView: View { 14 | @EnvironmentObject var navigationState: NavigationState 15 | @EnvironmentObject var errorHandler: ErrorHandler 16 | 17 | @StateObject private var model: BookmarkModel 18 | @Environment(\.appContext) var appContext 19 | 20 | init(model: @autoclosure @escaping () -> BookmarkModel) { 21 | _model = StateObject(wrappedValue: model()) 22 | } 23 | 24 | var body: some View { 25 | VStack { 26 | listView 27 | } 28 | .navigationTitle(model.bookmark.title) 29 | .background(Color.KinoPub.background) 30 | .task { 31 | await model.fetchItems() 32 | } 33 | .handleError(state: $errorHandler.state) 34 | } 35 | 36 | var listView: some View { 37 | GeometryReader { geometryProxy in 38 | ContentItemsListView(width: geometryProxy.size.width, items: $model.items, onLoadMoreContent: { item in 39 | 40 | }, onRefresh: { 41 | await model.refresh() 42 | }, navigationLinkProvider: { item in 43 | BookmarksRoutesLinkProvider().link(for: item) 44 | }) 45 | } 46 | } 47 | } 48 | // 49 | //struct BookmarkView_Previews: PreviewProvider { 50 | // @StateObject static var navState = NavigationState() 51 | // 52 | // static var previews: some View { 53 | // MainView(catalog: MediaCatalog(itemsService: VideoContentServiceMock(), authState: AuthState(authService: AuthorizationServiceMock(), accessTokenService: AccessTokenServiceMock()))) 54 | // .environmentObject(navState) 55 | // } 56 | //} 57 | // 58 | -------------------------------------------------------------------------------- /Packages/KinoPubLogging/Sources/KinoPubLogging/Logger+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger+Extension.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | /// An enumeration representing different categories for logging. 12 | /// These categories can be used to categorize and filter logs. 13 | enum LoggingCategory: String { 14 | case viewCycle // Represents logging related to the view's lifecycle events 15 | case analytics // Represents logging related to analytics events or data 16 | case backend // Represents logging related to backend interactions and responses 17 | case app // Represents general application-level logging 18 | case kit // Represents logging related to kit 19 | } 20 | 21 | public extension Logger { 22 | 23 | /// The application's bundle identifier, used as the subsystem for logging. 24 | private static var subsystem = Bundle.main.bundleIdentifier! 25 | 26 | /// Logger instance specific to `viewCycle` logging. 27 | static let viewCycle = Logger(subsystem: subsystem, category: LoggingCategory.viewCycle.rawValue) 28 | 29 | /// Logger instance specific to `analytics` logging. 30 | static let analytics = Logger(subsystem: subsystem, category: LoggingCategory.analytics.rawValue) 31 | 32 | /// Logger instance specific to `backend` logging. 33 | static let backend = Logger(subsystem: subsystem, category: LoggingCategory.backend.rawValue) 34 | 35 | /// Logger instance specific to general `app` logging. 36 | static let app = Logger(subsystem: subsystem, category: LoggingCategory.app.rawValue) 37 | 38 | /// Logger instance specific to general `kit` logging. 39 | static let kit = Logger(subsystem: subsystem, category: LoggingCategory.kit.rawValue) 40 | } 41 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/MediaItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 2.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | import OSLog 11 | import KinoPubLogging 12 | import KinoPubKit 13 | 14 | @MainActor 15 | class MediaItemModel: ObservableObject { 16 | 17 | private var itemsService: VideoContentService 18 | private var downloadManager: DownloadManager 19 | private var errorHandler: ErrorHandler 20 | public var linkProvider: NavigationLinkProvider 21 | public var mediaItemId: Int 22 | 23 | @Published public var mediaItem: MediaItem = MediaItem.mock() 24 | @Published public var itemLoaded: Bool = false 25 | 26 | init(mediaItemId: Int, 27 | itemsService: VideoContentService, 28 | downloadManager: DownloadManager, 29 | linkProvider: NavigationLinkProvider, 30 | errorHandler: ErrorHandler) { 31 | self.itemsService = itemsService 32 | self.mediaItemId = mediaItemId 33 | self.linkProvider = linkProvider 34 | self.errorHandler = errorHandler 35 | self.downloadManager = downloadManager 36 | } 37 | 38 | func fetchData() { 39 | Task { 40 | do { 41 | mediaItem = try await itemsService.fetchDetails(for: "\(mediaItemId)").item 42 | let mediaId = mediaItem.id 43 | mediaItem.seasons = mediaItem.seasons?.map({ $0.mediaId = mediaId; return $0 }) 44 | itemLoaded = true 45 | } catch { 46 | errorHandler.setError(error) 47 | } 48 | } 49 | } 50 | 51 | func startDownload(item: DownloadableMediaItem, file: FileInfo) { 52 | _ = downloadManager.startDownload(url: URL(string: file.url.http)!, withMetadata: DownloadMeta.make(from: item)) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/RequestBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestBuilder.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | internal class RequestBuilder { 11 | let baseURL: URL 12 | 13 | init(baseURL: URL) { 14 | self.baseURL = baseURL 15 | } 16 | 17 | func build(with endpoint: Endpoint) -> URLRequest? { 18 | guard let url = URL(string: endpoint.path, relativeTo: baseURL) else { return nil } 19 | 20 | var request = URLRequest(url: url) 21 | request.httpMethod = endpoint.method 22 | 23 | if let headers = endpoint.headers { 24 | for (key, value) in headers { 25 | request.addValue(value, forHTTPHeaderField: key) 26 | } 27 | } 28 | 29 | if let parameters = endpoint.parameters { 30 | switch endpoint.method { 31 | case "GET": 32 | request = convertParamsToURL(for: url, request: request, endpoint: endpoint) 33 | default: 34 | if endpoint.forceSendAsGetParams { 35 | request = convertParamsToURL(for: url, request: request, endpoint: endpoint) 36 | break 37 | } 38 | let bodyData = try? JSONSerialization.data(withJSONObject: parameters) 39 | request.httpBody = bodyData 40 | } 41 | } 42 | 43 | return request 44 | } 45 | 46 | private func convertParamsToURL(for url: URL, request: URLRequest, endpoint: Endpoint) -> URLRequest { 47 | var request = request 48 | var components = URLComponents(url: url, resolvingAgainstBaseURL: true)! 49 | if let parameters = endpoint.parameters { 50 | components.queryItems = parameters.sorted(by: { $0.key < $1.key }).map { (key, value) in 51 | return URLQueryItem(name: key, value: "\(value)") 52 | } 53 | } 54 | request.url = components.url 55 | return request 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/Episode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Episode.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Episode: Codable, Hashable, Identifiable { 11 | 12 | public let id: Int 13 | public let title: String 14 | public let thumbnail: String 15 | public let duration: Int 16 | public let tracks: Int 17 | public let number: Int 18 | public let ac3: Int 19 | public let audios: [EpisodeAudio] 20 | public let watched: Int 21 | public let watching: EpisodeWatching 22 | public let subtitles: [Subtitle] 23 | public let files: [FileInfo] 24 | public var seasonNumber: Int? 25 | public var mediaId: Int? 26 | 27 | public var fixedTitle: String { 28 | if title.isEmpty { 29 | return "Серия \(number)" 30 | } 31 | return title 32 | } 33 | 34 | public init(id: Int, title: String, thumbnail: String, duration: Int, tracks: Int, number: Int, ac3: Int, audios: [EpisodeAudio], watched: Int, watching: EpisodeWatching, subtitles: [Subtitle], files: [FileInfo]) { 35 | self.id = id 36 | self.title = title 37 | self.thumbnail = thumbnail 38 | self.duration = duration 39 | self.tracks = tracks 40 | self.number = number 41 | self.ac3 = ac3 42 | self.audios = audios 43 | self.watched = watched 44 | self.watching = watching 45 | self.subtitles = subtitles 46 | self.files = files 47 | } 48 | 49 | public static func == (lhs: Episode, rhs: Episode) -> Bool { 50 | lhs.id == rhs.id 51 | } 52 | 53 | public func hash(into hasher: inout Hasher) { 54 | hasher.combine(id) 55 | } 56 | } 57 | 58 | extension Episode: PlayableItem { 59 | public var trailer: Trailer? { nil } 60 | public var metadata: WatchingMetadata { 61 | WatchingMetadata(id: mediaId ?? id, video: number, season: seasonNumber) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Models/UserData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserData.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 9.08.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct UserData: Codable { 11 | 12 | public struct Subscription: Codable { 13 | public let active: Bool 14 | public let endTime: TimeInterval 15 | public let days: Double 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case active, days 19 | case endTime = "end_time" 20 | } 21 | } 22 | 23 | public struct Profile: Codable { 24 | public let name: String? 25 | public let avatar: String? 26 | } 27 | 28 | public struct Settings: Codable { 29 | public let showErotic: Bool 30 | public let showUncertain: Bool 31 | 32 | private enum CodingKeys: String, CodingKey { 33 | case showErotic = "show_erotic" 34 | case showUncertain = "show_uncertain" 35 | } 36 | } 37 | 38 | public let username: String 39 | public let registrationDate: TimeInterval 40 | public let settings: Settings 41 | public let subscription: Subscription 42 | public let profile: Profile 43 | public let skeleton: Bool? 44 | 45 | private enum CodingKeys: String, CodingKey { 46 | case username, subscription, profile, settings, skeleton 47 | case registrationDate = "reg_date" 48 | } 49 | } 50 | 51 | public extension UserData { 52 | var registrationDateFormatted: String { 53 | Date(timeIntervalSince1970: self.registrationDate).formatted() 54 | } 55 | } 56 | 57 | public extension UserData { 58 | static func mock() -> UserData { 59 | UserData(username: "Test User", 60 | registrationDate: 0, 61 | settings: Settings(showErotic: true, showUncertain: true), 62 | subscription: Subscription(active: true, endTime: 3333333, days: 20.5), 63 | profile: Profile(name: "Test User Full", avatar: ""), 64 | skeleton: true) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Tests/KinoPubBackendTests/RequestBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import KinoPubBackend 11 | 12 | struct RequestData: Endpoint { 13 | var path: String 14 | var method: String 15 | var headers: [String: String]? 16 | var parameters: [String: Any]? 17 | } 18 | 19 | class RequestBuilderTests: XCTestCase { 20 | 21 | var requestBuilder: RequestBuilder! 22 | let baseURL = URL(string: "https://api.example.com")! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | requestBuilder = RequestBuilder(baseURL: baseURL) 27 | } 28 | 29 | override func tearDown() { 30 | requestBuilder = nil 31 | super.tearDown() 32 | } 33 | 34 | func testBuildRequest_WithPath_ReturnsCorrectURL() { 35 | let requestData = RequestData(path: "/testPath", method: "GET") 36 | let request = requestBuilder.build(with: requestData) 37 | 38 | XCTAssertEqual(request?.url, URL(string: "https://api.example.com/testPath")) 39 | } 40 | 41 | func testBuildRequest_WithHeaders_SetsHeadersCorrectly() { 42 | let headers = ["Authorization": "Bearer token123"] 43 | let requestData = RequestData(path: "/testPath", method: "GET", headers: headers) 44 | let request = requestBuilder.build(with: requestData) 45 | 46 | XCTAssertEqual(request?.value(forHTTPHeaderField: "Authorization"), "Bearer token123") 47 | } 48 | 49 | func testBuildRequest_WithGETParameters_EncodesParametersInURL() { 50 | let parameters = ["key1": "value1", "key2": "value2"] 51 | let requestData = RequestData(path: "/testPath", method: "GET", parameters: parameters) 52 | let request = requestBuilder.build(with: requestData) 53 | 54 | let expectedURL = URL(string: "https://api.example.com/testPath?key1=value1&key2=value2")! 55 | XCTAssertEqual(request?.url, expectedURL) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Main/Shortcut/ShortcutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShortcutView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 28.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | import KinoPubUI 12 | 13 | struct ShortcutSelectionView: View { 14 | @Environment(\.dismiss) private var dismiss 15 | @Binding var shortcut: MediaShortcut 16 | @Binding var mediaType: MediaType 17 | 18 | var body: some View { 19 | ZStack { 20 | Color.KinoPub.background.edgesIgnoringSafeArea(.all) 21 | VStack { 22 | HStack { 23 | mediaTypesPicker 24 | shortcutPicker 25 | } 26 | #if os(macOS) 27 | KinoPubButton(title: "Apply".localized, color: .green) { 28 | dismiss() 29 | } 30 | .frame(width: 100, height: 30) 31 | .padding() 32 | #endif 33 | } 34 | 35 | } 36 | .background(Color.KinoPub.background) 37 | .presentationDetents([.height(180.0)]) 38 | .presentationDragIndicator(.visible) 39 | 40 | } 41 | 42 | var shortcutPicker: some View { 43 | Picker("", selection: $shortcut) { 44 | ForEach(MediaShortcut.allCases) { shortcut in 45 | Text(shortcut.title.localized) 46 | .tag(shortcut) 47 | } 48 | } 49 | #if os(iOS) 50 | .pickerStyle(.wheel) 51 | #endif 52 | #if os(macOS) 53 | .pickerStyle(.radioGroup) 54 | #endif 55 | .padding() 56 | } 57 | var mediaTypesPicker: some View { 58 | Picker("", selection: $mediaType) { 59 | ForEach(MediaType.allCases) { type in 60 | Text(type.title.localized) 61 | .tag(type) 62 | } 63 | } 64 | #if os(iOS) 65 | .pickerStyle(.wheel) 66 | #endif 67 | #if os(macOS) 68 | .pickerStyle(.radioGroup) 69 | #endif 70 | .padding() 71 | } 72 | } 73 | 74 | struct ShortcutSelectionView_Previews: PreviewProvider { 75 | static var previews: some View { 76 | ShortcutSelectionView(shortcut: .constant(.fresh), mediaType: .constant(.movie)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Sources/KinoPubBackend/Client/APIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIClient.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 21.07.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public class APIClient { 11 | private let session: URLSessionProtocol 12 | private let requestBuilder: RequestBuilder 13 | private let baseUrl: URL 14 | private var plugins: [APIClientPlugin] 15 | 16 | public init(baseUrl: String, plugins: [APIClientPlugin] = [], session: URLSessionProtocol = URLSessionImpl(session: .shared)) { 17 | self.baseUrl = URL(string: baseUrl)! 18 | self.plugins = plugins 19 | self.session = session 20 | self.requestBuilder = RequestBuilder(baseURL: self.baseUrl) 21 | } 22 | 23 | public func performRequest(with requestData: Endpoint, decodingType: T.Type) async throws -> T { 24 | guard let request = requestBuilder.build(with: requestData) else { 25 | throw APIClientError.invalidUrlParams 26 | } 27 | 28 | let preparedRequest = plugins.reduce(request) { $1.prepare(request) } 29 | // Notify plugins 30 | plugins.forEach { $0.willSend(preparedRequest) } 31 | 32 | let (data, response) = try await session.data(for: preparedRequest) 33 | 34 | // Notify plugins 35 | plugins.forEach { $0.didReceive(response, data: data) } 36 | 37 | return try decode(T.self, from: data, throwDecodingErrorImmediately: false) 38 | } 39 | 40 | private func decode(_ type: T.Type, 41 | from data: Data, 42 | throwDecodingErrorImmediately: Bool) throws -> T where T: Decodable { 43 | do { 44 | let result = try JSONDecoder().decode(T.self, from: data) 45 | return result 46 | } catch { 47 | if throwDecodingErrorImmediately { 48 | throw APIClientError.decodingError(error) 49 | } 50 | let result = try decode(BackendError.self, from: data, throwDecodingErrorImmediately: true) 51 | throw APIClientError.networkError(result) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/Tests/KinoPubBackendTests/APIClientTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import KinoPubBackend 3 | 4 | class APIClientTests: XCTestCase { 5 | 6 | var apiClient: APIClient! 7 | var sessionMock: URLSessionMock! 8 | 9 | override func setUp() { 10 | super.setUp() 11 | sessionMock = URLSessionMock() 12 | apiClient = APIClient(baseUrl: "https://api.example.com", session: sessionMock) 13 | } 14 | 15 | override func tearDown() { 16 | apiClient = nil 17 | sessionMock = nil 18 | super.tearDown() 19 | } 20 | 21 | func testPerformRequest_ReturnsDecodedData() async { 22 | // Given 23 | let json = """ 24 | { 25 | "access_token": "testToken", 26 | "token_type": "bearer", 27 | "expires_in": 12345, 28 | "refresh_token": "testRefreshToken" 29 | } 30 | """ 31 | sessionMock.data = json.data(using: .utf8, allowLossyConversion: true) 32 | 33 | // When 34 | do { 35 | let response: TokenResponse = try await apiClient.performRequest(with: RequestData(path: "/token", method: "GET"), decodingType: TokenResponse.self) 36 | 37 | // Then 38 | XCTAssertEqual(response.accessToken, "testToken") 39 | XCTAssertEqual(response.tokenType, "bearer") 40 | XCTAssertEqual(response.expiresIn, 12345) 41 | XCTAssertEqual(response.refreshToken, "testRefreshToken") 42 | } catch { 43 | XCTFail("Expected successful decoding but got error: \(error)") 44 | } 45 | } 46 | 47 | func testPerformRequest_WhenError_ThrowsError() async { 48 | // Given 49 | sessionMock.error = NSError(domain: "Test", code: 1234, userInfo: nil) 50 | 51 | // When 52 | do { 53 | let _: TokenResponse = try await apiClient.performRequest(with: RequestData(path: "/token", method: "GET"), decodingType: TokenResponse.self) 54 | XCTFail("Expected error but got a successful response") 55 | } catch { 56 | // Expected behavior 57 | } 58 | } 59 | 60 | // ... More tests based on different scenarios 61 | } 62 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/Authorization/AuthorizationServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorizationServiceImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | final class AuthorizationServiceImpl: AuthorizationService { 12 | 13 | private var apiClient: APIClient 14 | private var configuration: Configuration 15 | private var accessTokenService: AccessTokenService 16 | 17 | init(apiClient: APIClient, configuration: Configuration, accessTokenService: AccessTokenService) { 18 | self.apiClient = apiClient 19 | self.configuration = configuration 20 | self.accessTokenService = accessTokenService 21 | } 22 | 23 | func fetchDeviceCode() async throws -> VerificationResponse { 24 | let request = DeviceCodeRequest(grantType: .deviceCode, clientID: configuration.clientID, clientSecret: configuration.clientSecret) 25 | return try await apiClient.performRequest(with: request, decodingType: VerificationResponse.self) 26 | } 27 | 28 | func fetchToken(by verification: VerificationResponse) async throws { 29 | let request = DeviceCodeRequest(grantType: .deviceToken, clientID: configuration.clientID, clientSecret: configuration.clientSecret, code: verification.code) 30 | let token = try await apiClient.performRequest(with: request, decodingType: AccessToken.self) 31 | accessTokenService.set(token: token) 32 | } 33 | 34 | func refreshToken() async throws { 35 | guard let token: AccessToken = accessTokenService.token() else { 36 | return 37 | } 38 | 39 | let request = RefreshTokenRequest(clientID: configuration.clientID, 40 | clientSecret: configuration.clientSecret, 41 | refreshToken: token.refreshToken) 42 | let newToken = try await apiClient.performRequest(with: request, decodingType: AccessToken.self) 43 | accessTokenService.set(token: newToken) 44 | } 45 | 46 | func logout() { 47 | accessTokenService.clear() 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Downloads/DownloadsCatalog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadsCatalog.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 8.08.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | import KinoPubLogging 11 | import KinoPubKit 12 | import OSLog 13 | import Combine 14 | 15 | @MainActor 16 | class DownloadsCatalog: ObservableObject { 17 | 18 | private var downloadsDatabase: DownloadedFilesDatabase 19 | private var downloadManager: DownloadManager 20 | 21 | @Published public var downloadedItems: [DownloadedFileInfo] = [] 22 | @Published public var activeDownloads: [Download] = [] 23 | 24 | var cancellables = [AnyCancellable]() 25 | 26 | var isEmpty: Bool { 27 | downloadedItems.isEmpty && activeDownloads.isEmpty 28 | } 29 | 30 | init(downloadsDatabase: DownloadedFilesDatabase, downloadManager: DownloadManager) { 31 | self.downloadsDatabase = downloadsDatabase 32 | self.downloadManager = downloadManager 33 | } 34 | 35 | func refresh() { 36 | self.downloadedItems = downloadsDatabase.readData() ?? [] 37 | self.activeDownloads = downloadManager.activeDownloads.map({ $0.value }) 38 | cancellables.removeAll() 39 | self.activeDownloads.forEach({ 40 | let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() }) 41 | self.cancellables.append(c) 42 | }) 43 | 44 | } 45 | 46 | func deleteDownloadedItem(at indexSet: IndexSet) { 47 | for index in indexSet { 48 | let item = downloadedItems[index] 49 | downloadsDatabase.remove(fileInfo: item) 50 | } 51 | } 52 | 53 | func deleteActiveDownload(at indexSet: IndexSet) { 54 | for index in indexSet { 55 | let item = activeDownloads[index] 56 | downloadManager.removeDownload(for: item.url) 57 | } 58 | } 59 | 60 | func toggle(download: Download) { 61 | if download.state == .inProgress { 62 | download.pause() 63 | } else { 64 | download.resume() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Tests/KinoPubKitTests/FileSaverTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileSaverTests.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import KinoPubKit 11 | 12 | class FileSaverTests: XCTestCase { 13 | 14 | // MARK: - Test Variables 15 | 16 | var fileSaver: FileSaver! 17 | let fileManagerMock = FileManagerMock() 18 | 19 | // MARK: - Test Setup 20 | 21 | override func setUp() { 22 | super.setUp() 23 | fileSaver = FileSaver(fileManager: fileManagerMock) 24 | } 25 | 26 | override func tearDown() { 27 | fileSaver = nil 28 | super.tearDown() 29 | } 30 | 31 | // MARK: - Test Methods 32 | 33 | func testSaveFile_Success() { 34 | // Arrange 35 | let sourceURL = URL(string: "http://example.com/sourcefile.txt")! 36 | let destinationURL = URL(string: "file:///path/to/destinationfile.txt")! 37 | 38 | // Act & Assert 39 | XCTAssertNoThrow(try fileSaver.saveFile(from: sourceURL, to: destinationURL)) 40 | XCTAssertTrue(fileManagerMock.didRemoveItem) 41 | XCTAssertTrue(fileManagerMock.didMoveItem) 42 | } 43 | 44 | func testSaveFile_ThrowsError() { 45 | // Arrange 46 | let sourceURL = URL(string: "http://example.com/sourcefile.txt")! 47 | let destinationURL = URL(string: "file:///path/to/destinationfile.txt")! 48 | fileManagerMock.shouldThrowError = true 49 | 50 | // Act & Assert 51 | XCTAssertThrowsError(try fileSaver.saveFile(from: sourceURL, to: destinationURL)) 52 | XCTAssertTrue(fileManagerMock.didRemoveItem) 53 | XCTAssertFalse(fileManagerMock.didMoveItem) 54 | } 55 | 56 | func testGetDocumentsDirectoryURL() { 57 | // Arrange 58 | let filename = "testfile.txt" 59 | 60 | // Act 61 | let documentsDirectoryURL = fileSaver.getDocumentsDirectoryURL(forFilename: filename) 62 | 63 | // Assert 64 | XCTAssertEqual(documentsDirectoryURL.lastPathComponent, filename) 65 | XCTAssertEqual(documentsDirectoryURL.pathExtension, "") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/macOS/Sidebar/SidebarNavigationDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SidebarNavigationDetail.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 11.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | #if os(macOS) 12 | struct SidebarNavigationDetail: View { 13 | @Environment(\.appContext) var appContext 14 | @EnvironmentObject var navigationState: NavigationState 15 | @EnvironmentObject var errorHandler: ErrorHandler 16 | @EnvironmentObject var authState: AuthState 17 | 18 | @Binding var selection: NavigationTabs 19 | 20 | var body: some View { 21 | switch selection { 22 | case .main: 23 | main 24 | case .bookmarks: 25 | bookmarks 26 | case .downloads: 27 | downloads 28 | case .profile: 29 | profile 30 | } 31 | } 32 | 33 | var main: some View { 34 | MainView(catalog: MediaCatalog(itemsService: appContext.contentService, 35 | authState: authState, 36 | errorHandler: errorHandler)) 37 | } 38 | 39 | var bookmarks: some View { 40 | BookmarksView(catalog: BookmarksCatalog(itemsService: appContext.contentService, 41 | authState: authState, 42 | errorHandler: errorHandler)) 43 | } 44 | 45 | var downloads: some View { 46 | DownloadsView(catalog: DownloadsCatalog(downloadsDatabase: appContext.downloadedFilesDatabase, downloadManager: appContext.downloadManager)) 47 | } 48 | 49 | var profile: some View { 50 | ProfileView(model: ProfileModel(userService: appContext.userService, 51 | errorHandler: errorHandler, 52 | authState: authState)) 53 | } 54 | } 55 | 56 | struct SidebarNavigationDetail_Previews: PreviewProvider { 57 | struct Preview: View { 58 | @State private var selection: NavigationTabs = .main 59 | var body: some View { 60 | SidebarNavigationDetail(selection: $selection) 61 | } 62 | } 63 | static var previews: some View { 64 | Preview() 65 | } 66 | } 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Content/Modifiers/PosterStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PosterStyle.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 24.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | /// A view modifier that applies a poster style to a view. 11 | public struct PosterStyle: ViewModifier { 12 | /// The size options for the poster style. 13 | public enum Size { 14 | case small, regular, medium, big 15 | 16 | /// The width of the poster based on the size option. 17 | public var width: CGFloat { 18 | switch self { 19 | case .small: return 53 20 | case .regular: return 120 21 | case .medium: return 165 22 | case .big: return 250 23 | } 24 | } 25 | 26 | /// The height of the poster based on the size option. 27 | public var height: CGFloat { 28 | switch self { 29 | case .small: return 80 30 | case .regular: return 180 31 | case .medium: return 250 32 | case .big: return 375 33 | } 34 | } 35 | } 36 | 37 | public enum Orientation { 38 | case vertical 39 | case horizontal 40 | } 41 | 42 | let size: Size 43 | let orientation: Orientation 44 | 45 | public func body(content: Content) -> some View { 46 | var result: AnyView = AnyView(content) 47 | 48 | if orientation == .vertical { 49 | result = AnyView(result.frame(width: size.width, height: size.height)) 50 | } else { 51 | result = AnyView(result.frame(width: size.height, height: size.width)) 52 | } 53 | 54 | return result 55 | .cornerRadius(8) 56 | .shadow(radius: 8) 57 | } 58 | } 59 | 60 | public extension View { 61 | /// Applies the poster style to a view with the specified size option. 62 | /// 63 | /// - Parameter size: The size option for the poster style. 64 | /// - Parameter orientation: The orientation option for the poster style. 65 | /// - Returns: A modified view with the poster style applied. 66 | func posterStyle(size: PosterStyle.Size, orientation: PosterStyle.Orientation) -> some View { 67 | return ModifiedContent(content: self, modifier: PosterStyle(size: size, orientation: orientation)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Sources/KinoPubKit/Downloading/DownloadedFilesDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadedFilesDatabase.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubLogging 10 | import OSLog 11 | 12 | public protocol DownloadedFilesDataReading { 13 | associatedtype Meta: Codable & Equatable 14 | func readData() -> [DownloadedFileInfo]? 15 | } 16 | 17 | public protocol DownloadedFilesDataWriting { 18 | associatedtype Meta: Codable & Equatable 19 | func writeData(_ files: [DownloadedFileInfo]) 20 | func save(fileInfo: DownloadedFileInfo) 21 | } 22 | 23 | public class DownloadedFilesDatabase: DownloadedFilesDataReading, DownloadedFilesDataWriting { 24 | private let fileSaver: FileSaving 25 | private let dataFileURL: URL 26 | 27 | public init(fileSaver: FileSaving) { 28 | self.fileSaver = fileSaver 29 | self.dataFileURL = fileSaver.getDocumentsDirectoryURL(forFilename: "downloadedFiles.plist") 30 | } 31 | 32 | public func save(fileInfo: DownloadedFileInfo) { 33 | var currentData = readData() ?? [] 34 | currentData.append(fileInfo) 35 | writeData(currentData) 36 | Logger.kit.debug("[DOWNLOAD] save file info for: \(fileInfo.originalURL)") 37 | } 38 | 39 | public func readData() -> [DownloadedFileInfo]? { 40 | guard let data = try? Data(contentsOf: dataFileURL), 41 | let decodedData = try? PropertyListDecoder().decode([DownloadedFileInfo].self, from: data) else { 42 | return nil 43 | } 44 | return decodedData.sorted(by: { $0.downloadDate > $1.downloadDate }) 45 | } 46 | 47 | public func writeData(_ files: [DownloadedFileInfo]) { 48 | if let data = try? PropertyListEncoder().encode(files) { 49 | try? data.write(to: dataFileURL) 50 | } 51 | } 52 | 53 | public func remove(fileInfo: DownloadedFileInfo) { 54 | var currentData = readData() ?? [] 55 | currentData.removeAll(where: { $0.originalURL == fileInfo.originalURL }) 56 | writeData(currentData) 57 | try? fileSaver.removeFile(at: fileInfo.originalURL) 58 | Logger.kit.debug("[DOWNLOAD] file data removed: \(fileInfo.originalURL)") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/macOS/Sidebar/Sidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sidebar.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 11.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | #if os(macOS) 12 | struct Sidebar: View { 13 | 14 | @Binding var selection: NavigationTabs 15 | 16 | var body: some View { 17 | List(selection: $selection) { 18 | NavigationLink(value: NavigationTabs.main) { 19 | Label("Main", systemImage: "house") 20 | .foregroundStyle(Color.white) 21 | } 22 | .listRowBackground(selection == .main ? Color.KinoPub.accent : Color.clear) 23 | .tint(Color.clear) 24 | 25 | NavigationLink(value: NavigationTabs.bookmarks) { 26 | Label("Bookmarks", systemImage: "bookmark") 27 | .foregroundStyle(Color.white) 28 | } 29 | .listRowBackground(selection == .bookmarks ? Color.KinoPub.accent : Color.clear) 30 | .tint(Color.clear) 31 | 32 | NavigationLink(value: NavigationTabs.downloads) { 33 | Label("Downloads", systemImage: "arrow.down.circle") 34 | .foregroundStyle(Color.white) 35 | } 36 | .listRowBackground(selection == .downloads ? Color.KinoPub.accent : Color.clear) 37 | .tint(Color.clear) 38 | 39 | NavigationLink(value: NavigationTabs.profile) { 40 | Label("Profile", systemImage: "person.crop.circle") 41 | .foregroundStyle(Color.white) 42 | } 43 | .listRowBackground(selection == .profile ? Color.KinoPub.accent : Color.clear) 44 | .tint(Color.clear) 45 | } 46 | .scrollContentBackground(.hidden) 47 | .background(Color.KinoPub.background) 48 | .navigationTitle("Main") 49 | #if os(macOS) 50 | .navigationSplitViewColumnWidth(min: 200, ideal: 200) 51 | #endif 52 | } 53 | } 54 | 55 | struct Sidebar_Previews: PreviewProvider { 56 | struct Preview: View { 57 | @State private var selection: NavigationTabs = NavigationTabs.main 58 | var body: some View { 59 | Sidebar(selection: $selection) 60 | } 61 | } 62 | 63 | static var previews: some View { 64 | NavigationSplitView { 65 | Preview() 66 | } detail: { 67 | Text("Detail!") 68 | } 69 | } 70 | } 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Services/VideoContent/VideoContentServiceImpl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoContentServiceImpl.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 26.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | final class VideoContentServiceImpl: VideoContentService { 12 | 13 | private var apiClient: APIClient 14 | 15 | init(apiClient: APIClient) { 16 | self.apiClient = apiClient 17 | } 18 | 19 | func fetch(shortcut: MediaShortcut, contentType: MediaType, page: Int?) async throws -> PaginatedData { 20 | let request = ShortcutItemsRequest(shortcut: shortcut, contentType: contentType, page: page) 21 | let response = try await apiClient.performRequest(with: request, 22 | decodingType: PaginatedData.self) 23 | return response 24 | } 25 | 26 | func search(query: String?, page: Int?) async throws -> PaginatedData { 27 | let request = SearchItemsRequest(contentType: nil, page: page, query: query) 28 | let response = try await apiClient.performRequest(with: request, 29 | decodingType: PaginatedData.self) 30 | return response 31 | } 32 | 33 | func fetchDetails(for id: String) async throws -> SingleItemData { 34 | let request = ItemDetailsRequest(id: id) 35 | let response = try await apiClient.performRequest(with: request, 36 | decodingType: SingleItemData.self) 37 | return response 38 | } 39 | 40 | func fetchBookmarks() async throws -> ArrayData { 41 | let request = BookmarksRequest() 42 | let response = try await apiClient.performRequest(with: request, 43 | decodingType: ArrayData.self) 44 | return response 45 | } 46 | 47 | func fetchBookmarkItems(id: String) async throws -> ArrayData { 48 | let request = BookmarkItemsRequest(id: id) 49 | let response = try await apiClient.performRequest(with: request, 50 | decodingType: ArrayData.self) 51 | return response 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Auth/AuthState.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // AuthState.swift 4 | // KinoPubAppleClient 5 | // 6 | // Created by Kirill Kunst on 3.08.2023. 7 | // 8 | 9 | import Foundation 10 | import KinoPubBackend 11 | import KinoPubLogging 12 | import OSLog 13 | 14 | /// Represents the state of the user's authentication. 15 | enum UserState { 16 | case unauthorized 17 | case authorized 18 | } 19 | 20 | /// A class that manages the authentication state of the user. 21 | @MainActor 22 | final class AuthState: ObservableObject { 23 | @Published var userState: UserState = .unauthorized 24 | @Published var shouldShowAuthentication: Bool = false 25 | 26 | private var authService: AuthorizationService 27 | private var accessTokenService: AccessTokenService 28 | 29 | /// Initializes the `AuthState` with the provided services. 30 | /// - Parameters: 31 | /// - authService: The authorization service used for authentication. 32 | /// - accessTokenService: The access token service used for managing access tokens. 33 | init(authService: AuthorizationService, accessTokenService: AccessTokenService) { 34 | self.authService = authService 35 | self.accessTokenService = accessTokenService 36 | } 37 | 38 | /// Checks the authentication state of the user. 39 | func check() async { 40 | Logger.app.debug("Start auth state checking...") 41 | guard let _: AccessToken = accessTokenService.token() else { 42 | userState = .unauthorized 43 | shouldShowAuthentication = true 44 | Logger.app.debug("Auth state: unauthorized") 45 | return 46 | } 47 | 48 | await refreshToken() 49 | } 50 | 51 | private func refreshToken() async { 52 | Logger.app.debug("Refreshing token...") 53 | do { 54 | try await authService.refreshToken() 55 | userState = .authorized 56 | shouldShowAuthentication = false 57 | Logger.app.debug("Auth state: authorized") 58 | } catch { 59 | await MainActor.run { 60 | userState = .unauthorized 61 | shouldShowAuthentication = true 62 | Logger.app.debug("Failed to refresh token, auth state: unauthorized") 63 | } 64 | } 65 | } 66 | 67 | /// Logs out the user. 68 | func logout() { 69 | authService.logout() 70 | userState = .unauthorized 71 | shouldShowAuthentication = true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Navigation/Routes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Routes.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | enum MainRoutes: Hashable { 12 | case details(MediaItem) 13 | case seasons([Season]) 14 | case season(Season) 15 | case player(any PlayableItem) 16 | case trailerPlayer(any PlayableItem) 17 | 18 | func hash(into hasher: inout Hasher) { 19 | switch self { 20 | case .details(let item): 21 | hasher.combine(item) 22 | case .season(let season): 23 | hasher.combine(season) 24 | case .seasons(let seasons): 25 | hasher.combine(seasons) 26 | case .player(let item): 27 | hasher.combine(item.id) 28 | case .trailerPlayer(let item): 29 | hasher.combine(item.id) 30 | } 31 | } 32 | 33 | static func == (lhs: MainRoutes, rhs: MainRoutes) -> Bool { 34 | rhs.hashValue == lhs.hashValue 35 | } 36 | } 37 | 38 | enum BookmarksRoutes: Hashable { 39 | case bookmark(Bookmark) 40 | case details(MediaItem) 41 | case seasons([Season]) 42 | case season(Season) 43 | case player(any PlayableItem) 44 | case trailerPlayer(any PlayableItem) 45 | 46 | func hash(into hasher: inout Hasher) { 47 | switch self { 48 | case .bookmark(let bookmark): 49 | hasher.combine(bookmark) 50 | case .details(let item): 51 | hasher.combine(item) 52 | case .season(let season): 53 | hasher.combine(season) 54 | case .seasons(let seasons): 55 | hasher.combine(seasons) 56 | case .player(let item): 57 | hasher.combine(item.id) 58 | case .trailerPlayer(let item): 59 | hasher.combine(item.id) 60 | } 61 | } 62 | 63 | static func == (lhs: BookmarksRoutes, rhs: BookmarksRoutes) -> Bool { 64 | rhs.hashValue == lhs.hashValue 65 | } 66 | } 67 | 68 | enum DownloadsRoutes: Hashable { 69 | case player(any PlayableItem) 70 | case trailerPlayer(any PlayableItem) 71 | 72 | func hash(into hasher: inout Hasher) { 73 | switch self { 74 | case .player(let item): 75 | hasher.combine(item.id) 76 | case .trailerPlayer(let item): 77 | hasher.combine(item.id) 78 | } 79 | } 80 | 81 | static func == (lhs: DownloadsRoutes, rhs: DownloadsRoutes) -> Bool { 82 | rhs.hashValue == lhs.hashValue 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kino.pub Apple Cross-Platform Client 2 | 3 | This is the iOS, iPadOS and macOS client for the kino.pub service built with SwiftUI. 4 | 5 | # Requirements 6 | 7 | - macOS 13.0. 8 | - Xcode 15.0. 9 | - Swift 5.8. 10 | 11 | # Supported Platforms 12 | 13 | - iOS 16.0. 14 | - macOS 13.0. 15 | 16 | ## App Structure 17 | 18 | The app is structured using the Swift Package Manager with the following packages: 19 | 20 | - `KinoPubAppleClient` - The main app target containing shared code across platforms. 21 | - `KinoPubUI` - Reusable SwiftUI components. 22 | - `KinoPubKit` - Shared business logic. 23 | - `KinoPubBackend` - Networking layer. 24 | 25 | The main app target KinoPubAppleClient contains the following folders: 26 | 27 | - App - Application lifecycle code like app delegates. 28 | - Resources - Resources like GoogleService-Info.plist. 29 | - Views - SwiftUI view code organized by feature. 30 | - Services - Services for things like analytics and networking. 31 | - States - app states e.g. navigation, auth. 32 | - Custom - various custom classes. 33 | - Context - dependencies context e.g. services, managers. 34 | 35 | ## Third Party Libraries 36 | 37 | The following third party libraries are used: 38 | 39 | - [Firebase](https://firebase.google.com) - For authentication, analytics, crash reporting. 40 | - [PopupView](https://github.com/exyte/PopupView) - For displaying popups and overlays. 41 | - [KeychainAccess](https://github.com/kishikawakatsumi/KeychainAccess) - For storing data in the keychain. 42 | - [SkeletonUI](https://github.com/CSolanaM/SkeletonUI) - For rendering loading states. 43 | 44 | 45 | ## Packages 46 | 47 | - `KinoPubUI` - Contains reusable SwiftUI components like buttons, texts, etc. 48 | - `KinoPubKit` - Shared business logic for things like authentication and data models. 49 | - `KinoPubBackend` - Networking layer that interfaces with the kino.pub API. 50 | - `KinoPubLogging` - Small package containing extensions for OSLog. 51 | 52 | # Contributing 53 | 54 | - Suggest your idea as a [feature request](https://github.com/leoru/kinopub-apple-client/issues/new?assignees=&labels=&template=feature_request.md&title=) for this project. 55 | - Create a [bug report](https://github.com/leoru/kinopub-apple-client/issues/new?assignees=&labels=&template=bug_report.md&title=) to help us improve. 56 | - Propose your own fixes, suggestions and open a pull request with the changes. -------------------------------------------------------------------------------- /Packages/KinoPubUI/Sources/KinoPubUI/Components/Content/ContentItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentItemView.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 24.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | 12 | public struct ContentItemView: View { 13 | 14 | private var mediaItem: MediaItem 15 | 16 | init(mediaItem: MediaItem) { 17 | self.mediaItem = mediaItem 18 | } 19 | 20 | public var body: some View { 21 | VStack(alignment: .center) { 22 | ZStack { 23 | image 24 | if !(mediaItem.skeleton ?? false) { 25 | ratingsBlock 26 | } 27 | } 28 | VStack(alignment: .center) { 29 | title 30 | subtitle 31 | }.padding(.horizontal, 8) 32 | } 33 | .background(Color.clear) 34 | } 35 | 36 | var image: some View { 37 | AsyncImage(url: URL(string: mediaItem.posters.medium)) { image in 38 | image.resizable() 39 | .renderingMode(.original) 40 | .posterStyle(size: .medium, orientation: .vertical) 41 | } placeholder: { 42 | Color.KinoPub.skeleton 43 | .frame(width: PosterStyle.Size.medium.width, 44 | height: PosterStyle.Size.medium.height) 45 | } 46 | .cornerRadius(8) 47 | .skeleton(enabled: mediaItem.skeleton ?? false, 48 | size: CGSize(width: PosterStyle.Size.medium.width, 49 | height: PosterStyle.Size.medium.height)) 50 | } 51 | 52 | var ratingsBlock: some View { 53 | VStack { 54 | Spacer() 55 | ContentItemRatingView(imdbScore: mediaItem.imdbRating, 56 | kinopoiskScore: mediaItem.kinopoiskRating) 57 | .skeleton(enabled: mediaItem.skeleton ?? false) 58 | .padding(.bottom, 8) 59 | } 60 | } 61 | 62 | var title: some View { 63 | Text(mediaItem.localizedTitle) 64 | .lineLimit(1) 65 | .font(.system(size: 16.0, weight: .medium)) 66 | .foregroundStyle(Color.KinoPub.text) 67 | .skeleton(enabled: mediaItem.skeleton ?? false) 68 | } 69 | 70 | var subtitle: some View { 71 | Text(mediaItem.originalTitle) 72 | .lineLimit(1) 73 | .font(.system(size: 14.0, weight: .medium)) 74 | .foregroundStyle(Color.KinoPub.subtitle) 75 | .skeleton(enabled: mediaItem.skeleton ?? false) 76 | } 77 | 78 | } 79 | 80 | #Preview { 81 | ContentItemView(mediaItem: MediaItem.mock(skeleton: true)) 82 | } 83 | -------------------------------------------------------------------------------- /KinoPubAppleClient/States/Navigation/NavigationLinkProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationLinkProvider.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubBackend 10 | 11 | protocol NavigationLinkProvider { 12 | func link(for item: MediaItem) -> any Hashable 13 | func player(for item: any PlayableItem) -> any Hashable 14 | func trailerPlayer(for item: any PlayableItem) -> any Hashable 15 | func seasons(for seasons: [Season]) -> any Hashable 16 | func season(for season: Season) -> any Hashable 17 | } 18 | 19 | struct MainRoutesLinkProvider: NavigationLinkProvider { 20 | func link(for item: MediaItem) -> any Hashable { 21 | MainRoutes.details(item) 22 | } 23 | 24 | func player(for item: any PlayableItem) -> any Hashable { 25 | MainRoutes.player(item) 26 | } 27 | 28 | func trailerPlayer(for item: any PlayableItem) -> any Hashable { 29 | MainRoutes.trailerPlayer(item) 30 | } 31 | 32 | func seasons(for seasons: [Season]) -> any Hashable { 33 | MainRoutes.seasons(seasons) 34 | } 35 | 36 | func season(for season: Season) -> any Hashable { 37 | MainRoutes.season(season) 38 | } 39 | } 40 | 41 | struct BookmarksRoutesLinkProvider: NavigationLinkProvider { 42 | func link(for item: MediaItem) -> any Hashable { 43 | BookmarksRoutes.details(item) 44 | } 45 | 46 | func player(for item: any PlayableItem) -> any Hashable { 47 | BookmarksRoutes.player(item) 48 | } 49 | 50 | func trailerPlayer(for item: any PlayableItem) -> any Hashable { 51 | BookmarksRoutes.trailerPlayer(item) 52 | } 53 | 54 | func seasons(for seasons: [Season]) -> any Hashable { 55 | BookmarksRoutes.seasons(seasons) 56 | } 57 | 58 | func season(for season: Season) -> any Hashable { 59 | BookmarksRoutes.season(season) 60 | } 61 | } 62 | 63 | struct DownloadsRoutesLinkProvider: NavigationLinkProvider { 64 | func link(for item: MediaItem) -> any Hashable { 65 | BookmarksRoutes.details(item) 66 | } 67 | 68 | func player(for item: any PlayableItem) -> any Hashable { 69 | DownloadsRoutes.player(item) 70 | } 71 | 72 | func trailerPlayer(for item: any PlayableItem) -> any Hashable { 73 | DownloadsRoutes.trailerPlayer(item) 74 | } 75 | 76 | func seasons(for seasons: [Season]) -> any Hashable { 77 | "" 78 | } 79 | 80 | func season(for season: Season) -> any Hashable { 81 | "" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Downloads/DownloadedItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadedItemView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 8.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | import KinoPubUI 12 | 13 | public struct DownloadedItemView: View { 14 | 15 | private var mediaItem: DownloadMeta 16 | private var progress: Float? 17 | private var onDownloadStateChange: (Bool) -> Void 18 | 19 | public init(mediaItem: DownloadMeta, 20 | progress: Float?, 21 | onDownloadStateChange: @escaping (Bool) -> Void) { 22 | self.mediaItem = mediaItem 23 | self.progress = progress 24 | self.onDownloadStateChange = onDownloadStateChange 25 | } 26 | 27 | public var body: some View { 28 | HStack(alignment: .center) { 29 | image 30 | 31 | VStack(alignment: .leading) { 32 | title 33 | subtitle 34 | }.padding(.all, 5) 35 | 36 | if let progress = progress { 37 | Spacer() 38 | if progress != 1.0 { 39 | ProgressButton(progress: progress) { state in 40 | onDownloadStateChange(state == .pause) 41 | } 42 | .padding(.trailing, 16) 43 | } 44 | } else { 45 | Spacer() 46 | } 47 | } 48 | .padding(.vertical, 8) 49 | } 50 | 51 | var image: some View { 52 | AsyncImage(url: URL(string: mediaItem.imageUrl)) { image in 53 | image.resizable() 54 | .renderingMode(.original) 55 | .posterStyle(size: .small, orientation: .vertical) 56 | } placeholder: { 57 | Color.KinoPub.skeleton 58 | .frame(width: PosterStyle.Size.small.width, 59 | height: PosterStyle.Size.small.height) 60 | } 61 | .cornerRadius(8) 62 | } 63 | 64 | var title: some View { 65 | Text(mediaItem.localizedTitle) 66 | .lineLimit(1) 67 | .font(.system(size: 14.0, weight: .medium)) 68 | .foregroundStyle(Color.KinoPub.text) 69 | .padding(.bottom, 10) 70 | } 71 | 72 | var subtitle: some View { 73 | Text(mediaItem.originalTitle) 74 | .lineLimit(1) 75 | .font(.system(size: 12.0, weight: .medium)) 76 | .foregroundStyle(Color.KinoPub.subtitle) 77 | } 78 | 79 | } 80 | 81 | #Preview { 82 | DownloadedItemView(mediaItem: DownloadMeta.make(from: DownloadableMediaItem(name: "", files: [], mediaItem: MediaItem.mock(), watchingMetadata: WatchingMetadata(id: 0, video: nil, season: nil))), progress: nil) { _ in 83 | 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Main/Filter/FilterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterView.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 4.08.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | import KinoPubUI 12 | 13 | struct FilterView: View { 14 | 15 | @Environment(\.dismiss) private var dismiss 16 | @StateObject private var model: FilterModel 17 | 18 | init(model: @autoclosure @escaping () -> FilterModel) { 19 | _model = StateObject(wrappedValue: model()) 20 | } 21 | 22 | var body: some View { 23 | // NavigationView { 24 | VStack { 25 | HStack { 26 | Spacer() 27 | Form { 28 | typePicker 29 | yearSection 30 | imdbRatingSection 31 | } 32 | } 33 | HStack { 34 | KinoPubButton(title: "Clear".localized, color: .red) { 35 | dismiss() 36 | } 37 | .frame(width: 120, height: 30) 38 | KinoPubButton(title: "Apply".localized, color: .green) { 39 | dismiss() 40 | } 41 | .frame(width: 120, height: 30) 42 | } 43 | .padding() 44 | } 45 | .padding() 46 | 47 | 48 | // } 49 | .navigationTitle("Filter") 50 | #if os(iOS) 51 | .navigationBarTitleDisplayMode(.inline) 52 | #endif 53 | } 54 | 55 | var typePicker: some View { 56 | Picker("Type", selection: $model.mediaType) { 57 | ForEach(MediaType.allCases) { type in 58 | Text(type.title.localized) 59 | .tag(type) 60 | } 61 | } 62 | } 63 | 64 | var yearSection: some View { 65 | Section { 66 | Toggle("Release Year", isOn: $model.yearFilterEnabled) 67 | if model.yearFilterEnabled { 68 | numberPicker(title: "From", from: 1920, to: 2023, currentValue: $model.yearMin) 69 | numberPicker(title: "To", from: 1920, to: 2023, currentValue: $model.yearMax) 70 | } 71 | } 72 | } 73 | 74 | var imdbRatingSection: some View { 75 | Section { 76 | Toggle("IMDB Rating", isOn: $model.imdbFilterEnabled) 77 | if model.imdbFilterEnabled { 78 | numberPicker(title: "From", from: 0, to: 10, currentValue: $model.imdbMin) 79 | numberPicker(title: "To", from: 0, to: 10, currentValue: $model.imdbMax) 80 | } 81 | } 82 | } 83 | 84 | func numberPicker(title: String, from: Int, to: Int, currentValue: Binding) -> some View { 85 | Picker(title, selection: currentValue) { 86 | ForEach(from.. AuthModel) { 18 | _model = StateObject(wrappedValue: model()) 19 | } 20 | 21 | var body: some View { 22 | ZStack { 23 | Color.KinoPub.background.edgesIgnoringSafeArea(.all) 24 | VStack(spacing: 50) { 25 | titleView 26 | deviceCodeView 27 | activateButton 28 | } 29 | .padding(EdgeInsets.init(top: 0, leading: 16, bottom: 0, trailing: 16)) 30 | } 31 | .interactiveDismissDisabled(true) 32 | .task { 33 | model.fetchDeviceCode() 34 | } 35 | .onReceive(model.$close, perform: { shouldClose in 36 | if shouldClose { 37 | dismiss() 38 | } 39 | }) 40 | .handleError(state: $errorHandler.state) 41 | } 42 | 43 | var titleView: some View { 44 | VStack(spacing: 20) { 45 | Text("Auth_CodeActivationTitle") 46 | .font(.system(size: 24, weight: Font.Weight.semibold)) 47 | .frame(alignment: .leading) 48 | .foregroundColor(Color.KinoPub.text) 49 | Text("Auth_CodeActivationText") 50 | .lineLimit(nil) 51 | .font(.system(size: 16, weight: Font.Weight.regular)) 52 | .foregroundColor(Color.KinoPub.text) 53 | .multilineTextAlignment(.center) 54 | .padding(.top, 16) 55 | } 56 | .background(Color.KinoPub.background) 57 | .fixedSize(horizontal: false, vertical: true) 58 | } 59 | 60 | var deviceCodeView: some View { 61 | VStack(spacing: 5) { 62 | Text(model.deviceCode) 63 | .font(.system(size: 40, weight: Font.Weight.bold)) 64 | .foregroundColor(Color.KinoPub.text) 65 | .frame(minHeight: 44, idealHeight: 44, maxHeight: 44) 66 | Text("Auth_DeviceCode") 67 | .foregroundColor(Color.KinoPub.text) 68 | .font(.system(size: 16, weight: Font.Weight.regular)) 69 | } 70 | .fixedSize(horizontal: false, vertical: true) 71 | } 72 | 73 | var activateButton: some View { 74 | Button("Auth_Activate", action: { model.openActivationURL() }) 75 | #if os(macOS) 76 | .buttonStyle(PlainButtonStyle()) 77 | #endif 78 | .foregroundColor(Color.KinoPub.text) 79 | .padding(.all, 20.0) 80 | .overlay( 81 | RoundedRectangle(cornerRadius: 10) 82 | .stroke(Color.KinoPub.text, lineWidth: 2) 83 | ) 84 | } 85 | } 86 | 87 | // struct AuthView_Previews: PreviewProvider { 88 | // static var previews: some View { 89 | // AuthView() 90 | // } 91 | // } 92 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/MediaItem/Subviews/MediaItemFieldsCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaItemFieldsCard.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 31.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | import SkeletonUI 12 | 13 | struct MediaItemFieldsCard: View { 14 | 15 | var mediaItem: MediaItem 16 | var isSkeleton: Bool 17 | 18 | var body: some View { 19 | VStack(alignment: .leading) { 20 | Label("MediaItem_Description", systemImage: "book.pages") 21 | .foregroundStyle(Color.KinoPub.text) 22 | .font(Font.KinoPub.subheader) 23 | .skeleton(enabled: isSkeleton, size: CGSize(width: 200, height: 50)) 24 | 25 | VStack { 26 | data(key: "MediaItem_Title", value: "\(mediaItem.originalTitle)") 27 | data(key: "MediaItem_Year", value: "\(mediaItem.year)") 28 | data(key: "MediaItem_Duration", value: "\(mediaItem.duration.totalFormatted)") 29 | data(key: "MediaItem_Country", value: "\(mediaItem.countries.compactMap({ $0.title }).joined(separator: ","))") 30 | data(key: "MediaItem_Genre", value: "\(mediaItem.genres.compactMap({ $0.title ?? "" }).joined(separator: ","))") 31 | data(key: "MediaItem_Voice", value: "\(mediaItem.voice ?? "")") 32 | data(key: "MediaItem_Director", value: "\(mediaItem.director)") 33 | data(key: "MediaItem_Cast", value: "\(mediaItem.cast)") 34 | }.padding(.top, 8) 35 | 36 | } 37 | } 38 | 39 | func data(key: String, value: String) -> some View { 40 | HStack(content: { 41 | dataTitle(text: key) 42 | .skeleton(enabled: isSkeleton, size: CGSize(width: 100, height: 20)) 43 | Spacer() 44 | dataValue(text: value) 45 | .skeleton(enabled: isSkeleton, size: CGSize(width: 200, height: 20)) 46 | }) 47 | .padding(.top, 8) 48 | } 49 | 50 | var plot: some View { 51 | Text(mediaItem.plot) 52 | .font(.system(size: 11)) 53 | .foregroundStyle(Color.KinoPub.text) 54 | .padding(.top, 8) 55 | } 56 | 57 | func dataTitle(text: String) -> some View { 58 | Text(NSLocalizedString(text, comment: "")) 59 | .foregroundStyle(Color.KinoPub.subtitle) 60 | .font(Font.KinoPub.small) 61 | .padding(.horizontal, 5) 62 | } 63 | 64 | func dataValue(text: String) -> some View { 65 | Text(text) 66 | .foregroundStyle(Color.KinoPub.text) 67 | .font(Font.KinoPub.small) 68 | .padding(.horizontal, 5) 69 | .multilineTextAlignment(.trailing) 70 | } 71 | 72 | } 73 | 74 | struct MediaItemFieldsCard_Previews: PreviewProvider { 75 | struct Preview: View { 76 | var body: some View { 77 | MediaItemFieldsCard(mediaItem: MediaItem.mock(), isSkeleton: true) 78 | } 79 | } 80 | 81 | static var previews: some View { 82 | NavigationStack { 83 | Preview() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/Sources/KinoPubKit/Downloading/Download.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Download.swift 3 | // 4 | // 5 | // Created by Kirill Kunst on 22.07.2023. 6 | // 7 | 8 | import Foundation 9 | import KinoPubLogging 10 | import OSLog 11 | 12 | /// `Download` represents a downloadable resource. It provides methods for controlling the download, 13 | /// such as pausing and resuming, and notifies about the progress through a progress handler. 14 | public class Download: ObservableObject { 15 | /// The state of the download, such as not started, queued, in progress, or paused. 16 | public enum State: String { 17 | case notStarted 18 | case queued 19 | case inProgress 20 | case paused 21 | } 22 | 23 | /// URL for the download 24 | public let url: URL 25 | 26 | /// The current state of the download. 27 | public private(set) var state: State = .notStarted 28 | 29 | /// Progress value 30 | @Published public private(set) var progress: Float = 0.0 31 | 32 | /// Public metadata 33 | public let metadata: Meta 34 | 35 | // - Internal 36 | internal var task: URLSessionDownloadTask? 37 | 38 | private var resumeData: Data? 39 | 40 | private let manager: any DownloadManaging 41 | 42 | /// Initializes a `Download` object. 43 | /// - Parameters: 44 | /// - url: The URL of the resource to be downloaded. 45 | /// - metadata: Metadata associated with the download. 46 | /// - manager: An object that conforms to `DownloadManaging` to manage the download session. 47 | public init(url: URL, metadata: Meta, manager: any DownloadManaging) { 48 | self.url = url 49 | self.metadata = metadata 50 | self.manager = manager 51 | self.state = .queued 52 | Logger.kit.debug("[DOWNLOAD] Download for url: \(url) is queued") 53 | } 54 | 55 | /// Pauses the download. If the download is already paused or not in progress, this method has no effect. 56 | public func pause() { 57 | task?.cancel(byProducingResumeData: { [weak self] data in 58 | self?.resumeData = data 59 | self?.state = .paused 60 | Logger.kit.debug("[DOWNLOAD] Download for url: \(self?.url.absoluteString ?? "") is paused") 61 | }) 62 | task = nil 63 | } 64 | 65 | /// Resumes the download. If the download is already in progress, this method has no effect. 66 | public func resume() { 67 | if let resumeData = self.resumeData { 68 | task = manager.session.downloadTask(withResumeData: resumeData) 69 | } else { 70 | task = manager.session.downloadTask(with: URLRequest(url: url)) 71 | } 72 | state = .inProgress 73 | Logger.kit.debug("[DOWNLOAD] Download for url: \(self.url) is in progress") 74 | task?.resume() 75 | } 76 | 77 | internal func updateProgress(_ progress: Float) { 78 | self.progress = progress 79 | } 80 | 81 | } 82 | 83 | extension Download: Identifiable {} 84 | -------------------------------------------------------------------------------- /KinoPubAppleClient/Views/Auth/AuthModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthModel.swift 3 | // KinoPubAppleClient 4 | // 5 | // Created by Kirill Kunst on 27.07.2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import KinoPubBackend 11 | import KinoPubLogging 12 | import OSLog 13 | 14 | @MainActor 15 | class AuthModel: ObservableObject { 16 | 17 | private var authService: AuthorizationService 18 | private var authState: AuthState 19 | private var errorHandler: ErrorHandler 20 | 21 | @Published var deviceCode: String = "" 22 | @Published var close: Bool = false 23 | 24 | private var tempVerificationResponse: VerificationResponse? 25 | 26 | init(authService: AuthorizationService, authState: AuthState, errorHandler: ErrorHandler) { 27 | self.authService = authService 28 | self.authState = authState 29 | self.errorHandler = errorHandler 30 | } 31 | 32 | func fetchDeviceCode() { 33 | Logger.app.debug("Fetch device code...") 34 | errorHandler.reset() 35 | Task { 36 | do { 37 | let response = try await authService.fetchDeviceCode() 38 | self.deviceCode = response.userCode 39 | self.tempVerificationResponse = response 40 | Logger.app.debug("receive device code: \(response.userCode)") 41 | scheduleCheck(for: response) 42 | } catch { 43 | handleError(error) 44 | } 45 | } 46 | } 47 | 48 | func openActivationURL() { 49 | guard let urlString = tempVerificationResponse?.verificationUri, let url = URL(string: urlString) else { 50 | return 51 | } 52 | 53 | Logger.app.debug("open activation url: \(url)") 54 | 55 | #if os(iOS) 56 | UIApplication.shared.open(url) 57 | #endif 58 | } 59 | 60 | private func requestToken(by response: VerificationResponse) async throws { 61 | Logger.app.debug("request token...") 62 | do { 63 | try await authService.fetchToken(by: response) 64 | authState.userState = .authorized 65 | authState.shouldShowAuthentication = false 66 | Logger.app.debug("token requested") 67 | } catch { 68 | handleError(error, response: response) 69 | } 70 | } 71 | 72 | private func scheduleCheck(for response: VerificationResponse) { 73 | let timeout = UInt64(response.interval) 74 | 75 | Logger.app.debug("schedule next token scheck...") 76 | Task { 77 | try await Task.sleep(nanoseconds: timeout * 1_000_000_000) 78 | try await requestToken(by: response) 79 | } 80 | } 81 | 82 | private func handleError(_ error: Error, response: VerificationResponse? = nil) { 83 | Logger.app.debug("got error: \(error)") 84 | 85 | guard let error = error as? APIClientError else { 86 | return 87 | } 88 | 89 | if let response, error.isAuthorizationPending { 90 | scheduleCheck(for: response) 91 | return 92 | } 93 | 94 | errorHandler.setError(error) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/.swiftpm/xcode/xcshareddata/xcschemes/KinoPubKitTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 40 | 46 | 47 | 48 | 49 | 55 | 56 | 62 | 63 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Packages/KinoPubBackend/.swiftpm/xcode/xcshareddata/xcschemes/KinoPubBackendTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 40 | 46 | 47 | 48 | 49 | 55 | 56 | 62 | 63 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Packages/KinoPubKit/.swiftpm/xcode/xcshareddata/xcschemes/KinoPubKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | --------------------------------------------------------------------------------