macOS / iPad app fixes + support drop in the editor + global new post button

This commit is contained in:
Thomas Ricouard 2023-01-17 13:02:05 +01:00
parent 7f6419ebae
commit 899ccd8ad7
9 changed files with 174 additions and 44 deletions

View file

@ -34,6 +34,7 @@
9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; }; 9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; };
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; }; 9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; };
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTab.swift */; }; 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTab.swift */; };
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4A48182976B21900A1A038 /* ProfileTab.swift */; };
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; }; 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; };
9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; }; 9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; };
9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; }; 9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; };
@ -125,6 +126,7 @@
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; }; 9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; }; 9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
9F398AB229360A4C00A889F2 /* TimelineTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTab.swift; sourceTree = "<group>"; }; 9F398AB229360A4C00A889F2 /* TimelineTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTab.swift; sourceTree = "<group>"; };
9F4A48182976B21900A1A038 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = "<group>"; };
9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = "<group>"; }; 9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = "<group>"; };
9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = "<group>"; }; 9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = "<group>"; };
9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; }; 9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; };
@ -267,6 +269,7 @@
9F35DB4B2952005C00B3281A /* MessagesTab.swift */, 9F35DB4B2952005C00B3281A /* MessagesTab.swift */,
9F55C68C2955968700F94077 /* ExploreTab.swift */, 9F55C68C2955968700F94077 /* ExploreTab.swift */,
9F2B92F5295AE04800DE16D0 /* Tabs.swift */, 9F2B92F5295AE04800DE16D0 /* Tabs.swift */,
9F4A48182976B21900A1A038 /* ProfileTab.swift */,
); );
path = Tabs; path = Tabs;
sourceTree = "<group>"; sourceTree = "<group>";
@ -537,6 +540,7 @@
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRouteur.swift in Sources */,
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */,
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,
639CDF9C296AC82F00C35E58 /* SafariRouteur.swift in Sources */, 639CDF9C296AC82F00C35E58 /* SafariRouteur.swift in Sources */,
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */, 9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,

View file

