Show Move to Inbox in following items toolbar

This commit is contained in:
Jackson Harper
2023-12-19 14:30:36 +08:00
parent e20424ff1b
commit 49ddaee0be
11 changed files with 239 additions and 58 deletions

View File

@ -3,7 +3,9 @@ import SwiftUI
import Views
struct CustomToolBar: View {
let isFollowing: Bool
let isArchived: Bool
let moveToInboxAction: () -> Void
let archiveAction: () -> Void
let unarchiveAction: () -> Void
let shareAction: () -> Void
@ -30,7 +32,9 @@ struct CustomToolBar: View {
.frame(height: 0.5)
.frame(maxWidth: .infinity)
HStack(spacing: 0) {
if isArchived {
if isFollowing {
ToolBarButton(image: Image.tabLibrary, action: moveToInboxAction)
} else if isArchived {
ToolBarButton(image: Image.toolbarUnarchive, action: unarchiveAction)
} else {
ToolBarButton(image: Image.toolbarArchive, action: archiveAction)

View File

@ -57,7 +57,7 @@ struct LabelsView: View {
let trimmedLabelName = viewModel.labelSearchFilter.trimmingCharacters(in: .whitespacesAndNewlines)
Image(systemName: "tag").foregroundColor(.blue)
Text(
viewModel.labelSearchFilter.count > 0 ?
viewModel.labelSearchFilter.count > 0 && viewModel.labelSearchFilter != ZWSP ?
"Create: \"\(trimmedLabelName)\" label" :
LocalText.createLabelMessage
).foregroundColor(.blue)
@ -106,16 +106,6 @@ struct CreateLabelView: View {
var innerBody: some View {
VStack {
HStack {
if !newLabelName.isEmpty, newLabelColor != .clear {
TextChip(text: newLabelName, color: newLabelColor)
} else {
Text(LocalText.labelsViewAssignNameColor).font(.appBody)
}
Spacer()
}
.padding(.bottom, 8)
TextField(LocalText.labelNamePlaceholder, text: $newLabelName)
.textFieldStyle(StandardTextFieldStyle())
.onChange(of: newLabelName) { inputLabelName in

View File

@ -2,19 +2,24 @@ import Models
import PopupView
import Services
import SwiftUI
import Transmission
import Views
@MainActor final class NewsletterEmailsViewModel: ObservableObject {
@Published var isLoading = false
@Published var showAddressCopied = false
@Published var emails = [NewsletterEmail]()
func loadEmails(dataService: DataService) async {
isLoading = true
if let objectIDs = try? await dataService.newsletterEmails() {
do {
let objectIDs = try await dataService.newsletterEmails()
await dataService.viewContext.perform { [weak self] in
self?.emails = objectIDs.compactMap { dataService.viewContext.object(with: $0) as? NewsletterEmail }
}
} catch {
print("ERROR LOADING EMAILS: ", error)
}
isLoading = false
@ -39,16 +44,17 @@ struct NewsletterEmailsView: View {
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = NewsletterEmailsViewModel()
@State var showSnackbar = false
@State var showAddressCopied = false
@State var snackbarOperation: SnackbarOperation?
func snackbar(message: String) {
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
showSnackbar = true
}
var body: some View {
Group {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $showAddressCopied) {
MessageToast()
} label: {
EmptyView()
}
#if os(iOS)
Form {
innerBody
@ -61,20 +67,6 @@ struct NewsletterEmailsView: View {
#endif
}
.task { await viewModel.loadEmails(dataService: dataService) }
.popup(isPresented: $showSnackbar) {
if let operation = snackbarOperation {
Snackbar(isShowing: $showSnackbar, operation: operation)
} else {
EmptyView()
}
} customize: {
$0
.type(.toast)
.autohideIn(2)
.position(.bottom)
.animation(.spring())
.closeOnTapOutside(true)
}
}
private var innerBody: some View {
@ -97,28 +89,77 @@ struct NewsletterEmailsView: View {
if !viewModel.emails.isEmpty {
Section(header: Text(LocalText.newsletterEmailsExisting)) {
ForEach(viewModel.emails) { newsletterEmail in
Button(
action: {
#if os(iOS)
UIPasteboard.general.string = newsletterEmail.email
#endif
#if os(macOS)
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString])
#endif
snackbar(message: "Email copied")
},
label: { Text(newsletterEmail.unwrappedEmail) }
)
ForEach(viewModel.emails) { email in
NewsletterEmailRow(viewModel: viewModel, email: email, folderSelection: email.folder)
}
}
}
}
.navigationTitle(LocalText.emailsGeneric)
}
}
struct NewsletterEmailRow: View {
@StateObject var viewModel: NewsletterEmailsViewModel
@State var email: NewsletterEmail
@State var folderSelection: String?
var body: some View {
VStack {
Button(
action: {
#if os(iOS)
UIPasteboard.general.string = email.email
#endif
#if os(macOS)
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString])
#endif
viewModel.showAddressCopied = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(2000)) {
viewModel.showAddressCopied = false
}
},
label: { Text(email.unwrappedEmail).font(Font.appFootnote) }
)
Divider()
Picker("Destination Folder", selection: $folderSelection) {
Text("Inbox").tag("inbox")
Text("Following").tag("following")
}
.pickerStyle(MenuPickerStyle())
.onChange(of: folderSelection) { _ in
// Task {
// viewModel.showOperationToast = true
// await viewModel.updateSubscription(dataService: dataService, subscription: subscription, folder: newValue)
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
// viewModel.showOperationToast = false
// }
// }
}
}
}
}
struct MessageToast: View {
var body: some View {
VStack {
HStack {
Text("Address copied")
Spacer()
}
.padding(10)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(Color(hex: "2A2A2A"))
.cornerRadius(4.0)
.tint(Color.green)
}
.padding(.bottom, 70)
.padding(.horizontal, 10)
.ignoresSafeArea(.all, edges: .bottom)
}
}

View File

@ -73,6 +73,19 @@ typealias OperationStatusHandler = (_: OperationStatus) -> Void
operationStatus = .failure
}
}
func updateSubscription(dataService: DataService, subscription: Subscription, folder: String? = nil, fetchContent: Bool? = nil) async {
operationMessage = "Updating subscription..."
operationStatus = .isPerforming
do {
try await dataService.updateSubscription(subscription.subscriptionID, folder: folder, fetchContent: fetchContent)
operationMessage = "Subscription updated"
operationStatus = .success
} catch {
operationMessage = "Failed to update subscription"
operationStatus = .failure
}
}
}
struct OperationToast: View {
@ -186,6 +199,8 @@ struct SubscriptionsView: View {
subscription: presentingSubscription,
viewModel: viewModel,
dataService: dataService,
prefetchContent: presentingSubscription.fetchContent,
folderSelection: presentingSubscription.folder,
dismiss: { showSubscriptionsSheet = false },
unsubscribe: { subscription in
showSubscriptionsSheet = false
@ -365,13 +380,24 @@ struct SubscriptionSettingsView: View {
Text("Inbox").tag("inbox")
Text("Following").tag("following")
}
.pickerStyle(MenuPickerStyle()) // makes the picker appear as a menu
.onAppear {
folderSelection = subscription.folder
print("FOLDER: ", folderSelection)
.pickerStyle(MenuPickerStyle())
.onChange(of: folderSelection) { newValue in
Task {
viewModel.showOperationToast = true
await viewModel.updateSubscription(dataService: dataService, subscription: subscription, folder: newValue)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
viewModel.showOperationToast = false
}
}
}
.onChange(of: folderSelection) { _ in
print("CHANGED FOLDER: ", folderSelection)
.onChange(of: prefetchContent) { newValue in
Task {
viewModel.showOperationToast = true
await viewModel.updateSubscription(dataService: dataService, subscription: subscription, fetchContent: newValue)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
viewModel.showOperationToast = false
}
}
}
}
}.listStyle(.insetGrouped)

View File

@ -3,6 +3,7 @@ import Models
import PopupView
import Services
import SwiftUI
import Transmission
import Utils
import Views
import WebKit
@ -367,6 +368,11 @@ struct WebReaderContainerView: View {
var body: some View {
ZStack {
WindowLink(level: .alert, transition: .move(edge: .bottom), isPresented: $viewModel.showOperationToast) {
ReaderOperationToast(viewModel: viewModel)
} label: {
EmptyView()
}
if let articleContent = viewModel.articleContent {
WebReader(
item: item,
@ -568,7 +574,9 @@ struct WebReaderContainerView: View {
}
if navBarVisible {
CustomToolBar(
isFollowing: item.folder == "following",
isArchived: item.isArchived,
moveToInboxAction: moveToInbox,
archiveAction: archive,
unarchiveAction: archive,
shareAction: share,
@ -615,6 +623,25 @@ struct WebReaderContainerView: View {
}
}
func moveToInbox() {
Task {
viewModel.showOperationToast = true
viewModel.operationMessage = "Moving to library..."
viewModel.operationStatus = .isPerforming
do {
try await dataService.moveItem(itemID: item.unwrappedID, folder: "inbox")
viewModel.operationMessage = "Moved to library"
viewModel.operationStatus = .success
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
viewModel.showOperationToast = false
}
} catch {
viewModel.operationMessage = "Error moving"
viewModel.operationStatus = .failure
}
}
}
func archive() {
let isArchived = item.isArchived
dataService.archiveLink(objectID: item.objectID, archived: !isArchived)
@ -670,3 +697,37 @@ struct WebReaderContainerView: View {
openURL(url)
}
}
struct ReaderOperationToast: View {
@State var viewModel: WebReaderViewModel
var body: some View {
VStack {
HStack {
if viewModel.operationStatus == .isPerforming {
Text(viewModel.operationMessage ?? "Performing...")
Spacer()
ProgressView()
} else if viewModel.operationStatus == .success {
Text(viewModel.operationMessage ?? "Success")
Spacer()
} else if viewModel.operationStatus == .failure {
Text(viewModel.operationMessage ?? "Failure")
Spacer()
Button(action: { viewModel.showOperationToast = false }, label: {
Text("Done").bold()
})
}
}
.padding(10)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(Color(hex: "2A2A2A"))
.cornerRadius(4.0)
.tint(Color.green)
}
.padding(.bottom, 60)
.padding(.horizontal, 10)
.ignoresSafeArea(.all, edges: .bottom)
}
}

View File

@ -16,6 +16,10 @@ struct SafariWebLink: Identifiable {
@Published var isDownloadingAudio: Bool = false
@Published var audioDownloadTask: Task<Void, Error>?
@Published var operationMessage: String?
@Published var showOperationToast: Bool = false
@Published var operationStatus: OperationStatus = .none
@Published var showSnackbar: Bool = false
var snackbarOperation: SnackbarOperation?

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23B81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23B81" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Filter" representedClassName="Filter" syncable="YES" codeGenerationType="class">
<attribute name="defaultFilter" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="filter" optional="YES" attributeType="String"/>
@ -98,6 +98,7 @@
<attribute name="confirmationCode" optional="YES" attributeType="String"/>
<attribute name="email" attributeType="String"/>
<attribute name="emailId" attributeType="String"/>
<attribute name="folder" optional="YES" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="emailId"/>

View File

@ -20,6 +20,7 @@ public extension DataService {
InternalNewsletterEmail(
emailId: try $0.id(),
email: try $0.address(),
folder: try $0.folder(),
confirmationCode: try $0.confirmationCode()
)
}))

View File

@ -0,0 +1,50 @@
import CoreData
import Foundation
import Models
import SwiftGraphQL
public extension DataService {
func updateSubscription(_ subscriptionID: String, folder: String? = nil, fetchContent: Bool? = nil) async throws {
enum MutationResult {
case success(subscriptionID: String)
case error(errorMessage: String)
}
let selection = Selection<MutationResult, Unions.UpdateSubscriptionResult> {
try $0.on(
updateSubscriptionError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") },
updateSubscriptionSuccess: .init { .success(subscriptionID: try $0.subscription(selection: subscriptionSelection).subscriptionID) }
)
}
let mutation = Selection.Mutation {
try $0.updateSubscription(
input: InputObjects.UpdateSubscriptionInput(
fetchContent: OptionalArgument(fetchContent),
folder: OptionalArgument(folder),
id: subscriptionID
),
selection: selection
)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
return try await withCheckedThrowingContinuation { continuation in
send(mutation, to: path, headers: headers) { queryResult in
guard let payload = try? queryResult.get() else {
continuation.resume(throwing: BasicError.message(messageText: "network error"))
return
}
switch payload.data {
case .success:
continuation.resume()
case let .error(errorMessage: errorMessage):
continuation.resume(throwing: BasicError.message(messageText: errorMessage))
}
}
}
}
}

View File

@ -14,6 +14,7 @@ public extension DataService {
InternalNewsletterEmail(
emailId: try $0.id(),
email: try $0.address(),
folder: try $0.folder(),
confirmationCode: try $0.confirmationCode()
)
}

View File

@ -5,6 +5,7 @@ import Models
struct InternalNewsletterEmail {
let emailId: String
let email: String
let folder: String
let confirmationCode: String?
func persist(context: NSManagedObjectContext) -> NSManagedObjectID? {
@ -32,6 +33,7 @@ struct InternalNewsletterEmail {
newsletterEmail.emailId = emailId
newsletterEmail.email = email
newsletterEmail.folder = folder
newsletterEmail.confirmationCode = confirmationCode
return newsletterEmail
}