mirror of
https://github.com/yattee/yattee.git
synced 2025-12-12 19:18:16 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16b25df3bc | ||
|
|
a66e59a282 | ||
|
|
aeeedf3d63 | ||
|
|
3d35a60c7a | ||
|
|
8ffdd4d51f | ||
|
|
f871c7aaf5 | ||
|
|
32af2b385b | ||
|
|
f78545baf9 | ||
|
|
d95bcc4065 | ||
|
|
1efd9e2b90 | ||
|
|
59e5fcb37d | ||
|
|
47d68d7948 | ||
|
|
3e22c1ebde | ||
|
|
ede5d85693 | ||
|
|
4d5390ce2d | ||
|
|
91290d4736 | ||
|
|
7e7225c59f | ||
|
|
7b9bbd8974 | ||
|
|
c36dc67a72 | ||
|
|
71b25afa28 | ||
|
|
2c5eb18bc9 | ||
|
|
56c2e552f7 | ||
|
|
5ee869c02c | ||
|
|
6f91eedf4c | ||
|
|
0328656a44 | ||
|
|
8d11a92f97 | ||
|
|
f3a8a0977c | ||
|
|
3bbc2df431 | ||
|
|
5faa5f0a48 | ||
|
|
ca8586ce7f | ||
|
|
8efa68e65c | ||
|
|
04c15ed59a | ||
|
|
75772533fd | ||
|
|
3ca0f4cf2d | ||
|
|
5f745afecb | ||
|
|
e8c8c6b5b4 | ||
|
|
0585120bd8 | ||
|
|
1f43e11b8e |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,4 +1,21 @@
|
||||
## Build 152
|
||||
## Build 155
|
||||
* Fixed reported crashes
|
||||
* Minor performance improvements
|
||||
|
||||
## Previous Builds
|
||||
* Fixed issue where AVPlayer would pause playing on exiting fullscreen
|
||||
* Fixed issue with empty button appearing on subscriptions list on tvOS
|
||||
|
||||
* Fixed issue with AVPlayer not always using full width
|
||||
* Fixed issue with layout when switching backend while in fullscreen
|
||||
* Fixed issue with updating watched time on closing video
|
||||
* Fixed issue with adjusting AVPlayer playback rate when using system controls
|
||||
* Fixed issue where comments would load indefinitely
|
||||
* Improved Home buttons layout on tvOS
|
||||
* Reverted change to placeholders as were causing issues to properly display loading status, will be revisited in future
|
||||
* Fixed performance issue with swiping back to subscribed channels list
|
||||
* Fixed reported crashes
|
||||
|
||||
* Tapping second time on search tab button focuses the input field and selects entered query text (iOS)
|
||||
* Added Browsing setting "Keep channels with unwatched videos on top of subscriptions list"
|
||||
* Improved buttons and layout on tvOS
|
||||
@@ -10,9 +27,7 @@
|
||||
* Added remove context menu option for all types of recent items in Search
|
||||
* Added advanced setting "Show video context menu options to force selected backend"
|
||||
* Fixed reported crashes
|
||||
* Other minor fixes and improvements
|
||||
|
||||
## Previous Builds
|
||||
* Improved Home
|
||||
- Added menu with view options on iOS and toolbar buttons on macOS/tvOS
|
||||
- Added Home Settings
|
||||
|
||||
@@ -53,7 +53,7 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
|
||||
}
|
||||
|
||||
var instance: Instance! {
|
||||
Defaults[.instances].first { $0.id == instanceID } ?? Instance(app: app ?? .invidious, name: urlString, apiURLString: urlString)
|
||||
InstancesModel.shared.find(instanceID) ?? Instance(app: app ?? .invidious, name: urlString, apiURLString: urlString)
|
||||
}
|
||||
|
||||
var isPublic: Bool {
|
||||
|
||||
@@ -68,15 +68,14 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
|
||||
return
|
||||
}
|
||||
|
||||
loadCachedChannels(account)
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
let request = force ? self.resource?.load() : self.resource?.loadIfNeeded()
|
||||
guard request != nil else { return }
|
||||
|
||||
if request != nil {
|
||||
self.isLoading = true
|
||||
}
|
||||
self.loadCachedChannels(account)
|
||||
|
||||
self.isLoading = true
|
||||
|
||||
request?
|
||||
.onCompletion { [weak self] _ in
|
||||
|
||||
@@ -69,6 +69,7 @@ final class CommentsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func loadNextPage() {
|
||||
guard nextPageAvailable else { return }
|
||||
load(page: nextPage)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,11 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
func updateWatch(finished: Bool = false) {
|
||||
func updateWatch(finished: Bool = false, time: CMTime? = nil) {
|
||||
guard let currentVideo, saveHistory else { return }
|
||||
|
||||
let id = currentVideo.videoID
|
||||
let time = backend.currentTime
|
||||
let time = time ?? backend.currentTime
|
||||
let seconds = time?.seconds ?? 0
|
||||
let duration = playerTime.duration.seconds
|
||||
if seconds < 3 {
|
||||
@@ -63,7 +63,7 @@ extension PlayerModel {
|
||||
let results = try? backgroundContext.fetch(watchFetchRequest)
|
||||
|
||||
backgroundContext.perform { [weak self] in
|
||||
guard let self, finished || self.backend.isPlaying else {
|
||||
guard let self, finished || time != nil || self.backend.isPlaying else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -353,9 +353,11 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
self.model.lastSkipped = segment
|
||||
self.model.handleOnPlayStream(stream)
|
||||
self.model.play()
|
||||
}
|
||||
} else {
|
||||
self.model.handleOnPlayStream(stream)
|
||||
self.model.play()
|
||||
}
|
||||
}
|
||||
@@ -486,7 +488,9 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
if self.model.activeBackend == .appleAVPlayer,
|
||||
self.isAutoplaying(playerItem)
|
||||
{
|
||||
self.model.updateAspectRatio()
|
||||
if self.model.aspectRatio != self.aspectRatio {
|
||||
self.model.updateAspectRatio()
|
||||
}
|
||||
|
||||
if self.startPictureInPictureOnPlay,
|
||||
let controller = self.model.pipController,
|
||||
@@ -608,7 +612,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
self.timeObserverThrottle.execute {
|
||||
self.model.updateWatch()
|
||||
self.model.updateWatch(time: self.currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,8 +642,18 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
if player.timeControlStatus == .playing {
|
||||
self.model.objectWillChange.send()
|
||||
if player.rate != Float(self.model.currentRate) {
|
||||
player.rate = Float(self.model.currentRate)
|
||||
|
||||
if let rate = self.model.rateToRestore, player.rate != rate {
|
||||
player.rate = rate
|
||||
self.model.rateToRestore = nil
|
||||
}
|
||||
|
||||
if player.rate > 0, player.rate != Float(self.model.currentRate) {
|
||||
if self.model.avPlayerUsesSystemControls {
|
||||
self.model.currentRate = Double(player.rate)
|
||||
} else {
|
||||
player.rate = Float(self.model.currentRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,7 +666,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
#endif
|
||||
|
||||
self.timeObserverThrottle.execute {
|
||||
self.model.updateWatch()
|
||||
self.model.updateWatch(time: self.currentTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,6 +722,17 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
} else {
|
||||
stopMusicMode()
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if model.playingFullScreen {
|
||||
ControlOverlaysModel.shared.hide()
|
||||
model.navigation.presentingPlaybackSettings = false
|
||||
|
||||
model.onPlayStream.append { _ in
|
||||
self.controller.enterFullScreen(animated: true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var isStartingPiP: Bool {
|
||||
|
||||
@@ -267,9 +267,11 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
self.model.lastSkipped = segment
|
||||
self.play()
|
||||
self.model.handleOnPlayStream(stream)
|
||||
}
|
||||
} else {
|
||||
self.play()
|
||||
self.model.handleOnPlayStream(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,7 +425,7 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
|
||||
timeObserverThrottle.execute {
|
||||
self.model.updateWatch()
|
||||
self.model.updateWatch(time: self.currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,8 @@ final class PlayerModel: ObservableObject {
|
||||
private var currentArtwork: MPMediaItemArtwork?
|
||||
|
||||
var onPresentPlayer = [() -> Void]()
|
||||
var onPlayStream = [(Stream) -> Void]()
|
||||
var rateToRestore: Float?
|
||||
private var remoteCommandCenterConfigured = false
|
||||
|
||||
init() {
|
||||
@@ -541,6 +543,9 @@ final class PlayerModel: ObservableObject {
|
||||
if !self.backend.canPlayAtRate(currentRate) {
|
||||
currentRate = self.backend.suggestedPlaybackRates.last { $0 < currentRate } ?? 1.0
|
||||
}
|
||||
|
||||
self.rateToRestore = Float(currentRate)
|
||||
|
||||
self.backend.didChangeTo()
|
||||
|
||||
if wasPlaying {
|
||||
@@ -623,13 +628,14 @@ final class PlayerModel: ObservableObject {
|
||||
closing = true
|
||||
controls.presentingControls = false
|
||||
|
||||
self.prepareCurrentItemForHistory(finished: finished)
|
||||
|
||||
self.hide()
|
||||
|
||||
Delay.by(0.8) { [weak self] in
|
||||
guard let self else { return }
|
||||
self.closePiP()
|
||||
|
||||
self.prepareCurrentItemForHistory(finished: finished)
|
||||
withAnimation {
|
||||
self.currentItem = nil
|
||||
}
|
||||
@@ -1078,7 +1084,9 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.aspectRatio = self.backend.aspectRatio
|
||||
withAnimation {
|
||||
self.aspectRatio = self.backend.aspectRatio
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1093,4 +1101,11 @@ final class PlayerModel: ObservableObject {
|
||||
guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" }
|
||||
return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))"
|
||||
}
|
||||
|
||||
func handleOnPlayStream(_ stream: Stream) {
|
||||
backend.setRate(currentRate)
|
||||
|
||||
onPlayStream.forEach { $0(stream) }
|
||||
onPlayStream.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ extension PlayerModel {
|
||||
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
|
||||
historyVideos.append(video)
|
||||
}
|
||||
updateWatch(finished: finished)
|
||||
updateWatch(finished: finished, time: backend.currentTime)
|
||||
}
|
||||
|
||||
if let video = currentItem.video,
|
||||
|
||||
@@ -15,14 +15,15 @@ struct ChannelPlaylistView: View {
|
||||
var player = PlayerModel.shared
|
||||
@ObservedObject private var recents = RecentsModel.shared
|
||||
|
||||
@State private var isLoading = false
|
||||
|
||||
private var items: [ContentItem] {
|
||||
ContentItem.array(of: store.item?.videos ?? [])
|
||||
}
|
||||
|
||||
private var resource: Resource? {
|
||||
accounts.api.channelPlaylist(playlist.id)
|
||||
let resource = accounts.api.channelPlaylist(playlist.id)
|
||||
resource?.addObserver(store)
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -47,7 +48,7 @@ struct ChannelPlaylistView: View {
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
#endif
|
||||
VerticalCells(items: items, isLoading: isLoading)
|
||||
VerticalCells(items: items)
|
||||
.environment(\.inChannelPlaylistView, true)
|
||||
}
|
||||
.environment(\.listingStyle, channelPlaylistListingStyle)
|
||||
@@ -55,16 +56,11 @@ struct ChannelPlaylistView: View {
|
||||
if let cache = ChannelPlaylistsCacheModel.shared.retrievePlaylist(playlist) {
|
||||
store.replace(cache)
|
||||
}
|
||||
isLoading = true
|
||||
resource?
|
||||
.load()
|
||||
.onSuccess { response in
|
||||
if let playlist: ChannelPlaylist = response.typedContent() {
|
||||
ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist)
|
||||
store.replace(playlist)
|
||||
}
|
||||
resource?.loadIfNeeded()?.onSuccess { response in
|
||||
if let playlist: ChannelPlaylist = response.typedContent() {
|
||||
ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist)
|
||||
}
|
||||
.onCompletion { _ in isLoading = false }
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
|
||||
@@ -65,7 +65,7 @@ struct ChannelVideosView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
#endif
|
||||
|
||||
VerticalCells(items: contentItems, isLoading: resource?.isLoading ?? false, edgesIgnoringSafeArea: verticalCellsEdgesIgnoringSafeArea) {
|
||||
VerticalCells(items: contentItems, edgesIgnoringSafeArea: verticalCellsEdgesIgnoringSafeArea) {
|
||||
if let description = presentedChannel?.description, !description.isEmpty {
|
||||
Button {
|
||||
withAnimation(.spring()) {
|
||||
|
||||
@@ -42,9 +42,21 @@ struct FavoriteItemView: View {
|
||||
.padding(.leading, 15)
|
||||
#endif
|
||||
|
||||
if limitedItems.isEmpty {
|
||||
EmptyItems(isLoading: resource?.isLoading ?? false) { reloadVisibleWatches() }
|
||||
.padding(.vertical, 10)
|
||||
if limitedItems.isEmpty, !(resource?.isLoading ?? false) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(emptyItemsText)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if hideShorts || hideWatched {
|
||||
AccentButton(text: "Disable filters", maxWidth: nil, verticalPadding: 0, minHeight: 30) {
|
||||
hideShorts = false
|
||||
hideWatched = false
|
||||
reloadVisibleWatches()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
#if os(tvOS)
|
||||
.padding(.horizontal, 40)
|
||||
#else
|
||||
@@ -61,7 +73,7 @@ struct FavoriteItemView: View {
|
||||
#if os(tvOS)
|
||||
.padding(.leading, 40)
|
||||
#else
|
||||
.padding(.leading, 15)
|
||||
.padding(.horizontal, 15)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -101,6 +113,19 @@ struct FavoriteItemView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var emptyItemsText: String {
|
||||
var filterText = ""
|
||||
if hideShorts && hideWatched {
|
||||
filterText = "(watched and shorts hidden)"
|
||||
} else if hideShorts {
|
||||
filterText = "(shorts hidden)"
|
||||
} else if hideWatched {
|
||||
filterText = "(watched hidden)"
|
||||
}
|
||||
|
||||
return "No videos to show".localized() + " " + filterText.localized()
|
||||
}
|
||||
|
||||
var contextMenu: some View {
|
||||
Group {
|
||||
if item.section == .history {
|
||||
@@ -179,7 +204,8 @@ struct FavoriteItemView: View {
|
||||
}
|
||||
|
||||
func watch(_ item: ContentItem) -> Watch? {
|
||||
watches.first { $0.videoID == item.video.videoID }
|
||||
guard let id = item.video?.videoID else { return nil }
|
||||
return watches.first { $0.videoID == id }
|
||||
}
|
||||
|
||||
var widgetListingStyle: WidgetListingStyle {
|
||||
|
||||
@@ -32,23 +32,8 @@ struct HomeView: View {
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack {
|
||||
HStack {
|
||||
#if os(tvOS)
|
||||
Group {
|
||||
if showOpenActionsInHome {
|
||||
AccentButton(text: "Open Video", imageSystemName: "globe") {
|
||||
NavigationModel.shared.presentingOpenVideos = true
|
||||
}
|
||||
}
|
||||
AccentButton(text: "Locations", imageSystemName: "globe") {
|
||||
NavigationModel.shared.presentingAccounts = true
|
||||
}
|
||||
|
||||
AccentButton(text: "Settings", imageSystemName: "gear") {
|
||||
NavigationModel.shared.presentingSettings = true
|
||||
}
|
||||
}
|
||||
#else
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
if showOpenActionsInHome {
|
||||
AccentButton(text: "Files", imageSystemName: "folder") {
|
||||
NavigationModel.shared.presentingFileImporter = true
|
||||
@@ -61,16 +46,36 @@ struct HomeView: View {
|
||||
}
|
||||
.frame(maxWidth: 40)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
HStack {
|
||||
if showOpenActionsInHome {
|
||||
Button {
|
||||
NavigationModel.shared.presentingOpenVideos = true
|
||||
} label: {
|
||||
Label("Open Video", systemImage: "globe")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
NavigationModel.shared.presentingAccounts = true
|
||||
} label: {
|
||||
Label("Locations", systemImage: "globe")
|
||||
}
|
||||
Spacer()
|
||||
HideWatchedButtons()
|
||||
HideShortsButtons()
|
||||
HomeSettingsButton()
|
||||
Button {
|
||||
NavigationModel.shared.presentingSettings = true
|
||||
} label: {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.font(.caption)
|
||||
.imageScale(.small)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
.padding(.top, 15)
|
||||
|
||||
@@ -15,8 +15,8 @@ import SwiftUI
|
||||
false
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
|
||||
#if os(iOS)
|
||||
#if os(iOS)
|
||||
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
|
||||
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
|
||||
if PlayerModel.shared.currentVideoIsLandscape {
|
||||
let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
|
||||
@@ -27,34 +27,40 @@ import SwiftUI
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
let wasPlaying = player.isPlaying
|
||||
coordinator.animate(alongsideTransition: nil) { context in
|
||||
#if os(iOS)
|
||||
func playerViewController(_: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
let wasPlaying = player.isPlaying
|
||||
coordinator.animate(alongsideTransition: nil) { context in
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
}
|
||||
#endif
|
||||
if !context.isCancelled {
|
||||
#if os(iOS)
|
||||
self.player.lockedOrientation = nil
|
||||
if !context.isCancelled {
|
||||
#if os(iOS)
|
||||
self.player.lockedOrientation = nil
|
||||
|
||||
if Constants.isIPhone {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
if Constants.isIPhone {
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
|
||||
}
|
||||
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
}
|
||||
if wasPlaying {
|
||||
self.player.play()
|
||||
}
|
||||
|
||||
self.player.playingFullScreen = false
|
||||
#endif
|
||||
self.player.playingFullScreen = false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForFullScreenExitWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
withAnimation(nil) {
|
||||
player.presentingPlayer = true
|
||||
}
|
||||
|
||||
completionHandler(true)
|
||||
}
|
||||
#endif
|
||||
|
||||
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {}
|
||||
|
||||
@@ -86,14 +92,6 @@ import SwiftUI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForFullScreenExitWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
|
||||
withAnimation(nil) {
|
||||
player.presentingPlayer = true
|
||||
}
|
||||
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -93,15 +93,17 @@ extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate {
|
||||
|
||||
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {}
|
||||
|
||||
func playerViewController(
|
||||
_: AVPlayerViewController,
|
||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||
) {}
|
||||
#if os(iOS)
|
||||
func playerViewController(
|
||||
_: AVPlayerViewController,
|
||||
willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||
) {}
|
||||
|
||||
func playerViewController(
|
||||
_: AVPlayerViewController,
|
||||
willEndFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||
) {}
|
||||
func playerViewController(
|
||||
_: AVPlayerViewController,
|
||||
willEndFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator
|
||||
) {}
|
||||
#endif
|
||||
|
||||
func playerViewController(
|
||||
_: AVPlayerViewController,
|
||||
|
||||
@@ -28,13 +28,7 @@ struct PlaybackSettings: View {
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
#if DEBUG
|
||||
// TODO: remove
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
Self._printChanges()
|
||||
}
|
||||
#endif
|
||||
return ScrollView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Button {
|
||||
@@ -69,21 +63,24 @@ struct PlaybackSettings: View {
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
|
||||
HStack {
|
||||
controlsHeader("Rate".localized())
|
||||
Spacer()
|
||||
HStack(spacing: rateButtonsSpacing) {
|
||||
decreaseRateButton
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .decreaseRate)
|
||||
#endif
|
||||
rateButton
|
||||
increaseRateButton
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .increaseRate)
|
||||
#endif
|
||||
if player.activeBackend == .mpv || !player.avPlayerUsesSystemControls {
|
||||
HStack {
|
||||
controlsHeader("Rate".localized())
|
||||
Spacer()
|
||||
HStack(spacing: rateButtonsSpacing) {
|
||||
decreaseRateButton
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .decreaseRate)
|
||||
#endif
|
||||
rateButton
|
||||
increaseRateButton
|
||||
#if os(tvOS)
|
||||
.focused($focusedField, equals: .increaseRate)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if player.activeBackend == .mpv {
|
||||
HStack {
|
||||
controlsHeader("Captions".localized())
|
||||
|
||||
@@ -25,7 +25,7 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.frame(maxWidth: geometry.size.width)
|
||||
.frame(width: geometry.size.width)
|
||||
.frame(maxHeight: maxHeight)
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
@@ -90,13 +90,7 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
|
||||
var videoPlayer: some View {
|
||||
#if DEBUG
|
||||
// TODO: remove
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
Self._printChanges()
|
||||
}
|
||||
#endif
|
||||
return GeometryReader { geometry in
|
||||
GeometryReader { geometry in
|
||||
HStack(spacing: 0) {
|
||||
content
|
||||
.onAppear {
|
||||
@@ -382,7 +376,7 @@ struct VideoPlayerView: View {
|
||||
.listStyle(.plain)
|
||||
#endif
|
||||
.frame(maxWidth: 350)
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
.background((colorScheme == .dark ? Color.black : Color.white).ignoresSafeArea())
|
||||
.transition(.move(edge: .bottom))
|
||||
}
|
||||
#elseif os(macOS)
|
||||
|
||||
@@ -70,7 +70,7 @@ struct PlaylistVideosView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VerticalCells(items: contentItems, isLoading: resource?.isLoading ?? false)
|
||||
VerticalCells(items: contentItems)
|
||||
.onAppear {
|
||||
guard contentItems.isEmpty else { return }
|
||||
loadResource()
|
||||
|
||||
@@ -63,11 +63,13 @@ struct PlaylistsView: View {
|
||||
var body: some View {
|
||||
SignInRequiredView(title: "Playlists".localized()) {
|
||||
VStack {
|
||||
VerticalCells(items: items, isLoading: resource?.isLoading ?? false) { if shouldDisplayHeader { header } }
|
||||
VerticalCells(items: items, allowEmpty: true) { if shouldDisplayHeader { header } }
|
||||
.environment(\.currentPlaylistID, currentPlaylist?.id)
|
||||
.environment(\.listingStyle, playlistListingStyle)
|
||||
|
||||
if model.all.isEmpty {
|
||||
if currentPlaylist != nil, items.isEmpty {
|
||||
hintText("Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"".localized())
|
||||
} else if model.all.isEmpty {
|
||||
hintText("You have no playlists\n\nTap on \"New Playlist\" to create one".localized())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,12 +244,22 @@ struct SearchView: View {
|
||||
if showRecentQueries {
|
||||
recentQueries
|
||||
} else {
|
||||
VerticalCells(items: state.store.collection, isLoading: state.isLoading) {
|
||||
VerticalCells(items: state.store.collection, allowEmpty: state.query.isEmpty) {
|
||||
if shouldDisplayHeader {
|
||||
header
|
||||
}
|
||||
}
|
||||
.environment(\.loadMoreContentHandler) { state.loadNextPage() }
|
||||
|
||||
if noResults {
|
||||
Text("No results")
|
||||
|
||||
if searchFiltersActive {
|
||||
Button("Reset search filters", action: resetFilters)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +280,12 @@ struct SearchView: View {
|
||||
searchDuration != .any || searchDate != .any
|
||||
}
|
||||
|
||||
private func resetFilters() {
|
||||
searchSortOrder = .relevance
|
||||
searchDate = .any
|
||||
searchDuration = .any
|
||||
}
|
||||
|
||||
private var noResults: Bool {
|
||||
state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty
|
||||
}
|
||||
|
||||
@@ -77,9 +77,11 @@ struct ChannelsView: View {
|
||||
.listRowSeparator(false)
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.background(
|
||||
NavigationLink(destination: ChannelVideosView(channel: channelForLink ?? Video.fixture.channel), isActive: $channelLinkActive, label: EmptyView.init)
|
||||
)
|
||||
#endif
|
||||
.onAppear {
|
||||
subscriptions.load()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct FeedView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VerticalCells(items: videos, isLoading: feed.isLoading) { if shouldDisplayHeader { header } }
|
||||
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
|
||||
.environment(\.loadMoreContentHandler) { feed.loadNextPage() }
|
||||
.onAppear {
|
||||
feed.loadResources()
|
||||
|
||||
@@ -22,51 +22,62 @@ struct TrendingView: View {
|
||||
}
|
||||
|
||||
@State private var error: RequestError?
|
||||
@State private var resource: Resource?
|
||||
@State private var isLoading = false
|
||||
|
||||
init(_ videos: [Video] = [Video]()) {
|
||||
self.videos = videos
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VerticalCells(items: trending, isLoading: isLoading) { if shouldDisplayHeader { header } }
|
||||
.environment(\.listingStyle, trendingListingStyle)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
RequestErrorButton(error: error)
|
||||
}
|
||||
#if os(macOS)
|
||||
ToolbarItemGroup {
|
||||
if let favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
}
|
||||
var resource: Resource {
|
||||
let newResource: Resource
|
||||
|
||||
categoryButton
|
||||
countryButton
|
||||
newResource = accounts.api.trending(country: country, category: category)
|
||||
newResource.addObserver(store)
|
||||
|
||||
return newResource
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
VerticalCells(items: trending) { if shouldDisplayHeader { header } }
|
||||
.environment(\.listingStyle, trendingListingStyle)
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
RequestErrorButton(error: error)
|
||||
}
|
||||
#if os(macOS)
|
||||
ToolbarItemGroup {
|
||||
if let favoriteItem {
|
||||
FavoriteButton(item: favoriteItem)
|
||||
.id(favoriteItem.id)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: category) { _ in updateResource() }
|
||||
.onChange(of: country) { _ in updateResource() }
|
||||
.onChange(of: accounts.current) { _ in updateResource() }
|
||||
.onChange(of: resource) { _ in
|
||||
isLoading = true
|
||||
resource?.load()
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
.onCompletion { _ in self.isLoading = false }
|
||||
}
|
||||
.onAppear { updateResource()
|
||||
}
|
||||
|
||||
categoryButton
|
||||
countryButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: resource) { _ in
|
||||
resource.load()
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
updateFavoriteItem()
|
||||
}
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()?
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(isPresented: $presentingCountrySelection) {
|
||||
TrendingCountry(selectedCountry: $country)
|
||||
}
|
||||
.fullScreenCover(isPresented: $presentingCountrySelection) {
|
||||
TrendingCountry(selectedCountry: $country)
|
||||
}
|
||||
#else
|
||||
.sheet(isPresented: $presentingCountrySelection) {
|
||||
.sheet(isPresented: $presentingCountrySelection) {
|
||||
TrendingCountry(selectedCountry: $country)
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 400, minHeight: 400)
|
||||
@@ -74,11 +85,9 @@ struct TrendingView: View {
|
||||
}
|
||||
.background(
|
||||
Button("Refresh") {
|
||||
isLoading = true
|
||||
resource?.load()
|
||||
resource.load()
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
.onCompletion { _ in self.isLoading = false }
|
||||
}
|
||||
.keyboardShortcut("r")
|
||||
.opacity(0)
|
||||
@@ -87,18 +96,16 @@ struct TrendingView: View {
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.refreshControl { refreshControl in
|
||||
resource?.load().onCompletion { _ in
|
||||
resource.load().onCompletion { _ in
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
.refreshable {
|
||||
DispatchQueue.main.async {
|
||||
isLoading = true
|
||||
resource?.load()
|
||||
resource.load()
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
.onCompletion { _ in self.isLoading = false }
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -124,13 +131,9 @@ struct TrendingView: View {
|
||||
}
|
||||
#else
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
let request = resource?.loadIfNeeded()
|
||||
if request != nil {
|
||||
isLoading = true
|
||||
}
|
||||
request?.onFailure { self.error = $0 }
|
||||
resource.loadIfNeeded()?
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
.onCompletion { _ in self.isLoading = false }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -222,7 +225,7 @@ struct TrendingView: View {
|
||||
private var countryButton: some View {
|
||||
Button(action: {
|
||||
presentingCountrySelection.toggle()
|
||||
resource?.removeObservers(ownedBy: store)
|
||||
resource.removeObservers(ownedBy: store)
|
||||
}) {
|
||||
#if os(iOS)
|
||||
Label("Country", systemImage: "flag")
|
||||
@@ -233,13 +236,6 @@ struct TrendingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateResource() {
|
||||
let resource = accounts.api.trending(country: country, category: category)
|
||||
resource.addObserver(store)
|
||||
self.resource = resource
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
private func updateFavoriteItem() {
|
||||
favoriteItem = FavoriteItem(section: .trending(country.rawValue, category.rawValue))
|
||||
}
|
||||
@@ -258,7 +254,7 @@ struct TrendingView: View {
|
||||
HideShortsButtons()
|
||||
|
||||
Button {
|
||||
resource?.load()
|
||||
resource.load()
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
} label: {
|
||||
|
||||
@@ -10,7 +10,7 @@ struct VerticalCells<Header: View>: View {
|
||||
@Environment(\.listingStyle) private var listingStyle
|
||||
|
||||
var items = [ContentItem]()
|
||||
var isLoading: Bool
|
||||
var allowEmpty = false
|
||||
var edgesIgnoringSafeArea = Edge.Set.horizontal
|
||||
|
||||
let header: Header?
|
||||
@@ -19,48 +19,32 @@ struct VerticalCells<Header: View>: View {
|
||||
|
||||
init(
|
||||
items: [ContentItem],
|
||||
isLoading: Bool,
|
||||
allowEmpty: Bool = false,
|
||||
edgesIgnoringSafeArea: Edge.Set = .horizontal,
|
||||
@ViewBuilder header: @escaping () -> Header? = { nil }
|
||||
) {
|
||||
self.items = items
|
||||
self.isLoading = isLoading
|
||||
self.allowEmpty = allowEmpty
|
||||
self.edgesIgnoringSafeArea = edgesIgnoringSafeArea
|
||||
self.header = header()
|
||||
}
|
||||
|
||||
init(
|
||||
items: [ContentItem],
|
||||
isLoading: Bool
|
||||
allowEmpty: Bool = false
|
||||
) where Header == EmptyView {
|
||||
self.init(items: items, isLoading: isLoading) { EmptyView() }
|
||||
self.init(items: items, allowEmpty: allowEmpty) { EmptyView() }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: scrollViewShowsIndicators) {
|
||||
Group {
|
||||
LazyVGrid(columns: adaptiveItem, alignment: .center) {
|
||||
Section(header: header) {
|
||||
ForEach(contentItems) { item in
|
||||
ContentItemView(item: item)
|
||||
.onAppear { loadMoreContentItemsIfNeeded(current: item) }
|
||||
}
|
||||
LazyVGrid(columns: adaptiveItem, alignment: .center) {
|
||||
Section(header: header) {
|
||||
ForEach(contentItems) { item in
|
||||
ContentItemView(item: item)
|
||||
.onAppear { loadMoreContentItemsIfNeeded(current: item) }
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.onAppear {
|
||||
gridSize = proxy.size
|
||||
}
|
||||
.onChange(of: proxy.size) { newValue in
|
||||
gridSize = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
if !isLoading && gridSize.height < 50 {
|
||||
EmptyItems()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -73,7 +57,7 @@ struct VerticalCells<Header: View>: View {
|
||||
}
|
||||
|
||||
var contentItems: [ContentItem] {
|
||||
items.isEmpty && isLoading ? (ContentItem.placeholders) : items.sorted { $0 < $1 }
|
||||
items.isEmpty ? (allowEmpty ? items : ContentItem.placeholders) : items.sorted { $0 < $1 }
|
||||
}
|
||||
|
||||
func loadMoreContentItemsIfNeeded(current item: ContentItem) {
|
||||
@@ -120,7 +104,7 @@ struct VerticalCells<Header: View>: View {
|
||||
|
||||
struct VeticalCells_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VerticalCells(items: ContentItem.array(of: Array(repeating: Video.fixture, count: 30)), isLoading: false)
|
||||
VerticalCells(items: ContentItem.array(of: Array(repeating: Video.fixture, count: 30)))
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct EmptyItems: View {
|
||||
@Default(.hideShorts) private var hideShorts
|
||||
@Default(.hideWatched) private var hideWatched
|
||||
|
||||
var isLoading = false
|
||||
var onDisableFilters: () -> Void = {}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
if isLoading {
|
||||
HStack(spacing: 10) {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text("Loading...")
|
||||
}
|
||||
} else {
|
||||
Text(emptyItemsText)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if hideShorts || hideWatched {
|
||||
AccentButton(text: "Disable filters", maxWidth: nil, verticalPadding: 0, minHeight: 30) {
|
||||
hideShorts = false
|
||||
hideWatched = false
|
||||
onDisableFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var emptyItemsText: String {
|
||||
var filterText = ""
|
||||
if hideShorts && hideWatched {
|
||||
filterText = "(watched and shorts hidden)"
|
||||
} else if hideShorts {
|
||||
filterText = "(shorts hidden)"
|
||||
} else if hideWatched {
|
||||
filterText = "(watched hidden)"
|
||||
}
|
||||
|
||||
return "No videos to show".localized() + " " + filterText.localized()
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyItems_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
EmptyItems()
|
||||
Spacer()
|
||||
EmptyItems(isLoading: true)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ struct PopularView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VerticalCells(items: videos, isLoading: resource?.isLoading ?? false) { if shouldDisplayHeader { header } }
|
||||
VerticalCells(items: videos) { if shouldDisplayHeader { header } }
|
||||
.onAppear {
|
||||
resource?.addObserver(store)
|
||||
resource?.loadIfNeeded()?
|
||||
|
||||
@@ -20,7 +20,6 @@ struct VideoContextMenuView: View {
|
||||
|
||||
@FetchRequest private var watchRequest: FetchedResults<Watch>
|
||||
|
||||
@Default(.saveHistory) private var saveHistory
|
||||
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
||||
|
||||
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
||||
@@ -44,7 +43,7 @@ struct VideoContextMenuView: View {
|
||||
removeAllFromQueueButton()
|
||||
}
|
||||
if !video.localStreamIsDirectory {
|
||||
if saveHistory {
|
||||
if Defaults[.saveHistory] {
|
||||
Section {
|
||||
if let watchedAtString {
|
||||
Text(watchedAtString)
|
||||
@@ -72,7 +71,7 @@ struct VideoContextMenuView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
if showPlayNowInBackendContextMenu {
|
||||
if Defaults[.showPlayNowInBackendContextMenu] {
|
||||
Section {
|
||||
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
|
||||
playNowInBackendButton(backend)
|
||||
|
||||
@@ -591,3 +591,7 @@
|
||||
"Disable filters" = "Filter deaktivieren";
|
||||
"Limit" = "Grenze";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Möchten Sie %@ wirklich aus den Favoriten entfernen?";
|
||||
"Play Now in MPV" = "Jetzt im MPV abspielen";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Kanäle mit ungesehenen Videos oben in der Abonnementliste halten";
|
||||
"Show video context menu options to force selected backend" = "Video-Kontextmenüoptionen anzeigen, um die Auswahl des Backends zu erzwingen";
|
||||
"Play Now in AVPlayer" = "Jetzt in AVPlayer abspielen";
|
||||
|
||||
@@ -589,3 +589,7 @@
|
||||
"Disable filters" = "Disable filters";
|
||||
"Limit" = "Limit";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Are you sure you want to remove %@ from Favorites?";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Keep channels with unwatched videos on top of subscriptions list";
|
||||
"Show video context menu options to force selected backend" = "Show video context menu options to force selected backend";
|
||||
"Play Now in MPV" = "Play Now in MPV";
|
||||
"Play Now in AVPlayer" = "Play Now in AVPlayer";
|
||||
|
||||
@@ -591,3 +591,7 @@
|
||||
"(watched hidden)" = "(regardées masquées)";
|
||||
"Limit" = "Limite";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Êtes-vous sûr de vouloir supprimer %@ des favoris ?";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Conserver les chaînes dont les vidéos n'ont pas été visionnées en haut de la liste des abonnements";
|
||||
"Show video context menu options to force selected backend" = "Afficher les options contextuelles de la vidéo pour forcer la sélection du backend";
|
||||
"Play Now in AVPlayer" = "Lire maintenant dans AVPlayer";
|
||||
"Play Now in MPV" = "Lire maintenant dans MPV";
|
||||
|
||||
@@ -510,7 +510,7 @@
|
||||
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "動画の冒頭で、同じ作成者の他の動画でも見られるアニメーション、静止画やクリップを含む部分。";
|
||||
"This information will be processed only on your device and used to connect you to the server in the specified country." = "この情報は、この端末上でのみ処理され、指定した国のサーバーに接続するために使用されます。";
|
||||
"Videos" = "動画";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "クレジットのポップアップやエンドカードが表示される、映像の最後のあたりに表示されます。";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "クレジットのポップアップや終了シーンが表示される、映像の最後のあたりに表示されます。";
|
||||
"Verified" = "認証済み";
|
||||
"Mark channel feed as unwatched" = "チャンネルフィードを未視聴にする";
|
||||
"Open expanded" = "展開したまま開く";
|
||||
@@ -588,3 +588,7 @@
|
||||
"(watched hidden)" = "(視聴済み非表示)";
|
||||
"Disable filters" = "絞り込み解除";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "お気に入りから %@ を除去しますか?";
|
||||
"Show video context menu options to force selected backend" = "強制的にバックエンドを選択するための動画のコンテキストメニューを表示";
|
||||
"Play Now in AVPlayer" = "AVPlayer で今すぐ再生";
|
||||
"Play Now in MPV" = "MPV で今すぐ再生";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "未視聴の動画があるチャンネルをチャンネル一覧の上部に維持";
|
||||
|
||||
@@ -582,7 +582,7 @@
|
||||
"Landscape left" = "Obrót w lewo";
|
||||
"Available" = "Dostępne";
|
||||
"Startup section" = "Sekcja startowa";
|
||||
"Home Settings" = "Ustawienia strony głównej";
|
||||
"Home Settings" = "Ustawienia głównej";
|
||||
"Watched: hidden" = "Obejrzane: ukryte";
|
||||
"Watched: visible" = "Obejrzane: widoczne";
|
||||
"No videos to show" = "Brak wideo do pokazania";
|
||||
@@ -592,3 +592,7 @@
|
||||
"(watched hidden)" = "(obejrzane ukryte)";
|
||||
"Limit" = "Limit";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Czy na pewno chcesz usunąć element: %@ z Ulubionych?";
|
||||
"Play Now in AVPlayer" = "Odtwórz teraz w AVPlayerze";
|
||||
"Play Now in MPV" = "Odtwórz teraz w MPV";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Kanały z nieobejrzanymi filmami na górze listy subskrypcji";
|
||||
"Show video context menu options to force selected backend" = "Pokaż opcje menu kontekstowego wideo, aby wymusić wybrany silnik";
|
||||
|
||||
@@ -591,3 +591,7 @@
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Tem certeza que deseja remover %@ dos Favoritos?";
|
||||
"No videos to show" = "Nenhum vídeo para mostrar";
|
||||
"(watched and shorts hidden)" = "(assistidos e shorts ocultos)";
|
||||
"Show video context menu options to force selected backend" = "Mostrar opções do menu contextual do vídeo para forçar o backend selecionado";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Manter canais com vídeos não vistos no topo da lista de inscrições";
|
||||
"Play Now in AVPlayer" = "Tocar Agora no AVPlayer";
|
||||
"Play Now in MPV" = "Tocar Agora em MPV";
|
||||
|
||||
@@ -591,3 +591,7 @@
|
||||
"(shorts hidden)" = "(shorts ascunse)";
|
||||
"Limit" = "Limită";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Sigur doriți să eliminați %@ din Favorite?";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "Păstrați canalele cu videoclipuri nevăzute în partea de sus a listei de abonamente";
|
||||
"Play Now in AVPlayer" = "Redă acum în AVPlayer";
|
||||
"Play Now in MPV" = "Redă acum în MPV";
|
||||
"Show video context menu options to force selected backend" = "Afișați opțiunile din meniul contextual video pentru a forța backend-ul selectat";
|
||||
|
||||
@@ -690,9 +690,6 @@
|
||||
379DC3D128BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; };
|
||||
379DC3D228BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; };
|
||||
379DC3D328BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; };
|
||||
379E7C2F2A20AF0A00AF8118 /* EmptyItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */; };
|
||||
379E7C302A20AF0A00AF8118 /* EmptyItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */; };
|
||||
379E7C312A20AF0A00AF8118 /* EmptyItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */; };
|
||||
379E7C332A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */; };
|
||||
379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */; };
|
||||
379E7C362A2105B900AF8118 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 379E7C352A2105B900AF8118 /* Introspect */; };
|
||||
@@ -1397,7 +1394,6 @@
|
||||
379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSettingsButton.swift; sourceTree = "<group>"; };
|
||||
379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = "<group>"; };
|
||||
379DC3D028BA4EB400B09677 /* Seek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Seek.swift; sourceTree = "<group>"; };
|
||||
379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyItems.swift; sourceTree = "<group>"; };
|
||||
379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableSearchTextField.swift; sourceTree = "<group>"; };
|
||||
379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */ = {isa = PBXFileReference; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = HideShortsButtons.swift; sourceTree = "<group>"; };
|
||||
379F141E289ECE7F00DE48B5 /* QualitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettings.swift; sourceTree = "<group>"; };
|
||||
@@ -1911,7 +1907,6 @@
|
||||
37FB285D272225E800A57617 /* ContentItemView.swift */,
|
||||
372CFD14285F2E2A00B0B54B /* ControlsBar.swift */,
|
||||
3748186D26A769D60084E870 /* DetailBadge.swift */,
|
||||
379E7C2E2A20AF0A00AF8118 /* EmptyItems.swift */,
|
||||
37599F37272B4D740087F250 /* FavoriteButton.swift */,
|
||||
379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */,
|
||||
37758C0A2A1D1C8B001FD900 /* HideWatchedButtons.swift */,
|
||||
@@ -3190,7 +3185,6 @@
|
||||
374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||
37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */,
|
||||
37D836BC294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
|
||||
379E7C2F2A20AF0A00AF8118 /* EmptyItems.swift in Sources */,
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
|
||||
@@ -3501,7 +3495,6 @@
|
||||
37599F35272B44000087F250 /* FavoritesModel.swift in Sources */,
|
||||
376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */,
|
||||
37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
379E7C302A20AF0A00AF8118 /* EmptyItems.swift in Sources */,
|
||||
377ABC45286E4B74009C986F /* ManifestedInstance.swift in Sources */,
|
||||
3795593727B08538007FF8F4 /* StreamControl.swift in Sources */,
|
||||
37772E0E2A216F8600608BD9 /* String+ReplacingHTMLEntities.swift in Sources */,
|
||||
@@ -3796,7 +3789,6 @@
|
||||
37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||
3788AC2926F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||
379E7C312A20AF0A00AF8118 /* EmptyItems.swift in Sources */,
|
||||
3718B9A52921A97F0003DB2E /* InspectorView.swift in Sources */,
|
||||
37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
||||
376BE50D27349108009AD608 /* BrowsingSettings.swift in Sources */,
|
||||
@@ -4061,7 +4053,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -4092,7 +4084,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -4123,7 +4115,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4143,7 +4135,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4303,7 +4295,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
@@ -4356,7 +4348,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -4408,7 +4400,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -4450,7 +4442,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@@ -4488,7 +4480,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4512,7 +4504,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4538,7 +4530,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4563,7 +4555,7 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4589,7 +4581,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -4629,7 +4621,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4670,7 +4662,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -4694,7 +4686,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 152;
|
||||
CURRENT_PROJECT_VERSION = 155;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "7f9fb5d43ecd4aa714c00746f54873f354403438"
|
||||
"revision" : "90aa750c92973005f086263e44c6224a1456bb12"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -150,7 +150,7 @@
|
||||
"location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "f94be38411297c71aa8df69c6875127e6d8d7a92"
|
||||
"revision" : "2abb11839f80ebb07a58ac5e146a1da664260c16"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -158,8 +158,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"state" : {
|
||||
"revision" : "5b3f3996c7a2a84d5f4ba0e03cd7d584154778f2",
|
||||
"version" : "0.3.1"
|
||||
"revision" : "c29b50a475120fdfa22573622464a7346aaf99a4",
|
||||
"version" : "0.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user