Update iconography and listview for ios, bump android

This commit is contained in:
Jackson Harper
2023-07-24 12:11:21 +08:00
parent 57894754a6
commit 590fc2ed25
67 changed files with 524 additions and 183 deletions

View File

@ -75,6 +75,7 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes += '/META-INF/{AL2.0,LGPL2.1}'
} }
} }
namespace 'app.omnivore.omnivore'
} }
dependencies { dependencies {

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="app.omnivore.omnivore">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

File diff suppressed because one or more lines are too long

View File

@ -3,13 +3,18 @@ buildscript {
compose_version = '1.3.1' compose_version = '1.3.1'
lifecycle_version = '2.5.1' lifecycle_version = '2.5.1'
hilt_version = '2.44.2' hilt_version = '2.44.2'
gradle_plugin_version = '7.3.1' gradle_plugin_version = '7.4.2'
room_version = '2.4.3' room_version = '2.4.3'
kotlin_version = '1.9.0'
} }
dependencies { dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath "com.android.tools.build:gradle:$gradle_plugin_version" classpath "com.android.tools.build:gradle:$gradle_plugin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
repositories {
mavenCentral()
} }
} }

View File

@ -144,6 +144,15 @@
"version" : "2.30908.0" "version" : "2.30908.0"
} }
}, },
{
"identity" : "popupview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/exyte/PopupView.git",
"state" : {
"revision" : "68349a0ae704b9a7041f756f3f4f460ddbf7ba8d",
"version" : "2.6.0"
}
},
{ {
"identity" : "promises", "identity" : "promises",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -25,7 +25,8 @@ let package = Package(
dependencies: [ dependencies: [
"Models", "Models",
.product(name: "Introspect", package: "SwiftUI-Introspect"), .product(name: "Introspect", package: "SwiftUI-Introspect"),
.product(name: "MarkdownUI", package: "swift-markdown-ui") .product(name: "MarkdownUI", package: "swift-markdown-ui"),
.productItem(name: "PopupView", package: "PopupView")
], ],
resources: [.process("Resources")] resources: [.process("Resources")]
), ),
@ -68,7 +69,8 @@ var dependencies: [Package.Dependency] {
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
.package(url: "https://github.com/segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"),
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0") .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
.package(url: "https://github.com/exyte/PopupView.git", from: "2.6.0")
] ]
// Comment out following line for macOS build // Comment out following line for macOS build
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "12.0.1")) deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "12.0.1"))

View File

@ -111,7 +111,7 @@
.frame(width: dim, height: dim) .frame(width: dim, height: dim)
.cornerRadius(6) .cornerRadius(6)
Image(systemName: "headphones") Image.headphones
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: dim / 2, height: dim / 2) .frame(width: dim / 2, height: dim / 2)

View File

