From 8d7d471f7ac05fe70691d5bf3435eb2487cc6b1f Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Mon, 13 Jun 2022 21:16:03 -0500 Subject: [PATCH 01/17] Delete feed by swiping --- TestApp/Sources/Data/Catalog.swift | 6 ++++++ TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift | 6 ++++++ .../Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/TestApp/Sources/Data/Catalog.swift b/TestApp/Sources/Data/Catalog.swift index 6bf6b2bf6..ff57758e4 100644 --- a/TestApp/Sources/Data/Catalog.swift +++ b/TestApp/Sources/Data/Catalog.swift @@ -49,4 +49,10 @@ final class CatalogRepository { try catalog.saved(db) } } + + func delete(ids: [Catalog.Id]) async throws { + try await db.write { db in + try Catalog.deleteAll(db, ids: ids) + } + } } diff --git a/TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift b/TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift index b65f0a48d..fd702d1db 100644 --- a/TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift +++ b/TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift @@ -25,6 +25,12 @@ struct Catalogs: View { ListRowItem(title: catalog.title) } } + .onDelete { offsets in + let catalogIds = offsets.map { catalogs[$0].id! } + Task { + try await viewModel.deleteCatalogs(ids: catalogIds) + } + } } .listStyle(DefaultListStyle()) } diff --git a/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift b/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift index 52cb86bdf..398dc7f8d 100644 --- a/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift +++ b/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift @@ -23,4 +23,8 @@ final class CatalogsViewModel: ObservableObject { var savedCatalog = catalog try? await catalogRepository.save(&savedCatalog) } + + func deleteCatalogs(ids: [Catalog.Id]) async throws { + try? await catalogRepository.delete(ids: ids) + } } From 3d954a4665569a624655e59cfef6c932bf503b0c Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Tue, 14 Jun 2022 20:43:09 -0500 Subject: [PATCH 02/17] Make OPDS feed navigation links work --- TestApp/Sources/Container.swift | 4 +++- .../CatalogDetail/CatalogDetail.swift | 20 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/TestApp/Sources/Container.swift b/TestApp/Sources/Container.swift index 1cc05b6a4..2ed47faa6 100644 --- a/TestApp/Sources/Container.swift +++ b/TestApp/Sources/Container.swift @@ -37,7 +37,9 @@ class Container { } func catalogDetail(with catalog: Catalog) -> CatalogDetail { - CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog)) + CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), + catalogDetail: catalogDetail(with:) + ) } // About diff --git a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift b/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift index e5c45a606..2da733787 100644 --- a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift +++ b/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift @@ -9,15 +9,17 @@ import SwiftUI struct CatalogDetail: View { @ObservedObject var viewModel: CatalogDetailViewModel + let catalogDetail: (Catalog) -> CatalogDetail var body: some View { VStack { if let parseData = viewModel.parseData { List(parseData.feed!.navigation, id: \.self) { link in - // NavigationLink(destination: CatalogDetail()) { - ListRowItem(title: link.title!) - // } + let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: link.title!) + } } .listStyle(DefaultListStyle()) } @@ -32,9 +34,19 @@ struct CatalogDetail: View { } } +// FIXME this causes a Swift compiler error segmentation fault 11 + +//struct CatalogDetail_Previews: PreviewProvider { +// static var previews: some View { +// let catalog = Catalog(title: "Test", url: "https://www.test.com") +// let catalogDetail: (Catalog) -> CatalogDetail = { CatalogDetail(CatalogDetailViewModel(catalog: catalog)) } +// CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), catalogDetail: catalogDetail) +// } +//} + struct CatalogDetail_Previews: PreviewProvider { static var previews: some View { let catalog = Catalog(title: "Test", url: "https://www.test.com") - CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog)) + CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), catalogDetail: { _ in fatalError() }) } } From 2748824c11b519b619fff316f29c11ded3b8ed19 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:40:51 -0500 Subject: [PATCH 03/17] Start building other lists on OPDS detail --- TestApp/Sources/Container.swift | 8 ++++- .../CatalogDetail/CatalogDetail.swift | 36 +++++++++++++++---- .../PublicationDetail/PublicationDetail.swift | 23 ++++++++++++ .../PublicationViewModel.swift | 18 ++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift create mode 100644 TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift diff --git a/TestApp/Sources/Container.swift b/TestApp/Sources/Container.swift index 2ed47faa6..bbb6ce383 100644 --- a/TestApp/Sources/Container.swift +++ b/TestApp/Sources/Container.swift @@ -5,6 +5,7 @@ // import Foundation +import R2Shared class Container { @@ -38,10 +39,15 @@ class Container { func catalogDetail(with catalog: Catalog) -> CatalogDetail { CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), - catalogDetail: catalogDetail(with:) + catalogDetail: catalogDetail(with:), + publicationDetail: publicationDetail(with:) ) } + func publicationDetail(with publication: Publication) -> PublicationDetail { + PublicationDetail(viewModel: PublicationViewModel(publication: publication)) + } + // About func about() -> About { diff --git a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift b/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift index 2da733787..b52f05799 100644 --- a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift +++ b/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift @@ -5,23 +5,45 @@ // import SwiftUI +import R2Shared struct CatalogDetail: View { @ObservedObject var viewModel: CatalogDetailViewModel let catalogDetail: (Catalog) -> CatalogDetail + let publicationDetail: (Publication) -> PublicationDetail var body: some View { VStack { if let parseData = viewModel.parseData { - List(parseData.feed!.navigation, id: \.self) { link in - let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) - NavigationLink(destination: catalogDetail(navigationLink)) { - ListRowItem(title: link.title!) + List() { + if (!(parseData.feed?.navigation.isEmpty)!) { + Section(header: Text("Navigation")) { + ForEach(parseData.feed!.navigation, id: \.self) { link in + let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: link.title!) + } + } + } + } + + // TODO This probably needs its own file + if (!(parseData.feed?.publications.isEmpty)!) { + Section(header: Text("Publications")) { + + } + } + + // TODO This probably needs its own file + if (!(parseData.feed?.groups.isEmpty)!) { + Section(header: Text("Groups")) { + + } } } - .listStyle(DefaultListStyle()) + .listStyle(GroupedListStyle()) } } .navigationTitle(viewModel.catalog.title) @@ -47,6 +69,8 @@ struct CatalogDetail: View { struct CatalogDetail_Previews: PreviewProvider { static var previews: some View { let catalog = Catalog(title: "Test", url: "https://www.test.com") - CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), catalogDetail: { _ in fatalError() }) + CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), catalogDetail: { _ in fatalError() }, + publicationDetail: { _ in fatalError() } + ) } } diff --git a/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift b/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift new file mode 100644 index 000000000..b49cc4127 --- /dev/null +++ b/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import SwiftUI +import R2Shared + +struct PublicationDetail: View { + + @ObservedObject var viewModel: PublicationViewModel + + var body: some View { + + } +} + +//struct PublicationDetail_Previews: PreviewProvider { +// static var previews: some View { +// PublicationDetail() +// } +//} diff --git a/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift b/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift new file mode 100644 index 000000000..5aa5f6a10 --- /dev/null +++ b/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift @@ -0,0 +1,18 @@ +// +// Copyright 2022 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import Foundation +import R2Shared + +final class PublicationViewModel : ObservableObject { + + @Published var publication: Publication + + init(publication: Publication) { + self.publication = publication + } + +} From 48b405a58212ebf1ccb980a433da889d6acda549 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Sat, 18 Jun 2022 12:03:10 -0500 Subject: [PATCH 04/17] Remove the use of ViewModels --- .../{Views/About => About/Views}/About.swift | 0 .../Views}/AddBookSheet.swift | 0 .../Views}/Bookshelf.swift | 20 +++++---- .../Views}/AddFeedSheet.swift | 0 .../Views}/CatalogDetail.swift | 30 +++++++++---- .../Views}/Catalogs.swift | 43 +++++++++++++------ .../Views}/PublicationDetail.swift | 6 +-- TestApp/Sources/Container.swift | 8 ++-- .../Views/Bookshelf/BookshelfViewModel.swift | 19 -------- .../CatalogDetailViewModel.swift | 25 ----------- .../Catalogs/Catalogs/CatalogsViewModel.swift | 30 ------------- .../PublicationViewModel.swift | 18 -------- 12 files changed, 69 insertions(+), 130 deletions(-) rename TestApp/Sources/{Views/About => About/Views}/About.swift (100%) rename TestApp/Sources/{Views/Bookshelf => Bookshelf/Views}/AddBookSheet.swift (100%) rename TestApp/Sources/{Views/Bookshelf => Bookshelf/Views}/Bookshelf.swift (68%) rename TestApp/Sources/{Views/Catalogs/Catalogs => Catalogs/Views}/AddFeedSheet.swift (100%) rename TestApp/Sources/{Views/Catalogs/CatalogDetail => Catalogs/Views}/CatalogDetail.swift (75%) rename TestApp/Sources/{Views/Catalogs/Catalogs => Catalogs/Views}/Catalogs.swift (58%) rename TestApp/Sources/{Views/Catalogs/PublicationDetail => Catalogs/Views}/PublicationDetail.swift (86%) delete mode 100644 TestApp/Sources/Views/Bookshelf/BookshelfViewModel.swift delete mode 100644 TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetailViewModel.swift delete mode 100644 TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift delete mode 100644 TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift diff --git a/TestApp/Sources/Views/About/About.swift b/TestApp/Sources/About/Views/About.swift similarity index 100% rename from TestApp/Sources/Views/About/About.swift rename to TestApp/Sources/About/Views/About.swift diff --git a/TestApp/Sources/Views/Bookshelf/AddBookSheet.swift b/TestApp/Sources/Bookshelf/Views/AddBookSheet.swift similarity index 100% rename from TestApp/Sources/Views/Bookshelf/AddBookSheet.swift rename to TestApp/Sources/Bookshelf/Views/AddBookSheet.swift diff --git a/TestApp/Sources/Views/Bookshelf/Bookshelf.swift b/TestApp/Sources/Bookshelf/Views/Bookshelf.swift similarity index 68% rename from TestApp/Sources/Views/Bookshelf/Bookshelf.swift rename to TestApp/Sources/Bookshelf/Views/Bookshelf.swift index 5dbb981fc..d9b36cada 100644 --- a/TestApp/Sources/Views/Bookshelf/Bookshelf.swift +++ b/TestApp/Sources/Bookshelf/Views/Bookshelf.swift @@ -8,23 +8,27 @@ import SwiftUI struct Bookshelf: View { - @ObservedObject var viewModel: BookshelfViewModel + let bookRepository: BookRepository + @State private var showingSheet = false + @State private var books: [Book] = [] var body: some View { NavigationView { VStack { // TODO figure out what the best column layout is for phones and tablets - if let books = viewModel.books { - let columns: [GridItem] = Array(repeating: .init(.adaptive(minimum: 170)), count: 2) - ScrollView { - LazyVGrid(columns: columns, spacing: 20) { - ForEach(books, id: \.self) { item in - BookCover(book: item) - } + let columns: [GridItem] = Array(repeating: .init(.adaptive(minimum: 170)), count: 2) + ScrollView { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(books, id: \.self) { item in + BookCover(book: item) } } + .onReceive(bookRepository.all()) { + books = $0 + } } + } .navigationTitle("Bookshelf") .toolbar(content: toolbarContent) diff --git a/TestApp/Sources/Views/Catalogs/Catalogs/AddFeedSheet.swift b/TestApp/Sources/Catalogs/Views/AddFeedSheet.swift similarity index 100% rename from TestApp/Sources/Views/Catalogs/Catalogs/AddFeedSheet.swift rename to TestApp/Sources/Catalogs/Views/AddFeedSheet.swift diff --git a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift similarity index 75% rename from TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift rename to TestApp/Sources/Catalogs/Views/CatalogDetail.swift index b52f05799..d97795b49 100644 --- a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -6,21 +6,24 @@ import SwiftUI import R2Shared +import ReadiumOPDS struct CatalogDetail: View { - @ObservedObject var viewModel: CatalogDetailViewModel + @State var catalog: Catalog + @State private var parseData: ParseData? + let catalogDetail: (Catalog) -> CatalogDetail let publicationDetail: (Publication) -> PublicationDetail var body: some View { VStack { - if let parseData = viewModel.parseData { + if let feed = parseData?.feed { List() { - if (!(parseData.feed?.navigation.isEmpty)!) { + if !feed.navigation.isEmpty { Section(header: Text("Navigation")) { - ForEach(parseData.feed!.navigation, id: \.self) { link in + ForEach(feed.navigation, id: \.self) { link in let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) NavigationLink(destination: catalogDetail(navigationLink)) { ListRowItem(title: link.title!) @@ -30,14 +33,14 @@ struct CatalogDetail: View { } // TODO This probably needs its own file - if (!(parseData.feed?.publications.isEmpty)!) { + if !feed.publications.isEmpty { Section(header: Text("Publications")) { } } // TODO This probably needs its own file - if (!(parseData.feed?.groups.isEmpty)!) { + if !feed.groups.isEmpty { Section(header: Text("Groups")) { } @@ -46,16 +49,25 @@ struct CatalogDetail: View { .listStyle(GroupedListStyle()) } } - .navigationTitle(viewModel.catalog.title) + .navigationTitle(catalog.title) .navigationBarTitleDisplayMode(.inline) .onAppear { Task { - await viewModel.parseFeed() + await parseFeed() } } } } +extension CatalogDetail { + + func parseFeed() async { + if let url = URL(string: catalog.url) { + self.parseData = try? await OPDSParser.parseURL(url: url) + } + } +} + // FIXME this causes a Swift compiler error segmentation fault 11 //struct CatalogDetail_Previews: PreviewProvider { @@ -69,7 +81,7 @@ struct CatalogDetail: View { struct CatalogDetail_Previews: PreviewProvider { static var previews: some View { let catalog = Catalog(title: "Test", url: "https://www.test.com") - CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), catalogDetail: { _ in fatalError() }, + CatalogDetail(catalog: catalog, catalogDetail: { _ in fatalError() }, publicationDetail: { _ in fatalError() } ) } diff --git a/TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift b/TestApp/Sources/Catalogs/Views/Catalogs.swift similarity index 58% rename from TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift rename to TestApp/Sources/Catalogs/Views/Catalogs.swift index fd702d1db..5bd13043e 100644 --- a/TestApp/Sources/Views/Catalogs/Catalogs/Catalogs.swift +++ b/TestApp/Sources/Catalogs/Views/Catalogs.swift @@ -9,31 +9,34 @@ import ReadiumOPDS struct Catalogs: View { - @ObservedObject var viewModel: CatalogsViewModel + let catalogRepository: CatalogRepository let catalogDetail: (Catalog) -> CatalogDetail @State private var showingSheet = false @State private var showingAlert = false + @State private var catalogs: [Catalog] = [] var body: some View { NavigationView { VStack { - if let catalogs = viewModel.catalogs { - List() { - ForEach(catalogs, id: \.id) { catalog in - NavigationLink(destination: catalogDetail(catalog)) { - ListRowItem(title: catalog.title) - } + List() { + ForEach(catalogs, id: \.id) { catalog in + NavigationLink(destination: catalogDetail(catalog)) { + ListRowItem(title: catalog.title) } - .onDelete { offsets in - let catalogIds = offsets.map { catalogs[$0].id! } - Task { - try await viewModel.deleteCatalogs(ids: catalogIds) - } + } + .onDelete { offsets in + let catalogIds = offsets.map { catalogs[$0].id! } + Task { + try await deleteCatalogs(ids: catalogIds) } } - .listStyle(DefaultListStyle()) } + .onReceive(catalogRepository.all()) { + catalogs = $0 ?? [] + } + .listStyle(DefaultListStyle()) + } .navigationTitle("Catalogs") .toolbar(content: toolbarContent) @@ -44,7 +47,7 @@ struct Catalogs: View { Task { do { _ = try await OPDSParser.parseURL(url: URL(string: url)!) - try await viewModel.addCatalog(catalog: Catalog(title: title, url: url)) + try await addCatalog(catalog: Catalog(title: title, url: url)) } catch { showingAlert = true } @@ -67,3 +70,15 @@ struct Catalogs: View { } } } + +extension Catalogs { + + func addCatalog(catalog: Catalog) async throws { + var savedCatalog = catalog + try? await catalogRepository.save(&savedCatalog) + } + + func deleteCatalogs(ids: [Catalog.Id]) async throws { + try? await catalogRepository.delete(ids: ids) + } +} diff --git a/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift b/TestApp/Sources/Catalogs/Views/PublicationDetail.swift similarity index 86% rename from TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift rename to TestApp/Sources/Catalogs/Views/PublicationDetail.swift index b49cc4127..9bb5c5c61 100644 --- a/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationDetail.swift +++ b/TestApp/Sources/Catalogs/Views/PublicationDetail.swift @@ -8,11 +8,11 @@ import SwiftUI import R2Shared struct PublicationDetail: View { - - @ObservedObject var viewModel: PublicationViewModel + + @State var publication: Publication var body: some View { - + Text("") } } diff --git a/TestApp/Sources/Container.swift b/TestApp/Sources/Container.swift index bbb6ce383..945ff018b 100644 --- a/TestApp/Sources/Container.swift +++ b/TestApp/Sources/Container.swift @@ -23,7 +23,7 @@ class Container { private lazy var bookRepository = BookRepository(db: db) func bookshelf() -> Bookshelf { - Bookshelf(viewModel: BookshelfViewModel(bookRepository: bookRepository)) + Bookshelf(bookRepository: bookRepository) } // Catalogs @@ -32,20 +32,20 @@ class Container { func catalogs() -> Catalogs { Catalogs( - viewModel: CatalogsViewModel(catalogRepository: catalogRepository), + catalogRepository: catalogRepository, catalogDetail: catalogDetail(with:) ) } func catalogDetail(with catalog: Catalog) -> CatalogDetail { - CatalogDetail(viewModel: CatalogDetailViewModel(catalog: catalog), + CatalogDetail(catalog: catalog, catalogDetail: catalogDetail(with:), publicationDetail: publicationDetail(with:) ) } func publicationDetail(with publication: Publication) -> PublicationDetail { - PublicationDetail(viewModel: PublicationViewModel(publication: publication)) + PublicationDetail(publication: publication) } // About diff --git a/TestApp/Sources/Views/Bookshelf/BookshelfViewModel.swift b/TestApp/Sources/Views/Bookshelf/BookshelfViewModel.swift deleted file mode 100644 index e4a62b230..000000000 --- a/TestApp/Sources/Views/Bookshelf/BookshelfViewModel.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright 2022 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import GRDB -import Combine -import Foundation - -final class BookshelfViewModel: ObservableObject { - - @Published var books: [Book]? - private var bookRepository: BookRepository - - init(bookRepository: BookRepository) { - self.bookRepository = bookRepository - } -} diff --git a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetailViewModel.swift b/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetailViewModel.swift deleted file mode 100644 index d2199babe..000000000 --- a/TestApp/Sources/Views/Catalogs/CatalogDetail/CatalogDetailViewModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright 2022 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import Foundation -import ReadiumOPDS -import SwiftUI - -final class CatalogDetailViewModel : ObservableObject { - - @Published var catalog: Catalog - @Published var parseData: ParseData? - - init(catalog: Catalog) { - self.catalog = catalog - } - - @MainActor func parseFeed() async { - if let url = URL(string: catalog.url) { - self.parseData = try? await OPDSParser.parseURL(url: url) - } - } -} diff --git a/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift b/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift deleted file mode 100644 index 398dc7f8d..000000000 --- a/TestApp/Sources/Views/Catalogs/Catalogs/CatalogsViewModel.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright 2022 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import GRDB -import Combine -import Foundation - -final class CatalogsViewModel: ObservableObject { - - @Published var catalogs: [Catalog]? - private var catalogRepository: CatalogRepository - - init(catalogRepository: CatalogRepository) { - self.catalogRepository = catalogRepository - catalogRepository.all() - .assign(to: &$catalogs) - } - - func addCatalog(catalog: Catalog) async throws { - var savedCatalog = catalog - try? await catalogRepository.save(&savedCatalog) - } - - func deleteCatalogs(ids: [Catalog.Id]) async throws { - try? await catalogRepository.delete(ids: ids) - } -} diff --git a/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift b/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift deleted file mode 100644 index 5aa5f6a10..000000000 --- a/TestApp/Sources/Views/Catalogs/PublicationDetail/PublicationViewModel.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright 2022 Readium Foundation. All rights reserved. -// Use of this source code is governed by the BSD-style license -// available in the top-level LICENSE file of the project. -// - -import Foundation -import R2Shared - -final class PublicationViewModel : ObservableObject { - - @Published var publication: Publication - - init(publication: Publication) { - self.publication = publication - } - -} From 3854491b1f684b7efcfeceef1dc41a6ae0568c1a Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Sun, 19 Jun 2022 21:43:23 -0500 Subject: [PATCH 05/17] Show publications for a OPDS feed --- .../Sources/Bookshelf/Views/Bookshelf.swift | 6 +++--- .../Sources/Catalogs/Views/CatalogDetail.swift | 14 +++++++++++++- TestApp/Sources/Views/BookCover.swift | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/TestApp/Sources/Bookshelf/Views/Bookshelf.swift b/TestApp/Sources/Bookshelf/Views/Bookshelf.swift index d9b36cada..000b152e8 100644 --- a/TestApp/Sources/Bookshelf/Views/Bookshelf.swift +++ b/TestApp/Sources/Bookshelf/Views/Bookshelf.swift @@ -17,11 +17,11 @@ struct Bookshelf: View { NavigationView { VStack { // TODO figure out what the best column layout is for phones and tablets - let columns: [GridItem] = Array(repeating: .init(.adaptive(minimum: 170)), count: 2) + let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) ScrollView { LazyVGrid(columns: columns, spacing: 20) { - ForEach(books, id: \.self) { item in - BookCover(book: item) + ForEach(books, id: \.self) { book in + BookCover(title: book.title, authors: book.authors, url: book.cover) } } .onReceive(bookRepository.all()) { diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index d97795b49..0cfa27f15 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -34,8 +34,20 @@ struct CatalogDetail: View { // TODO This probably needs its own file if !feed.publications.isEmpty { + let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) Section(header: Text("Publications")) { - + LazyVGrid(columns: columns) { + ForEach(feed.publications) { publication in + let authors = publication.metadata.authors + .map { $0.name } + .joined(separator: ", ") + if publication.images.count > 0 { + BookCover(title: publication.metadata.title, authors: authors, url: URL(string: publication.images[0].href)) + } else { + BookCover(title: publication.metadata.title, authors: authors) + } + } + } } } diff --git a/TestApp/Sources/Views/BookCover.swift b/TestApp/Sources/Views/BookCover.swift index 81511e1fd..0fc523ea9 100644 --- a/TestApp/Sources/Views/BookCover.swift +++ b/TestApp/Sources/Views/BookCover.swift @@ -7,10 +7,14 @@ import SwiftUI struct BookCover: View { - var book: Book + var title: String + var authors: String? + var url: URL? + var action: () -> Void = {} + var body: some View { VStack(alignment: .leading, spacing: 5) { - if let url = book.cover { + if (url != nil) { AsyncImage( url: url, content: { $0 @@ -19,11 +23,15 @@ struct BookCover: View { }, placeholder: { ProgressView() } ) + .frame(width: 150, height: 220) } else { Image(systemName: "book.closed") + .frame(width: 150, height: 220) } - Text(book.title) - Text(book.authors ?? "") + Text(title) + .frame(maxHeight: .infinity, alignment: .top) + Text(authors ?? "") + .frame(maxHeight: .infinity, alignment: .top) } } } @@ -31,6 +39,6 @@ struct BookCover: View { struct BookCover_Previews: PreviewProvider { static var previews: some View { let book = Book(title: "Test Title", authors: "Test Author", type: "application/epub+zip", path: "/test/path/") - BookCover(book: book) + BookCover(title: book.title, authors: book.authors) } } From f2e088e6dab9b841a274debe001eb91a36d9bbde Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Mon, 20 Jun 2022 21:00:14 -0500 Subject: [PATCH 06/17] Replace List with ScrollView --- .../Catalogs/Views/CatalogDetail.swift | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index 0cfa27f15..53c1fec58 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -18,48 +18,45 @@ struct CatalogDetail: View { var body: some View { - VStack { - if let feed = parseData?.feed { - List() { + ScrollView { + VStack(alignment: .leading) { + if let feed = parseData?.feed { if !feed.navigation.isEmpty { - Section(header: Text("Navigation")) { - ForEach(feed.navigation, id: \.self) { link in - let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) - NavigationLink(destination: catalogDetail(navigationLink)) { - ListRowItem(title: link.title!) - } + Text("Navigation").font(.title3) + ForEach(feed.navigation, id: \.self) { link in + let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: link.title!) } } } // TODO This probably needs its own file if !feed.publications.isEmpty { - let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) - Section(header: Text("Publications")) { + let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) + Text("Publications").font(.title3) LazyVGrid(columns: columns) { ForEach(feed.publications) { publication in let authors = publication.metadata.authors .map { $0.name } .joined(separator: ", ") - if publication.images.count > 0 { - BookCover(title: publication.metadata.title, authors: authors, url: URL(string: publication.images[0].href)) - } else { - BookCover(title: publication.metadata.title, authors: authors) - } + BookCover( + title: publication.metadata.title, + authors: authors, + url: publication.images.first + .map { URL(string: $0.href)! } + ) } } - } } // TODO This probably needs its own file if !feed.groups.isEmpty { - Section(header: Text("Groups")) { - - } + Text("Groups").font(.title3) } } - .listStyle(GroupedListStyle()) - } + }.frame(maxWidth: .infinity, alignment: .leading) + .padding([.trailing, .leading], 10) } .navigationTitle(catalog.title) .navigationBarTitleDisplayMode(.inline) From ce214bc4d8839d85f732cfe92e6c57021158ea29 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Mon, 20 Jun 2022 21:06:44 -0500 Subject: [PATCH 07/17] Start publication detail flow --- TestApp/Sources/Catalogs/Views/CatalogDetail.swift | 13 ++++++++----- .../Sources/Catalogs/Views/PublicationDetail.swift | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index 53c1fec58..82ecb3554 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -35,11 +35,12 @@ struct CatalogDetail: View { if !feed.publications.isEmpty { let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) Text("Publications").font(.title3) - LazyVGrid(columns: columns) { - ForEach(feed.publications) { publication in - let authors = publication.metadata.authors - .map { $0.name } - .joined(separator: ", ") + LazyVGrid(columns: columns) { + ForEach(feed.publications) { publication in + let authors = publication.metadata.authors + .map { $0.name } + .joined(separator: ", ") + NavigationLink(destination: publicationDetail(publication)) { BookCover( title: publication.metadata.title, authors: authors, @@ -47,7 +48,9 @@ struct CatalogDetail: View { .map { URL(string: $0.href)! } ) } + .buttonStyle(.plain) } + } } // TODO This probably needs its own file diff --git a/TestApp/Sources/Catalogs/Views/PublicationDetail.swift b/TestApp/Sources/Catalogs/Views/PublicationDetail.swift index 9bb5c5c61..89534993c 100644 --- a/TestApp/Sources/Catalogs/Views/PublicationDetail.swift +++ b/TestApp/Sources/Catalogs/Views/PublicationDetail.swift @@ -12,7 +12,7 @@ struct PublicationDetail: View { @State var publication: Publication var body: some View { - Text("") + Text(publication.metadata.title) } } From 518d9d5f3e699b30917a86d6c08890c831d32a69 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Sun, 26 Jun 2022 16:05:58 -0500 Subject: [PATCH 08/17] Make Publication Identifiable --- Sources/Shared/Publication/Publication.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Shared/Publication/Publication.swift b/Sources/Shared/Publication/Publication.swift index 74f7e60db..af39f819d 100644 --- a/Sources/Shared/Publication/Publication.swift +++ b/Sources/Shared/Publication/Publication.swift @@ -13,7 +13,7 @@ import CoreServices import Foundation /// Shared model for a Readium Publication. -public class Publication: Loggable { +public class Publication: Loggable, Identifiable { /// Format of the publication, if specified. @available(*, deprecated, message: "Use publication.conforms(to:) to check the profile of a Publication") From bd436297acf5e53d3ac126c0841c106bc0f44d63 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Mon, 27 Jun 2022 23:40:15 -0500 Subject: [PATCH 09/17] Start OPDS group publications in horizontal grid --- Sources/Shared/OPDS/Group.swift | 2 +- .../Catalogs/Views/CatalogDetail.swift | 32 ++++++++++++++++--- TestApp/Sources/Views/BookCover.swift | 10 +++--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Sources/Shared/OPDS/Group.swift b/Sources/Shared/OPDS/Group.swift index e84e5ea2b..bc9c63de8 100644 --- a/Sources/Shared/OPDS/Group.swift +++ b/Sources/Shared/OPDS/Group.swift @@ -10,7 +10,7 @@ // /// A substructure of a feed. -public class Group { +public class Group: Identifiable { public var metadata: OpdsMetadata public var links = [Link]() public var publications = [Publication]() diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index 82ecb3554..df8e53ca8 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -22,7 +22,7 @@ struct CatalogDetail: View { VStack(alignment: .leading) { if let feed = parseData?.feed { if !feed.navigation.isEmpty { - Text("Navigation").font(.title3) + Text("Navigation").font(.title2) ForEach(feed.navigation, id: \.self) { link in let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) NavigationLink(destination: catalogDetail(navigationLink)) { @@ -34,7 +34,7 @@ struct CatalogDetail: View { // TODO This probably needs its own file if !feed.publications.isEmpty { let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) - Text("Publications").font(.title3) + Text("Publications").font(.title2) LazyVGrid(columns: columns) { ForEach(feed.publications) { publication in let authors = publication.metadata.authors @@ -55,11 +55,33 @@ struct CatalogDetail: View { // TODO This probably needs its own file if !feed.groups.isEmpty { - Text("Groups").font(.title3) + Text("Groups").font(.title2) + let rows = [GridItem(.flexible(), alignment: .top)] + ForEach(feed.groups) { group in + Text(group.metadata.title).font(.title3) + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: rows, spacing: 30) { + ForEach(group.publications) { publication in + let authors = publication.metadata.authors + .map { $0.name } + .joined(separator: ", ") + NavigationLink(destination: publicationDetail(publication)) { + // FIXME Ideally the title and author should not be truncated + BookCover( + title: publication.metadata.title, + authors: authors, + url: publication.images.first + .map { URL(string: $0.href)! } + ) + } + .buttonStyle(.plain) + } + } + } + } } } - }.frame(maxWidth: .infinity, alignment: .leading) - .padding([.trailing, .leading], 10) + } } .navigationTitle(catalog.title) .navigationBarTitleDisplayMode(.inline) diff --git a/TestApp/Sources/Views/BookCover.swift b/TestApp/Sources/Views/BookCover.swift index 0fc523ea9..2b12346c7 100644 --- a/TestApp/Sources/Views/BookCover.swift +++ b/TestApp/Sources/Views/BookCover.swift @@ -13,26 +13,26 @@ struct BookCover: View { var action: () -> Void = {} var body: some View { - VStack(alignment: .leading, spacing: 5) { + VStack(alignment: .leading, spacing: 10) { if (url != nil) { AsyncImage( url: url, content: { $0 .resizable() .aspectRatio(contentMode: .fit) + .frame(width: 150, height: 220) }, placeholder: { ProgressView() } ) - .frame(width: 150, height: 220) } else { Image(systemName: "book.closed") + .resizable() + .aspectRatio(contentMode: .fit) .frame(width: 150, height: 220) } Text(title) - .frame(maxHeight: .infinity, alignment: .top) Text(authors ?? "") - .frame(maxHeight: .infinity, alignment: .top) - } + }.frame(maxWidth: 150) } } From ca63d2c01a8548570c1bda453367ba97eb8d49e7 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:48:27 -0500 Subject: [PATCH 10/17] Show group links and See All button --- .../Catalogs/Views/CatalogDetail.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index df8e53ca8..078f68e4d 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -33,6 +33,7 @@ struct CatalogDetail: View { // TODO This probably needs its own file if !feed.publications.isEmpty { + // TODO can this be reused in bookshelf and here? let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) Text("Publications").font(.title2) LazyVGrid(columns: columns) { @@ -57,8 +58,16 @@ struct CatalogDetail: View { if !feed.groups.isEmpty { Text("Groups").font(.title2) let rows = [GridItem(.flexible(), alignment: .top)] - ForEach(feed.groups) { group in - Text(group.metadata.title).font(.title3) + ForEach(feed.groups as [R2Shared.Group]) { group in + HStack { + Text(group.metadata.title).font(.title3) + if group.links.count > 0 { + let navigationLink = Catalog(title: group.links.first!.title ?? "Catalog", url: group.links.first!.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: "See All").frame(maxWidth: .infinity, alignment: .trailing) + } + } + } ScrollView(.horizontal, showsIndicators: false) { LazyHGrid(rows: rows, spacing: 30) { ForEach(group.publications) { publication in @@ -78,6 +87,12 @@ struct CatalogDetail: View { } } } + ForEach(group.navigation, id: \.self) { navigation in + let navigationLink = Catalog(title: navigation.title ?? "Catalog", url: navigation.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: navigation.title!) + } + } } } } From b15102a97182d2c2403f28e846c03dfae7c53a53 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Wed, 29 Jun 2022 20:33:01 -0500 Subject: [PATCH 11/17] Publication detail, clean up catalog detail --- TestApp/Sources/About/Views/About.swift | 1 + .../Catalogs/Views/CatalogDetail.swift | 44 +++++++++++-------- .../Catalogs/Views/PublicationDetail.swift | 33 +++++++++++++- TestApp/Sources/Views/Button.swift | 5 +++ 4 files changed, 63 insertions(+), 20 deletions(-) diff --git a/TestApp/Sources/About/Views/About.swift b/TestApp/Sources/About/Views/About.swift index a33a073f5..8a2d485e8 100644 --- a/TestApp/Sources/About/Views/About.swift +++ b/TestApp/Sources/About/Views/About.swift @@ -29,6 +29,7 @@ struct About: View { Text("R2 Reader wouldn't have been developed without the financial help of the French State.") Image("rf") } + .padding() .navigationTitle("About") } } diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index 078f68e4d..03df95532 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -22,7 +22,6 @@ struct CatalogDetail: View { VStack(alignment: .leading) { if let feed = parseData?.feed { if !feed.navigation.isEmpty { - Text("Navigation").font(.title2) ForEach(feed.navigation, id: \.self) { link in let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) NavigationLink(destination: catalogDetail(navigationLink)) { @@ -31,11 +30,12 @@ struct CatalogDetail: View { } } + Divider().frame(height: 50) + // TODO This probably needs its own file if !feed.publications.isEmpty { // TODO can this be reused in bookshelf and here? let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) - Text("Publications").font(.title2) LazyVGrid(columns: columns) { ForEach(feed.publications) { publication in let authors = publication.metadata.authors @@ -54,36 +54,40 @@ struct CatalogDetail: View { } } + + Divider().frame(height: 50) + // TODO This probably needs its own file if !feed.groups.isEmpty { - Text("Groups").font(.title2) let rows = [GridItem(.flexible(), alignment: .top)] ForEach(feed.groups as [R2Shared.Group]) { group in HStack { Text(group.metadata.title).font(.title3) - if group.links.count > 0 { + if !group.links.isEmpty { let navigationLink = Catalog(title: group.links.first!.title ?? "Catalog", url: group.links.first!.href) NavigationLink(destination: catalogDetail(navigationLink)) { ListRowItem(title: "See All").frame(maxWidth: .infinity, alignment: .trailing) } } } - ScrollView(.horizontal, showsIndicators: false) { - LazyHGrid(rows: rows, spacing: 30) { - ForEach(group.publications) { publication in - let authors = publication.metadata.authors - .map { $0.name } - .joined(separator: ", ") - NavigationLink(destination: publicationDetail(publication)) { - // FIXME Ideally the title and author should not be truncated - BookCover( - title: publication.metadata.title, - authors: authors, - url: publication.images.first - .map { URL(string: $0.href)! } - ) + if !group.publications.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: rows, spacing: 30) { + ForEach(group.publications) { publication in + let authors = publication.metadata.authors + .map { $0.name } + .joined(separator: ", ") + NavigationLink(destination: publicationDetail(publication)) { + // FIXME Ideally the title and author should not be truncated + BookCover( + title: publication.metadata.title, + authors: authors, + url: publication.images.first + .map { URL(string: $0.href)! } + ) + } + .buttonStyle(.plain) } - .buttonStyle(.plain) } } } @@ -93,11 +97,13 @@ struct CatalogDetail: View { ListRowItem(title: navigation.title!) } } + } } } } } + .padding() .navigationTitle(catalog.title) .navigationBarTitleDisplayMode(.inline) .onAppear { diff --git a/TestApp/Sources/Catalogs/Views/PublicationDetail.swift b/TestApp/Sources/Catalogs/Views/PublicationDetail.swift index 89534993c..b2776f2ab 100644 --- a/TestApp/Sources/Catalogs/Views/PublicationDetail.swift +++ b/TestApp/Sources/Catalogs/Views/PublicationDetail.swift @@ -12,7 +12,38 @@ struct PublicationDetail: View { @State var publication: Publication var body: some View { - Text(publication.metadata.title) + let authors = publication.metadata.authors + .map { $0.name } + .joined(separator: ", ") + ScrollView { + VStack { + AsyncImage( + url: publication.images.first + .map { URL(string: $0.href)! }, + content: { $0 + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 225, height: 330) + }, + placeholder: { ProgressView() } + ) + Text(publication.metadata.title).font(.largeTitle) + Text(authors).font(.title2) + Text(publication.metadata.description ?? "") + .padding([.top, .bottom], 20) + } + } + .padding() + .toolbar(content: toolbarContent) + } + + @ToolbarContentBuilder + private func toolbarContent() -> some ToolbarContent { + ToolbarItem(placement: .navigationBarTrailing) { + Button(.download) { + // TODO download the publication + } + } } } diff --git a/TestApp/Sources/Views/Button.swift b/TestApp/Sources/Views/Button.swift index 5dbad84c8..94c4812b5 100644 --- a/TestApp/Sources/Views/Button.swift +++ b/TestApp/Sources/Views/Button.swift @@ -10,6 +10,7 @@ enum ButtonKind { case add case cancel case save + case download } @ViewBuilder @@ -23,5 +24,9 @@ func Button(_ kind: ButtonKind, action: @escaping () -> Void) -> some View { Button("Cancel", action: action) case .save: Button("Save", action: action) + case .download: + Button(action: action) { + Label("Download", systemImage: "icloud.and.arrow.down") + } } } From a6bd4fc0e4c8eb2d927e20913b0375de2ad54309 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Sat, 9 Jul 2022 11:49:53 -0500 Subject: [PATCH 12/17] Move catalog group into its own file --- .../Catalogs/Views/CatalogDetail.swift | 41 +----------- .../Sources/Catalogs/Views/CatalogGroup.swift | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 39 deletions(-) create mode 100644 TestApp/Sources/Catalogs/Views/CatalogGroup.swift diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift index 03df95532..69ab64709 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogDetail.swift @@ -57,47 +57,10 @@ struct CatalogDetail: View { Divider().frame(height: 50) - // TODO This probably needs its own file if !feed.groups.isEmpty { - let rows = [GridItem(.flexible(), alignment: .top)] ForEach(feed.groups as [R2Shared.Group]) { group in - HStack { - Text(group.metadata.title).font(.title3) - if !group.links.isEmpty { - let navigationLink = Catalog(title: group.links.first!.title ?? "Catalog", url: group.links.first!.href) - NavigationLink(destination: catalogDetail(navigationLink)) { - ListRowItem(title: "See All").frame(maxWidth: .infinity, alignment: .trailing) - } - } - } - if !group.publications.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - LazyHGrid(rows: rows, spacing: 30) { - ForEach(group.publications) { publication in - let authors = publication.metadata.authors - .map { $0.name } - .joined(separator: ", ") - NavigationLink(destination: publicationDetail(publication)) { - // FIXME Ideally the title and author should not be truncated - BookCover( - title: publication.metadata.title, - authors: authors, - url: publication.images.first - .map { URL(string: $0.href)! } - ) - } - .buttonStyle(.plain) - } - } - } - } - ForEach(group.navigation, id: \.self) { navigation in - let navigationLink = Catalog(title: navigation.title ?? "Catalog", url: navigation.href) - NavigationLink(destination: catalogDetail(navigationLink)) { - ListRowItem(title: navigation.title!) - } - } - + CatalogGroup(group: group, publicationDetail: publicationDetail, catalogDetail: catalogDetail) + .padding([.bottom], 25) } } } diff --git a/TestApp/Sources/Catalogs/Views/CatalogGroup.swift b/TestApp/Sources/Catalogs/Views/CatalogGroup.swift new file mode 100644 index 000000000..e332e0171 --- /dev/null +++ b/TestApp/Sources/Catalogs/Views/CatalogGroup.swift @@ -0,0 +1,64 @@ +// +// CatalogGroup.swift +// TestApp +// +// Created by Steven Zeck on 6/27/22. +// + +import SwiftUI +import R2Shared + +struct CatalogGroup: View { + + var group: R2Shared.Group + let publicationDetail: (Publication) -> PublicationDetail + let catalogDetail: (Catalog) -> CatalogDetail + + var body: some View { + VStack(alignment: .leading) { + let rows = [GridItem(.flexible(), alignment: .top)] + HStack { + Text(group.metadata.title).font(.title3) + if !group.links.isEmpty { + let navigationLink = Catalog(title: group.links.first!.title ?? "Catalog", url: group.links.first!.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: "See All").frame(maxWidth: .infinity, alignment: .trailing) + } + } + } + if !group.publications.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: rows, spacing: 30) { + ForEach(group.publications) { publication in + let authors = publication.metadata.authors + .map { $0.name } + .joined(separator: ", ") + NavigationLink(destination: publicationDetail(publication)) { + // FIXME Ideally the title and author should not be truncated + BookCover( + title: publication.metadata.title, + authors: authors, + url: publication.images.first + .map { URL(string: $0.href)! } + ) + } + .buttonStyle(.plain) + } + } + } + } + ForEach(group.navigation, id: \.self) { navigation in + let navigationLink = Catalog(title: navigation.title ?? "Catalog", url: navigation.href) + NavigationLink(destination: catalogDetail(navigationLink)) { + ListRowItem(title: navigation.title!) + } + } + } + } +} + +//struct CatalogGroup_Previews: PreviewProvider { +// static var previews: some View { +// CatalogGroup() +// } +//} From 5722c6ad3239f61457eb099d945c32414b63703e Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Sat, 9 Jul 2022 12:18:16 -0500 Subject: [PATCH 13/17] Fix file header --- TestApp/Sources/Catalogs/Views/CatalogGroup.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/TestApp/Sources/Catalogs/Views/CatalogGroup.swift b/TestApp/Sources/Catalogs/Views/CatalogGroup.swift index e332e0171..61a1aaa47 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogGroup.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogGroup.swift @@ -1,8 +1,7 @@ // -// CatalogGroup.swift -// TestApp -// -// Created by Steven Zeck on 6/27/22. +// Copyright 2022 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. // import SwiftUI From 87258f3f868ff03659cf4fc3e7730bfdfcdb02e4 Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Mon, 11 Jul 2022 23:33:56 -0500 Subject: [PATCH 14/17] Fix re-parsing of OPDS feed, rename files --- ...{CatalogDetail.swift => CatalogFeed.swift} | 25 +++++++++---------- .../Sources/Catalogs/Views/CatalogGroup.swift | 6 ++--- .../{Catalogs.swift => CatalogList.swift} | 8 +++--- TestApp/Sources/Container.swift | 12 ++++----- 4 files changed, 25 insertions(+), 26 deletions(-) rename TestApp/Sources/Catalogs/Views/{CatalogDetail.swift => CatalogFeed.swift} (85%) rename TestApp/Sources/Catalogs/Views/{Catalogs.swift => CatalogList.swift} (93%) diff --git a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift b/TestApp/Sources/Catalogs/Views/CatalogFeed.swift similarity index 85% rename from TestApp/Sources/Catalogs/Views/CatalogDetail.swift rename to TestApp/Sources/Catalogs/Views/CatalogFeed.swift index 69ab64709..208c7515a 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogDetail.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogFeed.swift @@ -8,12 +8,12 @@ import SwiftUI import R2Shared import ReadiumOPDS -struct CatalogDetail: View { +struct CatalogFeed: View { @State var catalog: Catalog @State private var parseData: ParseData? - let catalogDetail: (Catalog) -> CatalogDetail + let catalogFeed: (Catalog) -> CatalogFeed let publicationDetail: (Publication) -> PublicationDetail var body: some View { @@ -24,14 +24,13 @@ struct CatalogDetail: View { if !feed.navigation.isEmpty { ForEach(feed.navigation, id: \.self) { link in let navigationLink = Catalog(title: link.title ?? "Catalog", url: link.href) - NavigationLink(destination: catalogDetail(navigationLink)) { + NavigationLink(destination: catalogFeed(navigationLink)) { ListRowItem(title: link.title!) } } + Divider().frame(height: 50) } - Divider().frame(height: 50) - // TODO This probably needs its own file if !feed.publications.isEmpty { // TODO can this be reused in bookshelf and here? @@ -52,14 +51,12 @@ struct CatalogDetail: View { .buttonStyle(.plain) } } + Divider().frame(height: 50) } - - Divider().frame(height: 50) - if !feed.groups.isEmpty { ForEach(feed.groups as [R2Shared.Group]) { group in - CatalogGroup(group: group, publicationDetail: publicationDetail, catalogDetail: catalogDetail) + CatalogGroup(group: group, publicationDetail: publicationDetail, catalogFeed: catalogFeed) .padding([.bottom], 25) } } @@ -71,13 +68,15 @@ struct CatalogDetail: View { .navigationBarTitleDisplayMode(.inline) .onAppear { Task { - await parseFeed() + if parseData == nil { + await parseFeed() + } } } } } -extension CatalogDetail { +extension CatalogFeed { func parseFeed() async { if let url = URL(string: catalog.url) { @@ -99,8 +98,8 @@ extension CatalogDetail { struct CatalogDetail_Previews: PreviewProvider { static var previews: some View { let catalog = Catalog(title: "Test", url: "https://www.test.com") - CatalogDetail(catalog: catalog, catalogDetail: { _ in fatalError() }, - publicationDetail: { _ in fatalError() } + CatalogFeed(catalog: catalog, catalogFeed: { _ in fatalError() }, + publicationDetail: { _ in fatalError() } ) } } diff --git a/TestApp/Sources/Catalogs/Views/CatalogGroup.swift b/TestApp/Sources/Catalogs/Views/CatalogGroup.swift index 61a1aaa47..553156c06 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogGroup.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogGroup.swift @@ -11,7 +11,7 @@ struct CatalogGroup: View { var group: R2Shared.Group let publicationDetail: (Publication) -> PublicationDetail - let catalogDetail: (Catalog) -> CatalogDetail + let catalogFeed: (Catalog) -> CatalogFeed var body: some View { VStack(alignment: .leading) { @@ -20,7 +20,7 @@ struct CatalogGroup: View { Text(group.metadata.title).font(.title3) if !group.links.isEmpty { let navigationLink = Catalog(title: group.links.first!.title ?? "Catalog", url: group.links.first!.href) - NavigationLink(destination: catalogDetail(navigationLink)) { + NavigationLink(destination: catalogFeed(navigationLink)) { ListRowItem(title: "See All").frame(maxWidth: .infinity, alignment: .trailing) } } @@ -48,7 +48,7 @@ struct CatalogGroup: View { } ForEach(group.navigation, id: \.self) { navigation in let navigationLink = Catalog(title: navigation.title ?? "Catalog", url: navigation.href) - NavigationLink(destination: catalogDetail(navigationLink)) { + NavigationLink(destination: catalogFeed(navigationLink)) { ListRowItem(title: navigation.title!) } } diff --git a/TestApp/Sources/Catalogs/Views/Catalogs.swift b/TestApp/Sources/Catalogs/Views/CatalogList.swift similarity index 93% rename from TestApp/Sources/Catalogs/Views/Catalogs.swift rename to TestApp/Sources/Catalogs/Views/CatalogList.swift index 5bd13043e..9f22e1762 100644 --- a/TestApp/Sources/Catalogs/Views/Catalogs.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogList.swift @@ -7,10 +7,10 @@ import SwiftUI import ReadiumOPDS -struct Catalogs: View { +struct CatalogList: View { let catalogRepository: CatalogRepository - let catalogDetail: (Catalog) -> CatalogDetail + let catalogFeed: (Catalog) -> CatalogFeed @State private var showingSheet = false @State private var showingAlert = false @@ -21,7 +21,7 @@ struct Catalogs: View { VStack { List() { ForEach(catalogs, id: \.id) { catalog in - NavigationLink(destination: catalogDetail(catalog)) { + NavigationLink(destination: catalogFeed(catalog)) { ListRowItem(title: catalog.title) } } @@ -71,7 +71,7 @@ struct Catalogs: View { } } -extension Catalogs { +extension CatalogList { func addCatalog(catalog: Catalog) async throws { var savedCatalog = catalog diff --git a/TestApp/Sources/Container.swift b/TestApp/Sources/Container.swift index 945ff018b..53d75f30f 100644 --- a/TestApp/Sources/Container.swift +++ b/TestApp/Sources/Container.swift @@ -30,16 +30,16 @@ class Container { private lazy var catalogRepository = CatalogRepository(db: db) - func catalogs() -> Catalogs { - Catalogs( + func catalogs() -> CatalogList { + CatalogList( catalogRepository: catalogRepository, - catalogDetail: catalogDetail(with:) + catalogFeed: catalogFeed(with:) ) } - func catalogDetail(with catalog: Catalog) -> CatalogDetail { - CatalogDetail(catalog: catalog, - catalogDetail: catalogDetail(with:), + func catalogFeed(with catalog: Catalog) -> CatalogFeed { + CatalogFeed(catalog: catalog, + catalogFeed: catalogFeed(with:), publicationDetail: publicationDetail(with:) ) } From 82a0bcb1cc86a17833335ea3d4e2f0827f2a7bdf Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Thu, 14 Jul 2022 19:46:12 -0500 Subject: [PATCH 15/17] Make suggested changes --- Sources/Shared/OPDS/Group.swift | 2 +- Sources/Shared/Publication/Publication.swift | 2 +- .../Sources/Bookshelf/Views/Bookshelf.swift | 2 +- .../Sources/Catalogs/Views/CatalogFeed.swift | 13 ++-- .../Common/Toolkit/Extensions/R2Shared.swift | 10 ++++ TestApp/Sources/Views/BookCover.swift | 59 ++++++++++++++----- 6 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 TestApp/Sources/Common/Toolkit/Extensions/R2Shared.swift diff --git a/Sources/Shared/OPDS/Group.swift b/Sources/Shared/OPDS/Group.swift index bc9c63de8..e84e5ea2b 100644 --- a/Sources/Shared/OPDS/Group.swift +++ b/Sources/Shared/OPDS/Group.swift @@ -10,7 +10,7 @@ // /// A substructure of a feed. -public class Group: Identifiable { +public class Group { public var metadata: OpdsMetadata public var links = [Link]() public var publications = [Publication]() diff --git a/Sources/Shared/Publication/Publication.swift b/Sources/Shared/Publication/Publication.swift index af39f819d..74f7e60db 100644 --- a/Sources/Shared/Publication/Publication.swift +++ b/Sources/Shared/Publication/Publication.swift @@ -13,7 +13,7 @@ import CoreServices import Foundation /// Shared model for a Readium Publication. -public class Publication: Loggable, Identifiable { +public class Publication: Loggable { /// Format of the publication, if specified. @available(*, deprecated, message: "Use publication.conforms(to:) to check the profile of a Publication") diff --git a/TestApp/Sources/Bookshelf/Views/Bookshelf.swift b/TestApp/Sources/Bookshelf/Views/Bookshelf.swift index 000b152e8..09fe26118 100644 --- a/TestApp/Sources/Bookshelf/Views/Bookshelf.swift +++ b/TestApp/Sources/Bookshelf/Views/Bookshelf.swift @@ -17,7 +17,7 @@ struct Bookshelf: View { NavigationView { VStack { // TODO figure out what the best column layout is for phones and tablets - let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2) + let columns: [GridItem] = [GridItem(.adaptive(minimum: 150 + 8))] ScrollView { LazyVGrid(columns: columns, spacing: 20) { ForEach(books, id: \.self) { book in diff --git a/TestApp/Sources/Catalogs/Views/CatalogFeed.swift b/TestApp/Sources/Catalogs/Views/CatalogFeed.swift index 208c7515a..eaf765bb5 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogFeed.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogFeed.swift @@ -10,7 +10,7 @@ import ReadiumOPDS struct CatalogFeed: View { - @State var catalog: Catalog + var catalog: Catalog @State private var parseData: ParseData? let catalogFeed: (Catalog) -> CatalogFeed @@ -33,7 +33,6 @@ struct CatalogFeed: View { // TODO This probably needs its own file if !feed.publications.isEmpty { - // TODO can this be reused in bookshelf and here? let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) LazyVGrid(columns: columns) { ForEach(feed.publications) { publication in @@ -45,7 +44,7 @@ struct CatalogFeed: View { title: publication.metadata.title, authors: authors, url: publication.images.first - .map { URL(string: $0.href)! } + .flatMap { URL(string: $0.href) } ) } .buttonStyle(.plain) @@ -66,11 +65,9 @@ struct CatalogFeed: View { .padding() .navigationTitle(catalog.title) .navigationBarTitleDisplayMode(.inline) - .onAppear { - Task { - if parseData == nil { - await parseFeed() - } + .task { + if parseData == nil { + await parseFeed() } } } diff --git a/TestApp/Sources/Common/Toolkit/Extensions/R2Shared.swift b/TestApp/Sources/Common/Toolkit/Extensions/R2Shared.swift new file mode 100644 index 000000000..23d2bd6c6 --- /dev/null +++ b/TestApp/Sources/Common/Toolkit/Extensions/R2Shared.swift @@ -0,0 +1,10 @@ +// +// Copyright 2022 Readium Foundation. All rights reserved. +// Use of this source code is governed by the BSD-style license +// available in the top-level LICENSE file of the project. +// + +import R2Shared + +extension R2Shared.Publication: Identifiable {} +extension R2Shared.Group : Identifiable {} diff --git a/TestApp/Sources/Views/BookCover.swift b/TestApp/Sources/Views/BookCover.swift index 2b12346c7..ecc8e446c 100644 --- a/TestApp/Sources/Views/BookCover.swift +++ b/TestApp/Sources/Views/BookCover.swift @@ -13,26 +13,53 @@ struct BookCover: View { var action: () -> Void = {} var body: some View { - VStack(alignment: .leading, spacing: 10) { - if (url != nil) { - AsyncImage( - url: url, - content: { $0 - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 150, height: 220) - }, - placeholder: { ProgressView() } - ) - } else { - Image(systemName: "book.closed") + VStack { + let width: CGFloat = 150 + cover + .frame(width: width, height: 220, alignment: .bottom) + labels + .frame(width: width, height: 60, alignment: .topLeading) + } + } + + @ViewBuilder + private var cover: some View { + if (url != nil) { + AsyncImage( + url: url, + content: { $0 .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 150, height: 220) - } + .shadow(radius: 2) + }, + placeholder: { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + ) + } else { + Image(systemName: "book.closed") + .resizable() + .aspectRatio(contentMode: .fit) + } + } + + @ViewBuilder + private var labels: some View { + VStack(alignment: .leading, spacing: 4) { Text(title) + .font(.headline) + .lineLimit(2) + // If both the title and authors are too large, makes sure that + // the title will take the priority to expand. + .layoutPriority(1) + Text(authors ?? "") - }.frame(maxWidth: 150) + .font(.subheadline) + .lineLimit(2) + } + // Scales down the fonts. +// .dynamicTypeSize(.small) } } From 7c114234039fffa07af9b03ae4a383a046f7a6bb Mon Sep 17 00:00:00 2001 From: Steven Zeck <8315038+stevenzeck@users.noreply.github.com> Date: Thu, 14 Jul 2022 19:53:56 -0500 Subject: [PATCH 16/17] Use adaptive grid in catalog feed --- TestApp/Sources/Catalogs/Views/CatalogFeed.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestApp/Sources/Catalogs/Views/CatalogFeed.swift b/TestApp/Sources/Catalogs/Views/CatalogFeed.swift index eaf765bb5..f13c83339 100644 --- a/TestApp/Sources/Catalogs/Views/CatalogFeed.swift +++ b/TestApp/Sources/Catalogs/Views/CatalogFeed.swift @@ -33,7 +33,7 @@ struct CatalogFeed: View { // TODO This probably needs its own file if !feed.publications.isEmpty { - let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top), count: 2) + let columns: [GridItem] = [GridItem(.adaptive(minimum: 150 + 8))] LazyVGrid(columns: columns) { ForEach(feed.publications) { publication in let authors = publication.metadata.authors From 2cafe449828bf957af956aeb3e13720cd1695de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Fri, 15 Jul 2022 11:18:52 +0200 Subject: [PATCH 17/17] Improve `BookCover` --- TestApp/Sources/Views/BookCover.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/TestApp/Sources/Views/BookCover.swift b/TestApp/Sources/Views/BookCover.swift index ecc8e446c..d4127b65f 100644 --- a/TestApp/Sources/Views/BookCover.swift +++ b/TestApp/Sources/Views/BookCover.swift @@ -18,7 +18,7 @@ struct BookCover: View { cover .frame(width: width, height: 220, alignment: .bottom) labels - .frame(width: width, height: 60, alignment: .topLeading) + .frame(width: width, alignment: .topLeading) } } @@ -49,17 +49,15 @@ struct BookCover: View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.headline) - .lineLimit(2) - // If both the title and authors are too large, makes sure that - // the title will take the priority to expand. - .layoutPriority(1) + .lineLimit(1) - Text(authors ?? "") + // Hack to reserve space for two lines of text. + // See https://sarunw.com/posts/how-to-force-two-lines-of-text-in-swiftui/ + Text((authors ?? "") + "\n") .font(.subheadline) + .foregroundStyle(.secondary) .lineLimit(2) } - // Scales down the fonts. -// .dynamicTypeSize(.small) } }