@ -21,12 +21,13 @@ struct IceCubesApp: App {
@StateObject private var watcher = StreamWatcher() @StateObject private var watcher = StreamWatcher()
@StateObject private var quickLook = QuickLook() @StateObject private var quickLook = QuickLook()
@StateObject private var theme = Theme.shared @StateObject private var theme = Theme.shared
@StateObject private var sidebarRouterPath = RouterPath()
@State private var selectedTab: Tab = .timeline @State private var selectedTab: Tab = .timeline
@State private var selectSidebarItem: Tab? = .timeline @State private var selectSidebarItem: Tab? = .timeline
@State private var popToRootTab: Tab = .other @State private var popToRootTab: Tab = .other
@State private var sideBarLoadedTabs: [Tab] = [] @State private var sideBarLoadedTabs: Set<Tab> = Set()
private var availableTabs: [Tab] { private var availableTabs: [Tab] {
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
} }
@ -54,6 +55,14 @@ struct IceCubesApp: App {
.edgesIgnoringSafeArea(.bottom) .edgesIgnoringSafeArea(.bottom)
}) })
} }
.commands {
CommandGroup(replacing: CommandGroupPlacement.newItem) {
Button("New post") {
sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.serverPreferences?.postVisibility ?? .pub)
}
.keyboardShortcut("n", modifiers: .command)
}
}
.onChange(of: scenePhase) { scenePhase in .onChange(of: scenePhase) { scenePhase in
handleScenePhase(scenePhase: scenePhase) handleScenePhase(scenePhase: scenePhase)
} }
@ -84,10 +93,11 @@ struct IceCubesApp: App {
private var sidebarView: some View { private var sidebarView: some View {
SideBarView(selectedTab: $selectedTab, SideBarView(selectedTab: $selectedTab,
popToRootTab: $popToRootTab, popToRootTab: $popToRootTab,
tabs: availableTabs) { tabs: availableTabs,
routerPath: sidebarRouterPath) {
ZStack { ZStack {
if let account = currentAccount.account, selectedTab == .profile { if selectedTab == .profile {
AccountDetailView(account: account) ProfileTab(popToRootTab: $popToRootTab)
} }
ForEach(availableTabs) { tab in ForEach(availableTabs) { tab in
if tab == selectedTab || sideBarLoadedTabs.contains(tab) { if tab == selectedTab || sideBarLoadedTabs.contains(tab) {
@ -96,9 +106,7 @@ struct IceCubesApp: App {
.opacity(tab == selectedTab ? 1 : 0) .opacity(tab == selectedTab ? 1 : 0)
.id(tab) .id(tab)
.onAppear { .onAppear {
if !sideBarLoadedTabs.contains(tab) { sideBarLoadedTabs.insert(tab)
sideBarLoadedTabs.append(tab)
}
} }
} else { } else {
EmptyView() EmptyView()
@ -186,4 +194,5 @@ class AppDelegate: NSObject, UIApplicationDelegate {
} }
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {} func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
} }

View file

@ -56,12 +56,9 @@ class AppQLPreviewCpntroller: QLPreviewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
navigationItem.rightBarButtonItem = closeButton if UIDevice.current.userInterfaceIdiom != .pad {
} navigationItem.rightBarButtonItem = closeButton
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationItem.rightBarButtonItem = closeButton
} }
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {

View file

@ -7,51 +7,106 @@ import SwiftUI
struct SideBarView<Content: View>: View { struct SideBarView<Content: View>: View {
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var theme: Theme @EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var userPreferences: UserPreferences
@Binding var selectedTab: Tab @Binding var selectedTab: Tab
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
var tabs: [Tab] var tabs: [Tab]
@ObservedObject var routerPath = RouterPath()
@ViewBuilder var content: () -> Content @ViewBuilder var content: () -> Content
private func badgeFor(tab: Tab) -> Int {
if tab == .notifications && selectedTab != tab {
return watcher.unreadNotificationsCount + userPreferences.pushNotificationsCount
}
return 0
}
private var profileView: some View {
Button {
selectedTab = .profile
} label: {
AppAccountsSelectorView(routeurPath: RouterPath(),
accountCreationEnabled: false,
avatarSize: .status)
}
.frame(width: .sidebarWidth, height: 60)
.background(selectedTab == .profile ? theme.secondaryBackgroundColor : .clear)
}
private func makeIconForTab(tab: Tab) -> some View {
ZStack(alignment: .topTrailing) {
Image(systemName: tab.iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundColor(tab == selectedTab ? theme.tintColor : .gray)
if let badge = badgeFor(tab: tab), badge > 0 {
ZStack {
Circle()
.fill(.red)
Text(String(badge))
.foregroundColor(.white)
.font(.footnote)
}
.frame(width: 20, height: 20)
.offset(x: 10, y: -10)
}
}
.contentShape(Rectangle())
.frame(width: .sidebarWidth, height: 50)
}
private var postButton: some View {
Button {
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.serverPreferences?.postVisibility ?? .pub)
} label: {
Image(systemName: "square.and.pencil")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 30)
}
.buttonStyle(.borderedProminent)
.keyboardShortcut("n", modifiers: .command)
}
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(alignment: .center) { ScrollView {
Button { VStack(alignment: .center) {
selectedTab = .profile profileView
} label: { ForEach(tabs) { tab in
AppAccountsSelectorView(routeurPath: RouterPath(), Button {
accountCreationEnabled: false, if tab == selectedTab {
avatarSize: .status) popToRootTab = .other
} DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
.frame(width: 80, height: 60) popToRootTab = tab
.background(selectedTab == .profile ? theme.secondaryBackgroundColor : .clear) }
ForEach(tabs) { tab in
Button {
if tab == selectedTab {
popToRootTab = .other
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
popToRootTab = tab
} }
selectedTab = tab
if tab == .notifications {
watcher.unreadNotificationsCount = 0
userPreferences.pushNotificationsCount = 0
}
} label: {
makeIconForTab(tab: tab)
} }
selectedTab = tab .background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear)
} label: {
Image(systemName: tab.iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundColor(tab == selectedTab ? theme.tintColor : .gray)
} }
.frame(width: 80, height: 50) postButton
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear) .padding(.top, 12)
Spacer()
} }
Spacer()
} }
.frame(width: 80) .frame(width: .sidebarWidth)
.background(.clear) .scrollContentBackground(.hidden)
.background(.thinMaterial)
Divider() Divider()
.edgesIgnoringSafeArea(.top) .edgesIgnoringSafeArea(.top)
content() content()
} }
.background(.thinMaterial) .background(.thinMaterial)
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
} }
} }

