mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-26 09:00:37 +00:00
Multi account sidebar + scaled font size on macOS + better iPad / macOS app UX
This commit is contained in:
parent
bb72327f52
commit
4143e82fbc
34 changed files with 277 additions and 129 deletions
|
@ -60,7 +60,6 @@ struct IceCubesApp: App {
|
|||
Button("New post") {
|
||||
sidebarRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.serverPreferences?.postVisibility ?? .pub)
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: .command)
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { scenePhase in
|
||||
|
@ -187,10 +186,12 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
PushNotificationsService.shared.pushToken = deviceToken
|
||||
Task {
|
||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
#if !DEBUG
|
||||
Task {
|
||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
||||
|
|
|
@ -5,6 +5,7 @@ import Env
|
|||
import SwiftUI
|
||||
|
||||
struct SideBarView<Content: View>: View {
|
||||
@EnvironmentObject private var appAccounts: AppAccountsManager
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var watcher: StreamWatcher
|
||||
|
@ -41,14 +42,14 @@ struct SideBarView<Content: View>: View {
|
|||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(tab == selectedTab ? theme.tintColor : .gray)
|
||||
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
|
||||
if let badge = badgeFor(tab: tab), badge > 0 {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.red)
|
||||
Text(String(badge))
|
||||
.foregroundColor(.white)
|
||||
.font(.footnote)
|
||||
.font(.caption)
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
.offset(x: 10, y: -10)
|
||||
|
@ -71,28 +72,58 @@ struct SideBarView<Content: View>: View {
|
|||
.keyboardShortcut("n", modifiers: .command)
|
||||
}
|
||||
|
||||
private func makeAccountButton(account: AppAccount) -> some View {
|
||||
Button {
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
selectedTab = .profile
|
||||
} else {
|
||||
withAnimation {
|
||||
appAccounts.currentAccount = account
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
AppAccountView(viewModel: .init(appAccount: account, isCompact: true))
|
||||
}
|
||||
.frame(width: .sidebarWidth, height: 50)
|
||||
.padding(.vertical, 8)
|
||||
.background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ?
|
||||
theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
|
||||
private var tabsView: some View {
|
||||
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)
|
||||
}
|
||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(alignment: .center) {
|
||||
profileView
|
||||
ForEach(tabs) { tab in
|
||||
Button {
|
||||
if tab == selectedTab {
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
popToRootTab = tab
|
||||
}
|
||||
if appAccounts.availableAccounts.isEmpty {
|
||||
tabsView
|
||||
} else {
|
||||
ForEach(appAccounts.availableAccounts) { account in
|
||||
makeAccountButton(account: account)
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
tabsView
|
||||
}
|
||||
selectedTab = tab
|
||||
if tab == .notifications {
|
||||
watcher.unreadNotificationsCount = 0
|
||||
userPreferences.pushNotificationsCount = 0
|
||||
}
|
||||
} label: {
|
||||
makeIconForTab(tab: tab)
|
||||
}
|
||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
postButton
|
||||
.padding(.top, 12)
|
||||
|
|
|
@ -118,7 +118,7 @@ struct AddAccountView: View {
|
|||
.tint(theme.labelColor)
|
||||
} else {
|
||||
Text("Sign in")
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
@ -139,13 +139,13 @@ struct AddAccountView: View {
|
|||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(instance.name)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(.primary)
|
||||
Text(instance.info?.shortDescription ?? "")
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(.gray)
|
||||
Text("\(instance.users) users ⸱ \(instance.statuses) posts")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
@ -158,13 +158,13 @@ struct AddAccountView: View {
|
|||
private var placeholderRow: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Loading...")
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(.primary)
|
||||
Text("Loading, loading, loading ....")
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(.gray)
|
||||
Text("Loading ...")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.redacted(reason: .placeholder)
|
||||
|
|
|
@ -71,9 +71,9 @@ struct SupportAppView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Loading ...")
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
Text("Loading subtitle...")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
|
@ -86,9 +86,9 @@ struct SupportAppView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(tip.title)
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
Text(tip.subtitle)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
|
|
@ -89,13 +89,13 @@ struct AddRemoteTimelineView: View {
|
|||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(instance.name)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(.primary)
|
||||
Text(instance.info?.shortDescription ?? "")
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(.gray)
|
||||
Text("\(instance.users) users ⸱ \(instance.statuses) posts")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ struct AccountDetailHeaderView: View {
|
|||
|
||||
if viewModel.relationship?.followedBy == true {
|
||||
Text("Follows You")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.fontWeight(.semibold)
|
||||
.padding(4)
|
||||
.background(.ultraThinMaterial)
|
||||
|
@ -109,9 +109,9 @@ struct AccountDetailHeaderView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
Text("@\(account.acct)")
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
@ -124,7 +124,7 @@ struct AccountDetailHeaderView: View {
|
|||
}
|
||||
}
|
||||
EmojiTextApp(account.note.asMarkdown, emojis: account.emojis)
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
.padding(.top, 8)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
|
@ -137,10 +137,10 @@ struct AccountDetailHeaderView: View {
|
|||
private func makeCustomInfoLabel(title: String, count: Int) -> some View {
|
||||
VStack {
|
||||
Text("\(count)")
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.tintColor)
|
||||
Text(title)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,16 +77,21 @@ public struct AccountDetailView: View {
|
|||
.background(theme.primaryBackgroundColor)
|
||||
}
|
||||
.onAppear {
|
||||
guard reasons != .placeholder else { return }
|
||||
isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
||||
viewModel.isCurrentUser = isCurrentUser
|
||||
viewModel.client = client
|
||||
Task {
|
||||
guard reasons != .placeholder else { return }
|
||||
isCurrentUser = currentAccount.account?.id == viewModel.accountId
|
||||
viewModel.isCurrentUser = isCurrentUser
|
||||
viewModel.client = client
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
Task {
|
||||
if viewModel.statuses.isEmpty {
|
||||
await viewModel.fetchStatuses()
|
||||
}
|
||||
}
|
||||
Task {
|
||||
await viewModel.fetchFamilliarFollowers()
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
Task {
|
||||
|
@ -150,7 +155,7 @@ public struct AccountDetailView: View {
|
|||
} label: {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("About")
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
Text("\(viewModel.fields.count) fields")
|
||||
.font(.caption2)
|
||||
}
|
||||
|
@ -167,7 +172,7 @@ public struct AccountDetailView: View {
|
|||
} label: {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
Text("\(tag.statusesCount) posts")
|
||||
.font(.caption2)
|
||||
}
|
||||
|
@ -185,7 +190,7 @@ public struct AccountDetailView: View {
|
|||
if !viewModel.familiarFollowers.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Also followed by")
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.padding(.leading, .layoutPadding)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 0) {
|
||||
|
@ -211,7 +216,7 @@ public struct AccountDetailView: View {
|
|||
ForEach(viewModel.fields) { field in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(field.name)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
HStack {
|
||||
if field.verifiedAt != nil {
|
||||
Image(systemName: "checkmark.seal")
|
||||
|
@ -220,7 +225,7 @@ public struct AccountDetailView: View {
|
|||
EmojiTextApp(field.value.asMarkdown, emojis: viewModel.account?.emojis ?? [])
|
||||
.foregroundColor(theme.tintColor)
|
||||
}
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
}
|
||||
.listRowBackground(field.verifiedAt != nil ? Color.green.opacity(0.15) : theme.primaryBackgroundColor)
|
||||
}
|
||||
|
@ -273,7 +278,7 @@ public struct AccountDetailView: View {
|
|||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
}
|
||||
.contextMenu {
|
||||
|
@ -317,7 +322,7 @@ public struct AccountDetailView: View {
|
|||
ForEach(viewModel.pinned) { status in
|
||||
VStack(alignment: .leading) {
|
||||
Label("Pinned post", systemImage: "pin.fill")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
StatusRowView(viewModel: .init(status: status))
|
||||
|
@ -336,7 +341,7 @@ public struct AccountDetailView: View {
|
|||
switch viewModel.accountState {
|
||||
case let .data(account):
|
||||
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
@ -105,7 +105,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
|||
let account: Account
|
||||
let featuredTags: [FeaturedTag]
|
||||
let relationships: [Relationship]
|
||||
let familiarFollowers: [FamiliarAccounts]
|
||||
}
|
||||
|
||||
func fetchAccount() async {
|
||||
|
@ -119,8 +118,6 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
|||
featuredTags = data.featuredTags
|
||||
featuredTags.sort { $0.statusesCountInt > $1.statusesCountInt }
|
||||
relationship = data.relationships.first
|
||||
familiarFollowers = data.familiarFollowers.first?.accounts ?? []
|
||||
|
||||
} catch {
|
||||
if let account {
|
||||
accountState = .data(account: account)
|
||||
|
@ -130,21 +127,23 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
func fetchAccountData(accountId: String, client: Client) async throws -> AccountData {
|
||||
private func fetchAccountData(accountId: String, client: Client) async throws -> AccountData {
|
||||
async let account: Account = client.get(endpoint: Accounts.accounts(id: accountId))
|
||||
async let featuredTags: [FeaturedTag] = client.get(endpoint: Accounts.featuredTags(id: accountId))
|
||||
if client.isAuth && !isCurrentUser {
|
||||
async let relationships: [Relationship] = client.get(endpoint: Accounts.relationships(ids: [accountId]))
|
||||
async let familiarFollowers: [FamiliarAccounts] = client.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
|
||||
return try await .init(account: account,
|
||||
featuredTags: featuredTags,
|
||||
relationships: relationships,
|
||||
familiarFollowers: familiarFollowers)
|
||||
relationships: relationships)
|
||||
}
|
||||
return try await .init(account: account,
|
||||
featuredTags: featuredTags,
|
||||
relationships: [],
|
||||
familiarFollowers: [])
|
||||
relationships: [])
|
||||
}
|
||||
|
||||
func fetchFamilliarFollowers() async {
|
||||
let familiarFollowers: [FamiliarAccounts]? = try? await client?.get(endpoint: Accounts.familiarFollowers(withAccount: accountId))
|
||||
self.familiarFollowers = familiarFollowers?.first?.accounts ?? []
|
||||
}
|
||||
|
||||
func fetchStatuses() async {
|
||||
|
|
|
@ -34,13 +34,13 @@ public struct AccountsListRow: View {
|
|||
AvatarView(url: viewModel.account.avatar, size: .status)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
EmojiTextApp(viewModel.account.safeDisplayName.asMarkdown, emojis: viewModel.account.emojis)
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.fontWeight(.semibold)
|
||||
Text("@\(viewModel.account.acct)")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
EmojiTextApp(viewModel.account.note.asMarkdown, emojis: viewModel.account.emojis)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.lineLimit(3)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
|
|
|
@ -13,6 +13,30 @@ public struct AppAccountView: View {
|
|||
}
|
||||
|
||||
public var body: some View {
|
||||
Group {
|
||||
if viewModel.isCompact {
|
||||
compactView
|
||||
} else {
|
||||
fullView
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var compactView: some View {
|
||||
HStack {
|
||||
if let account = viewModel.account {
|
||||
AvatarView(url: account.avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var fullView: some View {
|
||||
HStack {
|
||||
if let account = viewModel.account {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
|
@ -28,7 +52,7 @@ public struct AppAccountView: View {
|
|||
if let account = viewModel.account {
|
||||
EmojiTextApp(account.safeDisplayName.asMarkdown, emojis: account.emojis)
|
||||
Text("\(account.username)@\(viewModel.appAccount.server)")
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
@ -36,11 +60,6 @@ public struct AppAccountView: View {
|
|||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.fetchAccount()
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if appAccounts.currentAccount.id == viewModel.appAccount.id,
|
||||
let account = viewModel.account
|
||||
|
|
|
@ -6,6 +6,7 @@ import SwiftUI
|
|||
public class AppAccountViewModel: ObservableObject {
|
||||
let appAccount: AppAccount
|
||||
let client: Client
|
||||
let isCompact: Bool
|
||||
|
||||
@Published var account: Account?
|
||||
|
||||
|
@ -13,8 +14,9 @@ public class AppAccountViewModel: ObservableObject {
|
|||
"@\(account?.acct ?? "...")@\(appAccount.server)"
|
||||
}
|
||||
|
||||
public init(appAccount: AppAccount) {
|
||||
public init(appAccount: AppAccount, isCompact: Bool = false) {
|
||||
self.appAccount = appAccount
|
||||
self.isCompact = isCompact
|
||||
client = .init(server: appAccount.server, oauthToken: appAccount.oauthToken)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ struct ConversationsListRow: View {
|
|||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(conversation.accounts.map { $0.safeDisplayName }.joined(separator: ", "))
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer()
|
||||
|
@ -30,7 +30,7 @@ struct ConversationsListRow: View {
|
|||
.frame(width: 10, height: 10)
|
||||
}
|
||||
Text(conversation.lastStatus.createdAt.formatted)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
}
|
||||
Text(conversation.lastStatus.content.asRawText)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
|
60
Packages/DesignSystem/Sources/DesignSystem/Font.swift
Normal file
60
Packages/DesignSystem/Sources/DesignSystem/Font.swift
Normal file
|
@ -0,0 +1,60 @@
|
|||
import SwiftUI
|
||||
|
||||
extension Font {
|
||||
public static var scaledTitle: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 28))
|
||||
} else {
|
||||
return .title
|
||||
}
|
||||
}
|
||||
|
||||
public static var scaledHeadline: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 19), weight: .semibold)
|
||||
} else {
|
||||
return .headline
|
||||
}
|
||||
}
|
||||
|
||||
public static var scaledBody: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 19))
|
||||
} else {
|
||||
return .body
|
||||
}
|
||||
}
|
||||
|
||||
public static var scaledCallout: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 17))
|
||||
} else {
|
||||
return .callout
|
||||
}
|
||||
}
|
||||
|
||||
public static var scaledSubheadline: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 16))
|
||||
} else {
|
||||
return .subheadline
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static var scaledFootnote: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 15))
|
||||
} else {
|
||||
return .footnote
|
||||
}
|
||||
}
|
||||
|
||||
public static var scaledCaption: Font {
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .system(size: UIFontMetrics.default.scaledValue(for: 14))
|
||||
} else {
|
||||
return .caption
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,9 @@ public struct AvatarView: View {
|
|||
case .account:
|
||||
return .init(width: 80, height: 80)
|
||||
case .status:
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return .init(width: 48, height: 48)
|
||||
}
|
||||
return .init(width: 40, height: 40)
|
||||
case .embed:
|
||||
return .init(width: 34, height: 34)
|
||||
|
|
|
@ -18,10 +18,10 @@ public struct EmptyView: View {
|
|||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 50)
|
||||
Text(title)
|
||||
.font(.title)
|
||||
.font(.scaledTitle)
|
||||
.padding(.top, 16)
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ public struct ErrorView: View {
|
|||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 50)
|
||||
Text(title)
|
||||
.font(.title)
|
||||
.font(.scaledTitle)
|
||||
.padding(.top, 16)
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.gray)
|
||||
Button {
|
||||
|
|
|
@ -15,9 +15,9 @@ public struct TagRowView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
|
|
@ -35,7 +35,7 @@ public struct ListEditView: View {
|
|||
emojis: account.emojis)
|
||||
Text("@\(account.acct)")
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
|
|
@ -75,6 +75,16 @@ public struct OpenAIClient {
|
|||
public let object: String
|
||||
public let model: String
|
||||
public let choices: [Choice]
|
||||
|
||||
public var trimmedText: String {
|
||||
guard var text = choices.first?.text else {
|
||||
return ""
|
||||
}
|
||||
while text.first?.isNewline == true || text.first?.isWhitespace == true {
|
||||
text.removeFirst()
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
|
|
@ -56,18 +56,18 @@ struct NotificationRowView: View {
|
|||
append: {
|
||||
Text(" ") +
|
||||
Text(type.label())
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.fontWeight(.regular) +
|
||||
Text(" ⸱ ")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.fontWeight(.regular)
|
||||
.foregroundColor(.gray) +
|
||||
Text(notification.createdAt.formatted)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.fontWeight(.regular)
|
||||
.foregroundColor(.gray)
|
||||
})
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
}
|
||||
|
@ -93,14 +93,14 @@ struct NotificationRowView: View {
|
|||
} else {
|
||||
Group {
|
||||
Text("@\(notification.account.acct)")
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
if type == .follow {
|
||||
EmojiTextApp(notification.account.note.asMarkdown,
|
||||
emojis: notification.account.emojis)
|
||||
.lineLimit(3)
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
.foregroundColor(.gray)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
|
|
|
@ -23,7 +23,8 @@ public struct NotificationsListView: View {
|
|||
.padding(.top, 16)
|
||||
.frame(maxWidth: .maxColumnWidth)
|
||||
}
|
||||
.padding(.horizontal, .layoutPadding)
|
||||
.padding(.leading, .layoutPadding + 12)
|
||||
.padding(.trailing, .layoutPadding)
|
||||
.padding(.top, .layoutPadding)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
}
|
||||
|
|
|
@ -158,7 +158,7 @@ struct StatusEditorAccessoryView: View {
|
|||
private var characterCountView: some View {
|
||||
Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
}
|
||||
|
||||
private var availableLanguages: [(String, String?, String?)] {
|
||||
|
|
|
@ -34,10 +34,10 @@ struct StatusEditorAutoCompleteView: View {
|
|||
VStack(alignment: .leading) {
|
||||
EmojiTextApp(account.safeDisplayName.asMarkdown,
|
||||
emojis: account.emojis)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(theme.labelColor)
|
||||
Text("@\(account.acct)")
|
||||
.font(.caption)
|
||||
.font(.scaledCaption)
|
||||
.foregroundColor(theme.tintColor)
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ struct StatusEditorAutoCompleteView: View {
|
|||
viewModel.selectHashtagSuggestion(tag: tag)
|
||||
} label: {
|
||||
Text("#\(tag.name)")
|
||||
.font(.caption)
|
||||
.font(.scaledCaption)
|
||||
.foregroundColor(theme.tintColor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@ public struct StatusEditorView: View {
|
|||
VStack(alignment: .leading, spacing: 4) {
|
||||
privacyMenu
|
||||
Text("@\(account.acct)")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
@ -188,7 +188,7 @@ public struct StatusEditorView: View {
|
|||
Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName)
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.padding(4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
|
|
|
@ -358,12 +358,8 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
do {
|
||||
let client = OpenAIClient()
|
||||
let response = try await client.request(prompt)
|
||||
if var text = response.choices.first?.text {
|
||||
text.removeFirst()
|
||||
text.removeFirst()
|
||||
backupStatusText = statusText
|
||||
replaceTextWith(text: text)
|
||||
}
|
||||
backupStatusText = statusText
|
||||
replaceTextWith(text: response.trimmedText)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,14 +36,14 @@ public struct StatusEmbeddedView: View {
|
|||
AvatarView(url: account.avatar, size: .embed)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: account.emojis)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.fontWeight(.semibold)
|
||||
Group {
|
||||
Text("@\(account.acct)") +
|
||||
Text(" ⸱ ") +
|
||||
Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted)
|
||||
}
|
||||
.font(.caption)
|
||||
.font(.scaledCaption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,14 @@ class VideoPlayerViewModel: ObservableObject {
|
|||
self?.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
player?.pause()
|
||||
}
|
||||
|
||||
func play() {
|
||||
player?.play()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: self.player)
|
||||
|
@ -27,12 +35,24 @@ class VideoPlayerViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
struct VideoPlayerView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@StateObject var viewModel: VideoPlayerViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VideoPlayer(player: viewModel.player)
|
||||
}.onAppear {
|
||||
viewModel.preparePlayer()
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { scenePhase in
|
||||
switch scenePhase {
|
||||
case .background, .inactive:
|
||||
viewModel.pause()
|
||||
case .active:
|
||||
viewModel.play()
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ public struct StatusPollView: View {
|
|||
if !viewModel.votes.isEmpty || viewModel.poll.expired {
|
||||
Spacer()
|
||||
Text("\(percentForOption(option: option)) %")
|
||||
.font(.subheadline)
|
||||
.font(.scaledSubheadline)
|
||||
.frame(width: 40)
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ public struct StatusPollView: View {
|
|||
Text(viewModel.poll.expiresAt.asDate, style: .timer)
|
||||
}
|
||||
}
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ public struct StatusPollView: View {
|
|||
}
|
||||
Text(option.title)
|
||||
.foregroundColor(.white)
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ struct StatusActionsView: View {
|
|||
.foregroundColor(action.tintColor(viewModel: viewModel, theme: theme))
|
||||
if let count = action.count(viewModel: viewModel, theme: theme) {
|
||||
Text("\(count)")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,14 +114,14 @@ struct StatusActionsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.font(.scaledCaption)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
if viewModel.favouritesCount > 0 {
|
||||
Divider()
|
||||
NavigationLink(value: RouterDestinations.favouritedBy(id: viewModel.status.id)) {
|
||||
Text("\(viewModel.favouritesCount) favorites")
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ struct StatusActionsView: View {
|
|||
Divider()
|
||||
NavigationLink(value: RouterDestinations.rebloggedBy(id: viewModel.status.id)) {
|
||||
Text("\(viewModel.reblogsCount) boosts")
|
||||
.font(.callout)
|
||||
.font(.scaledCallout)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
|
|
|
@ -33,16 +33,16 @@ public struct StatusCardView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.lineLimit(3)
|
||||
if let description = card.description, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
.foregroundColor(.gray)
|
||||
.lineLimit(3)
|
||||
}
|
||||
Text(card.url.host() ?? card.url.absoluteString)
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(theme.tintColor)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
|
|
@ -174,14 +174,14 @@ public struct StatusMediaPreviewView: View {
|
|||
content: { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxHeight: imageMaxHeight)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: isNotifications ? imageMaxHeight : nil)
|
||||
.cornerRadius(4)
|
||||
},
|
||||
placeholder: {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.gray)
|
||||
.frame(maxHeight: imageMaxHeight)
|
||||
.frame(maxHeight: isNotifications ? imageMaxHeight : nil)
|
||||
.shimmering()
|
||||
}
|
||||
)
|
||||
|
@ -213,11 +213,11 @@ public struct StatusMediaPreviewView: View {
|
|||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.gray)
|
||||
.frame(maxHeight: imageMaxHeight)
|
||||
.frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
|
||||
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
|
||||
.shimmering()
|
||||
}
|
||||
}
|
||||
.frame(width: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
|
||||
.frame(maxWidth: isNotifications ? imageMaxHeight : proxy.frame(in: .local).width)
|
||||
.frame(height: imageMaxHeight)
|
||||
if sensitive {
|
||||
cornerSensitiveButton
|
||||
|
@ -228,7 +228,7 @@ public struct StatusMediaPreviewView: View {
|
|||
isAltAlertDisplayed = true
|
||||
} label: {
|
||||
Text("ALT")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
}
|
||||
.padding(4)
|
||||
.background(.thinMaterial)
|
||||
|
@ -243,7 +243,7 @@ public struct StatusMediaPreviewView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.frame(width: isNotifications ? imageMaxHeight : nil)
|
||||
.frame(maxWidth: isNotifications ? imageMaxHeight : nil)
|
||||
.frame(height: imageMaxHeight)
|
||||
}
|
||||
.onTapGesture {
|
||||
|
|
|
@ -106,7 +106,7 @@ public struct StatusRowView: View {
|
|||
Text("You boosted")
|
||||
}
|
||||
}
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
|
@ -131,7 +131,7 @@ public struct StatusRowView: View {
|
|||
Text("Replied to")
|
||||
Text(mention.username)
|
||||
}
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
.fontWeight(.semibold)
|
||||
.onTapGesture {
|
||||
|
@ -179,7 +179,7 @@ public struct StatusRowView: View {
|
|||
Group {
|
||||
if !status.spoilerText.isEmpty {
|
||||
EmojiTextApp(status.spoilerText.asMarkdown, emojis: status.emojis)
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
Button {
|
||||
withAnimation {
|
||||
viewModel.displaySpoiler.toggle()
|
||||
|
@ -192,7 +192,7 @@ public struct StatusRowView: View {
|
|||
if !viewModel.displaySpoiler {
|
||||
HStack {
|
||||
EmojiTextApp(status.content.asMarkdown, emojis: status.emojis)
|
||||
.font(.body)
|
||||
.font(.scaledBody)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handleStatus(status: status, url: url)
|
||||
})
|
||||
|
@ -249,7 +249,7 @@ public struct StatusRowView: View {
|
|||
}
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
EmojiTextApp(status.account.safeDisplayName.asMarkdown, emojis: status.account.emojis)
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
.fontWeight(.semibold)
|
||||
Group {
|
||||
Text("@\(status.account.acct)") +
|
||||
|
@ -258,7 +258,7 @@ public struct StatusRowView: View {
|
|||
Text(" ⸱ ") +
|
||||
Text(Image(systemName: viewModel.status.visibility.iconName))
|
||||
}
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ public class StatusRowViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
func navigateToDetail(routerPath: RouterPath) {
|
||||
guard !isFocused else { return }
|
||||
if isRemote, let url = status.reblog?.url ?? status.url {
|
||||
routerPath.navigate(to: .remoteStatusDetail(url: url))
|
||||
} else {
|
||||
|
|
|
@ -141,9 +141,9 @@ public struct TimelineView: View {
|
|||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("#\(tag.name)")
|
||||
.font(.headline)
|
||||
.font(.scaledHeadline)
|
||||
Text("\(tag.totalUses) recent posts from \(tag.totalAccounts) participants")
|
||||
.font(.footnote)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
|
Loading…
Reference in a new issue