mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-06-16 04:00:39 +00:00
208 lines
5.5 KiB
Swift
208 lines
5.5 KiB
Swift
//
|
|
// RSSAddNewFeed.swift
|
|
//
|
|
//
|
|
// Created by Duong Thai on 07/03/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CoreData
|
|
|
|
public struct RSSAddNewFeed: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var state: MachineState = .emptyInput
|
|
@State private var urlString = ""
|
|
@State private var feed: RSSFeed? = nil
|
|
@State private var downloadingTask: Task<(), Never>? = nil
|
|
|
|
public enum Context { case manager, sheet }
|
|
let context: Context
|
|
|
|
public var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("rss.addNewFeed.url.textField.label") {
|
|
VStack(alignment: .leading) {
|
|
TextField("rss.addNewFeed.url.textField.placeholder", text: $urlString)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
.keyboardType(.URL)
|
|
|
|
IndicatorView(state: $state)
|
|
}
|
|
}
|
|
|
|
FeedPreview(state: $state)
|
|
}
|
|
.navigationTitle("rss.addNewFeed.title")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
if self.context == .sheet {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Button {
|
|
dismiss()
|
|
feed?.managedObjectContext?.rollback()
|
|
} label: {
|
|
Image(systemName: "xmark")
|
|
}
|
|
}
|
|
}
|
|
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("rss.addNewFeed.action.add") {
|
|
dismiss()
|
|
try? feed?.managedObjectContext?.save()
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: urlString) {
|
|
state = state.receive(urlString: urlString)
|
|
}
|
|
.onChange(of: state) {
|
|
switch state {
|
|
case .downloading(let url):
|
|
downloadingTask?.cancel()
|
|
feed?.managedObjectContext?.rollback()
|
|
|
|
downloadingTask = Task {
|
|
let rssFeed = await RSSTools.load(feedURL: url)
|
|
if Task.isCancelled { return }
|
|
if let rssFeed { self.state = .downloaded(feed: rssFeed, url: url) }
|
|
else { self.state = .noData(url: url) }
|
|
}
|
|
case .downloaded(let feed, _):
|
|
self.feed = feed
|
|
default:
|
|
downloadingTask?.cancel()
|
|
feed?.managedObjectContext?.rollback()
|
|
}
|
|
}
|
|
.onDisappear { feed?.managedObjectContext?.rollback() }
|
|
}
|
|
}
|
|
|
|
public init(context: Context = .sheet) {
|
|
self.context = context
|
|
}
|
|
|
|
private struct IndicatorView: View {
|
|
@Binding var state: MachineState
|
|
@State private var opacity: CGFloat = 1
|
|
@State private var waitingDuration: TimeInterval = 0
|
|
@State private var animatingTask: Task<(), Never>? = nil
|
|
|
|
var body: some View {
|
|
switch state {
|
|
case .emptyInput, .downloaded:
|
|
EmptyView()
|
|
case .invalidURL, .noData:
|
|
Text({
|
|
if case .noData = state {
|
|
"rss.addNewFeed.input.noData"
|
|
} else {
|
|
"rss.addNewFeed.input.invalidURL"
|
|
}
|
|
}())
|
|
.font(.caption2)
|
|
.foregroundStyle(.red)
|
|
case .waiting, .downloading:
|
|
Text({
|
|
if case .waiting = state {
|
|
"rss.addNewFeed.input.waiting"
|
|
} else {
|
|
"rss.addNewFeed.input.downloading"
|
|
}
|
|
}())
|
|
.font(.caption2)
|
|
.foregroundStyle(.green)
|
|
.opacity(opacity)
|
|
.task {
|
|
waitingDuration = 0
|
|
animatingTask?.cancel()
|
|
|
|
animatingTask = Task {
|
|
while true {
|
|
try? await Task.sleep(for: .seconds(0.5))
|
|
if Task.isCancelled { return }
|
|
|
|
withAnimation(.easeInOut(duration: 0.5)) {
|
|
opacity = opacity == 1 ? 0 : 1
|
|
}
|
|
|
|
waitingDuration += 0.5
|
|
}
|
|
}
|
|
}
|
|
.onDisappear { animatingTask?.cancel() }
|
|
.onChange(of: waitingDuration) {
|
|
state = state.receive(waitingDuration: waitingDuration)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct FeedPreview: View {
|
|
@Binding var state: MachineState
|
|
@State private var items: [RSSItem] = []
|
|
|
|
var body: some View {
|
|
if case let .downloaded(feed, _) = state {
|
|
Section("rss.addNewFeed.feedPreview.label") {
|
|
List(items) { item in
|
|
RSSItemView(item)
|
|
}
|
|
}
|
|
.task { items = feed.toRSSItems().sorted { $0.date > $1.date } }
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum MachineState: Equatable {
|
|
case emptyInput
|
|
case invalidURL(string: String)
|
|
case waiting(url: URL)
|
|
case downloading(url: URL)
|
|
case downloaded(feed: RSSFeed, url: URL)
|
|
case noData(url: URL)
|
|
|
|
func receive(urlString: String) -> MachineState {
|
|
if urlString.isEmpty {
|
|
.emptyInput
|
|
} else if let url = Self.validateURL(urlString) {
|
|
.waiting(url: url)
|
|
} else {
|
|
.invalidURL(string: urlString)
|
|
}
|
|
}
|
|
|
|
func receive(waitingDuration: TimeInterval) -> MachineState {
|
|
switch self {
|
|
case .waiting(let url) where waitingDuration >= 3:
|
|
.downloading(url: url)
|
|
default:
|
|
self
|
|
}
|
|
}
|
|
|
|
private static func validateURL(_ string: String) -> URL? {
|
|
// TODO: improve this
|
|
guard !string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }
|
|
let pattern = "^(http|https)://"
|
|
|
|
let newURLString: String
|
|
if let _ = string.range(of: pattern, options: .regularExpression) {
|
|
newURLString = string
|
|
} else {
|
|
newURLString = "https://\(string)"
|
|
}
|
|
|
|
return URL(string: newURLString)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
RSSAddNewFeed()
|
|
}
|