View file

@ -0,0 +1,50 @@
import Account
import AppAccount
import Conversations
import Env
import Models
import Network
import Shimmer
import SwiftUI
struct ProfileTab: View {
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var routeurPath = RouterPath()
@Binding var popToRootTab: Tab
var body: some View {
NavigationStack(path: $routeurPath.path) {
if let account = currentAccount.account {
AccountDetailView(account: account)
.withAppRouteur()
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
.toolbar {
if UIDevice.current.userInterfaceIdiom != .pad {
ToolbarItem(placement: .navigationBarLeading) {
AppAccountsSelectorView(routeurPath: routeurPath)
}
}
}
.id(currentAccount.account?.id)
} else {
AccountDetailView(account: .placeholder())
.redacted(reason: .placeholder)
.shimmering()
}
}
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .messages {
routeurPath.path = []
}
}
.onChange(of: currentAccount.account?.id) { _ in
routeurPath.path = []
}
.onAppear {
routeurPath.client = client
}
.withSafariRouteur()
.environmentObject(routeurPath)
}
}

View file

@ -5,4 +5,5 @@ public extension CGFloat {
static let dividerPadding: CGFloat = 2 static let dividerPadding: CGFloat = 2
static let statusColumnsSpacing: CGFloat = 8 static let statusColumnsSpacing: CGFloat = 8
static let maxColumnWidth: CGFloat = 650 static let maxColumnWidth: CGFloat = 650
static let sidebarWidth: CGFloat = 80
} }

View file

@ -1,5 +1,6 @@
import Foundation import Foundation
import UIKit import UIKit
import UniformTypeIdentifiers
@MainActor @MainActor
enum StatusEditorUTTypeSupported: String, CaseIterable { enum StatusEditorUTTypeSupported: String, CaseIterable {
@ -9,6 +10,10 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
case image = "public.image" case image = "public.image"
case jpeg = "public.jpeg" case jpeg = "public.jpeg"
case png = "public.png" case png = "public.png"
static func types() -> [UTType] {
[.url, .text, .plainText, .image, .jpeg, .png]
}
func loadItemContent(item: NSItemProvider) async throws -> Any? { func loadItemContent(item: NSItemProvider) async throws -> Any? {
let result = try await item.loadItem(forTypeIdentifier: rawValue) let result = try await item.loadItem(forTypeIdentifier: rawValue)

View file

@ -67,6 +67,7 @@ public struct StatusEditorView: View {
viewModel: viewModel) viewModel: viewModel)
} }
} }
.onDrop(of: StatusEditorUTTypeSupported.types(), delegate: viewModel)
.onAppear { .onAppear {
viewModel.client = client viewModel.client = client
viewModel.currentAccount = currentAccount.account viewModel.currentAccount = currentAccount.account

View file

@ -238,7 +238,7 @@ public class StatusEditorViewModel: ObservableObject {
for range in urlRanges { for range in urlRanges {
statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand), statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand),
.underlineStyle: NSUnderlineStyle.single, .underlineStyle: NSUnderlineStyle.single.rawValue,
.underlineColor: UIColor(theme?.tintColor ?? .brand)], .underlineColor: UIColor(theme?.tintColor ?? .brand)],
range: NSRange(location: range.location, length: range.length)) range: NSRange(location: range.location, length: range.length))
} }
@ -451,3 +451,11 @@ public class StatusEditorViewModel: ObservableObject {
data: data) data: data)
} }
} }
extension StatusEditorViewModel: DropDelegate {
public func performDrop(info: DropInfo) -> Bool {
let item = info.itemProviders(for: StatusEditorUTTypeSupported.types())
processItemsProvider(items: item)
return true
}
}