Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
levitatingpineapple committed Jan 13, 2024
0 parents commit dd8f2cc
Show file tree
Hide file tree
Showing 108 changed files with 5,171 additions and 0 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/docc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Compile and Deploy Documentation

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: macos-13
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Initialise Submodule
run: git submodule init && git submodule update
- name: Select Xcode 15.1
run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer
- name: Create Documentation Archive
run: >
xcrun xcodebuild docbuild \
-scheme FeedRadar \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath .derivedData
- name: Transform Archive for Static Hosting
run: >
xcrun docc process-archive transform-for-static-hosting \
.derivedData/Build/Products/Debug-iphonesimulator/FeedRadar.doccarchive \
--output-path .docs \
--hosting-base-path feed-radar
- name: Setup Pages
id: pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: .docs
deploy:
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
24 changes: 24 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Run Unit Tests

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build_and_test:
name: Build and Test
runs-on: macos-13

steps:
- name: Checkout
uses: actions/checkout@v3
- name: Initialise Submodule
run: git submodule init && git submodule update
- name: Select Xcode 15.1
run: sudo xcode-select -s /Applications/Xcode_15.1.app/Contents/Developer
- name: Build
run: xcodebuild build-for-testing -scheme "FeedRadar" -destination "platform=iOS Simulator,name=iPhone 14,OS=17.2" | xcbeautify
- name: Test
run: xcodebuild test-without-building -scheme "FeedRadar" -destination "platform=iOS Simulator,name=iPhone 14,OS=17.2" | xcbeautify
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
*.pbxuser
xcuserdata/

.derivedData
.docs
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "Submodules/Readability"]
path = Submodules/Readability
url = https://github.com/mozilla/readability.git
Binary file added .readme/app.webp
Binary file not shown.
Binary file added .readme/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .readme/extract.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .readme/fetch.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .readme/media.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .readme/testFlight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions App/Data/ConditionalHeaders.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation

struct ConditionalHeaders: Codable {
private let source: URL
private let lastModified: String?
private let etag: String?

/// Fetches ``ConditionalHeaders`` from `UserDefaults`
/// - Parameter source: source, for which the headers where previously stored
init?(source: URL) {
let stored = UserDefaults.standard
.data(forKey: .conditionalHeadersKey(source: source))
.flatMap { ConditionalHeaders(rawValue: $0) }
if let stored { self = stored }
else { return nil }
}

/// Extracts ``ConditionalHeaders`` from a `URLResponse`
/// - Parameter response: Response must be `HTTPURLResponse`
init?(response: URLResponse, source: URL) {
if let httpReponse = response as? HTTPURLResponse {
self.source = source
lastModified = httpReponse.value(forHTTPHeaderField: "last-modified")
etag = httpReponse.value(forHTTPHeaderField: "etag")
if etag == nil && lastModified == nil { return nil }
} else {
return nil
}
}

/// Request, decorated with headers for conditionaly fetching feeds
///
/// Only one of two headers is used with ``lastModified``
/// being the preferred one,
/// as various servers require different formatting for the ``etag``
/// like removing the `W/` (weak etag) prefix or surrounding quotes.
var request: URLRequest {
var request = URLRequest(
url: source,
cachePolicy: .reloadIgnoringLocalCacheData
)
if let lastModified {
request.addValue(lastModified, forHTTPHeaderField: "if-modified-since")
} else if let etag {
request.addValue(etag, forHTTPHeaderField: "if-none-match")
}
return request
}

/// Stores ``ConditionalHeaders`` in `UserDefaults`
func store() {
UserDefaults.standard.setValue(
rawValue,
forKey: .conditionalHeadersKey(source: source)
)
}
}

extension ConditionalHeaders: RawRepresentable {
init?(rawValue: Data) {
self = try! JSONDecoder().decode(Self.self, from: rawValue)
}

var rawValue: Data {
try! JSONEncoder().encode(self)
}
}

146 changes: 146 additions & 0 deletions App/Data/Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import Foundation
import AVKit
import SwiftUI
import os.log

extension Logger {
static let sync = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: "Sync"
)
static let store = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: "Store"
)
}

