Add favourite quick touch :)

This commit is contained in:
Marcin Czachursk 2023-02-02 17:35:41 +01:00
parent 9fbd39657e
commit ee3407dd69
6 changed files with 225 additions and 105 deletions

View File

@ -63,6 +63,7 @@
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B721D296C458700EE59EC /* BlurredImage.swift */; };
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7220296C49A300EE59EC /* EmptyButtonStyle.swift */; };
F86B7223296C4BF500EE59EC /* ContentWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7222296C4BF500EE59EC /* ContentWarning.swift */; };
F86FB555298BF83F000131F0 /* FavouriteTouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86FB554298BF83F000131F0 /* FavouriteTouch.swift */; };
F8764187298ABB520057D362 /* ViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8764186298ABB520057D362 /* ViewState.swift */; };
F8764189298ABEC80057D362 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8764188298ABEC80057D362 /* ErrorView.swift */; };
F876418B298AC1B80057D362 /* NoDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F876418A298AC1B80057D362 /* NoDataView.swift */; };
@ -184,6 +185,7 @@
F86B721D296C458700EE59EC /* BlurredImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredImage.swift; sourceTree = "<group>"; };
F86B7220296C49A300EE59EC /* EmptyButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyButtonStyle.swift; sourceTree = "<group>"; };
F86B7222296C4BF500EE59EC /* ContentWarning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarning.swift; sourceTree = "<group>"; };
F86FB554298BF83F000131F0 /* FavouriteTouch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavouriteTouch.swift; sourceTree = "<group>"; };
F8764186298ABB520057D362 /* ViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewState.swift; sourceTree = "<group>"; };
F8764188298ABEC80057D362 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
F876418A298AC1B80057D362 /* NoDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoDataView.swift; sourceTree = "<group>"; };
@ -407,6 +409,7 @@
F88E4D53297EA7EE0057491A /* MarkdownFormattedText.swift */,
F8764188298ABEC80057D362 /* ErrorView.swift */,
F876418A298AC1B80057D362 /* NoDataView.swift */,
F86FB554298BF83F000131F0 /* FavouriteTouch.swift */,
);
path = Widgets;
sourceTree = "<group>";
@ -679,6 +682,7 @@
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */,
F89D6C4429718092001DA3D4 /* AccentsSection.swift in Sources */,
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */,
F86FB555298BF83F000131F0 /* FavouriteTouch.swift in Sources */,
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
F8C5E55F2988E92600ADF6A7 /* AccountModel.swift in Sources */,
F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */,

View File