@ -29,6 +29,8 @@ struct AnimatingCellHeight: AnimatableModifier {
@State var searchPresented = false @State var searchPresented = false
@State var addLinkPresented = false @State var addLinkPresented = false
@State var settingsPresented = false @State var settingsPresented = false
@State var isListScrolled = false
@State var listTitle = ""
@EnvironmentObject var dataService: DataService @EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController @EnvironmentObject var audioController: AudioController
@ -53,6 +55,8 @@ struct AnimatingCellHeight: AnimatableModifier {
} }
} }
HomeFeedView( HomeFeedView(
listTitle: $listTitle,
isListScrolled: $isListScrolled,
prefersListLayout: $prefersListLayout, prefersListLayout: $prefersListLayout,
viewModel: viewModel viewModel: viewModel
) )
@ -93,17 +97,18 @@ struct AnimatingCellHeight: AnimatableModifier {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .barLeading) { ToolbarItem(placement: .barLeading) {
// Button(action: { VStack(alignment: .leading) {
// viewModel.showFiltersModal = true
// }, label: {
HStack(alignment: .center) {
let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName let title = (LinkedItemFilter(rawValue: viewModel.appliedFilter) ?? LinkedItemFilter.inbox).displayName
Text(title) Text(title)
.font(Font.system(size: 18, weight: .semibold)) .font(Font.system(size: isListScrolled ? 10 : 18, weight: .semibold))
// Image(systemName: "chevron.down")
// .font(Font.system(size: 13, weight: .regular)) if isListScrolled {
Text(listTitle)
.font(Font.system(size: 15, weight: .regular))
.foregroundColor(Color.appGrayText)
}
}.frame(maxWidth: .infinity, alignment: .leading) }.frame(maxWidth: .infinity, alignment: .leading)
// })
} }
ToolbarItem(placement: .barTrailing) { ToolbarItem(placement: .barTrailing) {
Button("", action: {}) Button("", action: {})
@ -148,9 +153,7 @@ struct AnimatingCellHeight: AnimatableModifier {
Label("Add Link", systemImage: "plus.square") Label("Add Link", systemImage: "plus.square")
}) })
}, label: { }, label: {
Image(systemName: "ellipsis") Image.utilityMenu
.foregroundColor(.appGrayTextContrast)
.frame(width: 24, height: 24)
}) })
} else { } else {
EmptyView() EmptyView()
@ -232,13 +235,19 @@ struct AnimatingCellHeight: AnimatableModifier {
@MainActor @MainActor
struct HomeFeedView: View { struct HomeFeedView: View {
@EnvironmentObject var dataService: DataService @EnvironmentObject var dataService: DataService
@Binding var listTitle: String
@Binding var isListScrolled: Bool
@Binding var prefersListLayout: Bool @Binding var prefersListLayout: Bool
@ObservedObject var viewModel: HomeFeedViewModel @ObservedObject var viewModel: HomeFeedViewModel
@State var showSnackbar = false
@State var snackbarOperation: SnackbarOperation?
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
if prefersListLayout || !enableGrid { if prefersListLayout || !enableGrid {
HomeFeedListView(prefersListLayout: $prefersListLayout, viewModel: viewModel) HomeFeedListView(listTitle: $listTitle, isListScrolled: $isListScrolled, prefersListLayout: $prefersListLayout, viewModel: viewModel)
} else { } else {
HomeFeedGridView(viewModel: viewModel) HomeFeedGridView(viewModel: viewModel)
} }
@ -251,6 +260,33 @@ struct AnimatingCellHeight: AnimatableModifier {
self.viewModel.negatedLabels = $1 self.viewModel.negatedLabels = $1
} }
} }
.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)
}
.onReceive(NSNotification.operationSuccessPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
snackbarOperation = SnackbarOperation(message: message,
undoAction: notification.userInfo?["undoAction"] as? SnackbarUndoAction)
showSnackbar = true
}
}
.onReceive(NSNotification.operationFailedPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
showSnackbar = true
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
}
}
} }
} }
@ -258,6 +294,8 @@ struct AnimatingCellHeight: AnimatableModifier {
@EnvironmentObject var dataService: DataService @EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController @EnvironmentObject var audioController: AudioController
@Binding var listTitle: String
@Binding var isListScrolled: Bool
@Binding var prefersListLayout: Bool @Binding var prefersListLayout: Bool
@State private var showHideFeatureAlert = false @State private var showHideFeatureAlert = false
@ -417,6 +455,48 @@ struct AnimatingCellHeight: AnimatableModifier {
} }
} }
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value _: inout CGPoint, nextValue _: () -> CGPoint) {}
}
@State var topItem: LinkedItem?
func setTopItem(_ item: LinkedItem) {
if let date = item.savedAt, let daysAgo = Calendar.current.dateComponents([.day], from: date, to: Date()).day {
if daysAgo < 1 {
let formatter = DateFormatter()
formatter.timeStyle = .none
formatter.dateStyle = .long
formatter.doesRelativeDateFormatting = true
if let str = formatter.string(for: date) {
listTitle = str.capitalized
}
} else if daysAgo < 2 {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
if let str = formatter.string(for: date) {
listTitle = str.capitalized
}
} else if daysAgo < 5 {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE"
if let str = formatter.string(for: date) {
listTitle = str
}
} else {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
if let str = formatter.string(for: date) {
listTitle = str
}
}
topItem = item
}
}
var body: some View { var body: some View {
let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10)
VStack(spacing: 0) { VStack(spacing: 0) {
@ -443,6 +523,16 @@ struct AnimatingCellHeight: AnimatableModifier {
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowSeparator(.hidden, edges: .all) .listRowSeparator(.hidden, edges: .all)
.modifier(AnimatingCellHeight(height: 190 + (Color.isDarkMode ? 13 : 13))) .modifier(AnimatingCellHeight(height: 190 + (Color.isDarkMode ? 13 : 13)))
.onDisappear {
withAnimation {
isListScrolled = true
}
}
.onAppear {
withAnimation {
isListScrolled = false
}
}
} }
ForEach(viewModel.items) { item in ForEach(viewModel.items) { item in
@ -450,6 +540,19 @@ struct AnimatingCellHeight: AnimatableModifier {
item: item, item: item,
viewModel: viewModel viewModel: viewModel
) )
.background(GeometryReader { geometry in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
if value.y < 100, value.y > 0 {
if let date = item.savedAt {
if topItem != item {
setTopItem(item)
}
}
}
}
.listRowSeparatorTint(Color.thBorderColor) .listRowSeparatorTint(Color.thBorderColor)
.listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset))
.contextMenu { .contextMenu {
@ -470,6 +573,7 @@ struct AnimatingCellHeight: AnimatableModifier {
.padding(0) .padding(0)
.listStyle(PlainListStyle()) .listStyle(PlainListStyle())
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.coordinateSpace(name: "scroll")
} }
.alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.", .alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.",
isPresented: $showHideFeatureAlert) { isPresented: $showHideFeatureAlert) {

View File

@ -5,6 +5,8 @@ import Views
@MainActor @MainActor
struct HomeView: View { struct HomeView: View {
@State private var viewModel: HomeFeedViewModel @State private var viewModel: HomeFeedViewModel
@State var showSnackbar = false
@State var snackbarOperation: SnackbarOperation?
init(viewModel: HomeFeedViewModel) { init(viewModel: HomeFeedViewModel) {
self.viewModel = viewModel self.viewModel = viewModel

View File

@ -7,8 +7,10 @@
import Foundation import Foundation
import Models import Models
import PopupView
import Services import Services
import SwiftUI import SwiftUI
import Views
struct LibraryTabView: View { struct LibraryTabView: View {
@EnvironmentObject var dataService: DataService @EnvironmentObject var dataService: DataService

View File

@ -10,15 +10,18 @@ import Views
] ]
public var body: some View { public var body: some View {
innerBody
}
public var innerBody: some View {
#if os(iOS) #if os(iOS)
if UIDevice.isIPad { if UIDevice.isIPad {
splitView return AnyView(splitView)
} else { } else {
// HomeView() return AnyView(LibraryTabView())
LibraryTabView()
} }
#elseif os(macOS) #else
splitView return AnyView(splitView)
#endif #endif
} }

View File

@ -1,4 +1,5 @@
import Models import Models
import PopupView
import Services import Services
import SwiftUI import SwiftUI
import Views import Views
@ -38,6 +39,14 @@ struct NewsletterEmailsView: View {
@EnvironmentObject var dataService: DataService @EnvironmentObject var dataService: DataService
@StateObject var viewModel = NewsletterEmailsViewModel() @StateObject var viewModel = NewsletterEmailsViewModel()
@State var showSnackbar = false
@State var snackbarOperation: SnackbarOperation?
func snackbar(message: String) {
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
showSnackbar = true
}
var body: some View { var body: some View {
Group { Group {
#if os(iOS) #if os(iOS)
@ -52,6 +61,20 @@ struct NewsletterEmailsView: View {
#endif #endif
} }
.task { await viewModel.loadEmails(dataService: dataService) } .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 { private var innerBody: some View {
@ -87,7 +110,7 @@ struct NewsletterEmailsView: View {
pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString]) pasteBoard.writeObjects([newsletterEmail.unwrappedEmail as NSString])
#endif #endif
Snackbar.show(message: "Email copied") snackbar(message: "Email copied")
}, },
label: { Text(newsletterEmail.unwrappedEmail) } label: { Text(newsletterEmail.unwrappedEmail) }
) )
@ -95,6 +118,7 @@ struct NewsletterEmailsView: View {
} }
} }
} }
.navigationTitle(LocalText.emailsGeneric) .navigationTitle(LocalText.emailsGeneric)
} }
} }

