diff --git a/Sora/Utils/SkeletonCells/SkeletonCell.swift b/Sora/Utils/SkeletonCells/SkeletonCell.swift index adf330a..deb6c67 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) // Maintains 2:3 aspect ratio .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) // Maintains 2:3 aspect ratio .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..4d2a104 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,23 @@ 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) { 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) + // Allow the image to expand to fill the available width of its grid cell. + .frame(maxWidth: .infinity) .cornerRadius(10) .clipped() .overlay( @@ -94,11 +103,10 @@ struct LibraryView: View { alignment: .topLeading ) } - Text(item.title) .font(.subheadline) .foregroundColor(.primary) - .lineLimit(2) + .lineLimit(1) .multilineTextAlignment(.leading) } } @@ -106,6 +114,12 @@ struct LibraryView: View { } } .padding(.horizontal, 20) + .onAppear { + updateOrientation() + } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + updateOrientation() + } } } .padding(.vertical, 20) @@ -135,6 +149,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/SearchView.swift b/Sora/Views/SearchView.swift index 914b585..de0bb1b 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 } @@ -38,10 +43,12 @@ struct SearchView: View { "Please wait...", "Almost there..." ] - + var body: some View { NavigationView { ScrollView { + let columnsCount = determineColumns() + VStack(spacing: 0) { HStack { SearchBar(text: $searchText, onSearchButtonClicked: performSearch) @@ -79,9 +86,13 @@ struct SearchView: View { if !searchText.isEmpty { if isSearching { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 16) { - ForEach(0..<2, id: \.self) { _ in - SearchSkeletonCell() + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) // Spacing between items + let availableWidth = UIScreen.main.bounds.width - totalSpacing + let cellWidth = availableWidth / CGFloat(columnsCount) + + 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)