Show Move to Inbox in following items toolbar
This commit is contained in:
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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?
|
||||
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -20,6 +20,7 @@ public extension DataService {
|
||||
InternalNewsletterEmail(
|
||||
emailId: try $0.id(),
|
||||
email: try $0.address(),
|
||||
folder: try $0.folder(),
|
||||
confirmationCode: try $0.confirmationCode()
|
||||
)
|
||||
}))
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ public extension DataService {
|
||||
InternalNewsletterEmail(
|
||||
emailId: try $0.id(),
|
||||
email: try $0.address(),
|
||||
folder: try $0.folder(),
|
||||
confirmationCode: try $0.confirmationCode()
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user