Multi account sidebar + scaled font size on macOS + better iPad / macOS app UX

This commit is contained in:
Thomas Ricouard 2023-01-17 19:41:46 +01:00
parent bb72327f52
commit 4143e82fbc
34 changed files with 277 additions and 129 deletions

View file

@ -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) {}

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View 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
}
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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()

View file

@ -35,7 +35,7 @@ public struct ListEditView: View {
emojis: account.emojis)
Text("@\(account.acct)")
.foregroundColor(.gray)
.font(.footnote)
.font(.scaledFootnote)
}
}
.listRowBackground(theme.primaryBackgroundColor)

View file

@ -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() {}

View file

@ -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)

View file

@ -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)
}

View file

@ -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?)] {

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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 {}
}

View file

@ -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)
}
}

View file

@ -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
}
})
}
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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()