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