View File

@ -29,7 +29,7 @@
do { do {
try await dataService.leaveGroup(groupID: recommendationGroup.id) try await dataService.leaveGroup(groupID: recommendationGroup.id)
Snackbar.show(message: "You have left the club.") // Snackbar.show(message: "You have left the club.")
} catch { } catch {
return false return false
} }
@ -182,7 +182,7 @@
pasteBoard.writeObjects([highlightParams.quote as NSString]) pasteBoard.writeObjects([highlightParams.quote as NSString])
#endif #endif
Snackbar.show(message: "Invite link copied") // Snackbar.show(message: "Invite link copied")
}, label: { }, label: {
Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))") Text("[\(viewModel.recommendationGroup.inviteUrl)](\(viewModel.recommendationGroup.inviteUrl))")
.font(.appCaption) .font(.appCaption)

View File

@ -114,7 +114,7 @@ struct SubscriptionsView: View {
Button("Yes", role: .destructive) { Button("Yes", role: .destructive) {
Task { Task {
let unsubscribed = await viewModel.cancelSubscription(dataService: dataService) let unsubscribed = await viewModel.cancelSubscription(dataService: dataService)
Snackbar.show(message: unsubscribed ? "Subscription cancelled." : "Could not unsubscribe.") // Snackbar.show(message: unsubscribed ? "Subscription cancelled." : "Could not unsubscribe.")
} }
} }
Button("No", role: .cancel) { Button("No", role: .cancel) {

View File

@ -64,17 +64,6 @@ struct InnerRootView: View {
} }
} }
#endif #endif
.snackBar(isShowing: $viewModel.showSnackbar, operation: viewModel.snackbarOperation)
// Schedule the dismissal every time we present the snackbar.
.onChange(of: viewModel.showSnackbar) { newValue in
if newValue {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
viewModel.showSnackbar = false
}
}
}
}
} }
} else { } else {
WelcomeView() WelcomeView()
@ -92,21 +81,6 @@ struct InnerRootView: View {
.frame(minWidth: 400, idealWidth: 1200, minHeight: 400, idealHeight: 1200) .frame(minWidth: 400, idealWidth: 1200, minHeight: 400, idealHeight: 1200)
#endif #endif
} }
#if os(iOS)
.onReceive(NSNotification.operationSuccessPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
viewModel.snackbarOperation = SnackbarOperation(message: message,
undoAction: notification.userInfo?["undoAction"] as? SnackbarUndoAction)
viewModel.showSnackbar = true
}
}
.onReceive(NSNotification.operationFailedPublisher) { notification in
if let message = notification.userInfo?["message"] as? String {
viewModel.showSnackbar = true
viewModel.snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
}
}
#endif
.onOpenURL { Authenticator.handleGoogleURL(url: $0) } .onOpenURL { Authenticator.handleGoogleURL(url: $0) }
} }

