├── 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 |
--------------------------------------------------------------------------------