mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-03-14 14:12:41 +00:00
Merge branch 'main' into tip-system
This commit is contained in:
commit
7b9544d1e0
22 changed files with 281 additions and 131 deletions
|
@ -1514,7 +1514,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate43";
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate49 AppIconAlternate48 AppIconAlternate47 AppIconAlternate43";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
|
@ -1570,7 +1570,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate43";
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIconAlternate0 AppIconAlternate6 AppIconAlternate7 AppIconAlternate8 AppIconAlternate10 AppIconAlternate11 AppIconAlternate12 AppIconAlternate13 AppIconAlternate14 AppIconAlternate15 AppIconAlternate16 AppIconAlternate17 AppIconAlternate19 AppIconAlternate18 AppIconAlternate20 AppIconAlternate21 AppIconAlternate22 AppIconAlternate23 AppIconAlternate24 AppIconAlternate25 AppIconAlternate26 AppIconAlternate27 AppIconAlternate28 AppIconAlternate29 AppIconAlternate30 AppIconAlternate31 AppIconAlternate32 AppIconAlternate33 AppIconAlternate34 AppIconAlternate35 AppIconAlternate36 AppIconAlternate37 AppIconAlternate38 AppIconAlternate39 AppIconAlternate40 AppIconAlternate42 AppIconAlternate2 AppIconAlternate41 AppIconAlternate45 AppIconAlternate44 AppIconAlternate1 AppIconAlternate4 AppIconAlternate3 AppIconAlternate5 AppIconAlternate46 AppIconAlternate9 AppIconAlternate49 AppIconAlternate48 AppIconAlternate47 AppIconAlternate43";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
|
|
|
@ -122,6 +122,7 @@ struct AppView: View {
|
|||
.tag(tab)
|
||||
}
|
||||
}
|
||||
.id(availableTabs.count) /// Resets the TabView state when the number of tabs changes to avoid navigation bar issues and prevent crashes
|
||||
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||
tabview.tabBar.isHidden = horizontalSizeClass == .regular
|
||||
tabview.customizableViewControllers = []
|
||||
|
|
|
@ -136,11 +136,12 @@ struct AddAccountView: View {
|
|||
instance = nil
|
||||
instanceFetchError = nil
|
||||
}
|
||||
} catch _ as DecodingError {
|
||||
} catch _ as ServerError {
|
||||
instance = nil
|
||||
instanceFetchError = "account.add.error.instance-not-supported"
|
||||
} catch {
|
||||
instance = nil
|
||||
instanceFetchError = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ struct IconSelectorView: View {
|
|||
case alt39, alt40, alt41, alt42, alt43
|
||||
case alt44, alt45
|
||||
case alt46
|
||||
case alt47, alt48
|
||||
case alt49
|
||||
|
||||
var appIconName: String {
|
||||
return "AppIconAlternate\(rawValue)"
|
||||
|
@ -51,6 +53,8 @@ struct IconSelectorView: View {
|
|||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", icons: [.alt39, .alt40, .alt41, .alt42, .alt43]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Simone Margio", icons: [.alt44, .alt45]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Peter Broqvist (@PKB)", icons: [.alt47, .alt48]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Oz Tsori (@oztsori)", icons: [.alt49]),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -15,17 +15,19 @@ struct ToolbarTab: ToolbarContent {
|
|||
|
||||
var body: some ToolbarContent {
|
||||
if !isSecondaryColumn {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
Button {
|
||||
withAnimation {
|
||||
userPreferences.isSidebarExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Image(systemName: "sidebar.squares.left")
|
||||
} else {
|
||||
Image(systemName: "sidebar.left")
|
||||
if horizontalSizeClass == .regular {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
Button {
|
||||
withAnimation {
|
||||
userPreferences.isSidebarExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Image(systemName: "sidebar.squares.left")
|
||||
} else {
|
||||
Image(systemName: "sidebar.left")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 143 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate47-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 234 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate48-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate49-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -710,6 +710,12 @@
|
|||
"%@ ist keine valide Instanz" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%@ no es una instancia válida"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -13521,6 +13527,12 @@
|
|||
"value" : "Delete avatar"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Eliminar imagen de perfil"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -13539,6 +13551,12 @@
|
|||
"value" : "Verwijder profielfoto"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Avatarı Sil"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -14032,6 +14050,12 @@
|
|||
"value" : "Delete header"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Eliminar cabecera"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -14050,6 +14074,12 @@
|
|||
"value" : "Verwijder omslagfoto"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Başlığı Sil"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -28582,8 +28612,8 @@
|
|||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"value" : "Enlaces que son tendencia"
|
||||
"state" : "translated",
|
||||
"value" : "Noticias"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
|
@ -28642,8 +28672,8 @@
|
|||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"value" : "Yükselişteki Bağlantılar"
|
||||
"state" : "translated",
|
||||
"value" : "Trend Bağlantılar"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
|
@ -28762,7 +28792,7 @@
|
|||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yükselişteki Gönderiler"
|
||||
"value" : "Trend Gönderiler"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
|
@ -28881,7 +28911,7 @@
|
|||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yükselişteki Etiketler"
|
||||
"value" : "Trend Etiketler"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
|
@ -35814,7 +35844,7 @@
|
|||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Ajustes ..."
|
||||
}
|
||||
},
|
||||
|
@ -35874,7 +35904,7 @@
|
|||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "Ayarlar ..."
|
||||
}
|
||||
},
|
||||
|
@ -41454,6 +41484,12 @@
|
|||
"value" : "Einen Status veröffentlichen"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Hacer una publicación"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -41633,6 +41669,12 @@
|
|||
"value" : "Bilder veröffentlichen"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Publicar imágenes"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -41674,6 +41716,12 @@
|
|||
"value" : "Status auf Mastodon veröffentlichen"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Hacer una publicación en Mastodon"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -62385,7 +62433,26 @@
|
|||
}
|
||||
},
|
||||
"Show Content Gradient" : {
|
||||
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mostrar contenido con gradiente"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "İçerik Gradyanını Göster"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "启用渐变嘟文背景"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.action.bookmark" : {
|
||||
"extractionState" : "manual",
|
||||
|
@ -78056,7 +78123,7 @@
|
|||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Yükselişte"
|
||||
"value" : "Trend"
|
||||
}
|
||||
},
|
||||
"uk" : {
|
||||
|
@ -82904,14 +82971,14 @@
|
|||
"plural" : {
|
||||
"one" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"value" : "%lld persona hablando"
|
||||
"state" : "translated",
|
||||
"value" : "%lld publicación"
|
||||
}
|
||||
},
|
||||
"other" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"value" : "%lld personas hablando"
|
||||
"state" : "translated",
|
||||
"value" : "%lld publicaciones"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83096,14 +83163,14 @@
|
|||
"plural" : {
|
||||
"one" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"value" : "%lld kişi konuşuyor"
|
||||
"state" : "translated",
|
||||
"value" : "%lld gönderi"
|
||||
}
|
||||
},
|
||||
"other" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"value" : "%lld kişi konuşuyor"
|
||||
"state" : "translated",
|
||||
"value" : "%lld gönderi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83132,13 +83199,13 @@
|
|||
"plural" : {
|
||||
"one" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "%lld 人正在参与讨论"
|
||||
}
|
||||
},
|
||||
"other" : {
|
||||
"stringUnit" : {
|
||||
"state" : "needs_review",
|
||||
"state" : "translated",
|
||||
"value" : "%lld 人正在参与讨论"
|
||||
}
|
||||
}
|
||||
|
@ -83254,6 +83321,12 @@
|
|||
"value" : "Ice Cubes benutzen, um einen Status auf Mastodon zu veröffentlichen"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Usar Ice Cubes para hacer una publicación en Mastodon"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -83295,6 +83368,12 @@
|
|||
"value" : "Ice Cubes benutzen, um einen Status mit einem Bild auf Mastodon zu veröffentlichen"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Usar Ice Cubes para hacer una publicación con una imagen en Mastodon"
|
||||
}
|
||||
},
|
||||
"eu" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
|
@ -83421,4 +83500,4 @@
|
|||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ public let availableColorsSets: [ColorSetCouple] =
|
|||
.init(light: ConstellationLight(), dark: ConstellationDark()),
|
||||
.init(light: ThreadsLight(), dark: ThreadsDark())]
|
||||
|
||||
public protocol ColorSet {
|
||||
public protocol ColorSet: Sendable {
|
||||
var name: ColorSetName { get }
|
||||
var scheme: ColorScheme { get }
|
||||
var tintColor: Color { get set }
|
||||
|
@ -18,11 +18,11 @@ public protocol ColorSet {
|
|||
var labelColor: Color { get set }
|
||||
}
|
||||
|
||||
public enum ColorScheme: String {
|
||||
public enum ColorScheme: String, Sendable {
|
||||
case dark, light
|
||||
}
|
||||
|
||||
public enum ColorSetName: String {
|
||||
public enum ColorSetName: String, Sendable {
|
||||
case iceCubeDark = "Ice Cube - Dark"
|
||||
case iceCubeLight = "Ice Cube - Light"
|
||||
case iceCubeNeonDark = "Ice Cube Neon - Dark"
|
||||
|
@ -39,7 +39,7 @@ public enum ColorSetName: String {
|
|||
case threadsDark = "Threads - Dark"
|
||||
}
|
||||
|
||||
public struct ColorSetCouple: Identifiable {
|
||||
public struct ColorSetCouple: Identifiable, Sendable {
|
||||
public var id: String {
|
||||
dark.name.rawValue + light.name.rawValue
|
||||
}
|
||||
|
|
|
@ -22,6 +22,23 @@ struct MediaUIAttachmentImageView: View {
|
|||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
.draggable(MediaUIImageTransferable(url: url))
|
||||
.contextMenu {
|
||||
MediaUIShareLink(url: url, type: .image)
|
||||
Button {
|
||||
Task {
|
||||
let transferable = MediaUIImageTransferable(url: url)
|
||||
UIPasteboard.general.image = UIImage(data: await transferable.fetchData())
|
||||
}
|
||||
} label: {
|
||||
Label("status.media.contextmenu.copy", systemImage: "doc.on.doc")
|
||||
}
|
||||
Button {
|
||||
UIPasteboard.general.url = url
|
||||
} label: {
|
||||
Label("status.action.copy-link", systemImage: "link")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
16
Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift
Normal file
16
Packages/MediaUI/Sources/MediaUI/MediaUIShareLink.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
import SwiftUI
|
||||
|
||||
struct MediaUIShareLink: View, @unchecked Sendable {
|
||||
let url: URL
|
||||
let type: DisplayType
|
||||
|
||||
var body: some View {
|
||||
if type == .image {
|
||||
let transferable = MediaUIImageTransferable(url: url)
|
||||
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
|
||||
image: transferable))
|
||||
} else {
|
||||
ShareLink(item: url)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,13 +6,7 @@ struct ShareToolbarItem: ToolbarContent, @unchecked Sendable {
|
|||
|
||||
var body: some ToolbarContent {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
if type == .image {
|
||||
let transferable = MediaUIImageTransferable(url: url)
|
||||
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
|
||||
image: transferable))
|
||||
} else {
|
||||
ShareLink(item: url)
|
||||
}
|
||||
MediaUIShareLink(url: url, type: type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ public extension StatusEditor {
|
|||
didSet {
|
||||
if let itemsProvider {
|
||||
mediaContainers = []
|
||||
processItemsProvider(items: itemsProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ public struct StatusRowView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.if(viewModel.url != nil) { $0.draggable(viewModel.url!) }
|
||||
.contextMenu {
|
||||
contextMenu
|
||||
.onAppear {
|
||||
|
|
|
@ -105,7 +105,11 @@ import SwiftUI
|
|||
status.reblog?.inReplyToId != nil || status.reblog?.inReplyToAccountId != nil ||
|
||||
status.inReplyToId != nil || status.inReplyToAccountId != nil
|
||||
}
|
||||
|
||||
|
||||
var url: URL? {
|
||||
(status.reblog?.url ?? status.url).flatMap(URL.init(string:))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeBackgroundColor(isHomeTimeline: Bool) -> some View {
|
||||
if isHomeTimeline, theme.showContentGradient {
|
||||
|
|
|
@ -81,6 +81,7 @@ public struct StatusRowCardView: View {
|
|||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.draggable(url)
|
||||
.contextMenu {
|
||||
ShareLink(item: url) {
|
||||
Label("status.card.share", systemImage: "square.and.arrow.up")
|
||||
|
|
|
@ -88,9 +88,7 @@ struct StatusRowContextMenu: View {
|
|||
Divider()
|
||||
|
||||
Menu("status.action.share-title") {
|
||||
if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url,
|
||||
let url = URL(string: urlString)
|
||||
{
|
||||
if let url = viewModel.url {
|
||||
ShareLink(item: url,
|
||||
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
|
||||
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText))
|
||||
|
@ -133,7 +131,7 @@ struct StatusRowContextMenu: View {
|
|||
}
|
||||
}
|
||||
|
||||
if let url = URL(string: viewModel.status.reblog?.url ?? viewModel.status.url ?? "") {
|
||||
if let url = viewModel.url {
|
||||
Button { UIApplication.shared.open(url) } label: {
|
||||
Label("status.action.view-in-browser", systemImage: "safari")
|
||||
}
|
||||
|
@ -152,7 +150,7 @@ struct StatusRowContextMenu: View {
|
|||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = viewModel.status.reblog?.url ?? viewModel.status.url
|
||||
UIPasteboard.general.url = viewModel.url
|
||||
} label: {
|
||||
Label("status.action.copy-link", systemImage: "link")
|
||||
}
|
||||
|
|
|
@ -307,109 +307,100 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
private func fetchNewPagesFrom(latestStatus: String, client: Client) async throws {
|
||||
canStreamEvents = false
|
||||
let initialTimeline = timeline
|
||||
var newStatuses: [Status] = await fetchNewPages(minId: latestStatus, maxPages: 5)
|
||||
|
||||
// Dedup statuses, a status with the same id could have been streamed in.
|
||||
|
||||
let newStatuses = await fetchAndDedupNewStatuses(latestStatus: latestStatus, client: client)
|
||||
|
||||
guard !newStatuses.isEmpty,
|
||||
isTimelineVisible,
|
||||
!Task.isCancelled,
|
||||
initialTimeline == timeline else {
|
||||
canStreamEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
await updateTimelineWithNewStatuses(newStatuses)
|
||||
|
||||
if !Task.isCancelled, let latest = await datasource.get().first {
|
||||
pendingStatusesObserver.isLoadingNewStatuses = true
|
||||
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAndDedupNewStatuses(latestStatus: String, client: Client) async -> [Status] {
|
||||
var newStatuses = await fetchNewPages(minId: latestStatus, maxPages: 5)
|
||||
let ids = await datasource.get().map(\.id)
|
||||
newStatuses = newStatuses.filter { status in
|
||||
!ids.contains(where: { $0 == status.id })
|
||||
}
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
|
||||
// If no new statuses, resume streaming and exit.
|
||||
guard !newStatuses.isEmpty else {
|
||||
canStreamEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
// If the timeline is not visible, we don't update it as it would mess up the user position.
|
||||
guard isTimelineVisible else {
|
||||
canStreamEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
// Return if task has been cancelled.
|
||||
guard !Task.isCancelled else {
|
||||
canStreamEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
// As this is a long runnign task we need to ensure that the user didn't changed the timeline filter.
|
||||
guard initialTimeline == timeline else {
|
||||
canStreamEvents = true
|
||||
return
|
||||
}
|
||||
|
||||
// Keep track of the top most status, so we can scroll back to it after view update.
|
||||
return newStatuses
|
||||
}
|
||||
|
||||
private func updateTimelineWithNewStatuses(_ newStatuses: [Status]) async {
|
||||
let topStatus = await datasource.getFiltered().first
|
||||
|
||||
// Insert new statuses in internal datasource.
|
||||
await datasource.insert(contentOf: newStatuses, at: 0)
|
||||
|
||||
// Cache statuses for timeline.
|
||||
await cache()
|
||||
|
||||
// Append new statuses in the timeline indicator.
|
||||
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map(\.id), at: 0)
|
||||
|
||||
// High chance the user is scrolled to the top.
|
||||
// We need to update the statuses state, and then scroll to the previous top most status.
|
||||
if let topStatus, visibileStatuses.contains(where: { $0.id == topStatus.id }), scrollToTopVisible {
|
||||
pendingStatusesObserver.disableUpdate = true
|
||||
let statuses = await datasource.getFiltered()
|
||||
statusesState = .display(statuses: statuses,
|
||||
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||
scrollToIndexAnimated = false
|
||||
scrollToIndex = newStatuses.count + 1
|
||||
DispatchQueue.main.async {
|
||||
self.pendingStatusesObserver.disableUpdate = false
|
||||
self.canStreamEvents = true
|
||||
}
|
||||
|
||||
let statuses = await datasource.getFiltered()
|
||||
let nextPageState: StatusesState.PagingState = statuses.count < 20 ? .none : .hasNextPage
|
||||
|
||||
if let topStatus = topStatus,
|
||||
visibileStatuses.contains(where: { $0.id == topStatus.id }),
|
||||
scrollToTopVisible {
|
||||
updateTimelineWithScrollToTop(newStatuses: newStatuses, statuses: statuses, nextPageState: nextPageState)
|
||||
} else {
|
||||
// This will keep the scroll position (if the list is scrolled) and prepend statuses on the top.
|
||||
let statuses = await datasource.getFiltered()
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses,
|
||||
nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||
canStreamEvents = true
|
||||
}
|
||||
updateTimelineWithAnimation(statuses: statuses, nextPageState: nextPageState)
|
||||
}
|
||||
|
||||
if !Task.isCancelled,
|
||||
let latest = await datasource.get().first
|
||||
{
|
||||
pendingStatusesObserver.isLoadingNewStatuses = true
|
||||
try await fetchNewPagesFrom(latestStatus: latest.id, client: client)
|
||||
}
|
||||
|
||||
// Refresh the timeline while keeping the scroll position to the top status.
|
||||
private func updateTimelineWithScrollToTop(newStatuses: [Status], statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||
pendingStatusesObserver.disableUpdate = true
|
||||
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
|
||||
scrollToIndexAnimated = false
|
||||
scrollToIndex = newStatuses.count + 1
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.pendingStatusesObserver.disableUpdate = false
|
||||
self?.canStreamEvents = true
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the timeline while keeping the user current position.
|
||||
// It works because a side effect of withAnimation is that it keep scroll position IF the List is not scrolled to the top.
|
||||
private func updateTimelineWithAnimation(statuses: [Status], nextPageState: StatusesState.PagingState) {
|
||||
withAnimation {
|
||||
statusesState = .display(statuses: statuses, nextPageState: nextPageState)
|
||||
canStreamEvents = true
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchNewPages(minId: String, maxPages: Int) async -> [Status] {
|
||||
guard let client else { return [] }
|
||||
var pagesLoaded = 0
|
||||
var allStatuses: [Status] = []
|
||||
var latestMinId = minId
|
||||
do {
|
||||
while
|
||||
!Task.isCancelled,
|
||||
let newStatuses: [Status] =
|
||||
try await client.get(endpoint: timeline.endpoint(sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: latestMinId,
|
||||
offset: datasource.get().count)),
|
||||
!newStatuses.isEmpty,
|
||||
pagesLoaded < maxPages
|
||||
{
|
||||
pagesLoaded += 1
|
||||
|
||||
for _ in 1...maxPages {
|
||||
if Task.isCancelled { break }
|
||||
|
||||
let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(
|
||||
sinceId: nil,
|
||||
maxId: nil,
|
||||
minId: latestMinId,
|
||||
offset: nil
|
||||
))
|
||||
|
||||
if newStatuses.isEmpty { break }
|
||||
|
||||
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
|
||||
|
||||
allStatuses.insert(contentsOf: newStatuses, at: 0)
|
||||
latestMinId = newStatuses.first?.id ?? ""
|
||||
latestMinId = newStatuses.first?.id ?? latestMinId
|
||||
}
|
||||
} catch {
|
||||
return allStatuses
|
||||
}
|
||||
|
||||
return allStatuses
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue