mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1691 from omnivore-app/feat/improve-ios-share-ext
Improve iOS share extension and syncing
This commit is contained in:
commit
19fe4087f5
24 changed files with 292 additions and 378 deletions
|
|
@ -1,8 +1,11 @@
|
|||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
public extension PlatformViewController {
|
||||
static func makeShareExtensionController(extensionContext: NSExtensionContext?) -> PlatformViewController {
|
||||
registerFonts()
|
||||
|
||||
let hostingController = PlatformHostingController(
|
||||
rootView: ShareExtensionView(extensionContext: extensionContext)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import SwiftUI
|
|||
import Utils
|
||||
import Views
|
||||
|
||||
// swiftlint:disable:next type_body_length
|
||||
public struct ShareExtensionView: View {
|
||||
let extensionContext: NSExtensionContext?
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@StateObject var labelsViewModel = LabelsViewModel()
|
||||
@StateObject private var viewModel = ShareExtensionViewModel()
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ public struct ShareExtensionView: View {
|
|||
@State var hideUntilReminded = false
|
||||
@State var previousLabels: [LinkedItemLabel]?
|
||||
@State var messageText: String?
|
||||
@State var showSearchLabels = false
|
||||
|
||||
@State var viewState = ViewState.mainView
|
||||
@State var showHighlightInstructionAlert = false
|
||||
|
|
@ -128,7 +129,7 @@ public struct ShareExtensionView: View {
|
|||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: 60)
|
||||
.padding()
|
||||
|
|
@ -173,31 +174,37 @@ public struct ShareExtensionView: View {
|
|||
Image(systemName: "chevron.right")
|
||||
.font(.appCallout)
|
||||
} else {
|
||||
VStack {
|
||||
ScrollView {
|
||||
LabelsMasonaryView(labels: labelsViewModel.labels,
|
||||
selectedLabels: labelsViewModel.selectedLabels,
|
||||
onLabelTap: onLabelTap)
|
||||
}.background(Color.appButtonBackground)
|
||||
.cornerRadius(8)
|
||||
VStack(spacing: 15) {
|
||||
SearchBar(searchTerm: $labelsViewModel.labelSearchFilter)
|
||||
|
||||
Button(
|
||||
action: { labelsViewModel.showCreateLabelModal = true },
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "plus")
|
||||
Text("Create label")
|
||||
Spacer()
|
||||
ScrollView {
|
||||
LabelsMasonaryView(labels: labelsViewModel.labels.applySearchFilter(labelsViewModel.labelSearchFilter),
|
||||
selectedLabels: labelsViewModel.selectedLabels.applySearchFilter(labelsViewModel.labelSearchFilter),
|
||||
onLabelTap: onLabelTap)
|
||||
Button(
|
||||
action: { labelsViewModel.showCreateLabelModal = true },
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "tag").foregroundColor(.blue)
|
||||
Text(
|
||||
labelsViewModel.labelSearchFilter.count > 0 ?
|
||||
"Create: \"\(labelsViewModel.labelSearchFilter)\" label" :
|
||||
LocalText.createLabelMessage
|
||||
).foregroundColor(.blue)
|
||||
.font(Font.system(size: 14))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
).buttonStyle(RoundedRectButtonStyle(color: .blue, textColor: .white))
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.padding(10)
|
||||
}.background(Color.appButtonBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.padding(viewState == .editingLabels ? 0 : 16)
|
||||
.background(viewState == .editingLabels ? Color.clear : Color.appButtonBackground)
|
||||
.frame(maxWidth: .infinity, maxHeight: viewState == .editingLabels ? .infinity : 60)
|
||||
.background(Color.appButtonBackground)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
|
|
@ -248,10 +255,10 @@ public struct ShareExtensionView: View {
|
|||
}
|
||||
|
||||
func onLabelTap(label: LinkedItemLabel, textChip _: TextChip) {
|
||||
if let selectedIndex = labelsViewModel.selectedLabels.firstIndex(of: label) {
|
||||
labelsViewModel.selectedLabels.remove(at: selectedIndex)
|
||||
if labelsViewModel.selectedLabels.contains(label) {
|
||||
labelsViewModel.selectedLabels.remove(label)
|
||||
} else {
|
||||
labelsViewModel.selectedLabels.append(label)
|
||||
labelsViewModel.selectedLabels.insert(label)
|
||||
}
|
||||
|
||||
if let linkedItem = viewModel.linkedItem {
|
||||
|
|
@ -367,22 +374,6 @@ public struct ShareExtensionView: View {
|
|||
.padding(.bottom, 12)
|
||||
} else {
|
||||
ZStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
if viewState == .editingLabels {
|
||||
if let linkedItem = self.viewModel.linkedItem {
|
||||
self.labelsViewModel.selectedLabels = previousLabels ?? []
|
||||
self.labelsViewModel.saveItemLabelChanges(itemID: linkedItem.unwrappedID,
|
||||
dataService: self.viewModel.services.dataService)
|
||||
}
|
||||
}
|
||||
viewState = .mainView
|
||||
}
|
||||
}, label: { Text(LocalText.cancelGeneric) })
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(viewState == .viewingHighlight ? 0.0 : 1.0)
|
||||
// Don't show viewState when viewing the highlight
|
||||
|
||||
Text(editingViewTitle).bold()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
|
|
@ -444,7 +435,7 @@ public struct ShareExtensionView: View {
|
|||
labelsSection
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
previousLabels = self.labelsViewModel.selectedLabels
|
||||
previousLabels = Array(self.labelsViewModel.selectedLabels)
|
||||
viewState = .editingLabels
|
||||
}
|
||||
}
|
||||
|
|
@ -484,95 +475,14 @@ public struct ShareExtensionView: View {
|
|||
viewModel.savePage(extensionContext: extensionContext)
|
||||
}
|
||||
.sheet(isPresented: $labelsViewModel.showCreateLabelModal) {
|
||||
CreateLabelView(viewModel: labelsViewModel)
|
||||
CreateLabelView(viewModel: labelsViewModel, newLabelName: labelsViewModel.labelSearchFilter)
|
||||
}
|
||||
.alert("Before saving an article select text in Safari to create a highlight on save.",
|
||||
isPresented: $showHighlightInstructionAlert) {
|
||||
Button("Ok", role: .cancel) { showHighlightInstructionAlert = false }
|
||||
}
|
||||
.environmentObject(viewModel.services.dataService)
|
||||
.task {
|
||||
await labelsViewModel.loadLabelsFromStore(dataService: viewModel.services.dataService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ApplyLabelsListView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@StateObject var viewModel = LabelsViewModel()
|
||||
|
||||
let linkedItem: LinkedItem?
|
||||
|
||||
func isSelected(_ label: LinkedItemLabel) -> Bool {
|
||||
viewModel.selectedLabels.contains(where: { $0.id == label.id })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(
|
||||
content: {
|
||||
ForEach(viewModel.labels.applySearchFilter(viewModel.labelSearchFilter), id: \.self) { label in
|
||||
Button(
|
||||
action: {
|
||||
if isSelected(label) {
|
||||
viewModel.selectedLabels.removeAll(where: { $0.id == label.id })
|
||||
} else {
|
||||
viewModel.selectedLabels.append(label)
|
||||
}
|
||||
if let linkedItem = linkedItem {
|
||||
viewModel.saveItemLabelChanges(itemID: linkedItem.unwrappedID, dataService: dataService)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
TextChip(feedItemLabel: label)
|
||||
Spacer()
|
||||
if isSelected(label) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.checkmarkBlue)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
.foregroundColor(.appGraySolid)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
)
|
||||
#if os(iOS)
|
||||
.listRowSeparator(.hidden)
|
||||
#endif
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
},
|
||||
header: {
|
||||
Text(LocalText.labelsGeneric)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
}
|
||||
)
|
||||
#if os(iOS)
|
||||
.listRowSeparator(.hidden)
|
||||
#endif
|
||||
Button(
|
||||
action: { viewModel.showCreateLabelModal = true },
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill").foregroundColor(.green)
|
||||
Text(LocalText.createLabelMessage).foregroundColor(.appGrayTextContrast)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.padding(.vertical, 0)
|
||||
.task {
|
||||
await viewModel.loadLabelsFromStore(dataService: dataService)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showCreateLabelModal) {
|
||||
CreateLabelView(viewModel: viewModel)
|
||||
}
|
||||
}.environmentObject(viewModel.services.dataService)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
import SwiftUI
|
||||
import Views
|
||||
|
||||
// TODO: maybe move this into Views package?
|
||||
// struct IconButtonView: View {
|
||||
// let title: String
|
||||
// let systemIconName: String
|
||||
// let action: () -> Void
|
||||
//
|
||||
// var body: some View {
|
||||
// Button(action: action) {
|
||||
// VStack(alignment: .center, spacing: 8) {
|
||||
// Image(systemName: systemIconName)
|
||||
// .font(.appTitle)
|
||||
// .foregroundColor(.appYellow48)
|
||||
// Text(title)
|
||||
// .font(.appBody)
|
||||
// .foregroundColor(.appGrayText)
|
||||
// }
|
||||
// .frame(
|
||||
// maxWidth: .infinity,
|
||||
// maxHeight: .infinity
|
||||
// )
|
||||
// .background(Color.appButtonBackground)
|
||||
// .cornerRadius(8)
|
||||
// }
|
||||
// .frame(height: 100)
|
||||
// }
|
||||
// }
|
||||
|
||||
struct CheckmarkButtonView: View {
|
||||
let titleText: String
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(
|
||||
action: action,
|
||||
label: {
|
||||
HStack {
|
||||
Text(titleText)
|
||||
Spacer()
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.appYellow48)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
)
|
||||
.buttonStyle(RectButtonStyle())
|
||||
}
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ struct HighlightsListView: View {
|
|||
}
|
||||
}
|
||||
}.sheet(item: $setLabelsHighlight) { highlight in
|
||||
ApplyLabelsView(mode: .highlight(highlight), onSave: { selectedLabels in
|
||||
ApplyLabelsView(mode: .highlight(highlight), isSearchFocused: false, onSave: { selectedLabels in
|
||||
hasHighlightMutations = true
|
||||
|
||||
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ import Views
|
|||
loadItems(isRefresh: true)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
|
||||
ApplyLabelsView(mode: .item(item), onSave: nil)
|
||||
ApplyLabelsView(mode: .item(item), isSearchFocused: false, onSave: nil)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
|
||||
LinkedItemMetadataEditView(item: item)
|
||||
|
|
@ -146,7 +146,7 @@ import Views
|
|||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
loadItems(isRefresh: false)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushJSONArticle"))) { notification in
|
||||
guard let jsonArticle = notification.userInfo?["article"] as? JSONArticle else { return }
|
||||
|
|
@ -191,7 +191,7 @@ import Views
|
|||
}
|
||||
.task {
|
||||
if viewModel.items.isEmpty {
|
||||
loadItems(isRefresh: true)
|
||||
loadItems(isRefresh: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ import Views
|
|||
group.addTask { await self.loadCurrentViewer(dataService: dataService) }
|
||||
group.addTask { await self.loadLabels(dataService: dataService) }
|
||||
group.addTask { await self.syncItems(dataService: dataService) }
|
||||
group.addTask { await self.updateFetchController(dataService: dataService) }
|
||||
await group.waitForAll()
|
||||
}
|
||||
|
||||
|
|
@ -199,9 +200,6 @@ import Views
|
|||
await loadSearchQuery(dataService: dataService, isRefresh: isRefresh)
|
||||
} else {
|
||||
updateFetchController(dataService: dataService)
|
||||
Task.detached(priority: .background) {
|
||||
await self.loadSearchQuery(dataService: dataService, isRefresh: isRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ struct ApplyLabelsView: View {
|
|||
var navTitle: String {
|
||||
switch self {
|
||||
case .item, .highlight:
|
||||
return "Assign Labels"
|
||||
return "Set Labels"
|
||||
case .list:
|
||||
return "Apply Label Filters"
|
||||
return "Set Labels"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,62 +23,68 @@ struct ApplyLabelsView: View {
|
|||
case .item, .highlight:
|
||||
return "Save"
|
||||
case .list:
|
||||
return "Apply"
|
||||
return "Done"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mode: Mode
|
||||
let isSearchFocused: Bool
|
||||
let onSave: (([LinkedItemLabel]) -> Void)?
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@StateObject var viewModel = LabelsViewModel()
|
||||
|
||||
enum ViewState {
|
||||
case mainView
|
||||
case editingTitle
|
||||
case editingLabels
|
||||
case viewingHighlight
|
||||
}
|
||||
|
||||
func isSelected(_ label: LinkedItemLabel) -> Bool {
|
||||
viewModel.selectedLabels.contains(where: { $0.id == label.id })
|
||||
}
|
||||
|
||||
var innerBody: some View {
|
||||
List {
|
||||
Section {
|
||||
Button(
|
||||
action: { viewModel.showCreateLabelModal = true },
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill").foregroundColor(.green)
|
||||
Text(LocalText.createLabelMessage).foregroundColor(.appGrayTextContrast)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
Section {
|
||||
ForEach(viewModel.labels.applySearchFilter(viewModel.labelSearchFilter), id: \.self) { label in
|
||||
Button(
|
||||
action: {
|
||||
if isSelected(label) {
|
||||
viewModel.selectedLabels.removeAll(where: { $0.id == label.id })
|
||||
} else {
|
||||
viewModel.selectedLabels.append(label)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
TextChip(feedItemLabel: label)
|
||||
Spacer()
|
||||
VStack {
|
||||
SearchBar(searchTerm: $viewModel.labelSearchFilter)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
List {
|
||||
Section {
|
||||
ForEach(viewModel.labels.applySearchFilter(viewModel.labelSearchFilter), id: \.self) { label in
|
||||
Button(
|
||||
action: {
|
||||
if isSelected(label) {
|
||||
Image(systemName: "checkmark")
|
||||
viewModel.selectedLabels.remove(label)
|
||||
} else {
|
||||
viewModel.selectedLabels.insert(label)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
TextChip(feedItemLabel: label).allowsHitTesting(false)
|
||||
Spacer()
|
||||
if isSelected(label) {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
#if os(macOS)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#endif
|
||||
)
|
||||
.padding(.vertical, 5)
|
||||
#if os(macOS)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#endif
|
||||
}
|
||||
createLabelButton
|
||||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle(mode.navTitle)
|
||||
#if os(iOS)
|
||||
|
|
@ -100,20 +106,43 @@ struct ApplyLabelsView: View {
|
|||
}
|
||||
#endif
|
||||
.sheet(isPresented: $viewModel.showCreateLabelModal) {
|
||||
CreateLabelView(viewModel: viewModel)
|
||||
CreateLabelView(viewModel: viewModel, newLabelName: viewModel.labelSearchFilter)
|
||||
}
|
||||
}
|
||||
|
||||
var createLabelButton: some View {
|
||||
Button(
|
||||
action: { viewModel.showCreateLabelModal = true },
|
||||
label: {
|
||||
HStack {
|
||||
Image(systemName: "tag").foregroundColor(.blue)
|
||||
Text(
|
||||
viewModel.labelSearchFilter.count > 0 ?
|
||||
"Create: \"\(viewModel.labelSearchFilter)\" label" :
|
||||
LocalText.createLabelMessage
|
||||
).foregroundColor(.blue)
|
||||
.font(Font.system(size: 14))
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.disabled(viewModel.isLoading)
|
||||
.listRowSeparator(.hidden, edges: .bottom)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
var saveItemChangesButton: some View {
|
||||
Button(
|
||||
action: {
|
||||
switch mode {
|
||||
case let .item(feedItem):
|
||||
viewModel.saveItemLabelChanges(itemID: feedItem.unwrappedID, dataService: dataService)
|
||||
onSave?(Array(viewModel.selectedLabels))
|
||||
case .highlight:
|
||||
onSave?(viewModel.selectedLabels)
|
||||
onSave?(Array(viewModel.selectedLabels))
|
||||
case .list:
|
||||
onSave?(viewModel.selectedLabels)
|
||||
onSave?(Array(viewModel.selectedLabels))
|
||||
}
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
},
|
||||
|
|
@ -136,10 +165,6 @@ struct ApplyLabelsView: View {
|
|||
EmptyView()
|
||||
} else {
|
||||
innerBody
|
||||
.searchable(
|
||||
text: $viewModel.labelSearchFilter,
|
||||
placement: .navigationBarDrawer(displayMode: .always)
|
||||
)
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
|
|
|
|||
|
|
@ -22,31 +22,32 @@ struct FilterByLabelsView: View {
|
|||
|
||||
var innerBody: some View {
|
||||
List {
|
||||
ForEach(viewModel.labels, id: \.self) { label in
|
||||
HStack {
|
||||
TextChip(feedItemLabel: label, negated: isNegated(label))
|
||||
Spacer()
|
||||
Button(action: {
|
||||
ForEach(viewModel.labels.applySearchFilter(viewModel.labelSearchFilter), id: \.self) { label in
|
||||
Button(
|
||||
action: {
|
||||
if isSelected(label) {
|
||||
viewModel.negatedLabels.append(label)
|
||||
viewModel.selectedLabels.removeAll(where: { $0.id == label.id })
|
||||
} else if isNegated(label) {
|
||||
viewModel.negatedLabels.removeAll(where: { $0.id == label.id })
|
||||
} else {
|
||||
viewModel.selectedLabels.append(label)
|
||||
}
|
||||
}, label: {
|
||||
if isNegated(label) {
|
||||
Image(systemName: "circle.slash")
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
TextChip(feedItemLabel: label).allowsHitTesting(false)
|
||||
Spacer()
|
||||
if isSelected(label) {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
if isSelected(label) {
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.vertical, 5)
|
||||
#if os(macOS)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.listStyle(PlainListStyle())
|
||||
.navigationTitle("Filter by Label")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
|
|
|
|||
|
|
@ -12,12 +12,8 @@ import Models
|
|||
import Views
|
||||
|
||||
struct LabelsMasonaryView: View {
|
||||
// var allLabels: [LinkedItemLabel]
|
||||
// var selectedLabels: [LinkedItemLabel]
|
||||
var onLabelTap: (LinkedItemLabel, TextChip) -> Void
|
||||
|
||||
var iteration = UUID().uuidString
|
||||
|
||||
@State private var totalHeight = CGFloat.zero
|
||||
private var labelItems: [(label: LinkedItemLabel, selected: Bool)]
|
||||
|
||||
|
|
@ -30,7 +26,9 @@ struct LabelsMasonaryView: View {
|
|||
let selected = selectedLabels.map { (label: $0, selected: true) }
|
||||
let unselected = allLabels.filter { !selectedLabels.contains($0) }.map { (label: $0, selected: false) }
|
||||
labelItems = (selected + unselected).sorted(by: { left, right in
|
||||
(left.label.name ?? "") < (right.label.name ?? "")
|
||||
let aTrimmed = left.label.unwrappedName.trimmingCharacters(in: .whitespaces)
|
||||
let bTrimmed = right.label.unwrappedName.trimmingCharacters(in: .whitespaces)
|
||||
return aTrimmed.caseInsensitiveCompare(bTrimmed) == .orderedAscending
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -39,8 +37,8 @@ struct LabelsMasonaryView: View {
|
|||
GeometryReader { geometry in
|
||||
self.generateContent(in: geometry)
|
||||
}
|
||||
}
|
||||
.frame(height: totalHeight)
|
||||
}.padding(5)
|
||||
.frame(height: totalHeight)
|
||||
}
|
||||
|
||||
private func generateContent(in geom: GeometryProxy) -> some View {
|
||||
|
|
@ -50,7 +48,8 @@ struct LabelsMasonaryView: View {
|
|||
return ZStack(alignment: .topLeading) {
|
||||
ForEach(self.labelItems, id: \.label.self) { label in
|
||||
self.item(for: label)
|
||||
.padding([.horizontal, .vertical], 6)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 5)
|
||||
.alignmentGuide(.leading, computeValue: { dim in
|
||||
if abs(width - dim.width) > geom.size.width {
|
||||
width = 0
|
||||
|
|
@ -77,7 +76,7 @@ struct LabelsMasonaryView: View {
|
|||
}
|
||||
|
||||
private func item(for item: (label: LinkedItemLabel, selected: Bool)) -> some View {
|
||||
let chip = TextChip(feedItemLabel: item.label, negated: false, checked: item.selected) { chip in
|
||||
let chip = TextChip(feedItemLabel: item.label, negated: false, checked: item.selected, padded: true) { chip in
|
||||
onLabelTap(item.label, chip)
|
||||
}
|
||||
return chip
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ struct LabelsView: View {
|
|||
Button(LocalText.cancelGeneric, role: .cancel) { self.labelToRemove = nil }
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showCreateLabelModal) {
|
||||
CreateLabelView(viewModel: viewModel)
|
||||
CreateLabelView(viewModel: viewModel, newLabelName: viewModel.labelSearchFilter)
|
||||
}
|
||||
.task { await viewModel.loadLabels(dataService: dataService, item: nil) }
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ struct LabelsView: View {
|
|||
Section(header: Text(LocalText.labelsGeneric)) {
|
||||
ForEach(viewModel.labels, id: \.id) { label in
|
||||
HStack {
|
||||
TextChip(feedItemLabel: label)
|
||||
TextChip(feedItemLabel: label).allowsHitTesting(false)
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
|
|
@ -85,9 +85,14 @@ struct CreateLabelView: View {
|
|||
@EnvironmentObject var dataService: DataService
|
||||
@ObservedObject var viewModel: LabelsViewModel
|
||||
|
||||
@State private var newLabelName = ""
|
||||
@State private var newLabelName: String
|
||||
@State private var newLabelColor = Color.clear
|
||||
|
||||
init(viewModel: LabelsViewModel, newLabelName: String = "") {
|
||||
self.viewModel = viewModel
|
||||
self.newLabelName = newLabelName
|
||||
}
|
||||
|
||||
var shouldDisableCreateButton: Bool {
|
||||
viewModel.isLoading || newLabelName.isEmpty || newLabelColor == .clear
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,24 +6,37 @@ import Views
|
|||
|
||||
@MainActor final class LabelsViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var selectedLabels = [LinkedItemLabel]()
|
||||
@Published var unselectedLabels = [LinkedItemLabel]()
|
||||
@Published var selectedLabels = Set<LinkedItemLabel>()
|
||||
@Published var unselectedLabels = Set<LinkedItemLabel>()
|
||||
@Published var labels = [LinkedItemLabel]()
|
||||
@Published var showCreateLabelModal = false
|
||||
@Published var labelSearchFilter = ""
|
||||
|
||||
func setLabels(_ labels: [LinkedItemLabel]) {
|
||||
self.labels = labels.sorted { $0.unwrappedName.trimmingCharacters(in: .whitespaces) < $1.unwrappedName.trimmingCharacters(in: .whitespaces) }
|
||||
self.labels = labels.sorted { left, right in
|
||||
let aTrimmed = left.unwrappedName.trimmingCharacters(in: .whitespaces)
|
||||
let bTrimmed = right.unwrappedName.trimmingCharacters(in: .whitespaces)
|
||||
return aTrimmed.caseInsensitiveCompare(bTrimmed) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
func loadLabels(
|
||||
dataService: DataService,
|
||||
item: LinkedItem? = nil,
|
||||
highlight: Highlight? = nil,
|
||||
initiallySelectedLabels: [LinkedItemLabel]? = nil
|
||||
) async {
|
||||
isLoading = true
|
||||
let selLabels = initiallySelectedLabels ?? item?.sortedLabels ?? highlight?.sortedLabels ?? []
|
||||
|
||||
await loadLabelsFromStore(dataService: dataService)
|
||||
for label in labels {
|
||||
if selLabels.contains(label) {
|
||||
selectedLabels.insert(label)
|
||||
} else {
|
||||
unselectedLabels.insert(label)
|
||||
}
|
||||
}
|
||||
|
||||
Task.detached(priority: .userInitiated) {
|
||||
if let labelIDs = try? await dataService.labels() {
|
||||
|
|
@ -31,12 +44,11 @@ import Views
|
|||
dataService.viewContext.performAndWait {
|
||||
self.setLabels(labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel })
|
||||
}
|
||||
let selLabels = initiallySelectedLabels ?? item?.sortedLabels ?? []
|
||||
for label in self.labels {
|
||||
if selLabels.contains(label) {
|
||||
self.selectedLabels.append(label)
|
||||
self.selectedLabels.insert(label)
|
||||
} else {
|
||||
self.unselectedLabels.append(label)
|
||||
self.unselectedLabels.insert(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,29 +58,6 @@ import Views
|
|||
isLoading = false
|
||||
}
|
||||
|
||||
func loadLabels(
|
||||
dataService: DataService,
|
||||
highlight: Highlight
|
||||
) async {
|
||||
isLoading = true
|
||||
|
||||
if let labelIDs = try? await dataService.labels() {
|
||||
dataService.viewContext.performAndWait {
|
||||
setLabels(labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel })
|
||||
}
|
||||
let selLabels = highlight.labels ?? []
|
||||
for label in labels {
|
||||
if selLabels.contains(label) {
|
||||
selectedLabels.append(label)
|
||||
} else {
|
||||
unselectedLabels.append(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func loadLabelsFromStore(dataService: DataService) async {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItemLabel> = LinkedItemLabel.fetchRequest()
|
||||
|
||||
|
|
@ -76,12 +65,8 @@ import Views
|
|||
try? fetchRequest.execute()
|
||||
}
|
||||
|
||||
if fetchedLabels?.count == 0 {
|
||||
await fetchLabelsFromNetwork(dataService: dataService)
|
||||
} else {
|
||||
setLabels(fetchedLabels ?? [])
|
||||
unselectedLabels = fetchedLabels ?? []
|
||||
}
|
||||
setLabels(fetchedLabels ?? [])
|
||||
unselectedLabels = Set(fetchedLabels ?? [])
|
||||
}
|
||||
|
||||
func fetchLabelsFromNetwork(dataService: DataService) async {
|
||||
|
|
@ -93,7 +78,7 @@ import Views
|
|||
}
|
||||
|
||||
setLabels(fetchedLabels)
|
||||
unselectedLabels = fetchedLabels
|
||||
unselectedLabels = Set(fetchedLabels)
|
||||
}
|
||||
|
||||
func createLabel(dataService: DataService, name: String, color: Color, description: String?) {
|
||||
|
|
@ -110,7 +95,7 @@ import Views
|
|||
|
||||
if let label = dataService.viewContext.object(with: labelObjectID) as? LinkedItemLabel {
|
||||
labels.insert(label, at: 0)
|
||||
unselectedLabels.insert(label, at: 0)
|
||||
selectedLabels.insert(label)
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
|
|
@ -120,8 +105,6 @@ import Views
|
|||
func deleteLabel(dataService: DataService, labelID: String, name: String) {
|
||||
dataService.removeLabel(labelID: labelID, name: name)
|
||||
labels.removeAll { $0.name == name }
|
||||
selectedLabels.removeAll { $0.name == name }
|
||||
unselectedLabels.removeAll { $0.name == name }
|
||||
}
|
||||
|
||||
func saveItemLabelChanges(itemID: String, dataService: DataService) {
|
||||
|
|
@ -131,14 +114,4 @@ import Views
|
|||
func saveHighlightLabelChanges(highlightID: String, dataService: DataService) {
|
||||
dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: selectedLabels.map(\.unwrappedID))
|
||||
}
|
||||
|
||||
func addLabelToItem(_ label: LinkedItemLabel) {
|
||||
selectedLabels.insert(label, at: 0)
|
||||
unselectedLabels.removeAll { $0.name == label.name }
|
||||
}
|
||||
|
||||
func removeLabelFromItem(_ label: LinkedItemLabel) {
|
||||
unselectedLabels.insert(label, at: 0)
|
||||
selectedLabels.removeAll { $0.name == label.name }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ struct WebReader: PlatformViewRepresentable {
|
|||
(webView as? OmnivoreWebView)?.updateTextContrast()
|
||||
(webView as? OmnivoreWebView)?.updateMaxWidthPercentage()
|
||||
(webView as? OmnivoreWebView)?.updateLineHeight()
|
||||
(webView as? OmnivoreWebView)?.updateLabels(labelsJSON: item.labelsJSONString)
|
||||
}
|
||||
|
||||
if showNavBarActionID != context.coordinator.previousShowNavBarActionID {
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ struct WebReaderContainerView: View {
|
|||
}
|
||||
|
||||
func menuItems(for item: LinkedItem) -> some View {
|
||||
let hasLabels = item.labels?.count == 0
|
||||
let hasLabels = item.labels?.count != 0
|
||||
return Group {
|
||||
Button(
|
||||
action: { showHighlightsView = true },
|
||||
|
|
@ -295,7 +295,11 @@ struct WebReaderContainerView: View {
|
|||
Button(LocalText.cancelGeneric, role: .cancel, action: {})
|
||||
}
|
||||
.sheet(isPresented: $showLabelsModal) {
|
||||
ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false })
|
||||
ApplyLabelsView(mode: .item(item), isSearchFocused: false, onSave: { labels in
|
||||
showLabelsModal = false
|
||||
item.labels = NSSet(array: labels)
|
||||
readerSettingsChangedTransactionID = UUID()
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showTitleEdit) {
|
||||
LinkedItemMetadataEditView(item: item)
|
||||
|
|
@ -410,7 +414,7 @@ struct WebReaderContainerView: View {
|
|||
}
|
||||
.sheet(isPresented: $showHighlightLabelsModal) {
|
||||
if let highlight = Highlight.lookup(byID: self.annotation, inContext: self.dataService.viewContext) {
|
||||
ApplyLabelsView(mode: .highlight(highlight)) { selectedLabels in
|
||||
ApplyLabelsView(mode: .highlight(highlight), isSearchFocused: false) { selectedLabels in
|
||||
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
|
||||
labelIDs: selectedLabels.map(\.unwrappedID),
|
||||
dataService: dataService)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ public extension Highlight {
|
|||
return highlight
|
||||
}
|
||||
|
||||
var sortedLabels: [LinkedItemLabel] {
|
||||
labels.asArray(of: LinkedItemLabel.self).sorted {
|
||||
($0.name ?? "").lowercased() < ($1.name ?? "").lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
func update(
|
||||
inContext context: NSManagedObjectContext,
|
||||
newAnnotation: String
|
||||
|
|
|
|||
|
|
@ -15,7 +15,30 @@
|
|||
]
|
||||
|
||||
if let heightRatio = heightRatio {
|
||||
constraints.append(child.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: heightRatio))
|
||||
let constraint = child.view.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: heightRatio)
|
||||
constraints.append(constraint)
|
||||
|
||||
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main) { _ in
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
if let parent = self.parent, let frame = constraint.firstItem?.frame {
|
||||
constraint.constant = parent.view.frame.height - frame.height - 10
|
||||
}
|
||||
|
||||
child.view.setNeedsLayout()
|
||||
child.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: OperationQueue.main) { _ in
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
constraint.constant = 0
|
||||
child.view.setNeedsLayout()
|
||||
child.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
constraints.append(child.view.topAnchor.constraint(equalTo: view.topAnchor))
|
||||
}
|
||||
|
|
@ -24,6 +47,23 @@
|
|||
|
||||
child.didMove(toParent: self)
|
||||
}
|
||||
//
|
||||
// @objc func keyboardWillShow(notification: Notification) {
|
||||
// if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
|
||||
// if self.view.frame.origin.y == 0{
|
||||
// self.view.frame.origin.y -= keyboardSize.height
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @objc func keyboardWillHide(notification: Notification) {
|
||||
// if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
|
||||
// if self.view.frame.origin.y != 0 {
|
||||
// self.view.frame.origin.y += keyboardSize.height
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,14 @@ public final class OmnivoreWebView: WKWebView {
|
|||
}
|
||||
}
|
||||
|
||||
public func updateLabels(labelsJSON: String) {
|
||||
do {
|
||||
try dispatchEvent(.updateLabels(labels: labelsJSON))
|
||||
} catch {
|
||||
showErrorInSnackbar("Error updating labels")
|
||||
}
|
||||
}
|
||||
|
||||
public func shareOriginalItem() {
|
||||
do {
|
||||
try dispatchEvent(.share)
|
||||
|
|
@ -379,6 +387,7 @@ public enum WebViewDispatchEvent {
|
|||
case copyHighlight
|
||||
case dismissHighlight
|
||||
case speakingSection(anchorIdx: String)
|
||||
case updateLabels(labels: String)
|
||||
|
||||
var script: String {
|
||||
get throws {
|
||||
|
|
@ -421,6 +430,8 @@ public enum WebViewDispatchEvent {
|
|||
return "dismissHighlight"
|
||||
case .speakingSection:
|
||||
return "speakingSection"
|
||||
case .updateLabels:
|
||||
return "updateLabels"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -441,6 +452,8 @@ public enum WebViewDispatchEvent {
|
|||
return "event.isDark = '\(isDark)';"
|
||||
case let .updateFontFamily(family: family):
|
||||
return "event.fontFamily = '\(family)';"
|
||||
case let .updateLabels(labels):
|
||||
return "event.labels = \(labels);"
|
||||
case let .saveAnnotation(annotation: annotation):
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(annotation) {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x34",
|
||||
"green" : "0xD2",
|
||||
"red" : "0xFF"
|
||||
"blue" : "0x57",
|
||||
"green" : "0xD4",
|
||||
"red" : "0xF8"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ public extension Font {
|
|||
}
|
||||
|
||||
/// 12pt, Inter-Regular
|
||||
static var appCaptionBold: Font {
|
||||
.customFont(InterFont.bold.rawValue, size: 12, relativeTo: .caption)
|
||||
static var appCaptionMedium: Font {
|
||||
.customFont(InterFont.medium.rawValue, size: 12, relativeTo: .caption)
|
||||
}
|
||||
|
||||
/// 11pt, Inter-Regular
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
"labels.generic" = "Labels";
|
||||
"emails.generic" = "Emails";
|
||||
"subscriptions.generic" = "Text to Speech";
|
||||
"textToSpeech.generic" = "Subscriptions";
|
||||
"subscriptions.generic" = "Subscriptions";
|
||||
"textToSpeech.generic" = "Text to Speech";
|
||||
"privacy.policy.generic" = "Privacy Policy";
|
||||
"termsAndConditions.generic" = "Terms and Conditions";
|
||||
"feedback.generic" = "Feedback";
|
||||
|
|
|
|||
|
|
@ -1,62 +1,59 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct SearchBar: View {
|
||||
private let horizontalPadding: Double
|
||||
@Binding var searchTerm: String
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
public init(
|
||||
searchTerm: Binding<String>,
|
||||
horizontalPadding: Double = 10
|
||||
searchTerm: Binding<String>
|
||||
) {
|
||||
self._searchTerm = searchTerm
|
||||
self.horizontalPadding = horizontalPadding
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
TextField("Search", text: $searchTerm)
|
||||
.padding(7)
|
||||
.padding(.horizontal, 25)
|
||||
.background(Color.systemGray6)
|
||||
.cornerRadius(8)
|
||||
.frame(height: 36)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.leading, 28)
|
||||
.padding(.trailing, 28)
|
||||
.focused($isFocused)
|
||||
.overlay(
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 10)
|
||||
.resizable()
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(.appGrayText)
|
||||
.padding(.leading, 8)
|
||||
|
||||
if self.searchTerm != "" {
|
||||
Button(
|
||||
action: {
|
||||
self.searchTerm = ""
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.gray)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
|
||||
if isFocused {
|
||||
Button(
|
||||
action: {
|
||||
self.searchTerm = ""
|
||||
self.isFocused = false
|
||||
},
|
||||
label: {
|
||||
Text(LocalText.cancelGeneric)
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
)
|
||||
.padding(.trailing, 10)
|
||||
.padding(.trailing, 8)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
}
|
||||
.background(Color.appButtonBackground)
|
||||
.cornerRadius(8)
|
||||
.frame(height: 36)
|
||||
.onChange(of: isFocused) { isFocused in
|
||||
if !isFocused {
|
||||
searchTerm = ""
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
isFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ public struct TextChip: View {
|
|||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
let checked: Bool
|
||||
let padded: Bool
|
||||
var onTap: ((TextChip) -> Void)?
|
||||
|
||||
public init(text: String, color: Color, negated: Bool = false) {
|
||||
|
|
@ -13,6 +14,7 @@ public struct TextChip: View {
|
|||
self.color = color
|
||||
self.negated = negated
|
||||
self.checked = false
|
||||
self.padded = false
|
||||
}
|
||||
|
||||
public init?(feedItemLabel: LinkedItemLabel, negated: Bool = false) {
|
||||
|
|
@ -22,9 +24,10 @@ public struct TextChip: View {
|
|||
self.color = color
|
||||
self.negated = negated
|
||||
self.checked = false
|
||||
self.padded = false
|
||||
}
|
||||
|
||||
public init?(feedItemLabel: LinkedItemLabel, negated: Bool = false, checked: Bool = false, onTap: ((TextChip) -> Void)?) {
|
||||
public init?(feedItemLabel: LinkedItemLabel, negated: Bool = false, checked: Bool = false, padded: Bool = false, onTap: ((TextChip) -> Void)?) {
|
||||
guard let color = Color(hex: feedItemLabel.color ?? "") else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -34,6 +37,7 @@ public struct TextChip: View {
|
|||
self.negated = negated
|
||||
self.onTap = onTap
|
||||
self.checked = checked
|
||||
self.padded = padded
|
||||
}
|
||||
|
||||
public let text: String
|
||||
|
|
@ -45,37 +49,28 @@ public struct TextChip: View {
|
|||
return .white
|
||||
}
|
||||
|
||||
if colorScheme == .light {
|
||||
return luminance > 0.5 ? .black : .white
|
||||
}
|
||||
|
||||
if luminance > 0.2 {
|
||||
return color
|
||||
}
|
||||
|
||||
// lighten the color by 20%
|
||||
return Color.lighten(color: color, by: 20)
|
||||
return luminance > 0.5 ? .black : .white
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
color.opacity(colorScheme == .dark ? 0.2 : 1)
|
||||
color.opacity(0.9)
|
||||
}
|
||||
|
||||
var checkedBorderColor: Color {
|
||||
colorScheme == .dark ? Color.white : Color.black
|
||||
}
|
||||
|
||||
var borderColor: Color {
|
||||
if colorScheme == .dark {
|
||||
return textColor
|
||||
} else {
|
||||
return color.opacity(0.7)
|
||||
}
|
||||
checked ? checkedBorderColor : Color.clear
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Text(text)
|
||||
.strikethrough(color: negated ? textColor : .clear)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 5)
|
||||
.font(.appCaptionBold)
|
||||
.padding(.horizontal, padded ? 10 : 8)
|
||||
.padding(.vertical, padded ? 8 : 5)
|
||||
.font(.appCaptionMedium)
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
.background(
|
||||
|
|
@ -84,18 +79,9 @@ public struct TextChip: View {
|
|||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(borderColor, lineWidth: 1)
|
||||
.stroke(borderColor, lineWidth: 2)
|
||||
)
|
||||
.padding(1)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if checked {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.appBody)
|
||||
.symbolVariant(.circle.fill)
|
||||
.foregroundStyle(Color.appBackground, Color.appGreenSuccess)
|
||||
.padding([.top, .trailing], -6)
|
||||
}
|
||||
}
|
||||
}.onTapGesture {
|
||||
if let onTap = onTap {
|
||||
onTap(self)
|
||||
|
|
|
|||
|
|
@ -50,8 +50,14 @@ const mutation = async (name, input) => {
|
|||
}
|
||||
|
||||
const App = () => {
|
||||
const [labels, setLabels] = React.useState(window.omnivoreArticle.labels)
|
||||
applyStoredTheme(false)
|
||||
|
||||
document.addEventListener('updateLabels', (event) => {
|
||||
console.log("updating labels: ", event.labels)
|
||||
setLabels(event.labels)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
|
|
@ -68,7 +74,7 @@ const App = () => {
|
|||
>
|
||||
<ArticleContainer
|
||||
article={window.omnivoreArticle}
|
||||
labels={window.omnivoreArticle.labels}
|
||||
labels={labels}
|
||||
isAppleAppEmbed={true}
|
||||
highlightBarDisabled={!window.enableHighlightBar}
|
||||
highlightsBaseURL="https://example.com"
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ Readability.prototype = {
|
|||
// Readability-readerable.js. Please keep both copies in sync.
|
||||
articleNegativeLookBehindCandidates: /breadcrumbs|breadcrumb|utils|trilist/i,
|
||||
articleNegativeLookAheadCandidates: /outstream(.?)_|sub(.?)_|m_|omeda-promo-|in-article-advert|block-ad-.*/i,
|
||||
unlikelyCandidates: /\bad\b|ai2html|banner|breadcrumbs|breadcrumb|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager(?!ow)|popup|yom-remote|copyright|keywords|outline|infinite-list|beta|recirculation|site-index|hide-for-print|post-end-share-cta|post-end-cta-full|post-footer|post-head|post-tag|li-date|main-navigation|programtic-ads|outstream_article|hfeed|comment-holder|back-to-top|show-up-next|onward-journey|topic-tracker|list-nav|block-ad-entity|adSpecs|gift-article-button|modal-title|in-story-masthead|share-tools|standard-dock|expanded-dock|margins-h|subscribe-dialog|icon|bumped|dvz-social-media-buttons/i,
|
||||
unlikelyCandidates: /\bad\b|ai2html|banner|breadcrumbs|breadcrumb|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager(?!ow)|popup|yom-remote|copyright|keywords|outline|infinite-list|beta|recirculation|site-index|hide-for-print|post-end-share-cta|post-end-cta-full|post-footer|post-head|post-tag|li-date|main-navigation|programtic-ads|outstream_article|hfeed|comment-holder|back-to-top|show-up-next|onward-journey|topic-tracker|list-nav|block-ad-entity|adSpecs|gift-article-button|modal-title|in-story-masthead|share-tools|standard-dock|expanded-dock|margins-h|subscribe-dialog|icon|bumped|dvz-social-media-buttons|post-toc|mobile-menu|mobile-navbar/i,
|
||||
// okMaybeItsACandidate: /and|article(?!-breadcrumb)|body|column|content|main|shadow|post-header/i,
|
||||
get okMaybeItsACandidate() {
|
||||
return new RegExp(`and|(?<!${this.articleNegativeLookAheadCandidates.source})article(?!-(${this.articleNegativeLookBehindCandidates.source}))|body|column|content|^(?!main-navigation|main-header)main|shadow|post-header|hfeed site|blog-posts hfeed|container-banners|menu-opacity|header-with-anchor-widget`, 'i')
|
||||
|
|
|
|||
Loading…
Reference in a new issue