Media browser tweaks (#3692)
* Move the media actions from the bottom bar into the details sheet. * Allow the media type picker to fill the width of the screen.
This commit is contained in:
@@ -9,46 +9,26 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
/// Reads the frame of the view and store it in `frame` binding.
|
||||
/// Reads the frame of the view and stores it in the `frame` binding.
|
||||
/// - Parameters:
|
||||
/// - frame: a `CGRect` binding
|
||||
/// - coordinateSpace: the coordinate space of the frame.
|
||||
func readFrame(_ frame: Binding<CGRect>, in coordinateSpace: CoordinateSpace = .local) -> some View {
|
||||
background(ViewFrameReader(frame: frame, coordinateSpace: coordinateSpace))
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to calculate the frame of a view.
|
||||
///
|
||||
/// Useful in situations as with `ZStack` where you might want to layout views using alignment guides.
|
||||
/// ```
|
||||
/// @State private var frame: CGRect = CGRect.zero
|
||||
/// ...
|
||||
/// SomeView()
|
||||
/// .background(ViewFrameReader(frame: $frame))
|
||||
/// ```
|
||||
private struct ViewFrameReader: View {
|
||||
@Binding var frame: CGRect
|
||||
var coordinateSpace: CoordinateSpace = .local
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
Color.clear
|
||||
.preference(key: FramePreferenceKey.self,
|
||||
value: geometry.frame(in: coordinateSpace))
|
||||
onGeometryChange(for: CGRect.self) { geometry in
|
||||
geometry.frame(in: coordinateSpace)
|
||||
} action: { newValue in
|
||||
frame.wrappedValue = newValue
|
||||
}
|
||||
.onPreferenceChange(FramePreferenceKey.self) { newValue in
|
||||
guard frame != newValue else { return }
|
||||
frame = newValue
|
||||
}
|
||||
|
||||
/// Reads the height of the view and stores it in the `height` binding.
|
||||
/// - Parameters:
|
||||
/// - height: a `CGFloat` binding
|
||||
func readHeight(_ height: Binding<CGFloat>) -> some View {
|
||||
onGeometryChange(for: CGFloat.self) { geometry in
|
||||
geometry.size.height
|
||||
} action: { newValue in
|
||||
height.wrappedValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A SwiftUI `PreferenceKey` for `CGRect` values such as a view's frame.
|
||||
private struct FramePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGRect = .zero
|
||||
|
||||
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,6 @@ class TimelineMediaPreviewItem: NSObject, QLPreviewItem, Identifiable {
|
||||
|
||||
enum TimelineMediaPreviewViewAction {
|
||||
case updateCurrentItem(TimelineMediaPreviewItem)
|
||||
case saveCurrentItem
|
||||
case showCurrentItemDetails
|
||||
case menuAction(TimelineItemMenuAction, item: TimelineMediaPreviewItem)
|
||||
case redactConfirmation(item: TimelineMediaPreviewItem)
|
||||
|
||||
@@ -59,14 +59,14 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
switch viewAction {
|
||||
case .updateCurrentItem(let item):
|
||||
Task { await updateCurrentItem(item) }
|
||||
case .saveCurrentItem:
|
||||
Task { await saveCurrentItem() }
|
||||
case .showCurrentItemDetails:
|
||||
state.bindings.mediaDetailsItem = state.currentItem
|
||||
case .menuAction(let action, let item):
|
||||
switch action {
|
||||
case .viewInRoomTimeline:
|
||||
actionsSubject.send(.viewInRoomTimeline(item.id))
|
||||
case .save:
|
||||
Task { await saveCurrentItem() }
|
||||
case .redact:
|
||||
state.bindings.redactConfirmationItem = item
|
||||
default:
|
||||
@@ -119,6 +119,9 @@ class TimelineMediaPreviewViewModel: TimelineMediaPreviewViewModelType {
|
||||
return
|
||||
}
|
||||
|
||||
// Dismiss the details sheet (nicer flow for images/video but _required_ in order to select a file directory).
|
||||
state.bindings.mediaDetailsItem = nil
|
||||
|
||||
do {
|
||||
switch state.currentItem.timelineItem {
|
||||
case is AudioRoomTimelineItem, is FileRoomTimelineItem:
|
||||
|
||||
@@ -12,6 +12,9 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
let item: TimelineMediaPreviewItem
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@State private var sheetHeight: CGFloat = .zero
|
||||
private let topPadding: CGFloat = 19
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@@ -19,10 +22,12 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
actions
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.readHeight($sheetHeight)
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.padding(.top, topPadding) // For the drag indicator
|
||||
.presentationDetents([.height(sheetHeight + topPadding)])
|
||||
.presentationDragIndicator(.visible)
|
||||
.padding(.top, 19) // For the drag indicator
|
||||
.presentationBackground(.compound.bgCanvasDefault)
|
||||
.preferredColorScheme(.dark)
|
||||
.sheet(item: $context.redactConfirmationItem) { item in
|
||||
@@ -95,12 +100,7 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
}
|
||||
|
||||
ForEach(actions.actions, id: \.self) { action in
|
||||
Button(role: action.isDestructive ? .destructive : nil) {
|
||||
context.send(viewAction: .menuAction(action, item: item))
|
||||
} label: {
|
||||
action.label
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
ActionButton(item: item, action: action, context: context)
|
||||
}
|
||||
|
||||
if !actions.secondaryActions.isEmpty {
|
||||
@@ -109,12 +109,7 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
}
|
||||
|
||||
ForEach(actions.secondaryActions, id: \.self) { action in
|
||||
Button(role: action.isDestructive ? .destructive : nil) {
|
||||
context.send(viewAction: .menuAction(action, item: item))
|
||||
} label: {
|
||||
action.label
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
ActionButton(item: item, action: action, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +130,38 @@ struct TimelineMediaPreviewDetailsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ActionButton: View {
|
||||
let item: TimelineMediaPreviewItem
|
||||
let action: TimelineItemMenuAction
|
||||
let context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
if action == .share {
|
||||
if let itemURL = item.fileHandle?.url {
|
||||
ShareLink(item: itemURL, message: item.caption.map(Text.init)) {
|
||||
action.label
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
}
|
||||
} else if action == .save {
|
||||
if item.fileHandle?.url != nil {
|
||||
button
|
||||
}
|
||||
} else {
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
var button: some View {
|
||||
Button(role: action.isDestructive ? .destructive : nil) {
|
||||
context.send(viewAction: .menuAction(action, item: item))
|
||||
} label: {
|
||||
action.label
|
||||
}
|
||||
.buttonStyle(.menuSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
@@ -145,6 +172,7 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
@Namespace private static var previewNamespace
|
||||
|
||||
static let viewModel = makeViewModel(contentType: .jpeg, isOutgoing: true)
|
||||
static let loadingViewModel = makeViewModel(contentType: .jpeg, isOutgoing: true, isDownloaded: false)
|
||||
static let unknownTypeViewModel = makeViewModel()
|
||||
static let presentedOnRoomViewModel = makeViewModel(isPresentedOnRoomScreen: true)
|
||||
|
||||
@@ -156,6 +184,13 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||
})
|
||||
|
||||
TimelineMediaPreviewDetailsView(item: loadingViewModel.state.currentItem,
|
||||
context: loadingViewModel.context)
|
||||
.previewDisplayName("Loading")
|
||||
.snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in
|
||||
state.currentItemActions?.secondaryActions.contains(.redact) ?? false
|
||||
})
|
||||
|
||||
TimelineMediaPreviewDetailsView(item: unknownTypeViewModel.state.currentItem,
|
||||
context: unknownTypeViewModel.context)
|
||||
.previewDisplayName("Unknown type")
|
||||
@@ -165,7 +200,10 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
.previewDisplayName("Incoming on Room")
|
||||
}
|
||||
|
||||
static func makeViewModel(contentType: UTType? = nil, isOutgoing: Bool = false, isPresentedOnRoomScreen: Bool = false) -> TimelineMediaPreviewViewModel {
|
||||
static func makeViewModel(contentType: UTType? = nil,
|
||||
isOutgoing: Bool = false,
|
||||
isDownloaded: Bool = true,
|
||||
isPresentedOnRoomScreen: Bool = false) -> TimelineMediaPreviewViewModel {
|
||||
let item = ImageRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: isOutgoing,
|
||||
@@ -183,13 +221,20 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
|
||||
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen)
|
||||
let timelineController = MockRoomTimelineController(timelineKind: timelineKind)
|
||||
timelineController.timelineItems = [item]
|
||||
return TimelineMediaPreviewViewModel(context: .init(item: item,
|
||||
viewModel: TimelineViewModel.mock(timelineKind: timelineKind,
|
||||
timelineController: timelineController),
|
||||
namespace: previewNamespace),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
photoLibraryManager: PhotoLibraryManagerMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appMediator: AppMediatorMock())
|
||||
|
||||
let viewModel = TimelineMediaPreviewViewModel(context: .init(item: item,
|
||||
viewModel: TimelineViewModel.mock(timelineKind: timelineKind,
|
||||
timelineController: timelineController),
|
||||
namespace: previewNamespace),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||
photoLibraryManager: PhotoLibraryManagerMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock(),
|
||||
appMediator: AppMediatorMock())
|
||||
|
||||
if isDownloaded {
|
||||
viewModel.context.send(viewAction: .updateCurrentItem(viewModel.state.currentItem))
|
||||
}
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
let item: TimelineMediaPreviewItem
|
||||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
|
||||
|
||||
@State private var sheetHeight: CGFloat = .zero
|
||||
private let topPadding: CGFloat = 19
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
@@ -21,10 +24,12 @@ struct TimelineMediaPreviewRedactConfirmationView: View {
|
||||
preview
|
||||
buttons
|
||||
}
|
||||
.readHeight($sheetHeight)
|
||||
}
|
||||
.presentationDetents([.medium])
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.padding(.top, topPadding) // For the drag indicator
|
||||
.presentationDetents([.height(sheetHeight + topPadding)])
|
||||
.presentationDragIndicator(.visible)
|
||||
.padding(.top, 19) // For the drag indicator
|
||||
.presentationBackground(.compound.bgCanvasDefault)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
@@ -50,9 +50,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
.overlay { downloadStatusIndicator }
|
||||
.toolbar { toolbar }
|
||||
.toolbar(toolbarVisibility, for: .navigationBar)
|
||||
.toolbar(toolbarVisibility, for: .bottomBar)
|
||||
.toolbarBackground(.visible, for: .navigationBar) // The toolbar's scrollEdgeAppearance isn't aware of the quicklook view 🤷♂️
|
||||
.toolbarBackground(.visible, for: .bottomBar)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) { caption }
|
||||
}
|
||||
@@ -112,6 +110,7 @@ struct TimelineMediaPreviewScreen: View {
|
||||
.padding(16)
|
||||
.background {
|
||||
BlurEffectView(style: .systemChromeMaterial) // Darkest material available, matches the bottom bar when content is beneath.
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
@@ -137,11 +136,6 @@ struct TimelineMediaPreviewScreen: View {
|
||||
}
|
||||
.tint(.compound.textActionPrimary)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .bottomBar) {
|
||||
bottomBarContent
|
||||
.tint(.compound.textActionPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
private var toolbarHeader: some View {
|
||||
@@ -155,22 +149,6 @@ struct TimelineMediaPreviewScreen: View {
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomBarContent: some View {
|
||||
HStack(spacing: 8) {
|
||||
if let url = currentItem.fileHandle?.url {
|
||||
ShareLink(item: url, subject: nil, message: currentItem.caption.map(Text.init)) {
|
||||
CompoundIcon(\.shareIos)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button { context.send(viewAction: .saveCurrentItem) } label: {
|
||||
CompoundIcon(\.downloadIos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - QuickLook
|
||||
|
||||
@@ -19,19 +19,7 @@ struct MediaEventsTimelineScreen: View {
|
||||
.background(.compound.bgCanvasDefault)
|
||||
// Doesn't play well with the transformed scrollView
|
||||
.toolbarBackground(.visible, for: .navigationBar)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Picker("", selection: $context.screenMode) {
|
||||
Text(L10n.screenMediaBrowserListModeMedia)
|
||||
.padding()
|
||||
.tag(MediaEventsTimelineScreenMode.media)
|
||||
Text(L10n.screenMediaBrowserListModeFiles)
|
||||
.padding()
|
||||
.tag(MediaEventsTimelineScreenMode.files)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
}
|
||||
.toolbar { toolbar }
|
||||
.environmentObject(context.viewState.activeTimelineContextProvider())
|
||||
.environment(\.timelineContext, context.viewState.activeTimelineContextProvider())
|
||||
.onChange(of: context.screenMode) { _, _ in
|
||||
@@ -206,6 +194,27 @@ struct MediaEventsTimelineScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .principal) {
|
||||
Picker("", selection: $context.screenMode) {
|
||||
Text(L10n.screenMediaBrowserListModeMedia)
|
||||
.padding()
|
||||
.tag(MediaEventsTimelineScreenMode.media)
|
||||
Text(L10n.screenMediaBrowserListModeFiles)
|
||||
.padding()
|
||||
.tag(MediaEventsTimelineScreenMode.files)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(idealWidth: .greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
// Reserve the space trailing space to match the back button.
|
||||
CompoundIcon(\.search).hidden()
|
||||
}
|
||||
}
|
||||
|
||||
func tappedItem(_ item: RoomTimelineItemViewState) {
|
||||
context.send(viewAction: .tappedItem(item: item, namespace: zoomTransition))
|
||||
}
|
||||
|
||||
@@ -180,6 +180,10 @@ class TimelineInteractionHandler {
|
||||
analyticsService.trackInteraction(name: .PinnedMessageListViewTimeline)
|
||||
guard let eventID = itemID.eventID else { return }
|
||||
actionsSubject.send(.viewInRoomTimeline(eventID: eventID))
|
||||
case .share:
|
||||
break // Handled inline in the media preview screen with a ShareLink.
|
||||
case .save:
|
||||
break // Handled inline in the media preview screen.
|
||||
}
|
||||
|
||||
if action.switchToDefaultComposer {
|
||||
|
||||
@@ -74,6 +74,8 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
case pin
|
||||
case unpin
|
||||
case viewInRoomTimeline
|
||||
case share
|
||||
case save
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
@@ -128,7 +130,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
|
||||
var canAppearInMediaDetails: Bool {
|
||||
switch self {
|
||||
case .viewInRoomTimeline, .redact:
|
||||
case .viewInRoomTimeline, .share, .save, .redact:
|
||||
true
|
||||
default:
|
||||
false
|
||||
@@ -178,6 +180,10 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
Label(L10n.actionUnpin, icon: \.unpin)
|
||||
case .viewInRoomTimeline:
|
||||
Label(L10n.actionViewInTimeline, icon: \.visibilityOn)
|
||||
case .share:
|
||||
Label(L10n.actionShare, icon: \.shareIos)
|
||||
case .save:
|
||||
Label(L10n.actionSave, icon: \.downloadIos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,8 @@ struct TimelineItemMenuActionProvider {
|
||||
actions = actions.filter(\.canAppearInPinnedEventsTimeline)
|
||||
secondaryActions = secondaryActions.filter(\.canAppearInPinnedEventsTimeline)
|
||||
case .media:
|
||||
actions.append(.share)
|
||||
actions.append(.save)
|
||||
actions = actions.filter(\.canAppearInMediaDetails)
|
||||
secondaryActions = secondaryActions.filter(\.canAppearInMediaDetails)
|
||||
case .live, .detached:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eb3769ea66b8dc7312847f35ee71979324ed2f209a295ee0953afee377eb2176
|
||||
size 531223
|
||||
oid sha256:f37dab56ed2a7f3694246ecffd6576edf989417a6bb1035d1792739240a57073
|
||||
size 531168
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dfa699d21838219f07d90ae7e0b2e1e28cd9dfc96ce7c47eb6c87d5a421c3e4c
|
||||
size 524500
|
||||
oid sha256:b22f900e7f01932ed4496765925410071b353acb26b23418f75b63bdd117e870
|
||||
size 525956
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:51741b607cd001a1a7e8ce7ec03cdcbd35bc294a9b9bb76e05469561542a38f4
|
||||
size 146263
|
||||
oid sha256:55c63245de93fd3f295d495159923d9cdad7cb08774ac3de8c857a44913f8e55
|
||||
size 146447
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c63ab40372661a448449f81e772cff9e05615a5ed4f027996984a46ee201bf5
|
||||
size 779262
|
||||
oid sha256:b8fd8ada4138ebdd81900ae1ccfc10cc38bc44b53fab3f4d78b027df2a9b380c
|
||||
size 779410
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cbf40a036347c7da245c280264420c77f47b924f42027c5639b47dd59648182d
|
||||
size 548028
|
||||
oid sha256:b64b58f1f7865362bd3e98c978232f82e34c9ebb4a823e81d37f01a29c4f934a
|
||||
size 547715
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d600dd3d02c2c90a01e7536d6c68095e8bb666cd84ed4750b63296bde09b1dee
|
||||
size 539182
|
||||
oid sha256:67d030761a129b039731dd76ea6d5bddf03c3e9b4171562c3f6d6967cab8becf
|
||||
size 540819
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63987ffb9a91d412c7bc6dc813230d21223ae5b91781daede3acbf624ba4f752
|
||||
size 148838
|
||||
oid sha256:ff5bbb6dd0962f5246e39c9b38084a3e7d1ba320090bf47e79538f03e12a798d
|
||||
size 148498
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3d9a1af757a18d9ab7d3ff8d7ac18a29a41ca54f61a6ffc5bea5fdf5692fe8ed
|
||||
size 786023
|
||||
oid sha256:f495b919bbecfd4951393958da9dc400400c5d0a7d149e980f1cbd4cdc7486f8
|
||||
size 786163
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2ce1c43361e481251f9ee846ed645ae89f1de9442a8c52cd5ef1862b860fb91a
|
||||
size 341032
|
||||
oid sha256:2c93bab1debe8f92cbad2395d17631092eb6cde07795de02445ebbdee6a5d9a3
|
||||
size 341105
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c620f731b5af87b9b7477f717ca38f9b556a49d890bc5a78634d27f49eb27b9
|
||||
size 337316
|
||||
oid sha256:46257ee5140acc94eb5f089a2005e3d59db993650910c1e790630fdb723be1a3
|
||||
size 338390
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cfce1624b822fabe439d77f47fcedfbaab78b4b9f5dc20518c47d315f097dcd4
|
||||
size 87008
|
||||
oid sha256:6c9bc334c55493ebf517d89d146846856d98b7031e7dc97685a94df9ac0d6b3a
|
||||
size 87020
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71d2fe4ff56b2f7f457f341570aa9b55a61cd699f3969a3841ca8757b0529403
|
||||
size 857564
|
||||
oid sha256:eeb02f9b207d832cf22cdf5b41263c848de8d6033921b2c75a82b111d3d2af4b
|
||||
size 857630
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b407d22285115eedcc18300d3227b0834fd5d779f5f1c022dff72a4866f4a476
|
||||
size 355876
|
||||
oid sha256:27ad4f8bbd00dd05f355c45608afdb8a7394bb2162ea5b8cc3b050040cf0e62e
|
||||
size 355765
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12fe2c7958a06fbe01ee62ba85289c9fad10263aa86f9639dd50dc741776ba9e
|
||||
size 348701
|
||||
oid sha256:518bc53d8fc2da6cefdf604b6e4f2519242c2504002114d282d7e9a2b28b22d5
|
||||
size 349972
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa188dd55c8b9cdd46318ab24209a8458a61abe11b9c52fc05086724c4b7fb1d
|
||||
size 88770
|
||||
oid sha256:aa9e0604a29c55902623b10044f4ea1a4f6ced7ad21480bbc0526ab762e88449
|
||||
size 89081
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cebdb3063cf19ef4fe6194c6d6cb014bed63daf15580ba7cfdaf1f2ea04d117e
|
||||
size 863167
|
||||
oid sha256:3893b2e227eb259571120f24dcbcd43c0d82f4704eab6a3aead6202d8e5b1324
|
||||
size 863291
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a30bdebdc1c816e6ca11cc4259749ec3e7f873e89c38e5cc64b23b5b7dcf7ebb
|
||||
size 129684
|
||||
oid sha256:b1c4360730c31f9125d3d2ace4a4d5ede3ad17a01cbcf8da620feae58545085e
|
||||
size 133843
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f6ff841733941f55b065477a234c318de67cc068870ea560d8650093556e385
|
||||
size 106118
|
||||
oid sha256:3b07b8ae8634d772f23877f18975e7b6019b19622fefa38e6593b3751a53439c
|
||||
size 112211
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a30bdebdc1c816e6ca11cc4259749ec3e7f873e89c38e5cc64b23b5b7dcf7ebb
|
||||
size 129684
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a6045b90ca6ad9b362f26505caeda788504d98461a4bdc6003603f79ae25cc45
|
||||
size 112315
|
||||
oid sha256:3114ce6c98249458872ec5076306298695df84de12fed165a3e7b0ad1be97765
|
||||
size 118220
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90f32840b85ff12b6de880e8ab1890f4442b873fac71083093324fc02e9700d3
|
||||
size 128067
|
||||
oid sha256:67fb1a7177254ea5b137368245b7b2962aa1bd0a8cdf5b3ff0477cef431e0639
|
||||
size 132201
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7a1466df7ac398bb7f47bad54aeb8f6a3c6fb7ed29bcfa839feb4c39969afe19
|
||||
size 106123
|
||||
oid sha256:068e113b9440d414839d0696af336209a6c75489fc6177f18aa83ddea5ac4041
|
||||
size 112050
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90f32840b85ff12b6de880e8ab1890f4442b873fac71083093324fc02e9700d3
|
||||
size 128067
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3ac674ce8b1e9f8c508ed259d9dc50a4b58f2304d9a3e2d9903121676502091d
|
||||
size 111333
|
||||
oid sha256:9f5c34a012c55ebfe74c6b3be1d75297471ec783c085659af323656ef73ca405
|
||||
size 118554
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:339faa15c493a6e36f0c567b89eb6d036010c5637a0be03f15f7915306612320
|
||||
size 82212
|
||||
oid sha256:ea2a8badcc11edd0df3aa761aa77d9230a2db26af51b543c30d3543984e2a2f5
|
||||
size 87189
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:444149b32adb1da10c83c3f6a19da8bff11b26be4cda4a88e90d5ed50ac61b3d
|
||||
size 59813
|
||||
oid sha256:2a5123a501ee8f02590e4d900bc334e2d6d8b5744597fe0fbf4624f659f9ac30
|
||||
size 67791
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:339faa15c493a6e36f0c567b89eb6d036010c5637a0be03f15f7915306612320
|
||||
size 82212
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0cd944ccb9183bfa3c39e7a866b38835baa6c158338c74e05f816f99268d892e
|
||||
size 66083
|
||||
oid sha256:2f5609be4502573074d1d378651caeafa0706dc2667d297a0ce0bdc5e426afaf
|
||||
size 74240
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04241b16f28aa03f42de4e365eadf80fca0637494e891eb5c81a337d7693f476
|
||||
size 80107
|
||||
oid sha256:6cb794dd6e4980daa1a08dcdfedf2ac063afc0cd72cd8a4b478432fd5cf22a98
|
||||
size 85566
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce1be4e6bf0c06b2a1d0046d3c8eed0830292880365a10b048f02bdd15688246
|
||||
size 59774
|
||||
oid sha256:79219c3b45d512b459ae3024a5e6214a8b9841b4923096425df158c3cca6e46f
|
||||
size 66000
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04241b16f28aa03f42de4e365eadf80fca0637494e891eb5c81a337d7693f476
|
||||
size 80107
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:54f7a21d3defe67d94c06aa226b503fcc30de8717043e5ca13242c76a4272bc4
|
||||
size 64878
|
||||
oid sha256:fda0d3bb61b7c92457cd12259e1e3ff411ab5ce472d13de51c11e3f81b0958df
|
||||
size 72345
|
||||
|
||||
@@ -130,7 +130,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// When choosing to save the image.
|
||||
let item = context.viewState.currentItem
|
||||
context.send(viewAction: .saveCurrentItem)
|
||||
context.send(viewAction: .menuAction(.save, item: item))
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
// Then the image should be saved as a photo to the user's photo library.
|
||||
@@ -147,7 +147,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// When choosing to save the image.
|
||||
let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil }
|
||||
context.send(viewAction: .saveCurrentItem)
|
||||
context.send(viewAction: .menuAction(.save, item: context.viewState.currentItem))
|
||||
try await deferred.fulfill()
|
||||
|
||||
// Then the user should be prompted to allow access.
|
||||
@@ -163,7 +163,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// When choosing to save the video.
|
||||
let item = context.viewState.currentItem
|
||||
context.send(viewAction: .saveCurrentItem)
|
||||
context.send(viewAction: .menuAction(.save, item: item))
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
// Then the video should be saved as a video in the user's photo library.
|
||||
@@ -180,7 +180,7 @@ class TimelineMediaPreviewViewModelTests: XCTestCase {
|
||||
|
||||
// When choosing to save the file.
|
||||
let item = context.viewState.currentItem
|
||||
context.send(viewAction: .saveCurrentItem)
|
||||
context.send(viewAction: .menuAction(.save, item: item))
|
||||
try await Task.sleep(for: .seconds(0.5))
|
||||
|
||||
// Then the binding should be set for the user to export the file to their specified location.
|
||||
|
||||
Reference in New Issue
Block a user