diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index b783d4d..9db9b58 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -800,3 +800,4 @@ class CustomMediaPlayerViewController: UIViewController { // yes? Like the plural of the famous american rapper ye? -IBHRAD // low taper fade the meme is massive -cranci // cranci still doesnt have a job -seiike +// guys watch Clannad already - ibro diff --git a/Sora/Utils/SkeletonCells/SkeletonCell.swift b/Sora/Utils/SkeletonCells/SkeletonCell.swift index adf330a..342b2bf 100644 --- a/Sora/Utils/SkeletonCells/SkeletonCell.swift +++ b/Sora/Utils/SkeletonCells/SkeletonCell.swift @@ -8,17 +8,19 @@ import SwiftUI struct HomeSkeletonCell: View { + let cellWidth: CGFloat + var body: some View { VStack { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: 130, height: 195) + .frame(width: cellWidth, height: cellWidth * 1.5) .cornerRadius(10) .shimmering() RoundedRectangle(cornerRadius: 5) .fill(Color.gray.opacity(0.3)) - .frame(width: 130, height: 20) + .frame(width: cellWidth, height: 20) .padding(.top, 4) .shimmering() } @@ -26,15 +28,17 @@ struct HomeSkeletonCell: View { } struct SearchSkeletonCell: View { + let cellWidth: CGFloat + var body: some View { VStack(alignment: .leading, spacing: 8) { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) + .frame(width: cellWidth, height: cellWidth * 1.5) .shimmering() RoundedRectangle(cornerRadius: 5) .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 20) + .frame(width: cellWidth, height: 20) .shimmering() } } diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 7bd8130..1e5612f 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -12,7 +12,13 @@ struct LibraryView: View { @EnvironmentObject private var libraryManager: LibraryManager @EnvironmentObject private var moduleManager: ModuleManager + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + + @Environment(\.verticalSizeClass) var verticalSizeClass + @State private var continueWatchingItems: [ContinueWatchingItem] = [] + @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape private let columns = [ GridItem(.adaptive(minimum: 150), spacing: 12) @@ -21,6 +27,8 @@ struct LibraryView: View { var body: some View { NavigationView { ScrollView { + let columnsCount = determineColumns() + VStack(alignment: .leading, spacing: 12) { Text("Continue Watching") .font(.title2) @@ -67,22 +75,27 @@ struct LibraryView: View { .padding() .frame(maxWidth: .infinity) } else { - LazyVGrid(columns: columns, spacing: 12) { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) + let availableWidth = UIScreen.main.bounds.width - totalSpacing + let cellWidth = availableWidth / CGFloat(columnsCount) + ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { - VStack { + VStack(alignment: .leading) { ZStack { KFImage(URL(string: item.imageUrl)) .placeholder { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) + .aspectRatio(2/3, contentMode: .fit) .shimmering() } .resizable() - .aspectRatio(2/3, contentMode: .fill) - .frame(width: 150, height: 225) + .aspectRatio(contentMode: .fill) + .frame(height: cellWidth * 3 / 2) + .frame(maxWidth: cellWidth) .cornerRadius(10) .clipped() .overlay( @@ -94,11 +107,10 @@ struct LibraryView: View { alignment: .topLeading ) } - Text(item.title) .font(.subheadline) .foregroundColor(.primary) - .lineLimit(2) + .lineLimit(1) .multilineTextAlignment(.leading) } } @@ -106,6 +118,12 @@ struct LibraryView: View { } } .padding(.horizontal, 20) + .onAppear { + updateOrientation() + } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + updateOrientation() + } } } .padding(.vertical, 20) @@ -135,6 +153,20 @@ struct LibraryView: View { ContinueWatchingManager.shared.remove(item: item) continueWatchingItems.removeAll { $0.id == item.id } } + + private func updateOrientation() { + DispatchQueue.main.async { + isLandscape = UIDevice.current.orientation.isLandscape + } + } + + private func determineColumns() -> Int { + if UIDevice.current.userInterfaceIdiom == .pad { + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } } struct ContinueWatchingSection: View { diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 8b47f57..2ad0986 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -65,9 +65,10 @@ struct MediaInfoView: View { .shimmering() } .resizable() - .aspectRatio(2/3, contentMode: .fit) - .cornerRadius(10) + .aspectRatio(contentMode: .fill) .frame(width: 150, height: 225) + .clipped() + .cornerRadius(10) VStack(alignment: .leading, spacing: 4) { Text(title) diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 914b585..85802ba 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -17,14 +17,19 @@ struct SearchItem: Identifiable { struct SearchView: View { @AppStorage("selectedModuleId") private var selectedModuleId: String? + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + @StateObject private var jsController = JSController() @EnvironmentObject var moduleManager: ModuleManager + @Environment(\.verticalSizeClass) var verticalSizeClass @State private var searchItems: [SearchItem] = [] @State private var selectedSearchItem: SearchItem? @State private var isSearching = false @State private var searchText = "" @State private var hasNoResults = false + @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape private var selectedModule: ScrapingModule? { guard let id = selectedModuleId else { return nil } @@ -39,9 +44,31 @@ struct SearchView: View { "Almost there..." ] + private var columnsCount: Int { + if UIDevice.current.userInterfaceIdiom == .pad { + let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } + + private var cellWidth: CGFloat { + let keyWindow = UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) } + .first + let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero + let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) + let availableWidth = safeWidth - totalSpacing + return availableWidth / CGFloat(columnsCount) + } + var body: some View { NavigationView { ScrollView { + let columnsCount = determineColumns() + VStack(spacing: 0) { HStack { SearchBar(text: $searchText, onSearchButtonClicked: performSearch) @@ -79,9 +106,9 @@ struct SearchView: View { if !searchText.isEmpty { if isSearching { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) { - ForEach(0..<2, id: \.self) { _ in - SearchSkeletonCell() + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) { + ForEach(0.. Int { + if UIDevice.current.userInterfaceIdiom == .pad { + return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait + } else { + return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait + } + } } struct SearchBar: View { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index c9bbfbd..65f7bfe 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -14,6 +14,9 @@ struct SettingsViewGeneral: View { @AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false @AppStorage("multiThreads") private var multiThreadsEnabled: Bool = false @AppStorage("metadataProviders") private var metadataProviders: String = "AniList" + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + private let metadataProvidersList = ["AniList"] @EnvironmentObject var settings: Settings @@ -76,6 +79,45 @@ struct SettingsViewGeneral: View { // .tint(.accentColor) //} + Section(header: Text("Media Grid Layout"), footer: Text("Adjust the number of media items per row in portrait and landscape modes.")) { + HStack { + Spacer() + if UIDevice.current.userInterfaceIdiom == .pad { + Picker("Portrait Columns", selection: $mediaColumnsPortrait) { + ForEach(1..<6) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } else { + Picker("Portrait Columns", selection: $mediaColumnsPortrait) { + ForEach(1..<5) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } + } + HStack { + Spacer() + if UIDevice.current.userInterfaceIdiom == .pad { + Picker("Landscape Columns", selection: $mediaColumnsLandscape) { + ForEach(2..<9) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } else { + Picker("Landscape Columns", selection: $mediaColumnsLandscape) { + ForEach(2..<6) { i in + Text("\(i)").tag(i) + } + } + .pickerStyle(MenuPickerStyle()) + } + } + } + Section(header: Text("Modules"), footer: Text("Note that the modules will be replaced only if there is a different version string inside the JSON file.")) { Toggle("Refresh Modules on Launch", isOn: $refreshModulesOnLaunch) .tint(.accentColor)