mirror of
https://github.com/yattee/yattee.git
synced 2025-10-09 08:58:10 +00:00
Merge branch 'main' into use-mpvkit
This commit is contained in:
@@ -9,28 +9,26 @@ struct ChannelAvatarView: View {
|
||||
@ObservedObject private var accounts = AccountsModel.shared
|
||||
@ObservedObject private var subscribedChannels = SubscribedChannelsModel.shared
|
||||
|
||||
@State private var url: URL?
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Group {
|
||||
Group {
|
||||
if let url = channel?.thumbnailURLOrCached {
|
||||
if let url {
|
||||
ThumbnailView(url: url)
|
||||
} else {
|
||||
ZStack {
|
||||
Color(white: 0.6)
|
||||
.opacity(0.5)
|
||||
Color("PlaceholderColor")
|
||||
|
||||
Group {
|
||||
if let video, video.isLocal {
|
||||
Image(systemName: video.localStreamImageSystemName)
|
||||
} else {
|
||||
Image(systemName: "play.rectangle")
|
||||
}
|
||||
if let video, video.isLocal {
|
||||
Image(systemName: video.localStreamImageSystemName)
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 20))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 20))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.onAppear(perform: updateURL)
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
@@ -54,6 +52,16 @@ struct ChannelAvatarView: View {
|
||||
}
|
||||
.imageScale(.small)
|
||||
}
|
||||
|
||||
func updateURL() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
if let url = channel?.thumbnailURLOrCached {
|
||||
DispatchQueue.main.async {
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelAvatarView_Previews: PreviewProvider {
|
||||
|
@@ -61,7 +61,8 @@ struct ChannelListItem: View {
|
||||
private var label: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack {
|
||||
ChannelAvatarView(channel: channel)
|
||||
ChannelAvatarView(channel: channel, subscribedBadge: false)
|
||||
.id("channel-avatar-\(channel.id)")
|
||||
#if os(tvOS)
|
||||
.frame(width: 90, height: 90)
|
||||
#else
|
||||
|
@@ -40,7 +40,7 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let content = VStack {
|
||||
VStack {
|
||||
#if os(tvOS)
|
||||
VStack {
|
||||
HStack(spacing: 24) {
|
||||
@@ -181,19 +181,12 @@ struct ChannelVideosView: View {
|
||||
.navigationTitle(navigationTitle)
|
||||
#endif
|
||||
|
||||
return Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
content
|
||||
#if os(tvOS)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#endif
|
||||
#if !os(iOS)
|
||||
.focusScope(focusNamespace)
|
||||
#endif
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.background(Color.background(scheme: colorScheme))
|
||||
#endif
|
||||
#if !os(iOS)
|
||||
.focusScope(focusNamespace)
|
||||
#endif
|
||||
}
|
||||
|
||||
var verticalCellsEdgesIgnoringSafeArea: Edge.Set {
|
||||
@@ -212,6 +205,7 @@ struct ChannelVideosView: View {
|
||||
|
||||
var thumbnail: some View {
|
||||
ChannelAvatarView(channel: store.item?.channel)
|
||||
.id("channel-avatar-\(store.item?.channel?.id ?? "")")
|
||||
#if os(tvOS)
|
||||
.frame(width: 80, height: 80, alignment: .trailing)
|
||||
#else
|
||||
@@ -338,7 +332,8 @@ struct ChannelVideosView: View {
|
||||
private var resource: Resource? {
|
||||
guard let channel = presentedChannel else { return nil }
|
||||
|
||||
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
|
||||
let tabData = channel.tabs.first { $0.contentType == contentType }?.data
|
||||
let data = contentType != .videos ? tabData : nil
|
||||
let resource = accounts.api.channel(channel.id, contentType: contentType, data: data)
|
||||
|
||||
if contentType == .videos {
|
||||
@@ -451,7 +446,8 @@ struct ChannelVideosView: View {
|
||||
next = next ?? ""
|
||||
}
|
||||
|
||||
let data = contentType != .videos ? channel.tabs.first(where: { $0.contentType == contentType })?.data : nil
|
||||
let tabData = channel.tabs.first { $0.contentType == contentType }?.data
|
||||
let data = contentType != .videos ? tabData : nil
|
||||
accounts.api.channel(channel.id, contentType: contentType, data: data, page: next).load().onSuccess { response in
|
||||
if let page: ChannelPage = response.typedContent() {
|
||||
self.page = page
|
||||
|
@@ -63,16 +63,9 @@ struct Constants {
|
||||
|
||||
static func seekIcon(_ type: String, _ interval: TimeInterval) -> String {
|
||||
let interval = Int(interval)
|
||||
let allVersions = [10, 15, 30, 45, 60, 75, 90]
|
||||
let iOS15 = [5]
|
||||
let allVersions = [5, 10, 15, 30, 45, 60, 75, 90]
|
||||
let iconName = "go\(type).\(interval)"
|
||||
|
||||
if #available(iOS 15, macOS 12, *) {
|
||||
if iOS15.contains(interval) {
|
||||
return iconName
|
||||
}
|
||||
}
|
||||
|
||||
if allVersions.contains(interval) {
|
||||
return iconName
|
||||
}
|
||||
|
@@ -148,6 +148,9 @@ extension Defaults.Keys {
|
||||
#endif
|
||||
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
|
||||
|
||||
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
|
||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||
|
||||
#if os(tvOS)
|
||||
static let pauseOnHidingPlayerDefault = true
|
||||
#else
|
||||
|
@@ -34,8 +34,7 @@ struct DocumentsView: View {
|
||||
}
|
||||
.navigationTitle(directoryLabel)
|
||||
.padding(.horizontal)
|
||||
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||
.backport
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.refreshable {
|
||||
DispatchQueue.main.async {
|
||||
self.refresh()
|
||||
|
@@ -105,12 +105,6 @@ struct FavoriteItemView: View {
|
||||
reloadVisibleWatches()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Defaults.observe(.widgetsSettings) { _ in
|
||||
watchModel.watchesChanged()
|
||||
}
|
||||
.tieToLifetime(of: accounts)
|
||||
}
|
||||
}
|
||||
|
||||
var emptyItemsText: String {
|
||||
|
@@ -15,14 +15,10 @@ struct AccountViewButton: View {
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
if !accountPickerDisplaysUsername || !(model.current?.isPublic ?? true) {
|
||||
if #available(iOS 15, macOS 12, *) {
|
||||
if let name = model.current?.app?.rawValue.capitalized {
|
||||
Image(name)
|
||||
.resizable()
|
||||
.frame(width: accountImageSize, height: accountImageSize)
|
||||
} else {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
if let name = model.current?.app?.rawValue.capitalized {
|
||||
Image(name)
|
||||
.resizable()
|
||||
.frame(width: accountImageSize, height: accountImageSize)
|
||||
} else {
|
||||
Image(systemName: "globe")
|
||||
}
|
||||
|
@@ -38,7 +38,6 @@ struct AppSidebarPlaylists: View {
|
||||
|
||||
if accounts.app.userPlaylistsEndpointIncludesVideos, !playlist.videos.isEmpty {
|
||||
label
|
||||
.backport
|
||||
.badge(Text("\(playlist.videos.count)"))
|
||||
} else {
|
||||
label
|
||||
|
@@ -50,6 +50,8 @@ struct RecentNavigationLink<DestinationContent: View>: View {
|
||||
var recents = RecentsModel.shared
|
||||
@ObservedObject private var navigation = NavigationModel.shared
|
||||
|
||||
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
|
||||
|
||||
var recent: RecentItem
|
||||
var systemImage: String?
|
||||
let destination: DestinationContent
|
||||
@@ -71,9 +73,10 @@ struct RecentNavigationLink<DestinationContent: View>: View {
|
||||
HStack {
|
||||
if recent.type == .channel,
|
||||
let channel = recent.channel,
|
||||
channel.thumbnailURLOrCached != nil
|
||||
showChannelAvatarInChannelsLists
|
||||
{
|
||||
ChannelAvatarView(channel: channel, subscribedBadge: false)
|
||||
.id("channel-avatar-\(channel.id)")
|
||||
.frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize)
|
||||
|
||||
Text(channel.name)
|
||||
|
@@ -8,17 +8,23 @@ struct AppSidebarSubscriptions: View {
|
||||
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared
|
||||
|
||||
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
|
||||
@Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop
|
||||
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
|
||||
|
||||
@State private var channelLinkActive = false
|
||||
@State private var channelForLink: Channel?
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text("Subscriptions")) {
|
||||
ForEach(subscriptions.all) { channel in
|
||||
ForEach(channels) { channel in
|
||||
NavigationLink(tag: TabSelection.channel(channel.id), selection: $navigation.tabSelection) {
|
||||
LazyView(ChannelVideosView(channel: channel))
|
||||
} label: {
|
||||
HStack {
|
||||
if channel.thumbnailURLOrCached != nil {
|
||||
if showChannelAvatarInChannelsLists {
|
||||
ChannelAvatarView(channel: channel, subscribedBadge: false)
|
||||
.frame(width: Constants.sidebarChannelThumbnailSize, height: Constants.sidebarChannelThumbnailSize)
|
||||
|
||||
Text(channel.name)
|
||||
} else {
|
||||
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
|
||||
@@ -26,14 +32,10 @@ struct AppSidebarSubscriptions: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.backport
|
||||
.lineLimit(1)
|
||||
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil)
|
||||
}
|
||||
.contextMenu {
|
||||
if subscriptions.isSubscribing(channel.id) {
|
||||
toggleWatchedButton(channel)
|
||||
}
|
||||
|
||||
Button("Unsubscribe") {
|
||||
navigation.presentUnsubscribeAlert(channel, subscriptions: subscriptions)
|
||||
}
|
||||
@@ -43,29 +45,8 @@ struct AppSidebarSubscriptions: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func toggleWatchedButton(_ channel: Channel) -> some View {
|
||||
if feed.canMarkChannelAsWatched(channel.id) {
|
||||
markChannelAsWatchedButton(channel)
|
||||
} else {
|
||||
markChannelAsUnwatchedButton(channel)
|
||||
}
|
||||
}
|
||||
|
||||
func markChannelAsWatchedButton(_ channel: Channel) -> some View {
|
||||
Button {
|
||||
feed.markChannelAsWatched(channel.id)
|
||||
} label: {
|
||||
Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill")
|
||||
}
|
||||
.disabled(!feed.canMarkAllFeedAsWatched)
|
||||
}
|
||||
|
||||
func markChannelAsUnwatchedButton(_ channel: Channel) -> some View {
|
||||
Button {
|
||||
feed.markChannelAsUnwatched(channel.id)
|
||||
} label: {
|
||||
Label("Mark channel feed as unwatched", systemImage: "checkmark.circle")
|
||||
}
|
||||
var channels: [Channel] {
|
||||
keepChannelsWithUnwatchedFeedOnTop ? subscriptions.allByUnwatchedCount : subscriptions.all
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -95,7 +95,6 @@ struct AppTabNavigation: View {
|
||||
.accessibility(label: Text("Subscriptions"))
|
||||
}
|
||||
.tag(TabSelection.subscriptions)
|
||||
.backport
|
||||
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedText : nil)
|
||||
}
|
||||
|
||||
|
@@ -131,10 +131,6 @@ struct ContentView: View {
|
||||
|
||||
NavigationModel.shared.presentingOpenVideos = false
|
||||
}
|
||||
.onOpenURL { url in
|
||||
URLBookmarkModel.shared.saveBookmark(url)
|
||||
OpenURLHandler(navigationStyle: navigationStyle).handle(url)
|
||||
}
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
||||
AddToPlaylistView(video: navigation.videoToAddToPlaylist)
|
||||
@@ -173,16 +169,6 @@ struct ContentView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var navigationStyle: NavigationStyle {
|
||||
#if os(iOS)
|
||||
return horizontalSizeClass == .compact ? .tab : .sidebar
|
||||
#elseif os(tvOS)
|
||||
return .tab
|
||||
#else
|
||||
return .sidebar
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder var videoPlayer: some View {
|
||||
if player.presentingPlayer {
|
||||
playerView
|
||||
@@ -199,7 +185,6 @@ struct ContentView: View {
|
||||
|
||||
var playerView: some View {
|
||||
VideoPlayerView()
|
||||
.environment(\.navigationStyle, navigationStyle)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -79,7 +79,6 @@ struct Sidebar: View {
|
||||
Label("Subscriptions", systemImage: "star.circle")
|
||||
.accessibility(label: Text("Subscriptions"))
|
||||
}
|
||||
.backport
|
||||
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedText : nil)
|
||||
.contextMenu {
|
||||
playUnwatchedButton
|
||||
@@ -152,7 +151,8 @@ struct Sidebar: View {
|
||||
if case .recentlyOpened = selection {
|
||||
scrollView.scrollTo("recentlyOpened")
|
||||
return
|
||||
} else if case let .playlist(id) = selection {
|
||||
}
|
||||
if case let .playlist(id) = selection {
|
||||
scrollView.scrollTo(id)
|
||||
return
|
||||
}
|
||||
|
@@ -47,7 +47,7 @@ final class AppleAVPlayerViewController: UIViewController {
|
||||
infoViewControllers.append(infoViewController([.chapters], title: "Chapters"))
|
||||
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
||||
|
||||
var queueSections = [NowPlayingView.ViewSection.playingNext]
|
||||
let queueSections = [NowPlayingView.ViewSection.playingNext]
|
||||
|
||||
infoViewControllers.append(contentsOf: [
|
||||
infoViewController([.related], title: "Related"),
|
||||
|
@@ -7,19 +7,8 @@ struct ControlBackgroundModifier: ViewModifier {
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if enabled {
|
||||
if #available(iOS 15, macOS 12, *) {
|
||||
content
|
||||
.background(.thinMaterial)
|
||||
} else {
|
||||
content
|
||||
#if os(macOS)
|
||||
.background(VisualEffectBlur(material: .hudWindow))
|
||||
#elseif os(iOS)
|
||||
.background(VisualEffectBlur(blurStyle: .systemThinMaterial).edgesIgnoringSafeArea(edgesIgnoringSafeArea))
|
||||
#else
|
||||
content
|
||||
.background(.thinMaterial)
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
|
@@ -25,9 +25,8 @@ struct OpeningStream: View {
|
||||
if let selection = player.streamSelection {
|
||||
if selection.isLocal {
|
||||
return "Opening file...".localized()
|
||||
} else {
|
||||
return String(format: "Opening %@ stream...".localized(), selection.shortQuality)
|
||||
}
|
||||
return String(format: "Opening %@ stream...".localized(), selection.shortQuality)
|
||||
}
|
||||
|
||||
return "Loading streams...".localized()
|
||||
|
@@ -193,8 +193,11 @@ struct PlayerControls: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
#if os(tvOS)
|
||||
.onChange(of: model.presentingControls) { newValue in
|
||||
if newValue { focusedField = .play }
|
||||
else { focusedField = nil }
|
||||
if newValue {
|
||||
focusedField = .play
|
||||
} else {
|
||||
focusedField = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: focusedField) { _ in model.resetTimer() }
|
||||
#else
|
||||
@@ -252,8 +255,6 @@ struct PlayerControls: View {
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
}
|
||||
|
@@ -47,7 +47,7 @@ struct TVControls: UIViewRepresentable {
|
||||
|
||||
func updateUIView(_: UIView, context _: Context) {}
|
||||
|
||||
func makeCoordinator() -> TVControls.Coordinator {
|
||||
func makeCoordinator() -> Self.Coordinator {
|
||||
Coordinator(controlsArea)
|
||||
}
|
||||
|
||||
|
@@ -103,7 +103,9 @@ struct TimelineView: View {
|
||||
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
#if os(tvOS)
|
||||
.frame(maxHeight: 300, alignment: .bottom)
|
||||
#endif
|
||||
.offset(x: thumbTooltipOffset)
|
||||
.overlay(GeometryReader { proxy in
|
||||
Color.clear
|
||||
@@ -114,8 +116,9 @@ struct TimelineView: View {
|
||||
tooltipSize = proxy.size
|
||||
}
|
||||
})
|
||||
|
||||
#if os(tvOS)
|
||||
.frame(height: 80)
|
||||
#endif
|
||||
.opacity(dragging ? 1 : 0)
|
||||
.animation(.easeOut, value: thumbTooltipOffset)
|
||||
HStack(spacing: 4) {
|
||||
@@ -161,14 +164,6 @@ struct TimelineView: View {
|
||||
self.size = size
|
||||
}
|
||||
})
|
||||
.frame(maxHeight: playerControlsLayout.timelineHeight)
|
||||
#if !os(tvOS)
|
||||
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
|
||||
let target = (value.location.x / size.width) * units
|
||||
self.playerTime.currentTime = .secondsInDefaultTimescale(target)
|
||||
player.backend.seek(to: target, seekType: .userInteracted)
|
||||
})
|
||||
#endif
|
||||
|
||||
durationView
|
||||
.shadow(radius: 3)
|
||||
@@ -177,8 +172,14 @@ struct TimelineView: View {
|
||||
.frame(minWidth: 30, alignment: .trailing)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.highPriorityGesture(
|
||||
|
||||
.font(.system(size: playerControlsLayout.timeFontSize).monospacedDigit())
|
||||
.zIndex(2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
#if !os(tvOS)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 5, coordinateSpace: .global)
|
||||
.onChanged { value in
|
||||
if !dragging {
|
||||
@@ -210,11 +211,7 @@ struct TimelineView: View {
|
||||
controls.resetTimer()
|
||||
}
|
||||
)
|
||||
#endif
|
||||
.font(.system(size: playerControlsLayout.timeFontSize).monospacedDigit())
|
||||
.zIndex(2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder var durationView: some View {
|
||||
|
@@ -70,17 +70,15 @@ struct PlayerBackendView: View {
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
return verticalSizeClass == .compact ? safeAreaModel.safeArea.top : 0
|
||||
} else {
|
||||
return safeAreaModel.safeArea.top.isZero ? safeAreaModel.safeArea.bottom : safeAreaModel.safeArea.top
|
||||
}
|
||||
return safeAreaModel.safeArea.top.isZero ? safeAreaModel.safeArea.bottom : safeAreaModel.safeArea.top
|
||||
}
|
||||
|
||||
var controlsBottomPadding: Double {
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
return player.playingFullScreen || verticalSizeClass == .compact ? safeAreaModel.safeArea.bottom : 0
|
||||
} else {
|
||||
return player.playingFullScreen ? safeAreaModel.safeArea.bottom : 0
|
||||
}
|
||||
return player.playingFullScreen ? safeAreaModel.safeArea.bottom : 0
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@@ -18,8 +18,9 @@ struct RelatedView: View {
|
||||
|
||||
Color.clear.padding(.bottom, 50)
|
||||
.listRowBackground(Color.clear)
|
||||
.backport
|
||||
.listRowSeparator(false)
|
||||
#if os(iOS)
|
||||
.listRowSeparator(.hidden)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -219,25 +219,17 @@ struct CommentView: View {
|
||||
}
|
||||
|
||||
private var commentText: some View {
|
||||
Group {
|
||||
let text = Text(comment.text)
|
||||
#if os(macOS)
|
||||
.font(.system(size: 14))
|
||||
#elseif os(iOS)
|
||||
.font(.system(size: 15))
|
||||
#endif
|
||||
.lineSpacing(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
text
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
Text(comment.text)
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.font(.system(size: 14))
|
||||
#elseif os(iOS)
|
||||
.font(.system(size: 15))
|
||||
#endif
|
||||
.lineSpacing(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
private func openChannelAction() {
|
||||
|
@@ -32,13 +32,8 @@ struct CommentsView: View {
|
||||
|
||||
struct CommentsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
CommentsView()
|
||||
.previewInterfaceOrientation(.landscapeRight)
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
|
||||
CommentsView()
|
||||
.previewInterfaceOrientation(.landscapeRight)
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
}
|
||||
|
@@ -79,15 +79,10 @@ struct InspectorView: View {
|
||||
Text(detail.localized())
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
let value = Text(value).lineLimit(1)
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
value
|
||||
#if !os(tvOS)
|
||||
Text(value).lineLimit(1)
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
} else {
|
||||
value
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
|
@@ -30,8 +30,9 @@ struct PlayerQueueView: View {
|
||||
#endif
|
||||
Color.clear.padding(.bottom, 50)
|
||||
.listRowBackground(Color.clear)
|
||||
.backport
|
||||
.listRowSeparator(false)
|
||||
#if os(iOS)
|
||||
.listRowSeparator(.hidden)
|
||||
#endif
|
||||
}
|
||||
.environment(\.inNavigationView, false)
|
||||
}
|
||||
|
@@ -59,23 +59,15 @@ struct VideoDescription: View {
|
||||
|
||||
@ViewBuilder var textDescription: some View {
|
||||
#if !os(iOS)
|
||||
Group {
|
||||
if #available(macOS 12, *) {
|
||||
Text(description)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(shouldExpand ? 500 : Self.collapsedLines)
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
} else {
|
||||
Text(description)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(shouldExpand ? 500 : Self.collapsedLines)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.system(size: 14))
|
||||
.lineSpacing(3)
|
||||
Text(description)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(shouldExpand ? 500 : Self.collapsedLines)
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.system(size: 14))
|
||||
.lineSpacing(3)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -177,7 +169,8 @@ struct VideoDescription: View {
|
||||
{
|
||||
player.backend.seek(to: Double(time), seekType: .userInteracted)
|
||||
return
|
||||
} else if destination != nil {
|
||||
}
|
||||
if destination != nil {
|
||||
urlToOpen = yatteeURL
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ struct VideoDetails: View {
|
||||
|
||||
struct ChannelView: View {
|
||||
@ObservedObject private var model = PlayerModel.shared
|
||||
@Binding var detailsVisibility: Bool
|
||||
|
||||
var video: Video? { model.videoForDisplay }
|
||||
|
||||
@@ -33,14 +34,19 @@ struct VideoDetails: View {
|
||||
guard let channel = video?.channel else { return }
|
||||
NavigationModel.shared.openChannel(channel, navigationStyle: .sidebar)
|
||||
} label: {
|
||||
ChannelAvatarView(
|
||||
channel: video?.channel,
|
||||
video: video
|
||||
)
|
||||
.frame(maxWidth: 40, maxHeight: 40)
|
||||
.padding(.trailing, 5)
|
||||
if detailsVisibility {
|
||||
ChannelAvatarView(
|
||||
channel: video?.channel,
|
||||
video: video
|
||||
)
|
||||
} else {
|
||||
Circle()
|
||||
.foregroundColor(Color(white: 0.6).opacity(0.5))
|
||||
}
|
||||
}
|
||||
.frame(width: 40, height: 40)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.trailing, 5)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
@@ -56,18 +62,14 @@ struct VideoDetails: View {
|
||||
}
|
||||
|
||||
if let video, !video.isLocal {
|
||||
Group {
|
||||
Text("•")
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "person.2.fill")
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "person.2.fill")
|
||||
|
||||
if let channel = model.videoForDisplay?.channel {
|
||||
if let subscriptions = channel.subscriptionsString {
|
||||
Text(subscriptions)
|
||||
} else {
|
||||
Text("1234").redacted(reason: .placeholder)
|
||||
}
|
||||
if let channel = model.videoForDisplay?.channel {
|
||||
if let subscriptions = channel.subscriptionsString {
|
||||
Text(subscriptions)
|
||||
} else {
|
||||
Text("1234").redacted(reason: .placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,8 +96,6 @@ struct VideoDetails: View {
|
||||
HStack(spacing: 4) {
|
||||
publishedDateSection
|
||||
|
||||
Text("•")
|
||||
|
||||
HStack(spacing: 4) {
|
||||
if model.videoBeingOpened != nil || video?.viewsCount != nil {
|
||||
Image(systemName: "eye")
|
||||
@@ -194,7 +194,7 @@ struct VideoDetails: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
TitleView()
|
||||
if video != nil, !video!.isLocal {
|
||||
ChannelView()
|
||||
ChannelView(detailsVisibility: $detailsVisibility)
|
||||
.layoutPriority(1)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
#endif
|
||||
}
|
||||
|
||||
var ratio: CGFloat? {
|
||||
var ratio: CGFloat? { // swiftlint:disable:this no_cgfloat
|
||||
fullScreen ? detailsHiddenInFullScreen ? nil : usedAspectRatio : usedAspectRatio
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ struct VideoPlayerSizeModifier: ViewModifier {
|
||||
guard !fullScreen else {
|
||||
if detailsHiddenInFullScreen {
|
||||
return geometry.size.height
|
||||
} else {
|
||||
return geometry.size.width / usedAspectRatio
|
||||
}
|
||||
|
||||
return geometry.size.width / usedAspectRatio
|
||||
}
|
||||
|
||||
return max(geometry.size.height - VideoPlayerView.defaultMinimumHeightLeft, 0)
|
||||
|
@@ -274,7 +274,11 @@ struct VideoPlayerView: View {
|
||||
)
|
||||
.onHover { hovering in
|
||||
hoveringPlayer = hovering
|
||||
hovering ? player.controls.show() : player.controls.hide()
|
||||
if hovering {
|
||||
player.controls.show()
|
||||
} else {
|
||||
player.controls.hide()
|
||||
}
|
||||
}
|
||||
.gesture(player.controls.presentingOverlays ? nil : playerDragGesture)
|
||||
#if os(macOS)
|
||||
@@ -290,9 +294,6 @@ struct VideoPlayerView: View {
|
||||
}
|
||||
})
|
||||
#endif
|
||||
|
||||
.background(Color.black)
|
||||
|
||||
if !detailsHiddenInFullScreen {
|
||||
VideoDetails(
|
||||
video: player.videoForDisplay,
|
||||
|
@@ -91,13 +91,6 @@ struct PlaylistsView: View {
|
||||
loadResource()
|
||||
}
|
||||
#if os(iOS)
|
||||
.refreshControl { refreshControl in
|
||||
model.load(force: true) {
|
||||
model.reloadPlaylists.toggle()
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
.refreshable {
|
||||
DispatchQueue.main.async {
|
||||
model.load(force: true) { model.reloadPlaylists.toggle() }
|
||||
|
@@ -2,7 +2,6 @@ import Introspect
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 15.0, macOS 12, *)
|
||||
struct FocusableSearchTextField: View {
|
||||
@ObservedObject private var state = SearchModel.shared
|
||||
|
||||
|
@@ -95,11 +95,7 @@ struct SearchView: View {
|
||||
filtersMenu
|
||||
}
|
||||
|
||||
if #available(macOS 12, *) {
|
||||
FocusableSearchTextField()
|
||||
} else {
|
||||
SearchTextField()
|
||||
}
|
||||
FocusableSearchTextField()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -179,11 +175,7 @@ struct SearchView: View {
|
||||
searchMenu
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
if #available(iOS 15, *) {
|
||||
FocusableSearchTextField()
|
||||
} else {
|
||||
SearchTextField()
|
||||
}
|
||||
FocusableSearchTextField()
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
@@ -47,16 +47,16 @@ struct AccountValidationStatus: View {
|
||||
var validationStatusSystemImage: String {
|
||||
if isValidating {
|
||||
return "bolt.horizontal.fill"
|
||||
} else {
|
||||
return isValid ? "checkmark.circle.fill" : "xmark.circle.fill"
|
||||
}
|
||||
|
||||
return isValid ? "checkmark.circle.fill" : "xmark.circle.fill"
|
||||
}
|
||||
|
||||
var validationStatusColor: Color {
|
||||
if isValidating {
|
||||
return .accentColor
|
||||
} else {
|
||||
return isValid ? .green : .red
|
||||
}
|
||||
|
||||
return isValid ? .green : .red
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,8 @@ struct BrowsingSettings: View {
|
||||
@Default(.playerButtonIsExpanded) private var playerButtonIsExpanded
|
||||
@Default(.playerBarMaxWidth) private var playerBarMaxWidth
|
||||
@Default(.expandChannelDescription) private var expandChannelDescription
|
||||
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
|
||||
@Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing
|
||||
|
||||
@ObservedObject private var accounts = AccountsModel.shared
|
||||
|
||||
@@ -186,6 +188,9 @@ struct BrowsingSettings: View {
|
||||
}
|
||||
|
||||
Toggle("Keep channels with unwatched videos on top of subscriptions list", isOn: $keepChannelsWithUnwatchedFeedOnTop)
|
||||
|
||||
Toggle("Show channel avatars in channels lists", isOn: $showChannelAvatarInChannelsLists)
|
||||
Toggle("Show channel avatars in videos lists", isOn: $showChannelAvatarInVideosListing)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -200,7 +200,7 @@ struct QualityProfileForm: View {
|
||||
|
||||
@ViewBuilder var formatsPicker: some View {
|
||||
#if os(macOS)
|
||||
let list = ForEach(QualityProfile.Format.allCases, id: \.self) { format in
|
||||
ForEach(QualityProfile.Format.allCases, id: \.self) { format in
|
||||
MultiselectRow(
|
||||
title: format.description,
|
||||
selected: isFormatSelected(format),
|
||||
@@ -209,16 +209,8 @@ struct QualityProfileForm: View {
|
||||
toggleFormat(format, value: value)
|
||||
}
|
||||
}
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
|
||||
Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
.listStyle(.inset)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
#else
|
||||
ForEach(QualityProfile.Format.allCases, id: \.self) { format in
|
||||
|
@@ -175,24 +175,14 @@ struct QualitySettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
if #available(macOS 12.0, *) {
|
||||
#if os(macOS)
|
||||
List {
|
||||
list
|
||||
}
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
#else
|
||||
#if os(macOS)
|
||||
List {
|
||||
list
|
||||
#endif
|
||||
} else {
|
||||
#if os(macOS)
|
||||
List {
|
||||
list
|
||||
}
|
||||
#else
|
||||
list
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
#else
|
||||
list
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -243,7 +243,7 @@ struct SettingsView: View {
|
||||
private var windowHeight: Double {
|
||||
switch selection {
|
||||
case .browsing:
|
||||
return 720
|
||||
return 800
|
||||
case .player:
|
||||
return 480
|
||||
case .controls:
|
||||
|
@@ -50,15 +50,8 @@ struct SponsorBlockSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
Group {
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
.listStyle(.inset)
|
||||
}
|
||||
}
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
Spacer()
|
||||
#else
|
||||
ForEach(SponsorBlockAPI.categories, id: \.self) { category in
|
||||
|
@@ -12,6 +12,7 @@ struct ChannelsView: View {
|
||||
@Default(.showCacheStatus) private var showCacheStatus
|
||||
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
|
||||
@Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop
|
||||
@Default(.showChannelAvatarInChannelsLists) private var showChannelAvatarInChannelsLists
|
||||
|
||||
@State private var channelLinkActive = false
|
||||
@State private var channelForLink: Channel?
|
||||
@@ -21,10 +22,9 @@ struct ChannelsView: View {
|
||||
Section(header: header) {
|
||||
ForEach(channels) { channel in
|
||||
let label = HStack {
|
||||
if let url = channel.thumbnailURLOrCached {
|
||||
ThumbnailView(url: url)
|
||||
if showChannelAvatarInChannelsLists {
|
||||
ChannelAvatarView(channel: channel, subscribedBadge: false)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 35))
|
||||
} else {
|
||||
Image(systemName: RecentsModel.symbolSystemImage(channel.name))
|
||||
.imageScale(.large)
|
||||
@@ -34,8 +34,9 @@ struct ChannelsView: View {
|
||||
Text(channel.name)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.backport
|
||||
#if !os(tvOS)
|
||||
.badge(showUnwatchedFeedBadges ? feedCount.unwatchedByChannelText(channel) : nil)
|
||||
#endif
|
||||
|
||||
Group {
|
||||
#if os(tvOS)
|
||||
@@ -73,8 +74,9 @@ struct ChannelsView: View {
|
||||
|
||||
Color.clear.padding(.bottom, 50)
|
||||
.listRowBackground(Color.clear)
|
||||
.backport
|
||||
.listRowSeparator(false)
|
||||
#if os(iOS)
|
||||
.listRowSeparator(.hidden)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
@@ -89,12 +91,6 @@ struct ChannelsView: View {
|
||||
subscriptions.load(force: true)
|
||||
}
|
||||
#if os(iOS)
|
||||
.refreshControl { refreshControl in
|
||||
subscriptions.load(force: true) {
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
.refreshable {
|
||||
await subscriptions.load(force: true)
|
||||
}
|
||||
|
@@ -22,18 +22,12 @@ struct FeedView: View {
|
||||
feed.loadResources()
|
||||
}
|
||||
#if os(iOS)
|
||||
.refreshControl { refreshControl in
|
||||
feed.loadResources(force: true) {
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
.refreshable {
|
||||
await feed.loadResources(force: true)
|
||||
}
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
.background(
|
||||
.background(
|
||||
Button("Refresh") {
|
||||
feed.loadResources(force: true)
|
||||
}
|
||||
@@ -42,8 +36,8 @@ struct FeedView: View {
|
||||
)
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
feed.loadResources()
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
feed.loadResources()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@@ -16,11 +16,7 @@ struct TrendingCountry: View {
|
||||
VStack {
|
||||
#if !os(tvOS)
|
||||
HStack {
|
||||
if #available(iOS 15.0, macOS 12.0, *) {
|
||||
TextField("Country", text: $query, prompt: Text(Self.prompt))
|
||||
} else {
|
||||
TextField(Self.prompt, text: $query)
|
||||
}
|
||||
TextField("Country", text: $query, prompt: Text(Self.prompt))
|
||||
|
||||
Button("Done") { selectCountryAndDismiss() }
|
||||
.keyboardShortcut(.defaultAction)
|
||||
@@ -57,12 +53,8 @@ struct TrendingCountry: View {
|
||||
|
||||
return Group {
|
||||
#if os(macOS)
|
||||
if #available(macOS 12.0, *) {
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
} else {
|
||||
list
|
||||
}
|
||||
list
|
||||
.listStyle(.inset(alternatesRowBackgrounds: true))
|
||||
#else
|
||||
list
|
||||
#endif
|
||||
|
@@ -95,12 +95,7 @@ struct TrendingView: View {
|
||||
.navigationTitle("Trending")
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.refreshControl { refreshControl in
|
||||
resource.load().onCompletion { _ in
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
.backport
|
||||
|
||||
.refreshable {
|
||||
DispatchQueue.main.async {
|
||||
resource.load()
|
||||
|
@@ -41,9 +41,11 @@ struct URLParser {
|
||||
|
||||
if hasAnyOfPrefixes(path, Self.prefixes[.playlist]!) || queryItemValue("v") == "playlist" {
|
||||
return .playlist
|
||||
} else if hasAnyOfPrefixes(path, Self.prefixes[.channel]!) {
|
||||
}
|
||||
if hasAnyOfPrefixes(path, Self.prefixes[.channel]!) {
|
||||
return .channel
|
||||
} else if hasAnyOfPrefixes(path, Self.prefixes[.search]!) {
|
||||
}
|
||||
if hasAnyOfPrefixes(path, Self.prefixes[.search]!) {
|
||||
return .search
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListView: View {
|
||||
|
@@ -48,23 +48,19 @@ struct ThumbnailView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder var asyncImageIfAvailable: some View {
|
||||
if #available(iOS 15, macOS 12, *) {
|
||||
CachedAsyncImage(url: url, urlCache: BaseCacheModel.imageCache) { phase in
|
||||
switch phase {
|
||||
case let .success(image):
|
||||
image
|
||||
.resizable()
|
||||
case .failure:
|
||||
placeholder.onAppear {
|
||||
guard let url else { return }
|
||||
thumbnails.insertUnloadable(url)
|
||||
}
|
||||
default:
|
||||
placeholder
|
||||
CachedAsyncImage(url: url, urlCache: BaseCacheModel.imageCache, transaction: Transaction(animation: .default)) { phase in
|
||||
switch phase {
|
||||
case let .success(image):
|
||||
image
|
||||
.resizable()
|
||||
case .failure:
|
||||
placeholder.onAppear {
|
||||
guard let url else { return }
|
||||
thumbnails.insertUnloadable(url)
|
||||
}
|
||||
default:
|
||||
placeholder
|
||||
}
|
||||
} else {
|
||||
webImage
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ struct VideoBanner: View {
|
||||
@Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor
|
||||
@Default(.timeOnThumbnail) private var timeOnThumbnail
|
||||
@Default(.roundedThumbnails) private var roundedThumbnails
|
||||
@Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing
|
||||
|
||||
@Environment(\.inChannelView) private var inChannelView
|
||||
@Environment(\.inNavigationView) private var inNavigationView
|
||||
@@ -85,10 +86,9 @@ struct VideoBanner: View {
|
||||
if !inChannelView, !video.isLocal || video.localStreamIsRemoteURL {
|
||||
ChannelLinkView(channel: video.channel) {
|
||||
HStack(spacing: Constants.channelDetailsStackSpacing) {
|
||||
if let url = video.channel.thumbnailURLOrCached, video != .fixture {
|
||||
ThumbnailView(url: url)
|
||||
if video != .fixture, showChannelAvatarInVideosListing {
|
||||
ChannelAvatarView(channel: video.channel)
|
||||
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
channelLabel
|
||||
@@ -253,11 +253,11 @@ struct VideoBanner: View {
|
||||
private var timeLabel: String? {
|
||||
if let watch, let watchStoppedAtLabel, let videoDurationLabel, !watch.finished {
|
||||
return "\(watchStoppedAtLabel) / \(videoDurationLabel)"
|
||||
} else if let videoDurationLabel {
|
||||
return videoDurationLabel
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
if let videoDurationLabel {
|
||||
return videoDurationLabel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ViewBuilder private var timeView: some View {
|
||||
|
@@ -24,6 +24,7 @@ struct VideoCell: View {
|
||||
@Default(.watchedVideoStyle) private var watchedVideoStyle
|
||||
@Default(.watchedVideoBadgeColor) private var watchedVideoBadgeColor
|
||||
@Default(.watchedVideoPlayNowBehavior) private var watchedVideoPlayNowBehavior
|
||||
@Default(.showChannelAvatarInVideosListing) private var showChannelAvatarInVideosListing
|
||||
|
||||
private var navigation: NavigationModel { .shared }
|
||||
private var player: PlayerModel { .shared }
|
||||
@@ -161,17 +162,22 @@ struct VideoCell: View {
|
||||
|
||||
HStack(spacing: Constants.channelDetailsStackSpacing) {
|
||||
if !inChannelView,
|
||||
let url = video.channel.thumbnailURLOrCached,
|
||||
showChannelAvatarInVideosListing,
|
||||
video != .fixture
|
||||
{
|
||||
ChannelLinkView(channel: video.channel) {
|
||||
ThumbnailView(url: url)
|
||||
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
|
||||
.clipShape(Circle())
|
||||
if showChannelAvatarInVideosListing {
|
||||
ChannelAvatarView(channel: video.channel)
|
||||
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
|
||||
} else {
|
||||
channelLabel(badge: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !channelOnThumbnail, !inChannelView {
|
||||
if !channelOnThumbnail,
|
||||
!inChannelView
|
||||
{
|
||||
ChannelLinkView(channel: video.channel) {
|
||||
channelLabel(badge: false)
|
||||
}
|
||||
@@ -264,12 +270,9 @@ struct VideoCell: View {
|
||||
if !channelOnThumbnail, !inChannelView {
|
||||
ChannelLinkView(channel: video.channel) {
|
||||
HStack(spacing: Constants.channelDetailsStackSpacing) {
|
||||
if let url = video.channel.thumbnailURLOrCached,
|
||||
video != .fixture
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
if video != .fixture, showChannelAvatarInVideosListing {
|
||||
ChannelAvatarView(channel: video.channel)
|
||||
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
channelLabel(badge: false)
|
||||
@@ -295,9 +298,8 @@ struct VideoCell: View {
|
||||
video != .fixture
|
||||
{
|
||||
ChannelLinkView(channel: video.channel) {
|
||||
ThumbnailView(url: url)
|
||||
ChannelAvatarView(channel: video.channel)
|
||||
.frame(width: Constants.channelThumbnailSize, height: Constants.channelThumbnailSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import SwiftUI
|
||||
struct AccentButton: View {
|
||||
var text: String?
|
||||
var imageSystemName: String?
|
||||
var maxWidth: CGFloat? = .infinity
|
||||
var maxWidth: CGFloat? = .infinity // swiftlint:disable:this no_cgfloat
|
||||
var bold = true
|
||||
var verticalPadding = 10.0
|
||||
var horizontalPadding = 10.0
|
||||
|
@@ -167,6 +167,7 @@ struct ControlsBar: View {
|
||||
channel: model.videoForDisplay?.channel,
|
||||
video: model.videoForDisplay
|
||||
)
|
||||
.id("channel-avatar-\(model.videoForDisplay?.id ?? "")")
|
||||
.frame(width: barHeight - 10, height: barHeight - 10)
|
||||
}
|
||||
.contextMenu { contextMenu }
|
||||
@@ -176,12 +177,13 @@ struct ControlsBar: View {
|
||||
channel: model.videoForDisplay?.channel,
|
||||
video: model.videoForDisplay
|
||||
)
|
||||
.id("channel-avatar-\(model.videoForDisplay?.id ?? "")")
|
||||
#if !os(tvOS)
|
||||
.highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil)
|
||||
.gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil)
|
||||
.highPriorityGesture(playerButtonDoubleTapGesture != .nothing ? doubleTapGesture : nil)
|
||||
.gesture(playerButtonSingleTapGesture != .nothing ? singleTapGesture : nil)
|
||||
#endif
|
||||
.frame(width: barHeight - 10, height: barHeight - 10)
|
||||
.contextMenu { contextMenu }
|
||||
.frame(width: barHeight - 10, height: barHeight - 10)
|
||||
.contextMenu { contextMenu }
|
||||
}
|
||||
|
||||
if expansionState == .full {
|
||||
|
@@ -8,7 +8,7 @@ struct OpenSettingsButton: View {
|
||||
#endif
|
||||
|
||||
var body: some View {
|
||||
let button = Button {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
#if os(macOS)
|
||||
@@ -20,13 +20,7 @@ struct OpenSettingsButton: View {
|
||||
Label("Open Settings", systemImage: "gearshape.2")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
|
||||
button
|
||||
.buttonStyle(.borderedProminent)
|
||||
} else {
|
||||
button
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -47,14 +47,6 @@ struct PopularView: View {
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.refreshControl { refreshControl in
|
||||
resource?.load().onCompletion { _ in
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
.onFailure { self.error = $0 }
|
||||
.onSuccess { _ in self.error = nil }
|
||||
}
|
||||
.backport
|
||||
.refreshable {
|
||||
DispatchQueue.main.async {
|
||||
resource?.load()
|
||||
@@ -62,7 +54,7 @@ struct PopularView: View {
|
||||
.onSuccess { _ in self.error = nil }
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.toolbar {
|
||||
|
@@ -281,11 +281,7 @@ struct VideoContextMenuView: View {
|
||||
let label = Label("Remove…", systemImage: "trash.fill")
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
|
||||
if #available(iOS 15, macOS 12, *) {
|
||||
Button(role: .destructive, action: action) { label }
|
||||
} else {
|
||||
Button(action: action) { label }
|
||||
}
|
||||
Button(role: .destructive, action: action) { label }
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@@ -28,6 +28,7 @@ struct YatteeApp: App {
|
||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
#elseif os(iOS)
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
|
||||
@State private var configured = false
|
||||
@@ -55,6 +56,7 @@ struct YatteeApp: App {
|
||||
ContentView()
|
||||
.onAppear(perform: configure)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environment(\.navigationStyle, navigationStyle)
|
||||
#if os(macOS)
|
||||
.background(
|
||||
HostingWindowFinder { window in
|
||||
@@ -76,6 +78,12 @@ struct YatteeApp: App {
|
||||
#if os(iOS)
|
||||
.handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"]))
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
.onOpenURL { url in
|
||||
URLBookmarkModel.shared.saveBookmark(url)
|
||||
OpenURLHandler(navigationStyle: navigationStyle).handle(url)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
.handlesExternalEvents(matching: Set(["*"]))
|
||||
@@ -98,7 +106,7 @@ struct YatteeApp: App {
|
||||
HostingWindowFinder { window in
|
||||
Windows.playerWindow = window
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
NotificationCenter.default.addObserver( // swiftlint:disable:this discarded_notification_center_observer
|
||||
forName: NSWindow.willExitFullScreenNotification,
|
||||
object: window,
|
||||
queue: OperationQueue.main
|
||||
@@ -114,6 +122,10 @@ struct YatteeApp: App {
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environment(\.navigationStyle, .sidebar)
|
||||
.handlesExternalEvents(preferring: Set(["player", "*"]), allowing: Set(["player", "*"]))
|
||||
.onOpenURL { url in
|
||||
URLBookmarkModel.shared.saveBookmark(url)
|
||||
OpenURLHandler(navigationStyle: navigationStyle).handle(url)
|
||||
}
|
||||
}
|
||||
.handlesExternalEvents(matching: Set(["player", "*"]))
|
||||
|
||||
@@ -200,4 +212,14 @@ struct YatteeApp: App {
|
||||
|
||||
Defaults[.homeHistoryItems] = -1
|
||||
}
|
||||
|
||||
var navigationStyle: NavigationStyle {
|
||||
#if os(iOS)
|
||||
return horizontalSizeClass == .compact ? .tab : .sidebar
|
||||
#elseif os(tvOS)
|
||||
return .tab
|
||||
#else
|
||||
return .sidebar
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@@ -593,3 +593,5 @@
|
||||
"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";
|
||||
"Show channel avatars in videos lists" = "Show channel avatars in videos lists";
|
||||
"Show channel avatars in channels lists" = "Show channel avatars in channels lists";
|
||||
|
@@ -177,7 +177,7 @@
|
||||
"When partially watched video is played" = "一部視聴済みの動画の再生時";
|
||||
|
||||
/* Selected video was played on given date */
|
||||
"Watched %@" = "視聴日 %@";
|
||||
"Watched %@" = "前回の視聴 %@";
|
||||
"Welcome" = "ようこそ";
|
||||
"Yattee" = "Yattee";
|
||||
"Yattee %@ (build %@)" = "Yattee %@ (ビルド %@)";
|
||||
@@ -531,7 +531,7 @@
|
||||
"Switch to public locations" = "公開された場所に切り替え";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "有料/無料のプラットフォームかを問わず、いいね、登録などを明示的に操作を促す(例: 動画をクリック)。";
|
||||
"Proxy videos" = "動画閲覧にプロキシ使用";
|
||||
"Sections" = "表示する部分";
|
||||
"Sections" = "表示するボタン";
|
||||
"System controls show buttons for %@" = "システム制御「%@」用のボタンを表示";
|
||||
"You need to create an instance and accounts\nto access %@ section" = "%@ セクションの利用には\nインスタンスとアカウントの作成が必要";
|
||||
"You need to select an account\nto access %@ section" = "%@ セクションの利用には\nアカウントの選択が必要";
|
||||
|
@@ -439,7 +439,7 @@
|
||||
"Format" = "";
|
||||
"Verified" = "";
|
||||
"Show icons and text when space permits" = "";
|
||||
"Could not extract video ID" = "";
|
||||
"Could not extract video ID" = "Kunne ikke pakke ut video-ID";
|
||||
"Open Files" = "";
|
||||
"Driver" = "";
|
||||
"Show Open Videos quick actions" = "";
|
||||
@@ -456,34 +456,34 @@
|
||||
"FPS" = "";
|
||||
"Inspector visibility" = "";
|
||||
"Show Documents" = "";
|
||||
"Open logs in Finder" = "";
|
||||
"Open logs in Finder" = "Åpne loggføring i Finder";
|
||||
"Documents" = "";
|
||||
"Could not update your token." = "";
|
||||
"Could not update your token." = "Kunne ikke oppdatere symbolet ditt.";
|
||||
"Remove…" = "";
|
||||
"Hide" = "";
|
||||
"Actions buttons" = "";
|
||||
"Audio" = "";
|
||||
"Could not extract SID from received cookies: %@" = "";
|
||||
"Could not extract SID from received cookies: %@" = "Kunne ikke hente ut SID fra mottatte informasjonskapsler: %@";
|
||||
"Playback Mode" = "";
|
||||
"Clear Queue before opening" = "";
|
||||
"Could not create share link" = "";
|
||||
"Could not create share link" = "Kunne ikke opprette delingslenke";
|
||||
"Could not refresh Playlists" = "";
|
||||
"Could not refresh Subscriptions" = "";
|
||||
"Could not refresh Subscriptions" = "Kunne ikke gjenoppfriske abonnementer";
|
||||
"Translations" = "";
|
||||
"This URL could not be opened" = "";
|
||||
"For custom locations you can configure Frontend URL in Locations settings" = "";
|
||||
"Could not refresh Trending" = "";
|
||||
"If you want this app to be available in your language, join translation project." = "";
|
||||
"This video could not be opened" = "";
|
||||
"Could not open channel" = "";
|
||||
"Could not open playlist" = "";
|
||||
"Could not load streams" = "";
|
||||
"Could not open video" = "";
|
||||
"Could not extract channel information" = "";
|
||||
"Could not load video" = "";
|
||||
"Could not extract playlist ID" = "";
|
||||
"This video could not be opened" = "Kunne ikke åpne videoen";
|
||||
"Could not open channel" = "Kunne ikke åpne kanal";
|
||||
"Could not open playlist" = "Kunne ikke åpne spilleliste";
|
||||
"Could not load streams" = "Kunne ikke laste inn strømmer";
|
||||
"Could not open video" = "Kunne ikke åpne video";
|
||||
"Could not extract channel information" = "Kunne ikke hente kanalinfo";
|
||||
"Could not load video" = "Kunne ikke laste inn video";
|
||||
"Could not extract playlist ID" = "Kunne ikke hente ut spilleliste-ID";
|
||||
"Could not refresh Popular" = "";
|
||||
"Channel could not be found" = "";
|
||||
"Channel could not be found" = "Fant ikke kanalen";
|
||||
"Live Streams" = "";
|
||||
"Channel" = "";
|
||||
"No documents" = "";
|
||||
@@ -499,4 +499,4 @@
|
||||
"Playback history is empty" = "";
|
||||
"Copy%@link" = "";
|
||||
"Share%@link" = "";
|
||||
"Share Logs..." = "";
|
||||
"Share Logs..." = "Del logger …";
|
||||
|
@@ -596,3 +596,5 @@
|
||||
"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";
|
||||
"Show channel avatars in videos lists" = "Pokaż awatary kanałów na listach wideo";
|
||||
"Show channel avatars in channels lists" = "Pokaż awatary kanałów na listach kanałów";
|
||||
|
@@ -540,3 +540,58 @@
|
||||
"Show unwatched feed badges" = "Mostrar ícone de feed não visto";
|
||||
"Gesture: fowards" = "Gesto: para frente";
|
||||
"Controls Buttons" = "Botões de Controle";
|
||||
"Show video context menu options to force selected backend" = "Mostrar opções do menu contextual do vídeo para forçar o backend selecionado";
|
||||
"Play Now in AVPlayer" = "Tocar Agora no AVPlayer";
|
||||
"Play Now in MPV" = "Tocar Agora em MPV";
|
||||
"Enter account credentials to connect..." = "Insira as credenciais da conta para conectar…";
|
||||
"Show scroll to top button in comments" = "Mostrar botão de voltar ao topo nos comentários";
|
||||
"Cells" = "Células";
|
||||
"Mark all as unwatched" = "Marcar todos como não vistos";
|
||||
"Queue - shuffled" = "Fila - embaralhado";
|
||||
"Fullscreen" = "Ecrã inteiro";
|
||||
"Lock" = "Travar";
|
||||
"Description" = "Descrição";
|
||||
"Loop one" = "Um em loop";
|
||||
"Enter location address to connect..." = "Insira o endereço da localização para conectar…";
|
||||
"Seek" = "Vasculhar";
|
||||
"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";
|
||||
"Opened File" = "Ficheiro Aberto";
|
||||
"File Extension" = "Extensão do Ficheiro";
|
||||
"Opening file..." = "A abrir ficheiro…";
|
||||
"Close video and player on end" = "Fechar vídeo e player ao final";
|
||||
"Use system controls with AVPlayer" = "Usar controles do sistema com o AVPlayer";
|
||||
"Public account" = "Conta pública";
|
||||
"Your Accounts" = "As suas Contas";
|
||||
"Browse without account" = "Navegar sem uma conta";
|
||||
"Rotate when entering fullscreen on landscape video" = "Girar quando entrar no modo ecrã inteiro em vídeo em paisagem";
|
||||
"Landscape left" = "Paisagem à esquerda";
|
||||
"Landscape right" = "Paisagem à direita";
|
||||
"No rotation" = "Sem rotação";
|
||||
"Available" = "Disponível";
|
||||
"Startup section" = "Secção ao iniciar";
|
||||
"Home Settings" = "Ajustes da ecrã de Início";
|
||||
"Watched: hidden" = "Assistidos: ocultos";
|
||||
"Watched: visible" = "Assistidos: visíveis";
|
||||
"Disable filters" = "Desativar filtros";
|
||||
"(watched and shorts hidden)" = "(assistidos e shorts ocultos)";
|
||||
"No videos to show" = "Nenhum vídeo para mostrar";
|
||||
"(watched hidden)" = "(assistidos ocultos)";
|
||||
"(shorts hidden)" = "(shorts ocultos)";
|
||||
"Limit" = "Limite";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "Tem certeza que deseja remover %@ dos Favoritos?";
|
||||
"List" = "Lista";
|
||||
"Toggle size" = "Alternar tamanho";
|
||||
"Toggle player" = "Alternar player";
|
||||
"Do nothing" = "Fazer nada";
|
||||
"Show Next in Queue" = "Mostrar Próximo na Fila";
|
||||
"Show toggle watch status button" = "Mostrar botão de mudar estado de assistir";
|
||||
"Feed" = "Feed";
|
||||
"Next in Queue" = "Próximo na Fila";
|
||||
"Open channel" = "Abrir canal";
|
||||
"Inspector" = "Inspetor";
|
||||
"Open video description expanded" = "Abrir descrição do vídeo expandida";
|
||||
"Mark all as watched" = "Marcar todos como vistos";
|
||||
"Playback Settings" = "Configurações de Playback";
|
||||
"Replay" = "Replay";
|
||||
"Autoplay next" = "Tocar automaticamente próximo";
|
||||
"Stream" = "Stream";
|
||||
|
@@ -502,3 +502,96 @@
|
||||
"Shorts" = "短视频";
|
||||
"Verified" = "已验证";
|
||||
"Channel" = "频道";
|
||||
"Open expanded" = "展开";
|
||||
"Mark channel feed as watched" = "关注频道 Feed";
|
||||
"Short videos: visible" = "短视频:可见";
|
||||
"Player Bar" = "播放栏";
|
||||
"Short videos: hidden" = "短视频:隐藏";
|
||||
"Mark channel feed as unwatched" = "取消关注频道 Feed";
|
||||
"Play all unwatched" = "观看所有未观看视频";
|
||||
"Double tap gesture" = "双击手势";
|
||||
"Tap and hold channel thumbnail to open context menu with more actions" = "点击并按住频道缩略图以打开包含更多操作的上下文菜单";
|
||||
"Always show controls buttons" = "始终显示控制按钮";
|
||||
"Show video context menu options to force selected backend" = "显示视频内容目录选项来强制选择的后端";
|
||||
"Gesture: backwards" = "手势:向后";
|
||||
"Maximum width expanded" = "最大宽度展开";
|
||||
"Clear all" = "清除所有APP缓存";
|
||||
"Total size: %@" = "总大小:%@";
|
||||
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "手势设置控制玩家左右两侧双击手势的跳过间隔。更改系统控制设置需要重新启动。";
|
||||
"Play next item" = "播放下一个";
|
||||
"Subscribe/Unsubscribe" = "关注/取消关注";
|
||||
"Autoplay next" = "自动播放下一个";
|
||||
"Enter location address to connect..." = "输入链接地址以连接...";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "保留频道,并且未观看视频放在关注列表最上面";
|
||||
"Play Now in AVPlayer" = "在 AVPlayer 中播放当前项目";
|
||||
"Play Now in MPV" = "在 MPV 中播放当前项目";
|
||||
"Seek" = "探索";
|
||||
"Enter account credentials to connect..." = "输入账户凭据来连接...";
|
||||
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
|
||||
"Opened File" = "打开的文件";
|
||||
"File Extension" = "文件扩展";
|
||||
"Opening file..." = "打开文件中...";
|
||||
"Single tap gesture" = "单击手势";
|
||||
"Right click channel thumbnail to open context menu with more actions" = "右键单击频道缩略图以打开具有更多操作的上下文菜单";
|
||||
"Show unwatched feed badges" = "显示未观看的 Feed 标志";
|
||||
"Seeking" = "探索";
|
||||
"Gesture: fowards" = "手势:向前";
|
||||
"System controls" = "系统控制";
|
||||
"Controls button: backwards" = "控制按钮:向后";
|
||||
"Controls button: forwards" = "控制按钮:向前";
|
||||
"Hide player" = "隐藏播放器";
|
||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "手势设置控制双击播放器左/右的跳跃间隔。更改系统控制设置需要重新启动。";
|
||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "手势设置控制远程箭头按钮的跳过间隔(用于第二代 Siri Remote 或更新版本)。更改系统控制设置需要重新启动。";
|
||||
"Controls Buttons" = "控制按钮";
|
||||
"Actions Buttons" = "动作按钮";
|
||||
"Lock orientation" = "锁定屏幕方向";
|
||||
"Music Mode" = "音乐模式";
|
||||
"Close video" = "关闭视频";
|
||||
"Cache" = "缓存";
|
||||
"Show cache status" = "显示缓存状态";
|
||||
"Maximum feed items" = "最大 Feed 项目";
|
||||
"Open channels with description expanded" = "打开描述展开的频道";
|
||||
"Are you sure you want to clear cache?" = "你确定要清除缓存吗?";
|
||||
"Close video and player on end" = "在播放结束时关闭播放器和视频";
|
||||
"Use system controls with AVPlayer" = "使用 AVPlayer 与系统控制按钮";
|
||||
"Public account" = "公共账号";
|
||||
"Your Accounts" = "你的账号";
|
||||
"Browse without account" = "匿名浏览";
|
||||
"Rotate when entering fullscreen on landscape video" = "当观看全景视频,进入全屏时旋转";
|
||||
"Landscape left" = "全景视频左边";
|
||||
"Landscape right" = "全景视频右边";
|
||||
"No rotation" = "不旋转";
|
||||
"Available" = "可用";
|
||||
"Startup section" = "启动部分";
|
||||
"Watched: hidden" = "观看过:隐藏";
|
||||
"Watched: visible" = "观看后:显示";
|
||||
"Disable filters" = "禁用过滤器";
|
||||
"Home Settings" = "主页设置";
|
||||
"(watched and shorts hidden)" = "(观看后与短视频隐藏)";
|
||||
"No videos to show" = "没有可以显示的视频";
|
||||
"(watched hidden)" = "(已观看已隐藏)";
|
||||
"(shorts hidden)" = "(短视频已隐藏)";
|
||||
"Limit" = "限制";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "你确定要从收藏夹中删除 %@ 吗?";
|
||||
"List" = "列表";
|
||||
"Cells" = "Cells";
|
||||
"Toggle size" = "总大小";
|
||||
"Toggle player" = "切换播放器";
|
||||
"Do nothing" = "什么也不做(*^_^*)";
|
||||
"Show Next in Queue" = "在队列中显示下一个";
|
||||
"Show toggle watch status button" = "显示“切换观看状态”按钮";
|
||||
"Feed" = "Feed";
|
||||
"Next in Queue" = "队列中下一个";
|
||||
"Open channel" = "打开频道";
|
||||
"Inspector" = "检查器";
|
||||
"Open video description expanded" = "打开描述展开的视频";
|
||||
"Mark all as unwatched" = "标记所有未看过的";
|
||||
"Mark all as watched" = "标记所有看过的";
|
||||
"Queue - shuffled" = "队列 - 随机";
|
||||
"Playback Settings" = "回放设置";
|
||||
"Replay" = "重新播放";
|
||||
"Fullscreen" = "全屏";
|
||||
"Lock" = "锁定";
|
||||
"Description" = "描述";
|
||||
"Loop one" = "单个循环";
|
||||
"Stream" = "流播放";
|
||||
|
Reference in New Issue
Block a user