@ -11,12 +11,9 @@ struct HomeFeedView: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@State private var allItemsBottomLoaded = false
@State private var allItemsLoaded = false
@State private var state: ViewState = .loading
private static let initialColumns = 1
@State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns)
@FetchRequest var dbStatuses: FetchedResults<StatusData>
init(accountId: String) {
@ -26,11 +23,6 @@ struct HomeFeedView: View {
}
var body: some View {
self.mainBody()
}
@ViewBuilder
private func mainBody() -> some View {
switch state {
case .loading:
LoadingIndicator()
@ -41,55 +33,7 @@ struct HomeFeedView: View {
if self.dbStatuses.isEmpty {
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "Unfortunately, there are no photos here.")
} else {
ScrollView {
LazyVGrid(columns: gridColumns) {
ForEach(dbStatuses, id: \.self) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
self.upToDatePlaceholder()
}
NavigationLink(value: RouteurDestinations.status(
id: item.rebloggedStatusId ?? item.id,
blurhash: item.attachments().first?.blurhash,
highestImageUrl: item.attachments().getHighestImage()?.url,
metaImageWidth: item.attachments().first?.metaImageWidth,
metaImageHeight: item.attachments().first?.metaImageHeight)
) {
ImageRow(statusData: item)
}
.buttonStyle(EmptyButtonStyle())
}
if allItemsBottomLoaded == false {
LoadingIndicator()
.task {
do {
if let account = self.applicationState.account {
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account)
if newStatusesCount == 0 {
allItemsBottomLoaded = true
}
}
} catch {
ErrorService.shared.handle(error, message: "Error during download statuses from server.", showToastr: !Task.isCancelled)
}
}
}
}
}
.refreshable {
do {
if let account = self.applicationState.account {
if let lastSeenStatusId = try await HomeTimelineService.shared.loadOnTop(for: account) {
try await HomeTimelineService.shared.save(lastSeenStatusId: lastSeenStatusId, for: account)
self.applicationState.lastSeenStatusId = lastSeenStatusId
}
}
} catch {
ErrorService.shared.handle(error, message: "Error during download statuses from server.", showToastr: !Task.isCancelled)
}
}
self.timeline()
}
case .error(let error):
ErrorView(error: error) {
@ -100,13 +44,51 @@ struct HomeFeedView: View {
}
}
@ViewBuilder
private func timeline() -> some View {
ScrollView {
LazyVStack {
ForEach(dbStatuses, id: \.self) { item in
if self.shouldUpToDateBeVisible(statusId: item.id) {
self.upToDatePlaceholder()
}
ImageRow(statusData: item)
}
if allItemsLoaded == false {
LoadingIndicator()
.task {
do {
if let account = self.applicationState.account {
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account)
if newStatusesCount == 0 {
allItemsLoaded = true
}
}
} catch {
ErrorService.shared.handle(error, message: "Error during download statuses from server.", showToastr: !Task.isCancelled)
}
}
}
}
}
.refreshable {
do {
if let account = self.applicationState.account {
if let lastSeenStatusId = try await HomeTimelineService.shared.loadOnTop(for: account) {
try await HomeTimelineService.shared.save(lastSeenStatusId: lastSeenStatusId, for: account)
self.applicationState.lastSeenStatusId = lastSeenStatusId
}
}
} catch {
ErrorService.shared.handle(error, message: "Error during download statuses from server.", showToastr: !Task.isCancelled)
}
}
}
private func loadData() async {
do {
if self.dbStatuses.isEmpty == false {
self.state = .loaded
return
}
if let account = self.applicationState.account {
_ = try await HomeTimelineService.shared.loadOnTop(for: account)
}

View File

@ -0,0 +1,52 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct FavouriteTouch: View {
@State private var showThumb = 100
@State private var showCircle = 0
@State private var opacity = 1.0
private let finished: () -> Void
init(finished: @escaping () -> Void) {
self.finished = finished
}
var body: some View {
ZStack {
Circle()
.frame(width: 55, height: 55, alignment: .center)
.foregroundColor(.white.opacity(0.75))
.scaleEffect(CGFloat(showCircle))
Image(systemName: "hand.thumbsup.fill")
.font(.system(size: 26))
.foregroundColor(.black.opacity(0.4))
.clipShape(Rectangle().offset(y: CGFloat(showThumb)))
}
.opacity(opacity)
.onAppear {
withAnimation(Animation.interpolatingSpring(stiffness: 170, damping: 15)) {
showCircle = 1
}
withAnimation(Animation.easeInOut(duration: 0.5).delay(0.25)) {
showThumb = 0
}
withAnimation(Animation.easeInOut(duration: 0.5).delay(1.75)) {
opacity = 0
}
}
.task {
try? await Task.sleep(nanoseconds: 2_500_000_000)
self.finished()
}
}
}

View File

@ -7,12 +7,16 @@
import SwiftUI
struct ImageRow: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
private let status: StatusData
private let imageHeight: Double
private let imageWidth: Double
private let attachmentData: AttachmentData?
@State private var imageHeight: Double
@State private var imageWidth: Double
@State private var uiImage:UIImage?
@State private var showThumbImage = false
init(statusData: StatusData) {
self.status = statusData
@ -25,7 +29,9 @@ struct ImageRow: View {
} else if let attachmentData, let imageData = attachmentData.data, let uiImage = UIImage(data: imageData) {
self.uiImage = uiImage
let size = ImageSizeService.shared.calculate(for: attachmentData.url, width: uiImage.size.width, height: uiImage.size.height)
let size = ImageSizeService.shared.calculate(for: attachmentData.url,
width: uiImage.size.width,
height: uiImage.size.height)
self.imageWidth = size.width
self.imageHeight = size.height
} else if let attachmentData, attachmentData.metaImageWidth > 0 && attachmentData.metaImageHeight > 0 {
@ -53,9 +59,34 @@ struct ImageRow: View {
.transition(.opacity)
}
} else {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
ZStack {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.onTapGesture{
self.routerPath.navigate(to: .status(
id: status.rebloggedStatusId ?? status.id,
blurhash: status.attachments().first?.blurhash,
highestImageUrl: status.attachments().getHighestImage()?.url,
metaImageWidth: status.attachments().first?.metaImageWidth,
metaImageHeight: status.attachments().first?.metaImageHeight
))
}
.onLongPressGesture(minimumDuration: 0.2) {
Task {
try? await StatusService.shared.favourite(statusId: self.status.id, for: self.applicationState.account)
}
self.showThumbImage = true
HapticService.shared.touch()
}
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
}
}
}
}
if let count = self.status.attachments().count, count > 1 {
@ -77,7 +108,15 @@ struct ImageRow: View {
do {
if let imageData = try await RemoteFileService.shared.fetchData(url: attachmentData.url) {
HomeTimelineService.shared.update(attachment: attachmentData, withData: imageData)
self.uiImage = UIImage(data: imageData)
if let downloadedImage = UIImage(data: imageData) {
let size = ImageSizeService.shared.calculate(for: attachmentData.url,
width: downloadedImage.size.width,
height: downloadedImage.size.height)
self.imageWidth = size.width
self.imageHeight = size.height
self.uiImage = downloadedImage
}
}
} catch {
ErrorService.shared.handle(error, message: "Cannot download the image.")

View File

@ -9,11 +9,15 @@ import MastodonKit
import NukeUI
struct ImageRowAsync: View {
@EnvironmentObject var applicationState: ApplicationState
@EnvironmentObject var routerPath: RouterPath
@State public var statusViewModel: StatusModel
@State private var imageHeight: Double
@State private var imageWidth: Double
@State private var heightWasPrecalculated: Bool
@State private var showThumbImage = false
init(statusViewModel: StatusModel) {
self.statusViewModel = statusViewModel
@ -47,11 +51,27 @@ struct ImageRowAsync: View {
LazyImage(url: attachment.url) { state in
if let image = state.image {
if self.statusViewModel.sensitive {
ContentWarning(blurhash: attachment.blurhash, spoilerText: self.statusViewModel.spoilerText) {
image
ZStack {
ContentWarning(blurhash: attachment.blurhash, spoilerText: self.statusViewModel.spoilerText) {
self.imageView(image: image)
}
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
}
}
}
} else {
image
ZStack {
self.imageView(image: image)
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
}
}
}
}
} else if state.error != nil {
ZStack {
@ -96,6 +116,27 @@ struct ImageRowAsync: View {
}
}
private func imageView(image: NukeUI.Image) -> some View {
image
.onTapGesture{
self.routerPath.navigate(to: .status(
id: statusViewModel.id,
blurhash: statusViewModel.mediaAttachments.first?.blurhash,
highestImageUrl: statusViewModel.mediaAttachments.getHighestImage()?.url,
metaImageWidth: statusViewModel.getImageWidth(),
metaImageHeight: statusViewModel.getImageHeight()
))
}
.onLongPressGesture(minimumDuration: 0.2) {
Task {
try? await StatusService.shared.favourite(statusId: self.statusViewModel.id, for: self.applicationState.account)
}
self.showThumbImage = true
HapticService.shared.touch()
}
}
private func recalculateSizeOfDownloadedImage(imageResponse: ImageResponse) {
guard heightWasPrecalculated == false else {
return

View File

@ -11,39 +11,41 @@ struct StatusPlaceholder: View {
@State var imageBlurhash: String?
var body: some View {
VStack (alignment: .leading) {
if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) {
Image(uiImage: uiImage)
.resizable()
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
} else {
Rectangle()
.fill(Color.placeholderText)
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
.redacted(reason: .placeholder)
}
VStack(alignment: .leading) {
UsernameRow(accountId: "",
accountDisplayName: "Verylong Displayname",
accountUsername: "@username")
ScrollView {
VStack (alignment: .leading) {
if let imageBlurhash, let uiImage = UIImage(blurHash: imageBlurhash, size: CGSize(width: 32, height: 32)) {
Image(uiImage: uiImage)
.resizable()
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
} else {
Rectangle()
.fill(Color.placeholderText)
.frame(width: UIScreen.main.bounds.width, height: imageHeight)
.redacted(reason: .placeholder)
}
Text("Lorem ispum text something")
.foregroundColor(.lightGrayColor)
.font(.footnote)
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
.foregroundColor(.lightGrayColor)
.font(.footnote)
LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland")
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
VStack(alignment: .leading) {
UsernameRow(accountId: "",
accountDisplayName: "Verylong Displayname",
accountUsername: "@username")
Text("Lorem ispum text something")
.foregroundColor(.lightGrayColor)
.font(.footnote)
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
.foregroundColor(.lightGrayColor)
.font(.footnote)
LabelIcon(iconName: "mappin.and.ellipse", value: "Wroclaw, Poland")
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
}
.padding(8)
.redacted(reason: .placeholder)
.animatePlaceholder(isLoading: .constant(true))
}
.padding(8)
.redacted(reason: .placeholder)
.animatePlaceholder(isLoading: .constant(true))
}
}
}