View File

@ -18,8 +18,6 @@ public final class RootViewModel: ObservableObject {
@AppStorage(UserDefaultKey.shouldShowNewFeaturePrimer.rawValue) var shouldShowNewFeaturePrimer = false @AppStorage(UserDefaultKey.shouldShowNewFeaturePrimer.rawValue) var shouldShowNewFeaturePrimer = false
@Published var showMiniPlayer = false @Published var showMiniPlayer = false
@Published var showSnackbar = false
@Published var snackbarOperation: SnackbarOperation?
public init() { public init() {
registerFonts() registerFonts()

View File

@ -12,7 +12,7 @@ struct WebReader: PlatformViewRepresentable {
let tapHandler: () -> Void let tapHandler: () -> Void
let scrollPercentHandler: (Int) -> Void let scrollPercentHandler: (Int) -> Void
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void let navBarVisibilityUpdater: (Bool) -> Void
@Binding var readerSettingsChangedTransactionID: UUID? @Binding var readerSettingsChangedTransactionID: UUID?
@Binding var annotationSaveTransactionID: UUID? @Binding var annotationSaveTransactionID: UUID?
@ -82,7 +82,7 @@ struct WebReader: PlatformViewRepresentable {
context.coordinator.linkHandler = openLinkAction context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater context.coordinator.updateNavBarVisibility = navBarVisibilityUpdater
context.coordinator.scrollPercentHandler = scrollPercentHandler context.coordinator.scrollPercentHandler = scrollPercentHandler
context.coordinator.updateShowBottomBar = { newValue in context.coordinator.updateShowBottomBar = { newValue in
self.showBottomBar = newValue self.showBottomBar = newValue

View File

@ -1,5 +1,6 @@
import AVFoundation import AVFoundation
import Models import Models
import PopupView
import Services import Services
import SwiftUI import SwiftUI
import Utils import Utils
@ -18,7 +19,7 @@ struct WebReaderContainerView: View {
@State private var showNotebookView = false @State private var showNotebookView = false
@State private var hasPerformedHighlightMutations = false @State private var hasPerformedHighlightMutations = false
@State var showHighlightAnnotationModal = false @State var showHighlightAnnotationModal = false
@State private var navBarVisibilityRatio = 1.0 @State private var navBarVisible = true
@State private var progressViewOpacity = 0.0 @State private var progressViewOpacity = 0.0
@State var readerSettingsChangedTransactionID: UUID? @State var readerSettingsChangedTransactionID: UUID?
@State var annotationSaveTransactionID: UUID? @State var annotationSaveTransactionID: UUID?
@ -81,8 +82,8 @@ struct WebReaderContainerView: View {
private func tapHandler() { private func tapHandler() {
withAnimation(.easeIn(duration: 0.08)) { withAnimation(.easeIn(duration: 0.08)) {
navBarVisibilityRatio = navBarVisibilityRatio == 1 ? 0 : 1 navBarVisible = !navBarVisible
showBottomBar = navBarVisibilityRatio == 1 showBottomBar = navBarVisible
showNavBarActionID = UUID() showNavBarActionID = UUID()
} }
} }
@ -105,8 +106,8 @@ struct WebReaderContainerView: View {
showHighlightLabelsModal = true showHighlightLabelsModal = true
case "pageTapped": case "pageTapped":
withAnimation { withAnimation {
navBarVisibilityRatio = navBarVisibilityRatio == 1 ? 0 : 1 navBarVisible = !navBarVisible
showBottomBar = navBarVisibilityRatio == 1 showBottomBar = navBarVisible
showNavBarActionID = UUID() showNavBarActionID = UUID()
} }
default: default:
@ -118,8 +119,7 @@ struct WebReaderContainerView: View {
var audioNavbarItem: some View { var audioNavbarItem: some View {
if audioController.isLoadingItem(itemID: item.unwrappedID) { if audioController.isLoadingItem(itemID: item.unwrappedID) {
return AnyView(ProgressView() return AnyView(ProgressView()
.padding(.horizontal) .padding(.horizontal))
.scaleEffect(navBarVisibilityRatio))
} else { } else {
return AnyView(Button( return AnyView(Button(
action: { action: {
@ -143,36 +143,34 @@ struct WebReaderContainerView: View {
label: { label: {
textToSpeechButtonImage textToSpeechButtonImage
} }
) ))
.padding(.horizontal, 5)
.scaleEffect(navBarVisibilityRatio))
} }
} }
var textToSpeechButtonImage: some View { var textToSpeechButtonImage: some View {
if audioController.state == .stopped || audioController.itemAudioProperties?.itemID != self.item.id { if audioController.state == .stopped || audioController.itemAudioProperties?.itemID != self.item.id {
return Image(systemName: "headphones").font(.appTitleThree) return AnyView(Image.headphones)
} }
let name = audioController.isPlayingItem(itemID: item.unwrappedID) ? "pause.circle" : "play.circle" let name = audioController.isPlayingItem(itemID: item.unwrappedID) ? "pause.circle" : "play.circle"
return Image(systemName: name).font(.appNavbarIcon) return AnyView(Image(systemName: name).font(.appNavbarIcon))
} }
#endif #endif
var bottomButtons: some View { var bottomButtons: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
Button(action: archive, label: { Button(action: archive, label: {
Image(systemName: item.isArchived ? "tray.and.arrow.down" : "archivebox") item.isArchived ? Image.unarchive : Image.archive
}).frame(width: 48, height: 48) }).frame(width: 48, height: 48)
.padding(.leading, 8) .padding(.leading, 8)
Divider().opacity(0.8) Divider().opacity(0.8)
Button(action: delete, label: { Button(action: delete, label: {
Image(systemName: "trash") Image.remove
}).frame(width: 48, height: 48) }).frame(width: 48, height: 48)
Divider().opacity(0.8) Divider().opacity(0.8)
Button(action: editLabels, label: { Button(action: editLabels, label: {
Image(systemName: "tag") Image.label
}).frame(width: 48, height: 48) }).frame(width: 48, height: 48)
Divider().opacity(0.8) Divider().opacity(0.8)
@ -261,30 +259,29 @@ struct WebReaderContainerView: View {
} }
} }
let navBarOffset = 100
var navBar: some View { var navBar: some View {
HStack(alignment: .center, spacing: 15) { HStack(alignment: .center, spacing: 10) {
#if os(iOS) #if os(iOS)
Button( Button(
action: { self.presentationMode.wrappedValue.dismiss() }, action: { self.presentationMode.wrappedValue.dismiss() },
label: { label: {
Image(systemName: "chevron.backward") Image.chevronRight
.font(.appNavbarIcon) .padding(.horizontal, 10)
// .foregroundColor(.appGrayTextContrast) .padding(.vertical)
.padding()
} }
) )
.scaleEffect(navBarVisibilityRatio)
Spacer() Spacer()
#endif #endif
Button( Button(
action: { showNotebookView = true }, action: { showNotebookView = true },
label: { label: {
Image("notebook", bundle: Bundle(url: ViewsPackage.bundleURL)) Image.notebook
} }
) )
.padding(.horizontal, 5) .padding(.trailing, 4)
.scaleEffect(navBarVisibilityRatio)
#if os(iOS) #if os(iOS)
audioNavbarItem audioNavbarItem
@ -298,12 +295,10 @@ struct WebReaderContainerView: View {
} }
}, },
label: { label: {
Image(systemName: "textformat.size") Image.readerSettings
.font(.appNavbarIcon)
} }
) )
.padding(.horizontal, 5) .padding(.horizontal, 5)
.scaleEffect(navBarVisibilityRatio)
.popover(isPresented: $showPreferencesPopover) { .popover(isPresented: $showPreferencesPopover) {
webPreferencesPopoverView webPreferencesPopoverView
.frame(maxWidth: 400, maxHeight: 475) .frame(maxWidth: 400, maxHeight: 475)
@ -322,13 +317,8 @@ struct WebReaderContainerView: View {
}, },
label: { label: {
#if os(iOS) #if os(iOS)
Image(systemName: "ellipsis") Image.utilityMenu
.resizable(resizingMode: Image.ResizingMode.stretch)
.aspectRatio(contentMode: .fit)
// .foregroundColor(.appGrayTextContrast)
.frame(width: 20, height: 20)
.scaleEffect(navBarVisibilityRatio)
.padding()
#else #else
Text(LocalText.genericOptions) Text(LocalText.genericOptions)
#endif #endif
@ -338,12 +328,12 @@ struct WebReaderContainerView: View {
.frame(maxWidth: 100) .frame(maxWidth: 100)
.padding(.trailing, 16) .padding(.trailing, 16)
#else #else
.padding(.trailing, 3) .padding(.trailing, 16)
.padding(.bottom, 10)
#endif #endif
} }
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio) .tint(Color(hex: "#2A2A2A"))
.opacity(navBarVisibilityRatio) .frame(height: readerViewNavBarHeight)
.frame(maxWidth: .infinity)
.foregroundColor(ThemeManager.currentTheme.isDark ? .white : .black) .foregroundColor(ThemeManager.currentTheme.isDark ? .white : .black)
.background(ThemeManager.currentBgColor) .background(ThemeManager.currentBgColor)
.sheet(isPresented: $showLabelsModal) { .sheet(isPresented: $showLabelsModal) {
@ -403,8 +393,10 @@ struct WebReaderContainerView: View {
tapHandler: tapHandler, tapHandler: tapHandler,
scrollPercentHandler: scrollPercentHandler, scrollPercentHandler: scrollPercentHandler,
webViewActionHandler: webViewActionHandler, webViewActionHandler: webViewActionHandler,
navBarVisibilityRatioUpdater: { navBarVisibilityUpdater: { visible in
navBarVisibilityRatio = $0 withAnimation {
navBarVisible = visible
}
}, },
readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID, readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID,
annotationSaveTransactionID: $annotationSaveTransactionID, annotationSaveTransactionID: $annotationSaveTransactionID,
@ -505,7 +497,7 @@ struct WebReaderContainerView: View {
self.isRecovering = true self.isRecovering = true
Task { Task {
if !(await dataService.recoverItem(itemID: item.unwrappedID)) { if !(await dataService.recoverItem(itemID: item.unwrappedID)) {
Snackbar.show(message: "Error recovering item") viewModel.snackbar(message: "Error recovering item")
} else { } else {
await viewModel.loadContent( await viewModel.loadContent(
dataService: dataService, dataService: dataService,
@ -554,6 +546,8 @@ struct WebReaderContainerView: View {
#if os(iOS) #if os(iOS)
VStack(spacing: 0) { VStack(spacing: 0) {
navBar navBar
.offset(y: navBarVisible ? 0 : -150)
Spacer() Spacer()
if showBottomBar { if showBottomBar {
bottomButtons bottomButtons
@ -585,6 +579,20 @@ struct WebReaderContainerView: View {
// WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil) // WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil) WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
} }
.popup(isPresented: $viewModel.showSnackbar) {
if let operation = viewModel.snackbarOperation {
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)
} else {
EmptyView()
}
} customize: {
$0
.type(.toast)
.autohideIn(2)
.position(.bottom)
.animation(.spring())
.closeOnTapOutside(true)
}
} }
func archive() { func archive() {
@ -592,7 +600,7 @@ struct WebReaderContainerView: View {
#if os(iOS) #if os(iOS)
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
#endif #endif
Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox") viewModel.snackbar(message: !item.isArchived ? "Link archived" : "Link moved to Inbox")
} }
func recommend() { func recommend() {

View File

@ -19,7 +19,7 @@ final class WebReaderCoordinator: NSObject {
var previousReaderSettingsChangedUUID: UUID? var previousReaderSettingsChangedUUID: UUID?
var previousShowNavBarActionID: UUID? var previousShowNavBarActionID: UUID?
var previousShareActionID: UUID? var previousShareActionID: UUID?
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in } var updateNavBarVisibility: (Bool) -> Void = { _ in }
var updateShowBottomBar: (Bool) -> Void = { _ in } var updateShowBottomBar: (Bool) -> Void = { _ in }
var articleContentID = UUID() var articleContentID = UUID()
private var yOffsetAtStartOfDrag: Double? private var yOffsetAtStartOfDrag: Double?
@ -31,10 +31,10 @@ final class WebReaderCoordinator: NSObject {
super.init() super.init()
} }
var navBarVisibilityRatio: Double = 1.0 { var navBarVisible: Bool = true {
didSet { didSet {
isNavBarHidden = navBarVisibilityRatio == 0 isNavBarHidden = !navBarVisible
updateNavBarVisibilityRatio(navBarVisibilityRatio) updateNavBarVisibility(navBarVisible)
} }
} }
@ -98,30 +98,28 @@ extension WebReaderCoordinator: WKNavigationDelegate {
if yOffset == 0 { if yOffset == 0 {
scrollView.contentInset.top = readerViewNavBarHeight scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1 navBarVisible = true
return return
} }
if yOffset < 0 { if yOffset < 0 {
navBarVisibilityRatio = 1 navBarVisible = true
scrollView.contentInset.top = readerViewNavBarHeight scrollView.contentInset.top = readerViewNavBarHeight
return return
} }
if yOffset < readerViewNavBarHeight { if yOffset < readerViewNavBarHeight {
let isScrollingUp = yOffsetAtStartOfDrag ?? 0 > yOffset let isScrollingUp = yOffsetAtStartOfDrag ?? 0 > yOffset
navBarVisibilityRatio = isScrollingUp || yOffset < 0 ? 1 : min(1, 1 - (yOffset / readerViewNavBarHeight)) navBarVisible = isScrollingUp || yOffset < 0
scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight scrollView.contentInset.top = navBarVisible ? readerViewNavBarHeight : 0
return return
} }
guard let yOffsetAtStartOfDrag = yOffsetAtStartOfDrag else { return } guard let yOffsetAtStartOfDrag = yOffsetAtStartOfDrag else { return }
if yOffset > yOffsetAtStartOfDrag, !isNavBarHidden { if yOffset > yOffsetAtStartOfDrag, !isNavBarHidden {
let translation = yOffset - yOffsetAtStartOfDrag navBarVisible = false
let ratio = translation < readerViewNavBarHeight ? 1 - (translation / readerViewNavBarHeight) : 0 scrollView.contentInset.top = navBarVisible ? readerViewNavBarHeight : 0
navBarVisibilityRatio = min(ratio, 1)
scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight
} }
if yOffset + scrollView.visibleSize.height > scrollView.contentSize.height - 140 { if yOffset + scrollView.visibleSize.height > scrollView.contentSize.height - 140 {
@ -137,15 +135,15 @@ extension WebReaderCoordinator: WKNavigationDelegate {
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate, scrollView.contentOffset.y + scrollView.contentInset.top < (yOffsetAtStartOfDrag ?? 0) { if decelerate, scrollView.contentOffset.y + scrollView.contentInset.top < (yOffsetAtStartOfDrag ?? 0) {
scrollView.contentInset.top = readerViewNavBarHeight scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1 navBarVisible = true
} }
} }
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
scrollView.contentInset.top = readerViewNavBarHeight scrollView.contentInset.top = readerViewNavBarHeight
let navBarVisible = navBarVisibilityRatio == 1 let isVisible = navBarVisible
navBarVisibilityRatio = 1 navBarVisible = true
return navBarVisible return isVisible
} }
} }
#endif #endif

View File

@ -16,6 +16,14 @@ struct SafariWebLink: Identifiable {
@Published var isDownloadingAudio: Bool = false @Published var isDownloadingAudio: Bool = false
@Published var audioDownloadTask: Task<Void, Error>? @Published var audioDownloadTask: Task<Void, Error>?
@Published var showSnackbar: Bool = false
var snackbarOperation: SnackbarOperation?
func snackbar(message: String) {
snackbarOperation = SnackbarOperation(message: message, undoAction: nil)
showSnackbar = true
}
func hasOriginalUrl(_ item: LinkedItem) -> Bool { func hasOriginalUrl(_ item: LinkedItem) -> Bool {
if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host { if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host {
if host == "omnivore.app" { if host == "omnivore.app" {
@ -27,7 +35,7 @@ struct SafariWebLink: Identifiable {
} }
func downloadAudio(audioController: AudioController, item: LinkedItem) { func downloadAudio(audioController: AudioController, item: LinkedItem) {
Snackbar.show(message: "Downloading Offline Audio") snackbar(message: "Downloading Offline Audio")
isDownloadingAudio = true isDownloadingAudio = true
if let audioDownloadTask = audioDownloadTask { if let audioDownloadTask = audioDownloadTask {
@ -41,7 +49,7 @@ struct SafariWebLink: Identifiable {
DispatchQueue.main.async { DispatchQueue.main.async {
self.isDownloadingAudio = false self.isDownloadingAudio = false
if !canceled { if !canceled {
Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio") self.snackbar(message: downloaded ? "Audio file downloaded" : "Error downloading audio")
} }
} }
} }
@ -202,12 +210,12 @@ struct SafariWebLink: Identifiable {
func saveLink(dataService: DataService, url: URL) { func saveLink(dataService: DataService, url: URL) {
Task { Task {
do { do {
Snackbar.show(message: "Saving link") snackbar(message: "Saving link")
print("SAVING: ", url.absoluteString) print("SAVING: ", url.absoluteString)
_ = try await dataService.createPageFromUrl(id: UUID().uuidString, url: url.absoluteString) _ = try await dataService.createPageFromUrl(id: UUID().uuidString, url: url.absoluteString)
Snackbar.show(message: "Link saved") snackbar(message: "Link saved")
} catch { } catch {
Snackbar.show(message: "Error saving link") snackbar(message: "Error saving link")
} }
} }
} }
@ -215,14 +223,14 @@ struct SafariWebLink: Identifiable {
func saveLinkAndFetch(dataService: DataService, username: String, url: URL) { func saveLinkAndFetch(dataService: DataService, username: String, url: URL) {
Task { Task {
do { do {
Snackbar.show(message: "Saving link") snackbar(message: "Saving link")
let requestId = UUID().uuidString let requestId = UUID().uuidString
_ = try await dataService.createPageFromUrl(id: requestId, url: url.absoluteString) _ = try await dataService.createPageFromUrl(id: requestId, url: url.absoluteString)
Snackbar.show(message: "Link saved") snackbar(message: "Link saved")
await loadContent(dataService: dataService, username: username, itemID: requestId, retryCount: 0) await loadContent(dataService: dataService, username: username, itemID: requestId, retryCount: 0)
} catch { } catch {
Snackbar.show(message: "Error saving link") snackbar(message: "Error saving link")
} }
} }
} }

View File

@ -17,4 +17,15 @@ public extension Image {
static var tabHighlights: Image { Image("_tab_highlights", bundle: .module).renderingMode(.template) } static var tabHighlights: Image { Image("_tab_highlights", bundle: .module).renderingMode(.template) }
static var pinRotated: Image { Image("pin-rotated", bundle: .module) } static var pinRotated: Image { Image("pin-rotated", bundle: .module) }
static var chevronRight: Image { Image("chevron-right", bundle: .module) }
static var notebook: Image { Image("notebook", bundle: .module) }
static var headphones: Image { Image("headphones", bundle: .module) }
static var readerSettings: Image { Image("reader-settings", bundle: .module) }
static var utilityMenu: Image { Image("utility-menu", bundle: .module) }
static var archive: Image { Image("archive", bundle: .module) }
static var unarchive: Image { Image("unarchive", bundle: .module) }
static var remove: Image { Image("remove", bundle: .module) }
static var label: Image { Image("label", bundle: .module) }
} }

View File

@ -1,15 +0,0 @@
{
"images" : [
{
"filename" : "dots-three.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26" viewBox="0 0 26 26" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 14.21875 13 C 14.21875 13.671875 13.671875 14.21875 13 14.21875 C 12.328125 14.21875 11.78125 13.671875 11.78125 13 C 11.78125 12.328125 12.328125 11.78125 13 11.78125 C 13.671875 11.78125 14.21875 12.328125 14.21875 13 Z M 14.21875 13 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 20.71875 13 C 20.71875 13.671875 20.171875 14.21875 19.5 14.21875 C 18.828125 14.21875 18.28125 13.671875 18.28125 13 C 18.28125 12.328125 18.828125 11.78125 19.5 11.78125 C 20.171875 11.78125 20.71875 12.328125 20.71875 13 Z M 20.71875 13 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 7.71875 13 C 7.71875 13.671875 7.171875 14.21875 6.5 14.21875 C 5.828125 14.21875 5.28125 13.671875 5.28125 13 C 5.28125 12.328125 5.828125 11.78125 6.5 11.78125 C 7.171875 11.78125 7.71875 12.328125 7.71875 13 Z M 7.71875 13 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -17,5 +17,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@ -17,5 +17,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@ -17,5 +17,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@ -17,5 +17,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "archive.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "archive@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "archive@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "chevron-left.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chevron-left@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chevron-left@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "headphones.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "headphones@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "headphones@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "label.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "label@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "label@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,15 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "notebook.svg", "filename" : "notebooks.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "notebooks@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "notebooks@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#000000" viewBox="0 0 256 256"><path d="M184,112a8,8,0,0,1-8,8H112a8,8,0,0,1,0-16h64A8,8,0,0,1,184,112Zm-8,24H112a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16Zm48-88V208a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32H208A16,16,0,0,1,224,48ZM48,208H72V48H48Zm160,0V48H88V208H208Z"></path></svg>

Before

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

View File

@ -19,5 +19,8 @@
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
} }
} }

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "reader-settings.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "reader-settings@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "reader-settings@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "remove.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "remove@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "remove@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "unarchive.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "unarchive@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "unarchive@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "utility-menu.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "utility-menu@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "utility-menu@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

View File

@ -14,59 +14,51 @@ public struct SnackbarOperation {
public struct Snackbar: View { public struct Snackbar: View {
@Binding var isShowing: Bool @Binding var isShowing: Bool
private let presentingView: AnyView
private let operation: SnackbarOperation private let operation: SnackbarOperation
@Environment(\.colorScheme) private var colorScheme: ColorScheme @Environment(\.colorScheme) private var colorScheme: ColorScheme
init<PresentingView>( public init(
isShowing: Binding<Bool>, isShowing: Binding<Bool>,
presentingView: PresentingView,
operation: SnackbarOperation operation: SnackbarOperation
) where PresentingView: View { ) {
self._isShowing = isShowing self._isShowing = isShowing
self.presentingView = AnyView(presentingView)
self.operation = operation self.operation = operation
} }
public var body: some View { public var body: some View {
GeometryReader { geometry in VStack(alignment: .center) {
ZStack(alignment: .center) { HStack {
presentingView Text(operation.message)
VStack { .font(.appCallout)
Spacer() .foregroundColor(self.colorScheme == .light ? .white : .appTextDefault)
if isShowing { Spacer()
HStack { if let undoAction = operation.undoAction {
Text(operation.message) Button("Undo", action: {
.font(.appCallout) isShowing = false
.foregroundColor(self.colorScheme == .light ? .white : .appTextDefault) undoAction()
Spacer() })
if let undoAction = operation.undoAction { .font(.system(size: 16, weight: .bold))
Button("Undo", action: {
isShowing = false
undoAction()
})
.font(.system(size: 16, weight: .bold))
}
}
.padding()
.frame(width: min(380, geometry.size.width * 0.96), height: 44)
.background(self.colorScheme == .light ? Color.black : Color.white)
.cornerRadius(5)
.offset(x: 0, y: -8)
.shadow(color: .gray, radius: 2)
.animation(.spring(), value: true)
}
} }
} }
.frame(maxWidth: 380)
.frame(height: 44)
.padding(.horizontal, 15)
.background(self.colorScheme == .light ? Color.black : Color.white)
.cornerRadius(5)
.clipped()
Spacer(minLength: 20)
} }
.background(Color.clear)
.frame(height: 44 + 22)
} }
} }
public extension View { public extension View {
func snackBar(isShowing: Binding<Bool>, operation: SnackbarOperation?) -> some View { func snackBar(isShowing: Binding<Bool>, operation: SnackbarOperation?) -> some View {
if let operation = operation { if let operation = operation {
return AnyView(Snackbar(isShowing: isShowing, presentingView: self, operation: operation)) return AnyView(Snackbar(isShowing: isShowing, operation: operation))
} else { } else {
return AnyView(self) return AnyView(self)
} }