#12 Add bottom right/left menu items

This commit is contained in:
Marcin Czachursk 2023-04-06 13:19:55 +02:00
parent d38dd1499d
commit f6fb5b7a83
14 changed files with 365 additions and 114 deletions

View File

@ -28,6 +28,7 @@ extension ApplicationSettings {
@NSManaged public var showSensitive: Bool
@NSManaged public var showPhotoDescription: Bool
@NSManaged public var menuPosition: Int32
}
extension ApplicationSettings: Identifiable {

View File

@ -107,6 +107,12 @@ class ApplicationSettingsHandler {
CoreDataHandler.shared.save()
}
func set(menuPosition: MenuPosition) {
let defaultSettings = self.get()
defaultSettings.menuPosition = Int32(menuPosition.rawValue)
CoreDataHandler.shared.save()
}
private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ApplicationSettings(context: context)

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Vernissage-006.xcdatamodel</string>
<string>Vernissage-007.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E252" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
<attribute name="accessToken" optional="YES" attributeType="String"/>
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" optional="YES" attributeType="URI"/>
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
<attribute name="clientId" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="clientVapidKey" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" optional="YES" attributeType="URI"/>
<attribute name="id" attributeType="String"/>
<attribute name="lastSeenStatusId" optional="YES" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="refreshToken" optional="YES" attributeType="String"/>
<attribute name="serverUrl" attributeType="URI"/>
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="username" attributeType="String"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
</entity>
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
<attribute name="activeIcon" attributeType="String" defaultValueString="Default"/>
<attribute name="avatarShape" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="currentAccount" optional="YES" attributeType="String"/>
<attribute name="hapticAnimationEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticButtonPressEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticNotificationEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticRefreshEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticTabSelectionEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="lastRefreshTokens" attributeType="Date" defaultDateTimeInterval="694256400" usesScalarValueType="NO"/>
<attribute name="menuPosition" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="showPhotoDescription" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showSensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="theme" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
</entity>
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
<attribute name="blurhash" optional="YES" attributeType="String"/>
<attribute name="data" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
<attribute name="exifCamera" optional="YES" attributeType="String"/>
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
<attribute name="exifExposure" optional="YES" attributeType="String"/>
<attribute name="exifLens" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="metaImageHeight" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="metaImageWidth" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
<attribute name="statusId" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="url" attributeType="URI"/>
<relationship name="statusRelation" maxCount="1" deletionRule="Nullify" destinationEntity="StatusData" inverseName="attachmentsRelation" inverseEntity="StatusData"/>
</entity>
<entity name="StatusData" representedClassName="StatusData" syncable="YES">
<attribute name="accountAvatar" optional="YES" attributeType="URI"/>
<attribute name="accountDisplayName" optional="YES" attributeType="String"/>
<attribute name="accountId" attributeType="String"/>
<attribute name="accountUsername" optional="YES" attributeType="String"/>
<attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="applicationWebsite" optional="YES" attributeType="URI"/>
<attribute name="bookmarked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="favourited" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="favouritesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="inReplyToAccount" optional="YES" attributeType="String"/>
<attribute name="inReplyToId" optional="YES" attributeType="String"/>
<attribute name="muted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="pinned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="reblogged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="rebloggedAccountAvatar" optional="YES" attributeType="URI"/>
<attribute name="rebloggedAccountDisplayName" optional="YES" attributeType="String"/>
<attribute name="rebloggedAccountId" optional="YES" attributeType="String"/>
<attribute name="rebloggedAccountUsername" optional="YES" attributeType="String"/>
<attribute name="rebloggedStatusId" optional="YES" attributeType="String"/>
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibility" attributeType="String"/>
<relationship name="attachmentsRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
<relationship name="pixelfedAccount" maxCount="1" deletionRule="Nullify" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
</entity>
</model>

View File

@ -204,6 +204,10 @@
"settings.title.sourceCode" = "Source code";
"settings.title.rate" = "Rate Vernissage";
"settings.title.socials" = "Socials";
"settings.title.menuPosition" = "Menu position";
"settings.title.topMenu" = "Navigation bar";
"settings.title.bottomRightMenu" = "Bottom right";
"settings.title.bottomLeftMenu" = "Bottom left";
// Mark: Signin view.
"signin.navigationBar.title" = "Sign in to Pixelfed";

View File

@ -204,6 +204,10 @@
"settings.title.sourceCode" = "Iturburu kodea";
"settings.title.rate" = "Rate Vernissage";
"settings.title.socials" = "Socials";
"settings.title.menuPosition" = "Menu position";
"settings.title.topMenu" = "Navigation bar";
"settings.title.bottomRightMenu" = "Bottom right";
"settings.title.bottomLeftMenu" = "Bottom left";
// Mark: Signin view.
"signin.navigationBar.title" = "Hasi saioa Pixelfed-en";

View File

@ -204,6 +204,10 @@
"settings.title.sourceCode" = "Kod źródłowy";
"settings.title.rate" = "Oceń Vernissage";
"settings.title.socials" = "Społeczności";
"settings.title.menuPosition" = "Pozycja menu";
"settings.title.topMenu" = "Panel tytułowy";
"settings.title.bottomRightMenu" = "Dolny prawy";
"settings.title.bottomLeftMenu" = "Dolny lewy";
// Mark: Signin view.
"signin.navigationBar.title" = "Zaloguj się do Pixelfed";

13
Models/MenuPosition.swift Normal file
View File

@ -0,0 +1,13 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
public enum MenuPosition: Int {
case top = 1
case bottomRight = 2
case bottomLeft = 3
}

View File

@ -146,6 +146,9 @@
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */; };
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD31295F5029009B20C9 /* RemoteFileService.swift */; };
F8911A1729DE914D00770F44 /* NavigationMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8911A1629DE914D00770F44 /* NavigationMenu.swift */; };
F8911A1A29DEA0F500770F44 /* MenuPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8911A1929DEA0F500770F44 /* MenuPosition.swift */; };
F8911A1B29DEA0F500770F44 /* MenuPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8911A1929DEA0F500770F44 /* MenuPosition.swift */; };
F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F891E7CD29C35BF50022C449 /* ImageRowItem.swift */; };
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */ = {isa = PBXBuildFile; fileRef = F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */; };
F897978829681B9C00B22335 /* UserAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897978729681B9C00B22335 /* UserAvatar.swift */; };
@ -369,6 +372,9 @@
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; };
F88FAD31295F5029009B20C9 /* RemoteFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFileService.swift; sourceTree = "<group>"; };
F8911A1629DE914D00770F44 /* NavigationMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenu.swift; sourceTree = "<group>"; };
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-007.xcdatamodel"; sourceTree = "<group>"; };
F8911A1929DEA0F500770F44 /* MenuPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuPosition.swift; sourceTree = "<group>"; };
F891E7CD29C35BF50022C449 /* ImageRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowItem.swift; sourceTree = "<group>"; };
F891E7CF29C368750022C449 /* ImageRowItemAsync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowItemAsync.swift; sourceTree = "<group>"; };
F897978729681B9C00B22335 /* UserAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAvatar.swift; sourceTree = "<group>"; };
@ -692,6 +698,7 @@
F86167C7297FE781004D1F67 /* AvatarShape.swift */,
F89D6C3E29716E41001DA3D4 /* Theme.swift */,
F87AEB932986C51B00434FB6 /* AppConstants.swift */,
F8911A1929DEA0F500770F44 /* MenuPosition.swift */,
);
path = Models;
sourceTree = "<group>";
@ -937,6 +944,7 @@
F8C5E56129892CC300ADF6A7 /* FirstAppear.swift */,
F8CAE63D29B8902D001E0372 /* ClearButton.swift */,
F8E9391E29C0BCFA002BB3B8 /* ImageContextMenu.swift */,
F8911A1629DE914D00770F44 /* NavigationMenu.swift */,
);
path = ViewModifiers;
sourceTree = "<group>";
@ -1140,6 +1148,7 @@
F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */,
F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */,
F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */,
F8911A1B29DEA0F500770F44 /* MenuPosition.swift in Sources */,
F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */,
F864F78329BB9A6800B13921 /* StatusData+CoreDataProperties.swift in Sources */,
F864F78429BB9A6E00B13921 /* ApplicationSettings+CoreDataClass.swift in Sources */,
@ -1187,6 +1196,7 @@
F8864CF129ACFFB80020C534 /* View+Keyboard.swift in Sources */,
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */,
F8FA991E299FAB92007AB130 /* PhotoEditorView.swift in Sources */,
F8911A1729DE914D00770F44 /* NavigationMenu.swift in Sources */,
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */,
F87AEB972986D16D00434FB6 /* AuthorisationError.swift in Sources */,
F8742FC429990AFB00E9642B /* ClientError.swift in Sources */,
@ -1254,6 +1264,7 @@
F89A46DE296EABA20062125F /* StatusPlaceholderView.swift in Sources */,
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */,
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
F8911A1A29DEA0F500770F44 /* MenuPosition.swift in Sources */,
F8996DEB2971D29D0043EEC6 /* View+Transition.swift in Sources */,
F876418B298AC1B80057D362 /* NoDataView.swift in Sources */,
F8E9392129C0DA7E002BB3B8 /* LazyImageState+ImageResponse.swift in Sources */,
@ -1361,7 +1372,7 @@
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1372,7 +1383,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1389,7 +1400,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
@ -1400,7 +1411,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1537,7 +1548,7 @@
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1554,7 +1565,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1577,7 +1588,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 94;
CURRENT_PROJECT_VERSION = 95;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1594,7 +1605,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@ -1731,6 +1742,7 @@
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */,
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */,
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */,
F8B05ACC29B48DD000857221 /* Vernissage-004.xcdatamodel */,
@ -1739,7 +1751,7 @@
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
);
currentVersion = F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */;
currentVersion = F8911A1829DE9E5500770F44 /* Vernissage-007.xcdatamodel */;
path = Vernissage.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@ -85,6 +85,9 @@ public class ApplicationState: ObservableObject {
/// Updated user profile.
@Published var updatedProfile: Account?
/// Information which menu should be shown (top or bottom).
@Published var menuPosition = MenuPosition.top
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
self.account = accountModel
self.lastSeenStatusId = lastSeenStatusId

View File

@ -169,6 +169,10 @@ struct VernissageApp: App {
self.applicationState.showSensitive = defaultSettings.showSensitive
self.applicationState.showPhotoDescription = defaultSettings.showPhotoDescription
if let menuPosition = MenuPosition(rawValue: Int(defaultSettings.menuPosition)) {
self.applicationState.menuPosition = menuPosition
}
self.applicationState.hapticTabSelectionEnabled = defaultSettings.hapticTabSelectionEnabled
self.applicationState.hapticRefreshEnabled = defaultSettings.hapticRefreshEnabled
self.applicationState.hapticButtonPressEnabled = defaultSettings.hapticButtonPressEnabled

View File

@ -0,0 +1,74 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
public extension View {
func navigationMenu<MenuItems>(menuPosition: Binding<MenuPosition>,
@ViewBuilder menuItems: @escaping () -> MenuItems) -> some View where MenuItems: View {
modifier(NavigationMenu(menuPosition: menuPosition, menuItems: menuItems))
}
}
private struct NavigationMenu<MenuItems>: ViewModifier where MenuItems: View {
private let menuItems: () -> MenuItems
@Binding var menuPosition: MenuPosition
init(menuPosition: Binding<MenuPosition>, @ViewBuilder menuItems: @escaping () -> MenuItems) {
self.menuItems = menuItems
self._menuPosition = menuPosition
}
func body(content: Content) -> some View {
if self.menuPosition == .top {
content
} else {
ZStack {
content
VStack(alignment: .trailing) {
Spacer()
HStack {
if self.menuPosition == .bottomRight {
Spacer()
self.menuContent()
.padding(.trailing, 20)
}
if self.menuPosition == .bottomLeft {
self.menuContent()
.padding(.leading, 20)
Spacer()
}
}
}
}
}
}
@ViewBuilder
private func menuContent() -> some View {
Menu {
self.menuItems()
} label: {
Image(systemName: "line.3.horizontal.circle")
.resizable()
.foregroundColor(.mainTextColor.opacity(0.8))
.shadow(radius: 5)
.padding(8)
.frame(width: 44, height: 44)
.background(.ultraThinMaterial)
.clipShape(Circle())
}
}
}

View File

@ -32,21 +32,28 @@ struct MainView: View {
var body: some View {
self.getMainView()
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
self.getLeadingToolbar()
self.getPrincipalToolbar()
self.getTrailingToolbar()
}
.onChange(of: tipsStore.status) { status in
if status == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()
.navigationMenu(menuPosition: $applicationState.menuPosition) {
self.navigationMenuContent()
}
.navigationTitle(navBarTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
self.getLeadingToolbar()
if self.applicationState.menuPosition == .top {
self.getPrincipalToolbar()
}
self.getTrailingToolbar()
}
.onChange(of: tipsStore.status) { status in
if status == .successful {
withAnimation(.spring()) {
self.routerPath.presentedOverlay = .successPayment
self.tipsStore.reset()
}
}
}
}
}
@ViewBuilder
@ -92,97 +99,7 @@ struct MainView: View {
private func getPrincipalToolbar() -> some ToolbarContent {
ToolbarItem(placement: .principal) {
Menu {
Button {
self.switchView(to: .home)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .home))
Image(systemName: "house")
}
}
Button {
self.switchView(to: .local)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .local))
Image(systemName: "building")
}
}
Button {
self.switchView(to: .federated)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .federated))
Image(systemName: "globe.europe.africa")
}
}
Button {
self.switchView(to: .search)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .search))
Image(systemName: "magnifyingglass")
}
}
Divider()
Menu {
Button {
self.switchView(to: .trendingPhotos)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .trendingPhotos))
Image(systemName: "photo.stack")
}
}
Button {
self.switchView(to: .trendingTags)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .trendingTags))
Image(systemName: "tag")
}
}
Button {
self.switchView(to: .trendingAccounts)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .trendingAccounts))
Image(systemName: "person.3")
}
}
} label: {
HStack {
Text("mainview.tab.trending", comment: "Trending menu section")
Image(systemName: "chart.line.uptrend.xyaxis")
}
}
Divider()
Button {
self.switchView(to: .profile)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .profile))
Image(systemName: "person.crop.circle")
}
}
Button {
self.switchView(to: .notifications)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .notifications))
Image(systemName: "bell.badge")
}
}
self.navigationMenuContent()
} label: {
HStack {
Text(navBarTitle, comment: "Navbar title")
@ -244,6 +161,101 @@ struct MainView: View {
}
}
@ViewBuilder
private func navigationMenuContent() -> some View {
Button {
self.switchView(to: .home)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .home))
Image(systemName: "house")
}
}
Button {
self.switchView(to: .local)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .local))
Image(systemName: "building")
}
}
Button {
self.switchView(to: .federated)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .federated))
Image(systemName: "globe.europe.africa")
}
}
Button {
self.switchView(to: .search)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .search))
Image(systemName: "magnifyingglass")
}
}
Divider()
Menu {
Button {
self.switchView(to: .trendingPhotos)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .trendingPhotos))
Image(systemName: "photo.stack")
}
}
Button {
self.switchView(to: .trendingTags)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .trendingTags))
Image(systemName: "tag")
}
}
Button {
self.switchView(to: .trendingAccounts)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .trendingAccounts))
Image(systemName: "person.3")
}
}
} label: {
HStack {
Text("mainview.tab.trending", comment: "Trending menu section")
Image(systemName: "chart.line.uptrend.xyaxis")
}
}
Divider()
Button {
self.switchView(to: .profile)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .profile))
Image(systemName: "person.crop.circle")
}
}
Button {
self.switchView(to: .notifications)
} label: {
HStack {
Text(self.getViewTitle(viewMode: .notifications))
Image(systemName: "bell.badge")
}
}
}
@ViewBuilder
private func getAvatarImage(avatarUrl: URL?, avatarData: Data?) -> some View {
if let avatarData,

View File

@ -29,6 +29,12 @@ struct GeneralSectionView: View {
(Theme.dark, "settings.title.dark")
]
private let menuPositions: [(menuPosition: MenuPosition, name: LocalizedStringKey)] = [
(MenuPosition.top, "settings.title.topMenu"),
(MenuPosition.bottomRight, "settings.title.bottomRightMenu"),
(MenuPosition.bottomLeft, "settings.title.bottomLeftMenu")
]
var body: some View {
Section("settings.title.general") {
@ -64,6 +70,20 @@ struct GeneralSectionView: View {
self.applicationState.theme = theme
ApplicationSettingsHandler.shared.set(theme: theme)
}
// Menu position.
Picker(selection: $applicationState.menuPosition) {
ForEach(self.menuPositions, id: \.menuPosition) { item in
Text(item.name, comment: "Menu positions")
.tag(item.menuPosition)
}
} label: {
Text("settings.title.menuPosition", comment: "Menu position")
}
.onChange(of: self.applicationState.menuPosition) { menuPosition in
self.applicationState.menuPosition = menuPosition
ApplicationSettingsHandler.shared.set(menuPosition: menuPosition)
}
}
}
}