diff --git a/Package.swift b/Package.swift index ab623da..b6b2ce5 100644 --- a/Package.swift +++ b/Package.swift @@ -19,16 +19,17 @@ let package = Package( .executable(name: "orchardnestd", targets: ["orchardnestd"]) ], dependencies: [ - // Dependencies declare other packages that this package depends on. .package(url: "https://github.com/brightdigit/FeedKit.git", .branch("master")), - .package(url: "https://github.com/shibapm/Komondor", from: "1.0.5"), - .package(url: "https://github.com/eneko/SourceDocs", from: "1.2.1"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"), .package(name: "QueuesFluentDriver", url: "https://github.com/m-barthelemy/vapor-queues-fluent-driver.git", from: "0.3.8"), .package(name: "Plot", url: "https://github.com/johnsundell/plot.git", from: "0.8.0"), - .package(url: "https://github.com/JohnSundell/Ink.git", from: "0.1.0") + .package(url: "https://github.com/JohnSundell/Ink.git", from: "0.1.0"), + // dev + .package(url: "https://github.com/shibapm/Komondor", from: "1.0.5"), + .package(url: "https://github.com/eneko/SourceDocs", from: "1.2.1"), + .package(url: "https://github.com/shibapm/Rocket", from: "0.1.0") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Public/styles/style.css b/Public/styles/style.css index c2da572..de7b246 100644 --- a/Public/styles/style.css +++ b/Public/styles/style.css @@ -72,6 +72,10 @@ main nav.posts-filter ul li a.button{ margin-right: 1em; } +ul.articles li > .video-content iframe { + display: none; +} + ul.articles li > .social-share > ul{ list-style: none; display: inline-block; @@ -121,13 +125,3 @@ ul.articles li > ul.podcast-players > li img+div { ul.articles li > ul.podcast-players > li img+div > div:first-child { font-size: 0.7em; } -/* -ul.articles li > ul.podcast-players > li { - height: 2em; - display: inline-block; -} - - - - -*/ diff --git a/Resources/Views/about.md b/Resources/Views/about.md index 110783b..538dc2d 100644 --- a/Resources/Views/about.md +++ b/Resources/Views/about.md @@ -1,3 +1,6 @@ +--- +description: About Orchardnest +--- # About Coming Soon... diff --git a/Resources/Views/support.md b/Resources/Views/support.md index 4f92fbe..66ed44b 100644 --- a/Resources/Views/support.md +++ b/Resources/Views/support.md @@ -1,3 +1,6 @@ +--- +description: Support and FAQ +--- # Support Coming Soon... diff --git a/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift b/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift index 3788219..64e28a7 100644 --- a/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift +++ b/Sources/OrchardNestServer/Controllers/DB/Migration/EntryMigration.swift @@ -5,7 +5,7 @@ struct EntryMigration: Migration { func prepare(on database: Database) -> EventLoopFuture { database.schema(Entry.schema) .id() - .field("channel_id", .uuid, .required) + .field("channel_id", .uuid, .required, .references(Channel.schema, .id, onDelete: .cascade, onUpdate: .cascade)) .field("feed_id", .string, .required) .field("title", .string, .required) .field("summary", .string, .required) diff --git a/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift b/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift index def5389..7319217 100644 --- a/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift +++ b/Sources/OrchardNestServer/Controllers/Routing/HTMLController.swift @@ -140,11 +140,13 @@ extension Node where Context == HTML.BodyContext { } extension Node where Context == HTML.DocumentContext { - static func head(withSubtitle subtitle: String) -> Self { + static func head(withSubtitle subtitle: String, andDescription description: String) -> Self { return .head( .title("OrchardNest - \(subtitle)"), .meta(.charset(.utf8)), + .meta(.name("viewport"), .content("width=device-width, initial-scale=1")), + .meta(.name("description"), .content(description)), .raw(""" @@ -156,6 +158,8 @@ extension Node where Context == HTML.DocumentContext { gtag('config', 'G-GXSE03BMPF'); """), + .link(.rel(.preload), .href("https://fonts.googleapis.com/css2?family=Catamaran:wght@100;400;800&display=swap"), .attribute(named: "as", value: "style")), + .link(.rel(.preload), .href("/styles/elusive-icons/css/elusive-icons.min.css"), .attribute(named: "as", value: "style")), .link(.rel(.appleTouchIcon), .sizes("180x180"), .href("/apple-touch-icon.png")), .link(.rel(.appleTouchIcon), .type("image/png"), .sizes("32x32"), .href("/favicon-32x32.png")), .link(.rel(.appleTouchIcon), .type("image/png"), .sizes("16x16"), .href("/favicon-16x16.png")), @@ -164,9 +168,7 @@ extension Node where Context == HTML.DocumentContext { .meta(.name("msapplication-TileColor"), .content("#2b5797")), .meta(.name("theme-color"), .content("#ffffff")), .link(.rel(.stylesheet), .href("/styles/elusive-icons/css/elusive-icons.min.css")), - .link(.rel(.stylesheet), .href("/styles/normalize.css")), - .link(.rel(.stylesheet), .href("/styles/milligram.css")), .link(.rel(.stylesheet), .href("/styles/style.css")), .link(.rel(.stylesheet), .href("https://fonts.googleapis.com/css2?family=Catamaran:wght@100;400;800&display=swap")) @@ -175,7 +177,7 @@ extension Node where Context == HTML.DocumentContext { } extension Node where Context == HTML.ListContext { - static func li(forEntryItem item: EntryItem) -> Self { + static func li(forEntryItem item: EntryItem, formatDateWith formatter: DateFormatter) -> Self { return .li( .class("blog-post"), @@ -191,14 +193,22 @@ extension Node where Context == HTML.ListContext { ), .div( .class("publishedAt"), - .text(item.publishedAt.description) + .text(formatter.string(from: item.publishedAt)) ), .unwrap(item.youtubeID) { - .iframe( - .src("https://www.youtube.com/embed/" + $0), - .allow("accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"), - .allowfullscreen(true) + .div( + .class("video-content"), + .a( + .href(item.url), + .img(.src("https://img.youtube.com/vi/\($0)/mqdefault.jpg")) + ), + .iframe( + .attribute(named: "data-src", value: "https://www.youtube.com/embed/" + $0), + .allow("accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"), + .allowfullscreen(true) + ) ) + }, .div( .class("summary"), @@ -207,7 +217,7 @@ extension Node where Context == HTML.ListContext { .unwrap(item.podcastEpisodeURL) { .audio( .controls(true), - .attribute(named: "preload", value: "metadata"), + .attribute(named: "preload", value: "none"), .source( .src($0) ) @@ -295,6 +305,12 @@ extension EntryCategory { struct HTMLController { let views: [String: Markdown] + static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeStyle = .short + formatter.dateStyle = .medium + return formatter + }() init(views: [String: Markdown]?) { self.views = views ?? [String: Markdown]() @@ -330,7 +346,7 @@ struct HTMLController { } .map { (items) -> HTML in HTML( - .head(withSubtitle: "Swift Articles and News"), + .head(withSubtitle: "Swift Articles and News", andDescription: "Swift Articles and News of Category \(category)"), .body( .header(), .main( @@ -341,7 +357,7 @@ struct HTMLController { .ul( .class("articles column"), .forEach(items) { - .li(forEntryItem: $0) + .li(forEntryItem: $0, formatDateWith: Self.dateFormatter) } ) ) @@ -361,7 +377,7 @@ struct HTMLController { } let html = HTML( - .head(withSubtitle: "Support and FAQ"), + .head(withSubtitle: "Support and FAQ", andDescription: view.metadata["description"] ?? name), .body( .header(), .main( @@ -378,6 +394,50 @@ struct HTMLController { return req.eventLoop.future(html) } + func channel(req: Request) throws -> EventLoopFuture { + guard let channel = req.parameters.get("channel").flatMap(UUID.init(uuidString:)) else { + throw Abort(.notFound) + } + + return Entry.query(on: req.db) + .with(\.$channel) { builder in + builder.with(\.$podcasts).with(\.$youtubeChannels) + } + .join(parent: \.$channel) + .with(\.$podcastEpisodes) + .join(children: \.$podcastEpisodes, method: .left) + .with(\.$youtubeVideos) + .join(children: \.$youtubeVideos, method: .left) + .filter(Channel.self, \Channel.$id == channel) + .sort(\.$publishedAt, .descending) + .limit(32) + .all() + .flatMapEachThrowing { + try EntryItem(entry: $0) + } + .map { (items) -> HTML in + HTML( + .head(withSubtitle: "Swift Articles and News", andDescription: "Swift Articles and News"), + .body( + .header(), + .main( + .class("container"), + .filters(), + .section( + .class("row"), + .ul( + .class("articles column"), + .forEach(items) { + .li(forEntryItem: $0, formatDateWith: Self.dateFormatter) + } + ) + ) + ) + ) + ) + } + } + func index(req: Request) -> EventLoopFuture { return Entry.query(on: req.db).join(LatestEntry.self, on: \Entry.$id == \LatestEntry.$id) .with(\.$channel) { builder in @@ -398,7 +458,7 @@ struct HTMLController { } .map { (items) -> HTML in HTML( - .head(withSubtitle: "Swift Articles and News"), + .head(withSubtitle: "Swift Articles and News", andDescription: "Swift Articles and News"), .body( .header(), .main( @@ -409,7 +469,7 @@ struct HTMLController { .ul( .class("articles column"), .forEach(items) { - .li(forEntryItem: $0) + .li(forEntryItem: $0, formatDateWith: Self.dateFormatter) } ) ) @@ -425,5 +485,6 @@ extension HTMLController: RouteCollection { routes.get("", use: index) routes.get("category", ":category", use: category) routes.get(":page", use: page) + routes.get("channels", ":channel", use: channel) } } diff --git a/Sources/OrchardNestServer/RefreshJob.swift b/Sources/OrchardNestServer/RefreshJob.swift index fe2ece3..3d5ffa4 100644 --- a/Sources/OrchardNestServer/RefreshJob.swift +++ b/Sources/OrchardNestServer/RefreshJob.swift @@ -59,13 +59,20 @@ struct RefreshJob: ScheduledJob, Job { context.logger.info("downloading blog list...") - return context.application.client.get(URI(string: Self.url.absoluteString)).flatMapThrowing { (response) -> [LanguageContent] in + let blogsDownload = context.application.client.get(URI(string: Self.url.absoluteString)).flatMapThrowing { (response) -> [LanguageContent] in try response.content.decode([LanguageContent].self, using: decoder) - }.map(SiteCatalogMap.init).flatMap { (siteCatalogMap) -> EventLoopFuture in + }.map(SiteCatalogMap.init) + let ignoringFeedURLs = ChannelStatus.query(on: database).filter(\.$status == ChannelStatusType.ignore).field(\.$id).all().map { $0.compactMap { $0.id.flatMap(URL.init(string:)) }} + + return blogsDownload.and(ignoringFeedURLs).flatMap { (siteCatalogMap, ignoringFeedURLs) -> EventLoopFuture in let languages = siteCatalogMap.languages let categories = siteCatalogMap.categories - let organizedSites = siteCatalogMap.organizedSites + let organizedSites = siteCatalogMap.organizedSites.filter { + !ignoringFeedURLs.contains($0.site.feed_url) + } + + let channelCleanup = Channel.query(on: database).filter(\.$feedUrl ~~ ignoringFeedURLs.map { $0.absoluteString }).delete() let futureLanguages = languages.map { Language.from($0, on: database) }.flatten(on: database.eventLoop) let futureCategories = categories.map { Category.from($0.key, on: database) }.flatten(on: database.eventLoop) @@ -253,7 +260,7 @@ struct RefreshJob: ScheduledJob, Job { PodcastEpisode.upsert(newEpisode, on: database) } - return futYTVideos.and(futYTChannels).and(futPodEpisodes).and(podcastChannels).transform(to: ()) + return futYTVideos.and(futYTChannels).and(futPodEpisodes).and(podcastChannels).and(channelCleanup).transform(to: ()) } } }