extension String {
static let cloudKitStateSerializationKey = "cloudKitStateSerialization"
static let style: String = try! String(
contentsOf: Bundle.main.url(
forResource: "Style",
withExtension: "css"
)!
)
static let filterKey = "filter"
static let contentScaleKey = "contentScale"
static let isReadFilteredKey = "isReadFiltered"
static func iconKey(source: URL) -> String { "icon:" + source.absoluteString }
static func displayKey(source: URL) -> String { "display:" + source.absoluteString }
static func conditionalHeadersKey(source: URL) -> String { "conditionalHeaders:" + source.absoluteString }

var url: URL? { URL(string: self) }

/// - Returns: String without the prefix.
func strippingPrefix(_ prefix: String) -> String {
hasPrefix(prefix) ? String(dropFirst(prefix.count)) : self
}

/// A simple hashing algorithm. Not randomly seeded.
var stableHash: Int64 {
Int64(
bitPattern: self
.data(using: .utf8)!
.reduce(into: UInt64(5381)) { result, byte in
result = 0x7F * (result & 0x00FFFFFFFFFFFFFF) + UInt64(byte)
}
)
}
}

extension URL {
static var documents: URL {
FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask
).first!
}

var favicon: URL? {
host(percentEncoded: true).flatMap {
var components = URLComponents()
components.scheme = "https"
components.host = "www.google.com"
components.path = "/s2/favicons"
components.queryItems = [
URLQueryItem(name: "domain", value: $0),
URLQueryItem(name: "sz", value: "128")
]
return components.url
}
}

var base: URL? {
if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) {
components.path = String()
return components.url
} else {
return nil
}
}
}

extension UIImage {
func cropScaled(max: Double) -> UIImage {
let scaled = min(max, size.width, size.height)
return UIGraphicsImageRenderer(size: CGSize(width: scaled, height: scaled)).image { _ in
if size.width > size.height {
let clippedWidth = scaled * size.width / size.height
draw(
in: CGRect(
x: (scaled - clippedWidth) / 2,
y: .zero,
width: clippedWidth,
height: scaled
)
)
} else {
let clippedHeight = scaled * size.height / size.width
draw(
in: CGRect(
x: .zero,
y: (scaled - clippedHeight) / 2,
width: scaled,
height: clippedHeight
)
)
}
}
}
}

extension Data {
var scaledPng: Data? {
UIImage(data: self)?
.cropScaled(max: 128)
.pngData()
}
}

extension View {
func boxed(padded: Bool = true) -> some View {
self
.scaledToFit()
.padding(padded ? 6 : 0)
.frame(width: 32, height: 32)
.background(Color(.systemGray2).opacity(0.25))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
}

extension CGSize {
var aspectRatio: Double? {
width.isNormal && height.isNormal
? width / height
: nil
}
}

extension CMTime {
init(timeInterval: TimeInterval) {
self = CMTime(
seconds: timeInterval,
preferredTimescale: CMTimeScale(NSEC_PER_SEC)
)
}
}
55 changes: 55 additions & 0 deletions App/Data/Filter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import GRDB

/// Filters ``Item`` list. Used by ``Item.RequestIDs`` and ``Item.RequestCount``
struct Filter: Hashable, Codable {
var feed: Feed? = nil
var isRead: Bool? = nil
var isStarred: Bool? = nil

var title: String {
if let feed {
feed.title ?? feed.source.absoluteString
} else if isRead != nil || isStarred != nil {
[
isRead.flatMap { $0 ? "Read" : "Unread" },
isStarred.flatMap { $0 ? "Starred" : "Unstarred" }
]
.compactMap { $0 }
.joined(separator: " & ")
} else {
"Inbox"
}
}

private var filters: Array<SQLExpression> {
[
feed.flatMap { Item.Column.source.column == $0.source },
isRead.flatMap { Item.Column.isRead.column == $0 },
isStarred.flatMap { Item.Column.isStarred.column == $0 }
].compactMap { $0 }
}

var items: QueryInterfaceRequest<Item> {
filters.reduce(into: Item.all()) {
$0 = $0.filter($1)
}
}

/// A filter with additional unread filter applied
var unread: Filter {
var filter = self
filter.isRead = false
return filter
}
}

extension Filter: RawRepresentable {
init?(rawValue: Data) {
self = try! JSONDecoder().decode(Filter.self, from: rawValue)
}

var rawValue: Data {
try! JSONEncoder().encode(self)
}
}
Loading

0 comments on commit dd8f2cc

Please sign in to comment.