mirror of
https://github.com/VernissageApp/Vernissage.git
synced 2024-12-24 16:11:15 +01:00
Add favourite quick touch :)
This commit is contained in:
parent
9fbd39657e
commit
ee3407dd69
@ -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 */,
|
||||
|
@ -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)
|
||||
}
|
||||
|
52
Vernissage/Widgets/FavouriteTouch.swift
Normal file
52
Vernissage/Widgets/FavouriteTouch.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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.")
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user