Merge remote-tracking branch 'upstream/main' into zh-Hant-localization
35
.github/workflows/validate_translations.yml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
name: Validate Translations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, labeled, unlabeled, edited]
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Validate Translations
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- name: git checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: ruby versions
|
||||
run: |
|
||||
ruby --version
|
||||
gem --version
|
||||
bundler --version
|
||||
|
||||
- name: ruby setup
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3
|
||||
bundler-cache: true
|
||||
|
||||
# additional steps here, if needed
|
||||
|
||||
- name: Clone SwiftPolyglot
|
||||
run: git clone https://github.com/appdecostudio/SwiftPolyglot.git --branch 0.2.0
|
||||
|
||||
- name: Build and Run SwiftPolyglot
|
||||
run: |
|
||||
swift build --package-path ./SwiftPolyglot --configuration release
|
||||
swift run --package-path ./SwiftPolyglot swiftpolyglot "en,eu,be,ca,zh-Hans,zh-Hant,nl,en-GB,fr,de,it,ja,ko,nb,pl,pt-BR,es,tr,uk"
|
4
.gitignore
vendored
|
@ -89,4 +89,6 @@ fastlane/test_output
|
|||
|
||||
iOSInjectionProject/
|
||||
.DS_Store
|
||||
IceCubesApp.xcconfig
|
||||
IceCubesApp.xcconfig
|
||||
*.resolved
|
||||
buildServer.json
|
||||
|
|
14
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "sweetpad-lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"preLaunchTask": "sweetpad: launch"
|
||||
}
|
||||
]
|
||||
}
|
6
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"[swift]": {
|
||||
"editor.defaultFormatter": "sweetpad.sweetpad",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
}
|
|
@ -6,14 +6,13 @@
|
|||
//
|
||||
|
||||
import MobileCoreServices
|
||||
import Models
|
||||
import Network
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
import Models
|
||||
import Network
|
||||
|
||||
// Sample code was sending this from a thread to another, let asume @Sendable for this
|
||||
extension NSExtensionContext: @unchecked Sendable {}
|
||||
extension NSExtensionContext: @unchecked @retroactive Sendable {}
|
||||
|
||||
final class ActionRequestHandler: NSObject, NSExtensionRequestHandling, Sendable {
|
||||
enum Error: Swift.Error {
|
||||
|
@ -79,11 +78,17 @@ extension ActionRequestHandler {
|
|||
guard itemProvider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
|
||||
continue
|
||||
}
|
||||
guard let dictionary = try await itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier) as? [String: Any] else {
|
||||
guard
|
||||
let dictionary = try await itemProvider.loadItem(
|
||||
forTypeIdentifier: UTType.propertyList.identifier) as? [String: Any]
|
||||
else {
|
||||
throw Error.loadedItemHasWrongType
|
||||
}
|
||||
let input = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:]
|
||||
guard let absoluteStringUrl = input["url"] as? String, let url = URL(string: absoluteStringUrl) else {
|
||||
let input =
|
||||
dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [String: Any]? ?? [:]
|
||||
guard let absoluteStringUrl = input["url"] as? String,
|
||||
let url = URL(string: absoluteStringUrl)
|
||||
else {
|
||||
throw Error.urlNotFound
|
||||
}
|
||||
return url
|
||||
|
@ -96,7 +101,8 @@ extension ActionRequestHandler {
|
|||
private func output(wrapping deeplink: URL) -> [NSExtensionItem] {
|
||||
let results = ["deeplink": deeplink.absoluteString]
|
||||
let dictionary = [NSExtensionJavaScriptFinalizeArgumentKey: results]
|
||||
let provider = NSItemProvider(item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)
|
||||
let provider = NSItemProvider(
|
||||
item: dictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)
|
||||
let item = NSExtensionItem()
|
||||
item.attachments = [provider]
|
||||
return [item]
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
InfoPlist.strings
|
||||
IceCubesApp
|
||||
|
||||
Created by Thomas Durand on 27/01/2023.
|
||||
|
||||
*/
|
||||
|
||||
"CFBundleDisplayName" = "Apri con Ice Cubes";
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"originHash" : "800f54ad2dcf12c6d2dda1b14c463b8c702e2a6461b5cb072611d4016256adb3",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "bodega",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/mergesort/Bodega",
|
||||
"state" : {
|
||||
"revision" : "f0554077c178088ba11557bbdbb71775cc6a1b84",
|
||||
"version" : "2.1.0"
|
||||
"revision" : "bfd8871e9c2590d31b200e54c75428a71483afdf",
|
||||
"version" : "2.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -14,8 +15,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Dean151/ButtonKit",
|
||||
"state" : {
|
||||
"revision" : "377f5bab4ed047704316d531e0826d4de5ebf6a4",
|
||||
"version" : "0.1.1"
|
||||
"revision" : "d567519b297777c38dee56ef10201fef4962ff75",
|
||||
"version" : "0.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -23,17 +24,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/divadretlaw/EmojiText",
|
||||
"state" : {
|
||||
"revision" : "c54000aa9ccc048619054a5a2da2ce0576ffea18",
|
||||
"version" : "4.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "giphy-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Giphy/giphy-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "9c58a350a3381f1641f5a31cdcd162a406274892",
|
||||
"version" : "2.2.8"
|
||||
"revision" : "174a7bc7bd75650ad1acb5679dbb754296093de0",
|
||||
"version" : "4.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -42,16 +34,7 @@
|
|||
"location" : "https://github.com/evgenyneu/keychain-swift",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "f38cb0ada97847ac5068b915b8d2793b35435668"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "libwebp-xcode",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/libwebp-Xcode",
|
||||
"state" : {
|
||||
"revision" : "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4",
|
||||
"version" : "1.3.2"
|
||||
"revision" : "5e1b02b6a9dac2a759a1d5dbc175c86bd192a608"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -59,8 +42,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nicklockwood/LRUCache",
|
||||
"state" : {
|
||||
"revision" : "6d2b5246c9c98dcd498552bb22f08d55b12a8371",
|
||||
"version" : "1.0.4"
|
||||
"revision" : "542f0449556327415409ededc9c43a4bd0a397dc",
|
||||
"version" : "1.0.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -68,17 +51,17 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Nuke",
|
||||
"state" : {
|
||||
"revision" : "8ecbfc886da39bccb01c34abef5f2ff4073ad633",
|
||||
"version" : "12.4.0"
|
||||
"revision" : "0ead44350d2737db384908569c012fe67c421e4d",
|
||||
"version" : "12.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "purchases-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios.git",
|
||||
"location" : "https://github.com/RevenueCat/purchases-ios",
|
||||
"state" : {
|
||||
"revision" : "b3597e0aa5e1193b04d9e1ff04eaf5cab3b8737d",
|
||||
"version" : "4.34.0"
|
||||
"revision" : "7d55b964114a3d4a76791227cdc28577617596db",
|
||||
"version" : "4.43.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -95,8 +78,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/stephencelis/SQLite.swift.git",
|
||||
"state" : {
|
||||
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
|
||||
"version" : "0.14.1"
|
||||
"revision" : "a95fc6df17d108bd99210db5e8a9bac90fe984b8",
|
||||
"version" : "0.15.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -104,8 +87,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-cmark.git",
|
||||
"state" : {
|
||||
"revision" : "f218e5d7691f78b55bfa39b367763f4612486c35",
|
||||
"version" : "0.3.0"
|
||||
"revision" : "3ccff77b2dc5b96b77db3da0d68d28068593fa53",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -113,8 +96,17 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-markdown",
|
||||
"state" : {
|
||||
"revision" : "e4f95e2dc23097a1a9a1dfdfe3fe3ee44de77378",
|
||||
"version" : "0.3.0"
|
||||
"revision" : "8f79cb175981458a0a27e76cb42fee8e17b1a993",
|
||||
"version" : "0.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TelemetryDeck/SwiftSDK",
|
||||
"state" : {
|
||||
"revision" : "13a26cf125b70d695913eb9bea9f9b9c29da5790",
|
||||
"version" : "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -122,19 +114,46 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6",
|
||||
"version" : "2.6.1"
|
||||
"revision" : "3c2c7e1e72b8abd96eafbae80323c5c1e5317437",
|
||||
"version" : "2.7.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftui-introspect",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
|
||||
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||
"state" : {
|
||||
"revision" : "9e1cc02a65b22e09a8251261cccbccce02731fc5",
|
||||
"version" : "1.1.1"
|
||||
"revision" : "668a65735751432b640260c56dfa621cec568368",
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "wishkit-ios",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wishkit/wishkit-ios.git",
|
||||
"state" : {
|
||||
"revision" : "2b5eb8d1fb13706f8c14767a5239e34e403375f1",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "wishkit-ios-shared",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wishkit/wishkit-ios-shared.git",
|
||||
"state" : {
|
||||
"revision" : "118c9c482e4ad57c65d664283516425b98616483",
|
||||
"version" : "1.4.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "wrappinghstack",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/dkk/WrappingHStack",
|
||||
"state" : {
|
||||
"revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b",
|
||||
"version" : "2.2.11"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
"version" : 3
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1600"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9F7788C42BE652B1004E6BEF"
|
||||
BuildableName = "IceCubesAppWidgetsExtensionExtension.appex"
|
||||
BlueprintName = "IceCubesAppWidgetsExtensionExtension"
|
||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||
BuildableName = "Ice Cubes.app"
|
||||
BlueprintName = "IceCubesApp"
|
||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||
BuildableName = "Ice Cubes.app"
|
||||
BlueprintName = "IceCubesApp"
|
||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "_XCWidgetKind"
|
||||
value = ""
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "_XCWidgetDefaultView"
|
||||
value = "timeline"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "_XCWidgetFamily"
|
||||
value = "systemMedium"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||
BuildableName = "Ice Cubes.app"
|
||||
BlueprintName = "IceCubesApp"
|
||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
@ -56,8 +56,12 @@
|
|||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "1"
|
||||
BundleIdentifier = "com.thomasricouard.IceCubesApp"
|
||||
RemotePath = "/Users/dimillian/Library/Developer/CoreSimulator/Devices/8EF923D0-4CF1-49B6-B287-5F05AD5440C1/data/Containers/Bundle/Application/C447A1D1-9BC9-49C9-8FA5-130E8403972F/Ice Cubes.app">
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9FBFE638292A715500C250E9"
|
||||
|
@ -65,7 +69,7 @@
|
|||
BlueprintName = "IceCubesApp"
|
||||
ReferencedContainer = "container:IceCubesApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
LastUpgradeVersion = "1600"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import AVFoundation
|
||||
import Account
|
||||
import AppAccount
|
||||
import AVFoundation
|
||||
import DesignSystem
|
||||
import Env
|
||||
import KeychainSwift
|
||||
|
@ -21,64 +21,53 @@ struct AppView: View {
|
|||
@Environment(\.openWindow) var openWindow
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Binding var selectedTab: Tab
|
||||
@Binding var selectedTab: AppTab
|
||||
@Binding var appRouterPath: RouterPath
|
||||
|
||||
@State var popToRootTab: Tab = .other
|
||||
@State var iosTabs = iOSTabs.shared
|
||||
@State var sidebarTabs = SidebarTabs.shared
|
||||
@State var selectedTabScrollToTop: Int = -1
|
||||
|
||||
var body: some View {
|
||||
#if os(visionOS)
|
||||
switch UIDevice.current.userInterfaceIdiom {
|
||||
case .vision:
|
||||
tabBarView
|
||||
#else
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
case .pad, .mac:
|
||||
#if !os(visionOS)
|
||||
sidebarView
|
||||
} else {
|
||||
#else
|
||||
tabBarView
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
default:
|
||||
tabBarView
|
||||
}
|
||||
}
|
||||
|
||||
var availableTabs: [Tab] {
|
||||
var availableTabs: [AppTab] {
|
||||
guard appAccountsManager.currentClient.isAuth else {
|
||||
return Tab.loggedOutTab()
|
||||
return AppTab.loggedOutTab()
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
|
||||
return iosTabs.tabs
|
||||
} else if UIDevice.current.userInterfaceIdiom == .vision {
|
||||
return Tab.visionOSTab()
|
||||
return AppTab.visionOSTab()
|
||||
}
|
||||
return sidebarTabs.tabs.map { $0.tab }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var tabBarView: some View {
|
||||
TabView(selection: .init(get: {
|
||||
selectedTab
|
||||
}, set: { newTab in
|
||||
if newTab == .post {
|
||||
#if os(visionOS)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
if newTab == selectedTab {
|
||||
/// Stupid hack to trigger onChange binding in tab views.
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
popToRootTab = selectedTab
|
||||
}
|
||||
}
|
||||
|
||||
HapticManager.shared.fireHaptic(.tabSelection)
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
|
||||
selectedTab = newTab
|
||||
})) {
|
||||
TabView(
|
||||
selection: .init(
|
||||
get: {
|
||||
selectedTab
|
||||
},
|
||||
set: { newTab in
|
||||
updateTab(with: newTab)
|
||||
})
|
||||
) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
||||
tab.makeContentView(selectedTab: $selectedTab)
|
||||
.tabItem {
|
||||
if userPreferences.showiPhoneTabLabel {
|
||||
tab.label
|
||||
|
@ -94,11 +83,39 @@ struct AppView: View {
|
|||
}
|
||||
.id(appAccountsManager.currentClient.id)
|
||||
.withSheetDestinations(sheetDestinations: $appRouterPath.presentedSheet)
|
||||
.environment(\.selectedTabScrollToTop, selectedTabScrollToTop)
|
||||
}
|
||||
|
||||
private func badgeFor(tab: Tab) -> Int {
|
||||
private func updateTab(with newTab: AppTab) {
|
||||
if newTab == .post {
|
||||
#if os(visionOS)
|
||||
openWindow(
|
||||
value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
)
|
||||
#else
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
return
|
||||
}
|
||||
|
||||
HapticManager.shared.fireHaptic(.tabSelection)
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
|
||||
if selectedTab == newTab {
|
||||
selectedTabScrollToTop = newTab.rawValue
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
selectedTabScrollToTop = -1
|
||||
}
|
||||
} else {
|
||||
selectedTabScrollToTop = -1
|
||||
}
|
||||
|
||||
selectedTab = newTab
|
||||
}
|
||||
|
||||
private func badgeFor(tab: AppTab) -> Int {
|
||||
if tab == .notifications, selectedTab != tab,
|
||||
let token = appAccountsManager.currentAccount.oauthToken
|
||||
let token = appAccountsManager.currentAccount.oauthToken
|
||||
{
|
||||
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
|
||||
}
|
||||
|
@ -107,29 +124,32 @@ struct AppView: View {
|
|||
|
||||
#if !os(visionOS)
|
||||
var sidebarView: some View {
|
||||
SideBarView(selectedTab: $selectedTab,
|
||||
popToRootTab: $popToRootTab,
|
||||
tabs: availableTabs)
|
||||
{
|
||||
SideBarView(
|
||||
selectedTab: .init(
|
||||
get: {
|
||||
selectedTab
|
||||
},
|
||||
set: { newTab in
|
||||
updateTab(with: newTab)
|
||||
}), tabs: availableTabs
|
||||
) {
|
||||
HStack(spacing: 0) {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab
|
||||
.makeContentView(selectedTab: $selectedTab, popToRootTab: $popToRootTab)
|
||||
.tabItem {
|
||||
tab.label
|
||||
if #available(iOS 18.0, *) {
|
||||
baseTabView
|
||||
#if targetEnvironment(macCatalyst)
|
||||
.tabViewStyle(.sidebarAdaptable)
|
||||
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||
tabview.sidebar.isHidden = true
|
||||
}
|
||||
.tag(tab)
|
||||
}
|
||||
}
|
||||
.introspect(.tabView, on: .iOS(.v17)) { (tabview: UITabBarController) in
|
||||
tabview.tabBar.isHidden = horizontalSizeClass == .regular
|
||||
tabview.customizableViewControllers = []
|
||||
tabview.moreNavigationController.isNavigationBarHidden = true
|
||||
#else
|
||||
.tabViewStyle(.tabBarOnly)
|
||||
#endif
|
||||
} else {
|
||||
baseTabView
|
||||
}
|
||||
if horizontalSizeClass == .regular,
|
||||
appAccountsManager.currentClient.isAuth,
|
||||
userPreferences.showiPadSecondaryColumn
|
||||
appAccountsManager.currentClient.isAuth,
|
||||
userPreferences.showiPadSecondaryColumn
|
||||
{
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
notificationsSecondaryColumn
|
||||
|
@ -137,12 +157,33 @@ struct AppView: View {
|
|||
}
|
||||
}
|
||||
.environment(appRouterPath)
|
||||
.environment(\.selectedTabScrollToTop, selectedTabScrollToTop)
|
||||
}
|
||||
#endif
|
||||
|
||||
private var baseTabView: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(availableTabs) { tab in
|
||||
tab
|
||||
.makeContentView(selectedTab: $selectedTab)
|
||||
.toolbar(horizontalSizeClass == .regular ? .hidden : .visible, for: .tabBar)
|
||||
.tabItem {
|
||||
tab.label
|
||||
}
|
||||
.tag(tab)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.introspect(.tabView, on: .iOS(.v17, .v18)) { (tabview: UITabBarController) in
|
||||
tabview.tabBar.isHidden = horizontalSizeClass == .regular
|
||||
tabview.customizableViewControllers = []
|
||||
tabview.moreNavigationController.isNavigationBarHidden = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var notificationsSecondaryColumn: some View {
|
||||
NotificationsTab(selectedTab: .constant(.notifications),
|
||||
popToRootTab: $popToRootTab, lockedType: nil)
|
||||
NotificationsTab(selectedTab: .constant(.notifications), lockedType: nil)
|
||||
.environment(\.isSecondaryColumn, true)
|
||||
.frame(maxWidth: .secondaryColumnWidth)
|
||||
.id(appAccountsManager.currentAccount.id)
|
||||
|
|
|
@ -4,6 +4,12 @@ import SwiftUI
|
|||
extension IceCubesApp {
|
||||
@CommandsBuilder
|
||||
var appMenu: some Commands {
|
||||
CommandGroup(replacing: .appSettings) {
|
||||
Button("menu.settings") {
|
||||
appRouterPath.presentedSheet = .settings
|
||||
}
|
||||
.keyboardShortcut(",", modifiers: .command)
|
||||
}
|
||||
CommandGroup(replacing: .newItem) {
|
||||
Button("menu.new-window") {
|
||||
openWindow(id: "MainWindow")
|
||||
|
@ -11,9 +17,12 @@ extension IceCubesApp {
|
|||
.keyboardShortcut("n", modifiers: .shift)
|
||||
Button("menu.new-post") {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
openWindow(
|
||||
value: WindowDestinationEditor.newStatusEditor(
|
||||
visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
appRouterPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
appRouterPath.presentedSheet = .newStatusEditor(
|
||||
visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
}
|
||||
.keyboardShortcut("n", modifiers: .command)
|
||||
|
@ -54,5 +63,11 @@ extension IceCubesApp {
|
|||
}
|
||||
.keyboardShortcut("l", modifiers: .shift)
|
||||
}
|
||||
CommandGroup(replacing: .help) {
|
||||
Button("menu.help.github") {
|
||||
let url = URL(string: "https://github.com/Dimillian/IceCubesApp/issues")!
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import AppIntents
|
||||
import Env
|
||||
import MediaUI
|
||||
import StatusKit
|
||||
|
@ -22,20 +23,36 @@ extension IceCubesApp {
|
|||
.environment(theme)
|
||||
.environment(watcher)
|
||||
.environment(pushNotificationsService)
|
||||
.environment(appIntentService)
|
||||
.environment(\.isSupporter, isSupporter)
|
||||
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
|
||||
MediaUIView(selectedAttachment: selectedMediaAttachment,
|
||||
attachments: quickLook.mediaAttachments)
|
||||
if #available(iOS 18.0, *) {
|
||||
MediaUIView(
|
||||
selectedAttachment: selectedMediaAttachment,
|
||||
attachments: quickLook.mediaAttachments
|
||||
)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.presentationCornerRadius(16)
|
||||
.presentationSizing(.page)
|
||||
.withEnvironments()
|
||||
} else {
|
||||
MediaUIView(
|
||||
selectedAttachment: selectedMediaAttachment,
|
||||
attachments: quickLook.mediaAttachments
|
||||
)
|
||||
.presentationBackground(.ultraThinMaterial)
|
||||
.presentationCornerRadius(16)
|
||||
.withEnvironments()
|
||||
}
|
||||
}
|
||||
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
|
||||
if newValue != nil {
|
||||
pushNotificationsService.handledNotification = nil
|
||||
if appAccountsManager.currentAccount.oauthToken?.accessToken != newValue?.account.token.accessToken,
|
||||
let account = appAccountsManager.availableAccounts.first(where:
|
||||
{ $0.oauthToken?.accessToken == newValue?.account.token.accessToken })
|
||||
if appAccountsManager.currentAccount.oauthToken?.accessToken
|
||||
!= newValue?.account.token.accessToken,
|
||||
let account = appAccountsManager.availableAccounts.first(where: {
|
||||
$0.oauthToken?.accessToken == newValue?.account.token.accessToken
|
||||
})
|
||||
{
|
||||
appAccountsManager.currentAccount = account
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
|
@ -47,13 +64,14 @@ extension IceCubesApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: appIntentService.handledIntent) { _, _ in
|
||||
if let intent = appIntentService.handledIntent?.intent {
|
||||
handleIntent(intent)
|
||||
appIntentService.handledIntent = nil
|
||||
}
|
||||
}
|
||||
.withModelContainer()
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
.defaultSize(width: userPreferences.showiPadSecondaryColumn ? 1100 : 800, height: 1400)
|
||||
#elseif os(visionOS)
|
||||
.defaultSize(width: 800, height: 1200)
|
||||
#endif
|
||||
.commands {
|
||||
appMenu
|
||||
}
|
||||
|
@ -66,6 +84,11 @@ extension IceCubesApp {
|
|||
watcher.watch(streams: [.user, .direct])
|
||||
}
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
.windowResize()
|
||||
#elseif os(visionOS)
|
||||
.defaultSize(width: 800, height: 1200)
|
||||
#endif
|
||||
}
|
||||
|
||||
@SceneBuilder
|
||||
|
@ -74,7 +97,9 @@ extension IceCubesApp {
|
|||
Group {
|
||||
switch destination.wrappedValue {
|
||||
case let .newStatusEditor(visibility):
|
||||
StatusEditor.MainView(mode: .new(visibility: visibility))
|
||||
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
|
||||
case let .prefilledStatusEditor(text, visibility):
|
||||
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
|
||||
case let .editStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
case let .quoteStatusEditor(status):
|
||||
|
@ -90,6 +115,8 @@ extension IceCubesApp {
|
|||
}
|
||||
}
|
||||
.withEnvironments()
|
||||
.environment(\.isCatalystWindow, true)
|
||||
.environment(RouterPath())
|
||||
.withModelContainer()
|
||||
.applyTheme(theme)
|
||||
.frame(minWidth: 300, minHeight: 400)
|
||||
|
@ -101,8 +128,9 @@ extension IceCubesApp {
|
|||
Group {
|
||||
switch destination.wrappedValue {
|
||||
case let .mediaViewer(attachments, selectedAttachment):
|
||||
MediaUIView(selectedAttachment: selectedAttachment,
|
||||
attachments: attachments)
|
||||
MediaUIView(
|
||||
selectedAttachment: selectedAttachment,
|
||||
attachments: attachments)
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
|
@ -110,9 +138,43 @@ extension IceCubesApp {
|
|||
.withEnvironments()
|
||||
.withModelContainer()
|
||||
.applyTheme(theme)
|
||||
.environment(\.isCatalystWindow, true)
|
||||
.frame(minWidth: 300, minHeight: 400)
|
||||
}
|
||||
.defaultSize(width: 1200, height: 1000)
|
||||
.windowResizability(.contentMinSize)
|
||||
}
|
||||
|
||||
private func handleIntent(_: any AppIntent) {
|
||||
if let postIntent = appIntentService.handledIntent?.intent as? PostIntent {
|
||||
#if os(visionOS) || os(macOS)
|
||||
openWindow(
|
||||
value: WindowDestinationEditor.prefilledStatusEditor(
|
||||
text: postIntent.content ?? "",
|
||||
visibility: userPreferences.postVisibility))
|
||||
#else
|
||||
appRouterPath.presentedSheet = .prefilledStatusEditor(
|
||||
text: postIntent.content ?? "",
|
||||
visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
} else if let tabIntent = appIntentService.handledIntent?.intent as? TabIntent {
|
||||
selectedTab = tabIntent.tab.toAppTab
|
||||
} else if let imageIntent = appIntentService.handledIntent?.intent as? PostImageIntent,
|
||||
let urls = imageIntent.images?.compactMap({ $0.fileURL })
|
||||
{
|
||||
appRouterPath.presentedSheet = .imageURL(
|
||||
urls: urls,
|
||||
visibility: userPreferences.postVisibility)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Scene {
|
||||
func windowResize() -> some Scene {
|
||||
if #available(iOS 18.0, *) {
|
||||
return self.windowResizability(.contentSize)
|
||||
} else {
|
||||
return self.defaultSize(width: 1100, height: 1400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import AVFoundation
|
||||
import Account
|
||||
import AppAccount
|
||||
import AVFoundation
|
||||
import DesignSystem
|
||||
import Env
|
||||
import KeychainSwift
|
||||
|
@ -10,6 +10,7 @@ import RevenueCat
|
|||
import StatusKit
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import WishKit
|
||||
|
||||
@main
|
||||
struct IceCubesApp: App {
|
||||
|
@ -23,15 +24,24 @@ struct IceCubesApp: App {
|
|||
@State var currentAccount = CurrentAccount.shared
|
||||
@State var userPreferences = UserPreferences.shared
|
||||
@State var pushNotificationsService = PushNotificationsService.shared
|
||||
@State var appIntentService = AppIntentService.shared
|
||||
@State var watcher = StreamWatcher.shared
|
||||
@State var quickLook = QuickLook.shared
|
||||
@State var theme = Theme.shared
|
||||
|
||||
@State var selectedTab: Tab = .timeline
|
||||
@State var selectedTab: AppTab = .timeline
|
||||
@State var appRouterPath = RouterPath()
|
||||
|
||||
@State var isSupporter: Bool = false
|
||||
|
||||
init() {
|
||||
#if DEBUG
|
||||
// Enable "GraphReuseLogging" for debugging purpose
|
||||
// subsystem: "com.apple.SwiftUI" category: "GraphReuse"
|
||||
UserDefaults.standard.register(defaults: ["com.apple.SwiftUI.GraphReuseLogging": true])
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
appScene
|
||||
otherScenes
|
||||
|
@ -43,7 +53,8 @@ struct IceCubesApp: App {
|
|||
userPreferences.setClient(client: client)
|
||||
Task {
|
||||
await currentInstance.fetchCurrentInstance()
|
||||
watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi)
|
||||
watcher.setClient(
|
||||
client: client, instanceStreamingURL: currentInstance.instance?.urls?.streamingApi)
|
||||
watcher.watch(streams: [.user, .direct])
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +66,8 @@ struct IceCubesApp: App {
|
|||
case .active:
|
||||
watcher.watch(streams: [.user, .direct])
|
||||
UNUserNotificationCenter.current().setBadgeCount(0)
|
||||
userPreferences.reloadNotificationsCount(tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken))
|
||||
userPreferences.reloadNotificationsCount(
|
||||
tokens: appAccountsManager.availableAccounts.compactMap(\.oauthToken))
|
||||
Task {
|
||||
await userPreferences.refreshServerPreferences()
|
||||
}
|
||||
|
@ -79,19 +91,24 @@ struct IceCubesApp: App {
|
|||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
|
||||
{
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(
|
||||
_: UIApplication,
|
||||
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
try? AVAudioSession.sharedInstance().setCategory(.ambient, options: .mixWithOthers)
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
Telemetry.setup()
|
||||
Telemetry.signal("app.launched")
|
||||
WishKit.configure(with: "AF21AE07-3BA9-4FE2-BFB1-59A3B3941730")
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
func application(
|
||||
_: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
||||
) {
|
||||
PushNotificationsService.shared.pushToken = deviceToken
|
||||
Task {
|
||||
PushNotificationsService.shared.setAccounts(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
|
@ -101,16 +118,29 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
|
||||
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
||||
|
||||
func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
||||
UserPreferences.shared.reloadNotificationsCount(tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken))
|
||||
func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) async
|
||||
-> UIBackgroundFetchResult
|
||||
{
|
||||
UserPreferences.shared.reloadNotificationsCount(
|
||||
tokens: AppAccountsManager.shared.availableAccounts.compactMap(\.oauthToken))
|
||||
return .noData
|
||||
}
|
||||
|
||||
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
func application(
|
||||
_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions
|
||||
) -> UISceneConfiguration {
|
||||
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
||||
if connectingSceneSession.role == .windowApplication {
|
||||
configuration.delegateClass = SceneDelegate.self
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
override func buildMenu(with builder: UIMenuBuilder) {
|
||||
super.buildMenu(with: builder)
|
||||
builder.remove(menu: .document)
|
||||
builder.remove(menu: .toolbar)
|
||||
builder.remove(menu: .sidebar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,34 +18,53 @@ struct SideBarView<Content: View>: View {
|
|||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
|
||||
@Binding var selectedTab: Tab
|
||||
@Binding var popToRootTab: Tab
|
||||
var tabs: [Tab]
|
||||
@Binding var selectedTab: AppTab
|
||||
var tabs: [AppTab]
|
||||
@ViewBuilder var content: () -> Content
|
||||
|
||||
@State private var sidebarTabs = SidebarTabs.shared
|
||||
|
||||
private func badgeFor(tab: Tab) -> Int {
|
||||
private func badgeFor(tab: AppTab) -> Int {
|
||||
if tab == .notifications, selectedTab != tab,
|
||||
let token = appAccounts.currentAccount.oauthToken
|
||||
let token = appAccounts.currentAccount.oauthToken
|
||||
{
|
||||
return watcher.unreadNotificationsCount + (userPreferences.notificationsCount[token] ?? 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private func makeIconForTab(tab: Tab) -> some View {
|
||||
private func makeIconForTab(tab: AppTab) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
SideBarIcon(systemIconName: tab.iconName,
|
||||
isSelected: tab == selectedTab)
|
||||
HStack {
|
||||
SideBarIcon(
|
||||
systemIconName: tab.iconName,
|
||||
isSelected: tab == selectedTab)
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Text(tab.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24,
|
||||
height: 50
|
||||
)
|
||||
.background(
|
||||
tab == selectedTab ? theme.primaryBackgroundColor : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 8)
|
||||
)
|
||||
.cornerRadius(8)
|
||||
.shadow(color: tab == selectedTab ? .black.opacity(0.2) : .clear, radius: 5)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(tab == selectedTab ? theme.labelColor.opacity(0.1) : .clear, lineWidth: 1)
|
||||
)
|
||||
let badge = badgeFor(tab: tab)
|
||||
if badge > 0 {
|
||||
makeBadgeView(count: badge)
|
||||
}
|
||||
}
|
||||
.frame(width: .sidebarWidth - 24, height: 50)
|
||||
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
|
||||
private func makeBadgeView(count: Int) -> some View {
|
||||
|
@ -57,13 +76,15 @@ struct SideBarView<Content: View>: View {
|
|||
.font(.caption2)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
.offset(x: 14, y: -14)
|
||||
.offset(x: 5, y: -5)
|
||||
}
|
||||
|
||||
private var postButton: some View {
|
||||
Button {
|
||||
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||
openWindow(value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility))
|
||||
openWindow(
|
||||
value: WindowDestinationEditor.newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
)
|
||||
#else
|
||||
routerPath.presentedSheet = .newStatusEditor(visibility: userPreferences.postVisibility)
|
||||
#endif
|
||||
|
@ -75,6 +96,7 @@ struct SideBarView<Content: View>: View {
|
|||
.offset(x: 2, y: -2)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.help(AppTab.post.title)
|
||||
}
|
||||
|
||||
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
|
||||
|
@ -91,21 +113,48 @@ struct SideBarView<Content: View>: View {
|
|||
}
|
||||
} label: {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
AppAccountView(viewModel: .init(appAccount: account, isCompact: true),
|
||||
isParentPresented: .constant(false))
|
||||
if showBadge,
|
||||
let token = account.oauthToken,
|
||||
let notificationsCount = userPreferences.notificationsCount[token],
|
||||
notificationsCount > 0
|
||||
if userPreferences.isSidebarExpanded {
|
||||
AppAccountView(
|
||||
viewModel: .init(
|
||||
appAccount: account,
|
||||
isCompact: false,
|
||||
isInSettings: false),
|
||||
isParentPresented: .constant(false))
|
||||
} else {
|
||||
AppAccountView(
|
||||
viewModel: .init(
|
||||
appAccount: account,
|
||||
isCompact: true,
|
||||
isInSettings: false),
|
||||
isParentPresented: .constant(false))
|
||||
}
|
||||
if !userPreferences.isSidebarExpanded,
|
||||
showBadge,
|
||||
let token = account.oauthToken,
|
||||
let notificationsCount = userPreferences.notificationsCount[token],
|
||||
notificationsCount > 0
|
||||
{
|
||||
makeBadgeView(count: notificationsCount)
|
||||
}
|
||||
}
|
||||
.padding(.leading, userPreferences.isSidebarExpanded ? 16 : 0)
|
||||
}
|
||||
.frame(width: .sidebarWidth, height: 50)
|
||||
.help(accountButtonTitle(accountName: account.accountName))
|
||||
.frame(
|
||||
width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth, height: 50
|
||||
)
|
||||
.padding(.vertical, 8)
|
||||
.background(selectedTab == .profile && account.id == appAccounts.currentAccount.id ?
|
||||
theme.secondaryBackgroundColor : .clear)
|
||||
.background(
|
||||
selectedTab == .profile && account.id == appAccounts.currentAccount.id
|
||||
? theme.secondaryBackgroundColor : .clear)
|
||||
}
|
||||
|
||||
private func accountButtonTitle(accountName: String?) -> LocalizedStringKey {
|
||||
if let accountName {
|
||||
"tab.profile-account-\(accountName)"
|
||||
} else {
|
||||
AppTab.profile.title
|
||||
}
|
||||
}
|
||||
|
||||
private var tabsView: some View {
|
||||
|
@ -114,13 +163,6 @@ struct SideBarView<Content: View>: View {
|
|||
Button {
|
||||
// ensure keyboard is always dismissed when selecting a tab
|
||||
hideKeyboard()
|
||||
|
||||
if tab == selectedTab {
|
||||
popToRootTab = .other
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
popToRootTab = tab
|
||||
}
|
||||
}
|
||||
selectedTab = tab
|
||||
SoundEffectManager.shared.playSound(.tabSelection)
|
||||
if tab == .notifications {
|
||||
|
@ -132,6 +174,7 @@ struct SideBarView<Content: View>: View {
|
|||
} label: {
|
||||
makeIconForTab(tab: tab)
|
||||
}
|
||||
.help(tab.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,8 +189,9 @@ struct SideBarView<Content: View>: View {
|
|||
tabsView
|
||||
} else {
|
||||
ForEach(appAccounts.availableAccounts) { account in
|
||||
makeAccountButton(account: account,
|
||||
showBadge: account.id != appAccounts.currentAccount.id)
|
||||
makeAccountButton(
|
||||
account: account,
|
||||
showBadge: account.id != appAccounts.currentAccount.id)
|
||||
if account.id == appAccounts.currentAccount.id {
|
||||
tabsView
|
||||
}
|
||||
|
@ -155,17 +199,26 @@ struct SideBarView<Content: View>: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.frame(width: .sidebarWidth)
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(.thinMaterial)
|
||||
.safeAreaInset(edge: .bottom, content: {
|
||||
HStack {
|
||||
postButton
|
||||
.padding(.vertical, 24)
|
||||
}
|
||||
.frame(width: .sidebarWidth)
|
||||
.background(.thinMaterial)
|
||||
})
|
||||
.safeAreaInset(
|
||||
edge: .bottom,
|
||||
content: {
|
||||
HStack(spacing: 16) {
|
||||
postButton
|
||||
.padding(.vertical, 24)
|
||||
.padding(.leading, userPreferences.isSidebarExpanded ? 18 : 0)
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Text("menu.new-post")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.labelColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
|
||||
.background(.thinMaterial)
|
||||
})
|
||||
Divider().edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
content()
|
||||
|
@ -196,6 +249,7 @@ private struct SideBarIcon: View {
|
|||
self.isHovered = isHovered
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 40)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,10 @@ public struct ReportView: View {
|
|||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
TextField("report.comment.placeholder",
|
||||
text: $commentText,
|
||||
axis: .vertical)
|
||||
TextField(
|
||||
"report.comment.placeholder",
|
||||
text: $commentText,
|
||||
axis: .vertical)
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
|
@ -40,33 +41,35 @@ public struct ReportView: View {
|
|||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
isSendingReport = true
|
||||
Task {
|
||||
do {
|
||||
let _: ReportSent =
|
||||
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
|
||||
statusId: status.id,
|
||||
comment: commentText))
|
||||
dismiss()
|
||||
isSendingReport = false
|
||||
} catch {
|
||||
isSendingReport = false
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if isSendingReport {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("report.action.send")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
isSendingReport = true
|
||||
Task {
|
||||
do {
|
||||
let _: ReportSent =
|
||||
try await client.post(
|
||||
endpoint: Statuses.report(
|
||||
accountId: status.account.id,
|
||||
statusId: status.id,
|
||||
comment: commentText))
|
||||
dismiss()
|
||||
isSendingReport = false
|
||||
} catch {
|
||||
isSendingReport = false
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if isSendingReport {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("report.action.send")
|
||||
}
|
||||
}
|
||||
|
||||
CancelToolbarItem()
|
||||
}
|
||||
|
||||
CancelToolbarItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import LinkPresentation
|
|||
import Lists
|
||||
import MediaUI
|
||||
import Models
|
||||
import Notifications
|
||||
import StatusKit
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
|
@ -18,11 +19,13 @@ extension View {
|
|||
navigationDestination(for: RouterDestination.self) { destination in
|
||||
switch destination {
|
||||
case let .accountDetail(id):
|
||||
AccountDetailView(accountId: id, scrollToTopSignal: .constant(0))
|
||||
AccountDetailView(accountId: id)
|
||||
case let .accountDetailWithAccount(account):
|
||||
AccountDetailView(account: account, scrollToTopSignal: .constant(0))
|
||||
AccountDetailView(account: account)
|
||||
case let .accountSettingsWithAccount(account, appAccount):
|
||||
AccountSettingsView(account: account, appAccount: appAccount)
|
||||
case let .accountMediaGridView(account, initialMedia):
|
||||
AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia)
|
||||
case let .statusDetail(id):
|
||||
StatusDetailView(statusId: id)
|
||||
case let .statusDetailWithStatus(status):
|
||||
|
@ -35,13 +38,16 @@ extension View {
|
|||
TimelineView(timeline: .constant(.hashtag(tag: tag, accountId: accountId)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
case let .list(list):
|
||||
TimelineView(timeline: .constant(.list(list: list)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
case let .linkTimeline(url, title):
|
||||
TimelineView(timeline: .constant(.link(url: url, title: title)),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
canFilterTimeline: false)
|
||||
case let .following(id):
|
||||
AccountsListView(mode: .following(accountId: id))
|
||||
|
@ -57,12 +63,20 @@ extension View {
|
|||
TimelineView(timeline: .constant(.trending),
|
||||
pinnedFilters: .constant([]),
|
||||
selectedTagGroup: .constant(nil),
|
||||
scrollToTopSignal: .constant(0),
|
||||
canFilterTimeline: false)
|
||||
case let .trendingLinks(cards):
|
||||
TrendingLinksListView(cards: cards)
|
||||
case let .tagsList(tags):
|
||||
TagsListView(tags: tags)
|
||||
case .notificationsRequests:
|
||||
NotificationsRequestsListView()
|
||||
case let .notificationForAccount(accountId):
|
||||
NotificationsListView(lockedType: nil,
|
||||
lockedAccountId: accountId)
|
||||
case .blockedAccounts:
|
||||
AccountsListView(mode: .blocked)
|
||||
case .mutedAccounts:
|
||||
AccountsListView(mode: .muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +88,13 @@ extension View {
|
|||
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||
.withEnvironments()
|
||||
case let .newStatusEditor(visibility):
|
||||
StatusEditor.MainView(mode: .new(visibility: visibility))
|
||||
StatusEditor.MainView(mode: .new(text: nil, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .prefilledStatusEditor(text, visibility):
|
||||
StatusEditor.MainView(mode: .new(text: text, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .imageURL(urls, visibility):
|
||||
StatusEditor.MainView(mode: .imageURL(urls: urls, visibility: visibility))
|
||||
.withEnvironments()
|
||||
case let .editStatusEditor(status):
|
||||
StatusEditor.MainView(mode: .edit(status: status))
|
||||
|
@ -110,7 +130,7 @@ extension View {
|
|||
StatusEditHistoryView(statusId: status)
|
||||
.withEnvironments()
|
||||
case .settings:
|
||||
SettingsTabs(popToRootTab: .constant(.settings), isModal: true)
|
||||
SettingsTabs(isModal: true)
|
||||
.withEnvironments()
|
||||
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
|
||||
case .accountPushNotficationsSettings:
|
||||
|
@ -211,7 +231,7 @@ struct ActivityView: UIViewControllerRepresentable {
|
|||
func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext<ActivityView>) {}
|
||||
}
|
||||
|
||||
extension URL: Identifiable {
|
||||
extension URL: @retroactive Identifiable {
|
||||
public var id: String {
|
||||
absoluteString
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import AppAccount
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Observation
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
extension View {
|
||||
@MainActor func withSafariRouter() -> some View {
|
||||
|
@ -13,9 +15,11 @@ extension View {
|
|||
|
||||
@MainActor
|
||||
private struct SafariRouter: ViewModifier {
|
||||
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
|
||||
@Environment(Theme.self) private var theme
|
||||
@Environment(UserPreferences.self) private var preferences
|
||||
@Environment(RouterPath.self) private var routerPath
|
||||
@Environment(AppAccountsManager.self) private var appAccount
|
||||
|
||||
#if !os(visionOS)
|
||||
@State private var safariManager = InAppSafariManager()
|
||||
|
@ -23,15 +27,27 @@ private struct SafariRouter: ViewModifier {
|
|||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
// Open internal URL.
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
.environment(
|
||||
\.openURL,
|
||||
OpenURLAction { url in
|
||||
// Open internal URL.
|
||||
guard !isSecondaryColumn else { return .discarded }
|
||||
return routerPath.handle(url: url)
|
||||
}
|
||||
)
|
||||
.onOpenURL { url in
|
||||
// Open external URL (from icecubesapp://)
|
||||
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
|
||||
guard !isSecondaryColumn else { return }
|
||||
if url.absoluteString == "icecubesapp://subclub" {
|
||||
#if !os(visionOS)
|
||||
safariManager.dismiss()
|
||||
#endif
|
||||
return
|
||||
}
|
||||
let urlString = url.absoluteString.replacingOccurrences(
|
||||
of: AppInfo.scheme, with: "https://")
|
||||
guard let url = URL(string: urlString), url.host != nil else { return }
|
||||
_ = routerPath.handle(url: url)
|
||||
_ = routerPath.handleDeepLink(url: url)
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.urlHandler = { url in
|
||||
|
@ -44,6 +60,20 @@ private struct SafariRouter: ViewModifier {
|
|||
UIApplication.shared.open(url)
|
||||
return .handled
|
||||
}
|
||||
} else if url.query()?.contains("callback=") == false,
|
||||
url.host() == AppInfo.premiumInstance,
|
||||
let accountName = appAccount.currentAccount.accountName
|
||||
{
|
||||
let newURL = url.appending(queryItems: [
|
||||
.init(name: "callback", value: "icecubesapp://subclub"),
|
||||
.init(name: "id", value: "@\(accountName)"),
|
||||
])
|
||||
|
||||
#if !os(visionOS)
|
||||
return safariManager.open(newURL)
|
||||
#else
|
||||
return .systemAction
|
||||
#endif
|
||||
}
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
||||
|
@ -61,13 +91,13 @@ private struct SafariRouter: ViewModifier {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.background {
|
||||
WindowReader { window in
|
||||
safariManager.windowScene = window.windowScene
|
||||
#if !os(visionOS)
|
||||
.background {
|
||||
WindowReader { window in
|
||||
safariManager.windowScene = window.windowScene
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +129,13 @@ private struct SafariRouter: ViewModifier {
|
|||
return .handled
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
viewController.presentedViewController?.dismiss(animated: true)
|
||||
window?.resignKey()
|
||||
window?.isHidden = false
|
||||
window = nil
|
||||
}
|
||||
|
||||
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
||||
let window = window ?? UIWindow(windowScene: windowScene)
|
||||
|
|
@ -13,12 +13,10 @@ struct ExploreTab: View {
|
|||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(Client.self) private var client
|
||||
@State private var routerPath = RouterPath()
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
ExploreView(scrollToTopSignal: $scrollToTopSignal)
|
||||
ExploreView()
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
|
@ -28,15 +26,6 @@ struct ExploreTab: View {
|
|||
}
|
||||
.withSafariRouter()
|
||||
.environment(routerPath)
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
if newValue == .explore {
|
||||
if routerPath.path.isEmpty {
|
||||
scrollToTopSignal += 1
|
||||
} else {
|
||||
routerPath.path = []
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: client.id) {
|
||||
routerPath.path = []
|
||||
}
|
||||
|
|
|
@ -15,12 +15,10 @@ struct MessagesTab: View {
|
|||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@Environment(AppAccountsManager.self) private var appAccount
|
||||
@State private var routerPath = RouterPath()
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
ConversationsListView(scrollToTopSignal: $scrollToTopSignal)
|
||||
ConversationsListView()
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbar {
|
||||
|
@ -29,15 +27,6 @@ struct MessagesTab: View {
|
|||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
if newValue == .messages {
|
||||
if routerPath.path.isEmpty {
|
||||
scrollToTopSignal += 1
|
||||
} else {
|
||||
routerPath.path = []
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: client.id) {
|
||||
routerPath.path = []
|
||||
}
|
||||
|
|
|
@ -17,13 +17,7 @@ struct NavigationSheet<Content: View>: View {
|
|||
NavigationStack {
|
||||
content()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle")
|
||||
}
|
||||
}
|
||||
CloseToolbarItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,16 +20,14 @@ struct NotificationsTab: View {
|
|||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(PushNotificationsService.self) private var pushNotificationsService
|
||||
@State private var routerPath = RouterPath()
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
|
||||
@Binding var selectedTab: Tab
|
||||
@Binding var popToRootTab: Tab
|
||||
@Binding var selectedTab: AppTab
|
||||
|
||||
let lockedType: Models.Notification.NotificationType?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
NotificationsListView(lockedType: lockedType, scrollToTopSignal: $scrollToTopSignal)
|
||||
NotificationsListView(lockedType: lockedType)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbar {
|
||||
|
@ -51,15 +49,6 @@ struct NotificationsTab: View {
|
|||
}
|
||||
.withSafariRouter()
|
||||
.environment(routerPath)
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
if newValue == .notifications {
|
||||
if routerPath.path.isEmpty {
|
||||
scrollToTopSignal += 1
|
||||
} else {
|
||||
routerPath.path = []
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab) { _, _ in
|
||||
clearNotifications()
|
||||
}
|
||||
|
@ -68,7 +57,8 @@ struct NotificationsTab: View {
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
switch type {
|
||||
case .follow, .follow_request:
|
||||
routerPath.navigate(to: .accountDetailWithAccount(account: newValue.notification.account))
|
||||
routerPath.navigate(
|
||||
to: .accountDetailWithAccount(account: newValue.notification.account))
|
||||
default:
|
||||
if let status = newValue.notification.status {
|
||||
routerPath.navigate(to: .statusDetailWithStatus(status: status))
|
||||
|
@ -92,10 +82,14 @@ struct NotificationsTab: View {
|
|||
|
||||
private func clearNotifications() {
|
||||
if selectedTab == .notifications || isSecondaryColumn {
|
||||
if let token = appAccount.currentAccount.oauthToken {
|
||||
if let token = appAccount.currentAccount.oauthToken,
|
||||
userPreferences.notificationsCount[token] ?? 0 > 0
|
||||
{
|
||||
userPreferences.notificationsCount[token] = 0
|
||||
}
|
||||
watcher.unreadNotificationsCount = 0
|
||||
if watcher.unreadNotificationsCount > 0 {
|
||||
watcher.unreadNotificationsCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,32 +14,21 @@ struct ProfileTab: View {
|
|||
@Environment(Client.self) private var client
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
@State private var routerPath = RouterPath()
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
if let account = currentAccount.account {
|
||||
AccountDetailView(account: account, scrollToTopSignal: $scrollToTopSignal)
|
||||
AccountDetailView(account: account)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(account.id)
|
||||
} else {
|
||||
AccountDetailView(account: .placeholder(), scrollToTopSignal: $scrollToTopSignal)
|
||||
AccountDetailView(account: .placeholder())
|
||||
.redacted(reason: .placeholder)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
if newValue == .profile {
|
||||
if routerPath.path.isEmpty {
|
||||
scrollToTopSignal += 1
|
||||
} else {
|
||||
routerPath.path = []
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: client.id) {
|
||||
routerPath.path = []
|
||||
}
|
||||
|
|
|
@ -30,67 +30,85 @@ struct AboutView: View {
|
|||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(uiImage: .init(named: "AppIconAlternate0")!)
|
||||
Image(uiImage: .init(named: "AppIconAlternate0-image") ?? .init())
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate4")!)
|
||||
Image(uiImage: .init(named: "AppIconAlternate46-image") ?? .init())
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate17")!)
|
||||
Image(uiImage: .init(named: "AppIconAlternate17-image") ?? .init())
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Image(uiImage: .init(named: "AppIconAlternate23")!)
|
||||
Image(uiImage: .init(named: "AppIconAlternate23-image") ?? .init())
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.cornerRadius(4)
|
||||
Spacer()
|
||||
}
|
||||
#endif
|
||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!) {
|
||||
Link(
|
||||
destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/PRIVACY.MD")!
|
||||
) {
|
||||
Label("settings.support.privacy-policy", systemImage: "lock")
|
||||
}
|
||||
|
||||
Link(destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!) {
|
||||
Link(
|
||||
destination: URL(string: "https://github.com/Dimillian/IceCubesApp/blob/main/TERMS.MD")!
|
||||
) {
|
||||
Label("settings.support.terms-of-use", systemImage: "checkmark.shield")
|
||||
}
|
||||
} footer: {
|
||||
Text("\(versionNumber)©2023 Thomas Ricouard")
|
||||
Text("\(versionNumber)© 2024 Thomas Ricouard")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
followAccountsSection
|
||||
|
||||
Section("Telemetry") {
|
||||
Link(destination: .init(string: "https://telemetrydeck.com")!) {
|
||||
Label("Telemetry by TelemetryDeck", systemImage: "link")
|
||||
}
|
||||
Link(destination: .init(string: "https://telemetrydeck.com/privacy/")!) {
|
||||
Label("Privacy Policy", systemImage: "checkmark.shield")
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Text("""
|
||||
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||
Text(
|
||||
"""
|
||||
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||
|
||||
• [HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown)
|
||||
• [HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown)
|
||||
|
||||
• [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
||||
• [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
||||
|
||||
• [LRUCache](https://github.com/nicklockwood/LRUCache)
|
||||
• [LRUCache](https://github.com/nicklockwood/LRUCache)
|
||||
|
||||
• [Bodega](https://github.com/mergesort/Bodega)
|
||||
• [Bodega](https://github.com/mergesort/Bodega)
|
||||
|
||||
• [Nuke](https://github.com/kean/Nuke)
|
||||
• [Nuke](https://github.com/kean/Nuke)
|
||||
|
||||
• [SwiftSoup](https://github.com/scinfu/SwiftSoup.git)
|
||||
• [SwiftSoup](https://github.com/scinfu/SwiftSoup.git)
|
||||
|
||||
• [Atkinson Hyperlegible](https://github.com/googlefonts/atkinson-hyperlegible)
|
||||
• [Atkinson Hyperlegible](https://github.com/googlefonts/atkinson-hyperlegible)
|
||||
|
||||
• [OpenDyslexic](http://opendyslexic.org)
|
||||
• [OpenDyslexic](http://opendyslexic.org)
|
||||
|
||||
• [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
||||
• [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
||||
|
||||
• [RevenueCat](https://github.com/RevenueCat/purchases-ios)
|
||||
• [RevenueCat](https://github.com/RevenueCat/purchases-ios)
|
||||
|
||||
• [SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols)
|
||||
""")
|
||||
• [SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols)
|
||||
"""
|
||||
)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.scaledSubheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
@ -99,7 +117,7 @@ struct AboutView: View {
|
|||
.textCase(nil)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.task {
|
||||
|
@ -110,9 +128,11 @@ struct AboutView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle(Text("settings.about.title"))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
.navigationTitle(Text("settings.about.title"))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.environment(
|
||||
\.openURL,
|
||||
OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
}
|
||||
|
@ -125,14 +145,14 @@ struct AboutView: View {
|
|||
AccountsListRow(viewModel: dimillianAccount)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
} else {
|
||||
Section {
|
||||
ProgressView()
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -140,13 +160,15 @@ struct AboutView: View {
|
|||
private func fetchAccounts() async {
|
||||
await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
let viewModel = try await fetchAccountViewModel(account: "dimillian@mastodon.social")
|
||||
let viewModel = try await fetchAccountViewModel(
|
||||
client, account: "dimillian@mastodon.social")
|
||||
await MainActor.run {
|
||||
dimillianAccount = viewModel
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
let viewModel = try await fetchAccountViewModel(account: "icecubesapp@mastodon.online")
|
||||
let viewModel = try await fetchAccountViewModel(
|
||||
client, account: "icecubesapp@mastodon.online")
|
||||
await MainActor.run {
|
||||
iceCubesAccount = viewModel
|
||||
}
|
||||
|
@ -154,9 +176,12 @@ struct AboutView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchAccountViewModel(account: String) async throws -> AccountsListRowViewModel {
|
||||
private func fetchAccountViewModel(_ client: Client, account: String) async throws
|
||||
-> AccountsListRowViewModel
|
||||
{
|
||||
let dimillianAccount: Account = try await client.get(endpoint: Accounts.lookup(name: account))
|
||||
let rel: [Relationship] = try await client.get(endpoint: Accounts.relationships(ids: [dimillianAccount.id]))
|
||||
let rel: [Relationship] = try await client.get(
|
||||
endpoint: Accounts.relationships(ids: [dimillianAccount.id]))
|
||||
return .init(account: dimillianAccount, relationShip: rel.first)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ struct AccountSettingsView: View {
|
|||
Form {
|
||||
Section {
|
||||
Button {
|
||||
routerPath.presentedSheet = .accountFiltersList
|
||||
routerPath.presentedSheet = .accountEditInfo
|
||||
} label: {
|
||||
Label("account.action.edit-info", systemImage: "pencil")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
@ -47,20 +47,25 @@ struct AccountSettingsView: View {
|
|||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
if let subscription = pushNotifications.subscriptions.first(where: { $0.account.token == appAccount.oauthToken }) {
|
||||
if let subscription = pushNotifications.subscriptions.first(where: {
|
||||
$0.account.token == appAccount.oauthToken
|
||||
}) {
|
||||
NavigationLink(destination: PushNotificationsView(subscription: subscription)) {
|
||||
Label("settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right")
|
||||
Label(
|
||||
"settings.general.push-notifications", systemImage: "bell.and.waves.left.and.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
|
||||
Section {
|
||||
Label("settings.account.cached-posts-\(String(cachedPostsCount))", systemImage: "internaldrive")
|
||||
Label(
|
||||
"settings.account.cached-posts-\(String(cachedPostsCount))", systemImage: "internaldrive")
|
||||
Button("settings.account.action.delete-cache", role: .destructive) {
|
||||
Task {
|
||||
await timelineCache.clearCache(for: appAccountsManager.currentClient.id)
|
||||
cachedPostsCount = await timelineCache.cachedPostsCount(for: appAccountsManager.currentClient.id)
|
||||
cachedPostsCount = await timelineCache.cachedPostsCount(
|
||||
for: appAccountsManager.currentClient.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,15 +86,18 @@ struct AccountSettingsView: View {
|
|||
Task {
|
||||
let client = Client(server: appAccount.server, oauthToken: token)
|
||||
await timelineCache.clearCache(for: client.id)
|
||||
if let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token }) {
|
||||
if let sub = pushNotifications.subscriptions.first(where: {
|
||||
$0.account.token == token
|
||||
}) {
|
||||
await sub.deleteSubscription()
|
||||
}
|
||||
appAccountsManager.delete(account: appAccount)
|
||||
Telemetry.signal("account.removed")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("account.action.logout")
|
||||
Label("account.action.logout", systemImage: "trash")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +113,8 @@ struct AccountSettingsView: View {
|
|||
}
|
||||
}
|
||||
.task {
|
||||
cachedPostsCount = await timelineCache.cachedPostsCount(for: appAccountsManager.currentClient.id)
|
||||
cachedPostsCount = await timelineCache.cachedPostsCount(
|
||||
for: appAccountsManager.currentClient.id)
|
||||
}
|
||||
.navigationTitle(account.safeDisplayName)
|
||||
#if !os(visionOS)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import AppAccount
|
||||
import AuthenticationServices
|
||||
import Combine
|
||||
import DesignSystem
|
||||
import Env
|
||||
|
@ -10,10 +11,10 @@ import SwiftUI
|
|||
|
||||
@MainActor
|
||||
struct AddAccountView: View {
|
||||
@Environment(\.webAuthenticationSession) private var webAuthenticationSession
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.webAuthenticationSession) private var webAuthenticationSession
|
||||
|
||||
@Environment(AppAccountsManager.self) private var appAccountsManager
|
||||
@Environment(CurrentAccount.self) private var currentAccount
|
||||
|
@ -34,13 +35,14 @@ struct AddAccountView: View {
|
|||
private let instanceNamePublisher = PassthroughSubject<String, Never>()
|
||||
|
||||
private var sanitizedName: String {
|
||||
var name = instanceName
|
||||
var name =
|
||||
instanceName
|
||||
.replacingOccurrences(of: "http://", with: "")
|
||||
.replacingOccurrences(of: "https://", with: "")
|
||||
|
||||
if name.contains("@") {
|
||||
let parts = name.components(separatedBy: "@")
|
||||
name = parts[parts.count - 1] // [@]username@server.address.com
|
||||
name = parts[parts.count - 1] // [@]username@server.address.com
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
@ -55,9 +57,9 @@ struct AddAccountView: View {
|
|||
NavigationStack {
|
||||
Form {
|
||||
TextField("instance.url", text: $instanceName)
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.keyboardType(.URL)
|
||||
.textContentType(.URL)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
@ -86,71 +88,73 @@ struct AddAccountView: View {
|
|||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.toolbar {
|
||||
if !appAccountsManager.availableAccounts.isEmpty {
|
||||
CancelToolbarItem()
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
let instanceName = instanceName
|
||||
Task {
|
||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||
withAnimation {
|
||||
self.instances = instances
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
Task {
|
||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||
withAnimation {
|
||||
self.instances = instances
|
||||
}
|
||||
isSigninIn = false
|
||||
}
|
||||
.onChange(of: instanceName) {
|
||||
searchingTask.cancel()
|
||||
let instanceName = instanceName
|
||||
let instanceSocialClient = instanceSocialClient
|
||||
searchingTask = Task {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||
withAnimation {
|
||||
self.instances = instances
|
||||
}
|
||||
isSigninIn = false
|
||||
}
|
||||
.onChange(of: instanceName) {
|
||||
searchingTask.cancel()
|
||||
searchingTask = Task {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
|
||||
withAnimation {
|
||||
self.instances = instances
|
||||
}
|
||||
}
|
||||
getInstanceDetailTask.cancel()
|
||||
getInstanceDetailTask = Task {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
getInstanceDetailTask.cancel()
|
||||
getInstanceDetailTask = Task {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
do {
|
||||
// bare bones preflight for domain validity
|
||||
let instanceDetailClient = Client(server: sanitizedName)
|
||||
if
|
||||
instanceDetailClient.server.contains("."),
|
||||
instanceDetailClient.server.last != "."
|
||||
{
|
||||
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
|
||||
withAnimation {
|
||||
self.instance = instance
|
||||
instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
|
||||
}
|
||||
instanceFetchError = nil
|
||||
} else {
|
||||
instance = nil
|
||||
instanceFetchError = nil
|
||||
do {
|
||||
// bare bones preflight for domain validity
|
||||
let instanceDetailClient = Client(server: sanitizedName)
|
||||
if instanceDetailClient.server.contains("."),
|
||||
instanceDetailClient.server.last != "."
|
||||
{
|
||||
let instance: Instance = try await instanceDetailClient.get(
|
||||
endpoint: Instances.instance)
|
||||
withAnimation {
|
||||
self.instance = instance
|
||||
self.instanceName = sanitizedName // clean up the text box, principally to chop off the username if present so it's clear that you might not wind up siging in as the thing in the box
|
||||
}
|
||||
} catch _ as DecodingError {
|
||||
instance = nil
|
||||
instanceFetchError = "account.add.error.instance-not-supported"
|
||||
} catch {
|
||||
instanceFetchError = nil
|
||||
} else {
|
||||
instance = nil
|
||||
instanceFetchError = nil
|
||||
}
|
||||
} catch _ as ServerError {
|
||||
instance = nil
|
||||
instanceFetchError = "account.add.error.instance-not-supported"
|
||||
} catch {
|
||||
instance = nil
|
||||
instanceFetchError = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newValue in
|
||||
switch newValue {
|
||||
case .active:
|
||||
isSigninIn = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { _, newValue in
|
||||
switch newValue {
|
||||
case .active:
|
||||
isSigninIn = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,10 +181,10 @@ struct AddAccountView: View {
|
|||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.tintColor)
|
||||
.listRowBackground(theme.tintColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -219,20 +223,23 @@ struct AddAccountView: View {
|
|||
.foregroundStyle(theme.tintColor)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
|
||||
.foregroundStyle(Color.secondary)
|
||||
.lineLimit(10)
|
||||
Text(
|
||||
instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
?? ""
|
||||
)
|
||||
.foregroundStyle(Color.secondary)
|
||||
.lineLimit(10)
|
||||
}
|
||||
.font(.scaledFootnote)
|
||||
.padding(10)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
|
||||
.listRowSeparator(.hidden)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0))
|
||||
.listRowSeparator(.hidden)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -270,8 +277,9 @@ struct AddAccountView: View {
|
|||
private func signIn() async {
|
||||
signInClient = .init(server: sanitizedName)
|
||||
if let oauthURL = try? await signInClient?.oauthURL(),
|
||||
let url = try? await webAuthenticationSession.authenticate(using: oauthURL,
|
||||
callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: ""))
|
||||
let url = try? await webAuthenticationSession.authenticate(
|
||||
using: oauthURL,
|
||||
callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: ""))
|
||||
{
|
||||
await continueSignIn(url: url)
|
||||
} else {
|
||||
|
@ -288,9 +296,12 @@ struct AddAccountView: View {
|
|||
let oauthToken = try await client.continueOauthFlow(url: url)
|
||||
let client = Client(server: client.server, oauthToken: oauthToken)
|
||||
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
|
||||
appAccountsManager.add(account: AppAccount(server: client.server,
|
||||
accountName: "\(account.acct)@\(client.server)",
|
||||
oauthToken: oauthToken))
|
||||
Telemetry.signal("account.added")
|
||||
appAccountsManager.add(
|
||||
account: AppAccount(
|
||||
server: client.server,
|
||||
accountName: "\(account.acct)@\(client.server)",
|
||||
oauthToken: oauthToken))
|
||||
Task {
|
||||
pushNotifications.setAccounts(accounts: appAccountsManager.pushAccounts)
|
||||
await pushNotifications.updateSubscriptions(forceCreate: true)
|
||||
|
|
|
@ -30,11 +30,14 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("settings.content.sharing") {
|
||||
Picker("settings.content.sharing.share-button-behavior", selection: $userPreferences.shareButtonBehavior) {
|
||||
Picker(
|
||||
"settings.content.sharing.share-button-behavior",
|
||||
selection: $userPreferences.shareButtonBehavior
|
||||
) {
|
||||
ForEach(PreferredShareButtonBehavior.allCases, id: \.rawValue) { option in
|
||||
Text(option.title)
|
||||
.tag(option)
|
||||
|
@ -42,7 +45,7 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("settings.content.instance-settings") {
|
||||
|
@ -51,7 +54,7 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
.onChange(of: userPreferences.useInstanceContentSettings) { _, newVal in
|
||||
if newVal {
|
||||
|
@ -85,21 +88,27 @@ struct ContentSettingsView: View {
|
|||
Text("settings.content.collapse-long-posts-hint")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("settings.content.posting") {
|
||||
Picker("settings.content.default-visibility", selection: $userPreferences.appDefaultPostVisibility) {
|
||||
Picker(
|
||||
"settings.content.default-visibility",
|
||||
selection: $userPreferences.appDefaultPostVisibility
|
||||
) {
|
||||
ForEach(Visibility.allCases, id: \.rawValue) { vis in
|
||||
Text(vis.title).tag(vis)
|
||||
}
|
||||
}
|
||||
.disabled(userPreferences.useInstanceContentSettings)
|
||||
|
||||
Picker("settings.content.default-reply-visibility", selection: $userPreferences.appDefaultReplyVisibility) {
|
||||
Picker(
|
||||
"settings.content.default-reply-visibility",
|
||||
selection: $userPreferences.appDefaultReplyVisibility
|
||||
) {
|
||||
ForEach(Visibility.allCases, id: \.rawValue) { vis in
|
||||
if UserPreferences.getIntOfVisibility(vis) <=
|
||||
UserPreferences.getIntOfVisibility(userPreferences.postVisibility)
|
||||
if UserPreferences.getIntOfVisibility(vis)
|
||||
<= UserPreferences.getIntOfVisibility(userPreferences.postVisibility)
|
||||
{
|
||||
Text(vis.title).tag(vis)
|
||||
}
|
||||
|
@ -119,7 +128,7 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("timeline.content-filter.title") {
|
||||
|
@ -137,7 +146,7 @@ struct ContentSettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.content.navigation-title")
|
||||
|
|
|
@ -29,15 +29,16 @@ struct DisplaySettingsView: View {
|
|||
|
||||
@State private var isFontSelectorPresented = false
|
||||
|
||||
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
|
||||
client: Client(server: ""),
|
||||
routerPath: RouterPath()) // translate from latin button
|
||||
private let previewStatusViewModel = StatusRowViewModel(
|
||||
status: Status.placeholder(forSettings: true, language: "la"),
|
||||
client: Client(server: ""),
|
||||
routerPath: RouterPath()) // translate from latin button
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
Form {
|
||||
#if !os(visionOS)
|
||||
StatusRowView(viewModel: previewStatusViewModel)
|
||||
StatusRowExternalView(viewModel: previewStatusViewModel)
|
||||
.allowsHitTesting(false)
|
||||
.opacity(0)
|
||||
.hidden()
|
||||
|
@ -53,30 +54,30 @@ struct DisplaySettingsView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.task(id: localValues.tintColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.tintColor = localValues.tintColor
|
||||
}
|
||||
.task(id: localValues.primaryBackgroundColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
|
||||
}
|
||||
.task(id: localValues.secondaryBackgroundColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
|
||||
}
|
||||
.task(id: localValues.labelColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.labelColor = localValues.labelColor
|
||||
}
|
||||
.task(id: localValues.lineSpacing) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.lineSpacing = localValues.lineSpacing
|
||||
}
|
||||
.task(id: localValues.fontSizeScale) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.fontSizeScale = localValues.fontSizeScale
|
||||
}
|
||||
.task(id: localValues.tintColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.tintColor = localValues.tintColor
|
||||
}
|
||||
.task(id: localValues.primaryBackgroundColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.primaryBackgroundColor = localValues.primaryBackgroundColor
|
||||
}
|
||||
.task(id: localValues.secondaryBackgroundColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.secondaryBackgroundColor = localValues.secondaryBackgroundColor
|
||||
}
|
||||
.task(id: localValues.labelColor) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.labelColor = localValues.labelColor
|
||||
}
|
||||
.task(id: localValues.lineSpacing) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.lineSpacing = localValues.lineSpacing
|
||||
}
|
||||
.task(id: localValues.fontSizeScale) {
|
||||
do { try await Task.sleep(for: .microseconds(500)) } catch {}
|
||||
theme.fontSizeScale = localValues.fontSizeScale
|
||||
}
|
||||
#if !os(visionOS)
|
||||
examplePost
|
||||
#endif
|
||||
|
@ -85,7 +86,7 @@ struct DisplaySettingsView: View {
|
|||
|
||||
private var examplePost: some View {
|
||||
VStack(spacing: 0) {
|
||||
StatusRowView(viewModel: previewStatusViewModel)
|
||||
StatusRowExternalView(viewModel: previewStatusViewModel)
|
||||
.allowsHitTesting(false)
|
||||
.padding(.layoutPadding)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
|
@ -96,8 +97,10 @@ struct DisplaySettingsView: View {
|
|||
Rectangle()
|
||||
.fill(theme.secondaryBackgroundColor)
|
||||
.frame(height: 30)
|
||||
.mask(LinearGradient(gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]),
|
||||
startPoint: .top, endPoint: .bottom))
|
||||
.mask(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]),
|
||||
startPoint: .top, endPoint: .bottom))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,8 +112,11 @@ struct DisplaySettingsView: View {
|
|||
themeSelectorButton
|
||||
Group {
|
||||
ColorPicker("settings.display.theme.tint", selection: $localValues.tintColor)
|
||||
ColorPicker("settings.display.theme.background", selection: $localValues.primaryBackgroundColor)
|
||||
ColorPicker("settings.display.theme.secondary-background", selection: $localValues.secondaryBackgroundColor)
|
||||
ColorPicker(
|
||||
"settings.display.theme.background", selection: $localValues.primaryBackgroundColor)
|
||||
ColorPicker(
|
||||
"settings.display.theme.secondary-background",
|
||||
selection: $localValues.secondaryBackgroundColor)
|
||||
ColorPicker("settings.display.theme.text-color", selection: $localValues.labelColor)
|
||||
}
|
||||
.disabled(theme.followSystemColorScheme)
|
||||
|
@ -129,35 +135,40 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var fontSection: some View {
|
||||
Section("settings.display.section.font") {
|
||||
Picker("settings.display.font", selection: .init(get: { () -> FontState in
|
||||
if theme.chosenFont?.fontName == "OpenDyslexic-Regular" {
|
||||
return FontState.openDyslexic
|
||||
} else if theme.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" {
|
||||
return FontState.hyperLegible
|
||||
} else if theme.chosenFont?.fontName == ".AppleSystemUIFontRounded-Regular" {
|
||||
return FontState.SFRounded
|
||||
}
|
||||
return theme.chosenFontData != nil ? FontState.custom : FontState.system
|
||||
}, set: { newValue in
|
||||
switch newValue {
|
||||
case .system:
|
||||
theme.chosenFont = nil
|
||||
case .openDyslexic:
|
||||
theme.chosenFont = UIFont(name: "OpenDyslexic", size: 1)
|
||||
case .hyperLegible:
|
||||
theme.chosenFont = UIFont(name: "Atkinson Hyperlegible", size: 1)
|
||||
case .SFRounded:
|
||||
theme.chosenFont = UIFont.systemFont(ofSize: 1).rounded()
|
||||
case .custom:
|
||||
isFontSelectorPresented = true
|
||||
}
|
||||
})) {
|
||||
Picker(
|
||||
"settings.display.font",
|
||||
selection: .init(
|
||||
get: { () -> FontState in
|
||||
if theme.chosenFont?.fontName == "OpenDyslexic-Regular" {
|
||||
return FontState.openDyslexic
|
||||
} else if theme.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" {
|
||||
return FontState.hyperLegible
|
||||
} else if theme.chosenFont?.fontName == ".AppleSystemUIFontRounded-Regular" {
|
||||
return FontState.SFRounded
|
||||
}
|
||||
return theme.chosenFontData != nil ? FontState.custom : FontState.system
|
||||
},
|
||||
set: { newValue in
|
||||
switch newValue {
|
||||
case .system:
|
||||
theme.chosenFont = nil
|
||||
case .openDyslexic:
|
||||
theme.chosenFont = UIFont(name: "OpenDyslexic", size: 1)
|
||||
case .hyperLegible:
|
||||
theme.chosenFont = UIFont(name: "Atkinson Hyperlegible", size: 1)
|
||||
case .SFRounded:
|
||||
theme.chosenFont = UIFont.systemFont(ofSize: 1).rounded()
|
||||
case .custom:
|
||||
isFontSelectorPresented = true
|
||||
}
|
||||
})
|
||||
) {
|
||||
ForEach(FontState.allCases, id: \.rawValue) { fontState in
|
||||
Text(fontState.title).tag(fontState)
|
||||
}
|
||||
|
@ -165,7 +176,7 @@ struct DisplaySettingsView: View {
|
|||
.navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() })
|
||||
|
||||
VStack {
|
||||
Slider(value: $localValues.fontSizeScale, in: 0.5 ... 1.5, step: 0.1)
|
||||
Slider(value: $localValues.fontSizeScale, in: 0.5...1.5, step: 0.1)
|
||||
Text("settings.display.font.scaling-\(String(format: "%.1f", localValues.fontSizeScale))")
|
||||
.font(.scaledBody)
|
||||
}
|
||||
|
@ -174,16 +185,18 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
|
||||
VStack {
|
||||
Slider(value: $localValues.lineSpacing, in: 0.4 ... 10.0, step: 0.2)
|
||||
Text("settings.display.font.line-spacing-\(String(format: "%.1f", localValues.lineSpacing))")
|
||||
.font(.scaledBody)
|
||||
Slider(value: $localValues.lineSpacing, in: 0.4...10.0, step: 0.2)
|
||||
Text(
|
||||
"settings.display.font.line-spacing-\(String(format: "%.1f", localValues.lineSpacing))"
|
||||
)
|
||||
.font(.scaledBody)
|
||||
}
|
||||
.alignmentGuide(.listRowSeparatorLeading) { d in
|
||||
d[.leading]
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -224,22 +237,29 @@ struct DisplaySettingsView: View {
|
|||
Toggle("settings.display.show-reply-indentation", isOn: $userPreferences.showReplyIndentation)
|
||||
if userPreferences.showReplyIndentation {
|
||||
VStack {
|
||||
Slider(value: .init(get: {
|
||||
Double(userPreferences.maxReplyIndentation)
|
||||
}, set: { newVal in
|
||||
userPreferences.maxReplyIndentation = UInt(newVal)
|
||||
}), in: 1 ... 20, step: 1)
|
||||
Text("settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))")
|
||||
.font(.scaledBody)
|
||||
Slider(
|
||||
value: .init(
|
||||
get: {
|
||||
Double(userPreferences.maxReplyIndentation)
|
||||
},
|
||||
set: { newVal in
|
||||
userPreferences.maxReplyIndentation = UInt(newVal)
|
||||
}), in: 1...20, step: 1)
|
||||
Text(
|
||||
"settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))"
|
||||
)
|
||||
.font(.scaledBody)
|
||||
}
|
||||
.alignmentGuide(.listRowSeparatorLeading) { d in
|
||||
d[.leading]
|
||||
}
|
||||
}
|
||||
Toggle("settings.display.show-account-popover", isOn: $userPreferences.showAccountPopover)
|
||||
Toggle("Show Content Gradient", isOn: $theme.showContentGradient)
|
||||
Toggle("Compact Layout", isOn: $theme.compactLayoutPadding)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -252,7 +272,7 @@ struct DisplaySettingsView: View {
|
|||
Toggle("settings.display.show-ipad-column", isOn: $userPreferences.showiPadSecondaryColumn)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -266,7 +286,7 @@ struct DisplaySettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ struct HapticSettingsView: View {
|
|||
Toggle("settings.haptic.buttons", isOn: $userPreferences.hapticButtonPressEnabled)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.haptic.navigation-title")
|
||||
|
|
|
@ -17,7 +17,8 @@ struct IconSelectorView: View {
|
|||
}
|
||||
|
||||
case primary = 0
|
||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14, alt15
|
||||
case alt1, alt2, alt3, alt4, alt5, alt6, alt7, alt8, alt9, alt10, alt11, alt12, alt13, alt14,
|
||||
alt15
|
||||
case alt16, alt17, alt18, alt19, alt20, alt21
|
||||
case alt22, alt23, alt24, alt25, alt26
|
||||
case alt27, alt28, alt29
|
||||
|
@ -26,11 +27,16 @@ struct IconSelectorView: View {
|
|||
case alt38
|
||||
case alt39, alt40, alt41, alt42, alt43
|
||||
case alt44, alt45
|
||||
case alt46
|
||||
case alt46, alt47, alt48
|
||||
case alt49
|
||||
|
||||
var appIconName: String {
|
||||
return "AppIconAlternate\(rawValue)"
|
||||
}
|
||||
|
||||
var previewImageName: String {
|
||||
return "AppIconAlternate\(rawValue)-image"
|
||||
}
|
||||
}
|
||||
|
||||
struct IconSelector: Identifiable {
|
||||
|
@ -39,23 +45,45 @@ struct IconSelectorView: View {
|
|||
let icons: [Icon]
|
||||
|
||||
static let items = [
|
||||
IconSelector(title: "settings.app.icon.official".localized, icons: [
|
||||
.primary, .alt46, .alt1, .alt2, .alt3, .alt4,
|
||||
.alt5, .alt6, .alt7, .alt8,
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15,
|
||||
.alt16, .alt17, .alt18, .alt19, .alt20, .alt21]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Albert Kinng", icons: [.alt22, .alt23, .alt24, .alt25, .alt26]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Dan van Moll", icons: [.alt27, .alt28, .alt29]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)", icons: [.alt30, .alt31, .alt32, .alt33, .alt34, .alt35, .alt36]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)", icons: [.alt37]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live", icons: [.alt39, .alt40, .alt41, .alt42, .alt43]),
|
||||
IconSelector(title: "\("settings.app.icon.designed-by".localized) Simone Margio", icons: [.alt44, .alt45]),
|
||||
IconSelector(
|
||||
title: "settings.app.icon.official".localized,
|
||||
icons: [
|
||||
.primary, .alt46, .alt1, .alt2, .alt3, .alt4,
|
||||
.alt5, .alt6, .alt7, .alt8,
|
||||
.alt9, .alt10, .alt11, .alt12, .alt13, .alt14, .alt15,
|
||||
.alt16, .alt17, .alt18, .alt19, .alt20, .alt21,
|
||||
]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Albert Kinng",
|
||||
icons: [.alt22, .alt23, .alt24, .alt25, .alt26]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Dan van Moll",
|
||||
icons: [.alt27, .alt28, .alt29]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Chanhwi Joo (GitHub @te6-in)",
|
||||
icons: [.alt30, .alt31, .alt32, .alt33, .alt34, .alt35, .alt36]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) W. Kovács Ágnes (@wildgica)",
|
||||
icons: [.alt37]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Duncan Horne", icons: [.alt38]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) BeAware@social.beaware.live",
|
||||
icons: [.alt39, .alt40, .alt41, .alt42, .alt43]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Simone Margio",
|
||||
icons: [.alt44, .alt45]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Peter Broqvist (@PKB)",
|
||||
icons: [.alt47, .alt48]),
|
||||
IconSelector(
|
||||
title: "\("settings.app.icon.designed-by".localized) Oz Tsori (@oztsori)", icons: [.alt49]),
|
||||
]
|
||||
}
|
||||
|
||||
@Environment(Theme.self) private var theme
|
||||
@State private var currentIcon = UIApplication.shared.alternateIconName ?? Icon.primary.appIconName
|
||||
@State private var currentIcon =
|
||||
UIApplication.shared.alternateIconName ?? Icon.primary.appIconName
|
||||
|
||||
private let columns = [GridItem(.adaptive(minimum: 125, maximum: 1024))]
|
||||
|
||||
|
@ -75,7 +103,7 @@ struct IconSelectorView: View {
|
|||
.navigationTitle("settings.app.icon.navigation-title")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
.background(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -94,7 +122,7 @@ struct IconSelectorView: View {
|
|||
}
|
||||
} label: {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Image(uiImage: .init(named: icon.appIconName) ?? .init())
|
||||
Image(icon.previewImageName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minHeight: 125, maxHeight: 1024)
|
||||
|
|
|
@ -38,7 +38,7 @@ public struct InstanceInfoSection: View {
|
|||
LabeledContent("instance.info.domains", value: format(instance.stats.domainCount))
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
if let rules = instance.rules {
|
||||
|
@ -48,7 +48,7 @@ public struct InstanceInfoSection: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,78 +18,106 @@ struct PushNotificationsView: View {
|
|||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isEnabled = newValue
|
||||
if newValue {
|
||||
updateSubscription()
|
||||
} else {
|
||||
deleteSubscription()
|
||||
}
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isEnabled = newValue
|
||||
if newValue {
|
||||
updateSubscription()
|
||||
} else {
|
||||
deleteSubscription()
|
||||
}
|
||||
})
|
||||
) {
|
||||
Text("settings.push.main-toggle")
|
||||
}
|
||||
} footer: {
|
||||
Text("settings.push.main-toggle.description")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
if subscription.isEnabled {
|
||||
Section {
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isMentionNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isMentionNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isMentionNotificationEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isMentionNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})
|
||||
) {
|
||||
Label("settings.push.mentions", systemImage: "at")
|
||||
}
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isFollowNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isFollowNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isFollowNotificationEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isFollowNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})
|
||||
) {
|
||||
Label("settings.push.follows", systemImage: "person.badge.plus")
|
||||
}
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isFavoriteNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isFavoriteNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isFavoriteNotificationEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isFavoriteNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})
|
||||
) {
|
||||
Label("settings.push.favorites", systemImage: "star")
|
||||
}
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isReblogNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isReblogNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isReblogNotificationEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isReblogNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})
|
||||
) {
|
||||
Label("settings.push.boosts", image: "Rocket")
|
||||
}
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isPollNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isPollNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isPollNotificationEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isPollNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})
|
||||
) {
|
||||
Label("settings.push.polls", systemImage: "chart.bar")
|
||||
}
|
||||
Toggle(isOn: .init(get: {
|
||||
subscription.isNewPostsNotificationEnabled
|
||||
}, set: { newValue in
|
||||
subscription.isNewPostsNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})) {
|
||||
Toggle(
|
||||
isOn: .init(
|
||||
get: {
|
||||
subscription.isNewPostsNotificationEnabled
|
||||
},
|
||||
set: { newValue in
|
||||
subscription.isNewPostsNotificationEnabled = newValue
|
||||
updateSubscription()
|
||||
})
|
||||
) {
|
||||
Label("settings.push.new-posts", systemImage: "bubble.right")
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -106,7 +134,7 @@ struct PushNotificationsView: View {
|
|||
Text("settings.push.duplicate.footer")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.push.navigation-title")
|
||||
|
@ -114,9 +142,9 @@ struct PushNotificationsView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.task {
|
||||
await subscription.fetchSubscription()
|
||||
}
|
||||
.task {
|
||||
await subscription.fetchSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSubscription() {
|
||||
|
|
|
@ -29,7 +29,7 @@ struct RecenTagsSettingView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.general.recent-tags")
|
||||
|
@ -37,8 +37,8 @@ struct RecenTagsSettingView: View {
|
|||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ struct RemoteTimelinesSettingView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
Button {
|
||||
routerPath.presentedSheet = .addRemoteLocalTimeline
|
||||
|
@ -30,7 +30,7 @@ struct RemoteTimelinesSettingView: View {
|
|||
Label("settings.timeline.add", systemImage: "badge.plus.radiowaves.right")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.general.remote-timelines")
|
||||
|
@ -38,8 +38,8 @@ struct RemoteTimelinesSettingView: View {
|
|||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,10 +28,10 @@ struct SettingsTabs: View {
|
|||
@State private var cachedRemoved = false
|
||||
@State private var timelineCache = TimelineCache()
|
||||
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
let isModal: Bool
|
||||
|
||||
@State private var startingPoint: SettingsStartingPoint? = nil
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
Form {
|
||||
|
@ -39,31 +39,61 @@ struct SettingsTabs: View {
|
|||
accountsSection
|
||||
generalSection
|
||||
otherSections
|
||||
postStreamingSection
|
||||
AISection
|
||||
cacheSection
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.navigationTitle(Text("settings.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.toolbar {
|
||||
if isModal {
|
||||
ToolbarItem {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("action.done").bold()
|
||||
}
|
||||
.navigationTitle(Text("settings.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.toolbar {
|
||||
if isModal {
|
||||
ToolbarItem {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("action.done").bold()
|
||||
}
|
||||
}
|
||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
}
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn,
|
||||
!isModal
|
||||
{
|
||||
SecondaryColumnToolbarItem()
|
||||
}
|
||||
}
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.onAppear {
|
||||
startingPoint = RouterPath.settingsStartingPoint
|
||||
RouterPath.settingsStartingPoint = nil
|
||||
}
|
||||
.navigationDestination(item: $startingPoint) { targetView in
|
||||
switch targetView {
|
||||
case .display:
|
||||
DisplaySettingsView()
|
||||
case .haptic:
|
||||
HapticSettingsView()
|
||||
case .remoteTimelines:
|
||||
RemoteTimelinesSettingView()
|
||||
case .tagGroups:
|
||||
TagsGroupSettingView()
|
||||
case .recentTags:
|
||||
RecenTagsSettingView()
|
||||
case .content:
|
||||
ContentSettingsView()
|
||||
case .swipeActions:
|
||||
SwipeActionsSettingsView()
|
||||
case .tabAndSidebarEntries:
|
||||
EmptyView()
|
||||
case .translation:
|
||||
TranslationSettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.client = client
|
||||
|
@ -75,11 +105,6 @@ struct SettingsTabs: View {
|
|||
}
|
||||
.withSafariRouter()
|
||||
.environment(routerPath)
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
if newValue == .notifications {
|
||||
routerPath.path = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var accountsSection: some View {
|
||||
|
@ -108,24 +133,25 @@ struct SettingsTabs: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
addAccountButton
|
||||
if !appAccountsManager.availableAccounts.isEmpty {
|
||||
editAccountButton
|
||||
}
|
||||
addAccountButton
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func logoutAccount(account: AppAccount) async {
|
||||
if let token = account.oauthToken,
|
||||
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
|
||||
let sub = pushNotifications.subscriptions.first(where: { $0.account.token == token })
|
||||
{
|
||||
let client = Client(server: account.server, oauthToken: token)
|
||||
await timelineCache.clearCache(for: client.id)
|
||||
await sub.deleteSubscription()
|
||||
appAccountsManager.delete(account: account)
|
||||
Telemetry.signal("account.removed")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,7 +190,9 @@ struct SettingsTabs: View {
|
|||
NavigationLink(destination: TabbarEntriesSettingsView()) {
|
||||
Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone")
|
||||
}
|
||||
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
} else if UIDevice.current.userInterfaceIdiom == .pad
|
||||
|| UIDevice.current.userInterfaceIdiom == .mac
|
||||
{
|
||||
NavigationLink(destination: SidebarEntriesSettingsView()) {
|
||||
Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading")
|
||||
}
|
||||
|
@ -180,7 +208,7 @@ struct SettingsTabs: View {
|
|||
#endif
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -206,9 +234,6 @@ struct SettingsTabs: View {
|
|||
}
|
||||
.disabled(preferences.preferredBrowser != PreferredBrowser.inAppSafari)
|
||||
#endif
|
||||
Toggle(isOn: $preferences.isOpenAIEnabled) {
|
||||
Label("settings.other.hide-openai", systemImage: "faxmachine")
|
||||
}
|
||||
Toggle(isOn: $preferences.isSocialKeyboardEnabled) {
|
||||
Label("settings.other.social-keyboard", systemImage: "keyboard")
|
||||
}
|
||||
|
@ -224,7 +249,45 @@ struct SettingsTabs: View {
|
|||
Text("settings.section.other.footer")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var postStreamingSection: some View {
|
||||
@Bindable var preferences = preferences
|
||||
Section {
|
||||
Toggle(isOn: $preferences.isPostsStreamingEnabled) {
|
||||
Label("Posts streaming", systemImage: "clock.badge")
|
||||
}
|
||||
} header: {
|
||||
Text("Streaming")
|
||||
} footer: {
|
||||
Text(
|
||||
"Enabling post streaming will automatically add new posts at the top of your home timeline. Disable if you get performance issues."
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var AISection: some View {
|
||||
@Bindable var preferences = preferences
|
||||
Section {
|
||||
Toggle(isOn: $preferences.isOpenAIEnabled) {
|
||||
Label("settings.other.hide-openai", systemImage: "faxmachine")
|
||||
}
|
||||
} header: {
|
||||
Text("AI")
|
||||
} footer: {
|
||||
Text(
|
||||
"Disable to hide AI assisted tool options such as copywritting and alt-image description generated using AI. Uses OpenAI API. See our Privacy Policy for more information."
|
||||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -235,11 +298,16 @@ struct SettingsTabs: View {
|
|||
Label {
|
||||
Text("settings.app.icon")
|
||||
} icon: {
|
||||
let icon = IconSelectorView.Icon(string: UIApplication.shared.alternateIconName ?? "AppIcon")
|
||||
Image(uiImage: .init(named: icon.appIconName)!)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.cornerRadius(4)
|
||||
let icon = IconSelectorView.Icon(
|
||||
string: UIApplication.shared.alternateIconName ?? "AppIcon")
|
||||
if let image: UIImage = .init(named: icon.previewImageName) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.cornerRadius(4)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -254,7 +322,9 @@ struct SettingsTabs: View {
|
|||
Label("settings.app.support", systemImage: "wand.and.stars")
|
||||
}
|
||||
|
||||
if let reviewURL = URL(string: "https://apps.apple.com/app/id\(AppInfo.appStoreAppId)?action=write-review") {
|
||||
if let reviewURL = URL(
|
||||
string: "https://apps.apple.com/app/id\(AppInfo.appStoreAppId)?action=write-review")
|
||||
{
|
||||
Link(destination: reviewURL) {
|
||||
Label("settings.rate", systemImage: "link")
|
||||
}
|
||||
|
@ -262,19 +332,28 @@ struct SettingsTabs: View {
|
|||
.tint(theme.labelColor)
|
||||
}
|
||||
|
||||
NavigationLink(destination: AboutView()) {
|
||||
NavigationLink {
|
||||
AboutView()
|
||||
} label: {
|
||||
Label("settings.app.about", systemImage: "info.circle")
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
WishlistView()
|
||||
} label: {
|
||||
Label("Feature Requests", systemImage: "list.bullet.rectangle.portrait")
|
||||
}
|
||||
|
||||
} header: {
|
||||
Text("settings.section.app")
|
||||
} footer: {
|
||||
if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
||||
Text("settings.section.app.footer \(appVersion)").frame(maxWidth: .infinity, alignment: .center)
|
||||
Text("settings.section.app.footer \(appVersion)").frame(
|
||||
maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -282,7 +361,7 @@ struct SettingsTabs: View {
|
|||
Button {
|
||||
addAccountSheetPresented.toggle()
|
||||
} label: {
|
||||
Text("settings.account.add")
|
||||
Label("settings.account.add", systemImage: "person.badge.plus")
|
||||
}
|
||||
.sheet(isPresented: $addAccountSheetPresented) {
|
||||
AddAccountView()
|
||||
|
@ -290,21 +369,23 @@ struct SettingsTabs: View {
|
|||
}
|
||||
|
||||
private var editAccountButton: some View {
|
||||
Button(role: isEditingAccount ? .none : .destructive) {
|
||||
Button(role: .destructive) {
|
||||
withAnimation {
|
||||
isEditingAccount.toggle()
|
||||
}
|
||||
} label: {
|
||||
if isEditingAccount {
|
||||
Text("action.done")
|
||||
Label("action.done", systemImage: "person.badge.minus")
|
||||
.foregroundStyle(.red)
|
||||
} else {
|
||||
Text("account.action.logout")
|
||||
Label("account.action.logout", systemImage: "person.badge.minus")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var cacheSection: some View {
|
||||
Section("settings.section.cache") {
|
||||
Section {
|
||||
if cachedRemoved {
|
||||
Text("action.done")
|
||||
.transition(.move(edge: .leading))
|
||||
|
@ -316,9 +397,13 @@ struct SettingsTabs: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("settings.section.cache")
|
||||
} footer: {
|
||||
Text("Remove all cached images and videos")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ struct SidebarEntriesSettingsView: View {
|
|||
.onMove(perform: move)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
|
|
|
@ -72,21 +72,37 @@ struct SupportAppView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.alert("settings.support.alert.title", isPresented: $purchaseSuccessDisplayed, actions: {
|
||||
Button { purchaseSuccessDisplayed = false } label: { Text("alert.button.ok") }
|
||||
}, message: {
|
||||
.alert(
|
||||
"settings.support.alert.title", isPresented: $purchaseSuccessDisplayed,
|
||||
actions: {
|
||||
Button {
|
||||
purchaseSuccessDisplayed = false
|
||||
} label: {
|
||||
Text("alert.button.ok")
|
||||
}
|
||||
},
|
||||
message: {
|
||||
Text("settings.support.alert.message")
|
||||
})
|
||||
.alert("alert.error", isPresented: $purchaseErrorDisplayed, actions: {
|
||||
Button { purchaseErrorDisplayed = false } label: { Text("alert.button.ok") }
|
||||
}, message: {
|
||||
Text("settings.support.alert.error.message")
|
||||
})
|
||||
.onAppear {
|
||||
loadingProducts = true
|
||||
fetchStoreProducts()
|
||||
refreshUserInfo()
|
||||
}
|
||||
)
|
||||
.alert(
|
||||
"alert.error", isPresented: $purchaseErrorDisplayed,
|
||||
actions: {
|
||||
Button {
|
||||
purchaseErrorDisplayed = false
|
||||
} label: {
|
||||
Text("alert.button.ok")
|
||||
}
|
||||
},
|
||||
message: {
|
||||
Text("settings.support.alert.error.message")
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
loadingProducts = true
|
||||
fetchStoreProducts()
|
||||
refreshUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private func purchase(product: StoreProduct) async {
|
||||
|
@ -107,7 +123,8 @@ struct SupportAppView: View {
|
|||
private func fetchStoreProducts() {
|
||||
Purchases.shared.getProducts(Tip.allCases.map(\.productId)) { products in
|
||||
subscription = products.first(where: { $0.productIdentifier == Tip.supporter.productId })
|
||||
self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(by: { $0.price < $1.price })
|
||||
self.products = products.filter { $0.productIdentifier != Tip.supporter.productId }.sorted(
|
||||
by: { $0.price < $1.price })
|
||||
withAnimation {
|
||||
loadingProducts = false
|
||||
}
|
||||
|
@ -153,7 +170,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -166,15 +183,15 @@ struct SupportAppView: View {
|
|||
if customerInfo?.entitlements["Supporter"]?.isActive == true {
|
||||
Text(Image(systemName: "checkmark.seal.fill"))
|
||||
.foregroundColor(theme.tintColor)
|
||||
.baselineOffset(-1) +
|
||||
Text("settings.support.supporter.subscribed")
|
||||
.baselineOffset(-1)
|
||||
+ Text("settings.support.supporter.subscribed")
|
||||
.font(.scaledSubheadline)
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
Text(Image(systemName: "checkmark.seal.fill"))
|
||||
.foregroundColor(theme.tintColor)
|
||||
.baselineOffset(-1) +
|
||||
Text(Tip.supporter.title)
|
||||
.baselineOffset(-1)
|
||||
+ Text(Tip.supporter.title)
|
||||
.font(.scaledSubheadline)
|
||||
Text(Tip.supporter.subtitle)
|
||||
.font(.scaledFootnote)
|
||||
|
@ -192,7 +209,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -219,7 +236,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -240,7 +257,7 @@ struct SupportAppView: View {
|
|||
Text("settings.support.restore-purchase.explanation")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -262,7 +279,7 @@ struct SupportAppView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
.listRowBackground(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -14,32 +14,40 @@ struct SwipeActionsSettingsView: View {
|
|||
Label("settings.swipeactions.status.leading", systemImage: "arrow.right")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft,
|
||||
label: "settings.swipeactions.primary")
|
||||
.onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in
|
||||
if action == .none {
|
||||
userPreferences.swipeActionsStatusLeadingRight = .none
|
||||
}
|
||||
createStatusActionPicker(
|
||||
selection: $userPreferences.swipeActionsStatusLeadingLeft,
|
||||
label: "settings.swipeactions.primary"
|
||||
)
|
||||
.onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in
|
||||
if action == .none {
|
||||
userPreferences.swipeActionsStatusLeadingRight = .none
|
||||
}
|
||||
}
|
||||
|
||||
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingRight,
|
||||
label: "settings.swipeactions.secondary")
|
||||
.disabled(userPreferences.swipeActionsStatusLeadingLeft == .none)
|
||||
createStatusActionPicker(
|
||||
selection: $userPreferences.swipeActionsStatusLeadingRight,
|
||||
label: "settings.swipeactions.secondary"
|
||||
)
|
||||
.disabled(userPreferences.swipeActionsStatusLeadingLeft == .none)
|
||||
|
||||
Label("settings.swipeactions.status.trailing", systemImage: "arrow.left")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingRight,
|
||||
label: "settings.swipeactions.primary")
|
||||
.onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in
|
||||
if action == .none {
|
||||
userPreferences.swipeActionsStatusTrailingLeft = .none
|
||||
}
|
||||
createStatusActionPicker(
|
||||
selection: $userPreferences.swipeActionsStatusTrailingRight,
|
||||
label: "settings.swipeactions.primary"
|
||||
)
|
||||
.onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in
|
||||
if action == .none {
|
||||
userPreferences.swipeActionsStatusTrailingLeft = .none
|
||||
}
|
||||
}
|
||||
|
||||
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingLeft,
|
||||
label: "settings.swipeactions.secondary")
|
||||
.disabled(userPreferences.swipeActionsStatusTrailingRight == .none)
|
||||
createStatusActionPicker(
|
||||
selection: $userPreferences.swipeActionsStatusTrailingLeft,
|
||||
label: "settings.swipeactions.secondary"
|
||||
)
|
||||
.disabled(userPreferences.swipeActionsStatusTrailingRight == .none)
|
||||
|
||||
} header: {
|
||||
Text("settings.swipeactions.status")
|
||||
|
@ -47,11 +55,14 @@ struct SwipeActionsSettingsView: View {
|
|||
Text("settings.swipeactions.status.explanation")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Picker(selection: $userPreferences.swipeActionsIconStyle, label: Text("settings.swipeactions.icon-style")) {
|
||||
Picker(
|
||||
selection: $userPreferences.swipeActionsIconStyle,
|
||||
label: Text("settings.swipeactions.icon-style")
|
||||
) {
|
||||
ForEach(UserPreferences.SwipeActionsIconStyle.allCases, id: \.rawValue) { style in
|
||||
Text(style.description).tag(style)
|
||||
}
|
||||
|
@ -65,7 +76,7 @@ struct SwipeActionsSettingsView: View {
|
|||
Text("settings.swipeactions.use-theme-colors-explanation")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.swipeactions.navigation-title")
|
||||
|
@ -75,7 +86,9 @@ struct SwipeActionsSettingsView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
private func createStatusActionPicker(selection: Binding<StatusAction>, label: LocalizedStringKey) -> some View {
|
||||
private func createStatusActionPicker(selection: Binding<StatusAction>, label: LocalizedStringKey)
|
||||
-> some View
|
||||
{
|
||||
Picker(selection: selection, label: Text(label)) {
|
||||
Section {
|
||||
Text(StatusAction.none.displayName()).tag(StatusAction.none)
|
||||
|
|
|
@ -14,40 +14,50 @@ struct TabbarEntriesSettingsView: View {
|
|||
Form {
|
||||
Section {
|
||||
Picker("settings.tabs.first-tab", selection: $tabs.firstTab) {
|
||||
ForEach(Tab.allCases) { tab in
|
||||
tab.label.tag(tab)
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
if tab == tabs.firstTab || !tabs.tabs.contains(tab) {
|
||||
tab.label.tag(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker("settings.tabs.second-tab", selection: $tabs.secondTab) {
|
||||
ForEach(Tab.allCases) { tab in
|
||||
tab.label.tag(tab)
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
if tab == tabs.secondTab || !tabs.tabs.contains(tab) {
|
||||
tab.label.tag(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker("settings.tabs.third-tab", selection: $tabs.thirdTab) {
|
||||
ForEach(Tab.allCases) { tab in
|
||||
tab.label.tag(tab)
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
if tab == tabs.thirdTab || !tabs.tabs.contains(tab) {
|
||||
tab.label.tag(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker("settings.tabs.fourth-tab", selection: $tabs.fourthTab) {
|
||||
ForEach(Tab.allCases) { tab in
|
||||
tab.label.tag(tab)
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
if tab == tabs.fourthTab || !tabs.tabs.contains(tab) {
|
||||
tab.label.tag(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
Picker("settings.tabs.fifth-tab", selection: $tabs.fifthTab) {
|
||||
ForEach(Tab.allCases) { tab in
|
||||
tab.label.tag(tab)
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
if tab == tabs.fifthTab || !tabs.tabs.contains(tab) {
|
||||
tab.label.tag(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section {
|
||||
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("settings.general.tabbarEntries")
|
||||
|
|
|
@ -26,7 +26,7 @@ struct TagsGroupSettingView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Button {
|
||||
|
@ -35,7 +35,7 @@ struct TagsGroupSettingView: View {
|
|||
Label("timeline.filter.add-tag-groups", systemImage: "plus")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("timeline.filter.tag-groups")
|
||||
|
@ -43,8 +43,8 @@ struct TagsGroupSettingView: View {
|
|||
#if !os(visionOS)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,18 +11,15 @@ struct TranslationSettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
deepLToggle
|
||||
if preferences.alwaysUseDeepl {
|
||||
translationSelector
|
||||
if preferences.preferredTranslationType == .useDeepl {
|
||||
Section("settings.translation.user-api-key") {
|
||||
deepLPicker
|
||||
SecureField("settings.translation.user-api-key", text: $apiKey)
|
||||
.textContentType(.password)
|
||||
}
|
||||
.onAppear {
|
||||
readValue()
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
if apiKey.isEmpty {
|
||||
|
@ -33,10 +30,11 @@ struct TranslationSettingsView: View {
|
|||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
backgroundAPIKey
|
||||
autoDetectSection
|
||||
}
|
||||
.navigationTitle("settings.translation.navigation-title")
|
||||
|
@ -44,23 +42,43 @@ struct TranslationSettingsView: View {
|
|||
.scrollContentBackground(.hidden)
|
||||
.background(theme.secondaryBackgroundColor)
|
||||
#endif
|
||||
.onChange(of: apiKey) {
|
||||
writeNewValue()
|
||||
}
|
||||
.onAppear(perform: updatePrefs)
|
||||
.onChange(of: apiKey) {
|
||||
writeNewValue()
|
||||
}
|
||||
.onAppear(perform: updatePrefs)
|
||||
.onAppear(perform: readValue)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var deepLToggle: some View {
|
||||
private var translationSelector: some View {
|
||||
@Bindable var preferences = preferences
|
||||
Toggle(isOn: $preferences.alwaysUseDeepl) {
|
||||
Text("settings.translation.always-deepl")
|
||||
Picker("Translation Service", selection: $preferences.preferredTranslationType) {
|
||||
ForEach(allTTCases, id: \.self) { type in
|
||||
Text(type.description).tag(type)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
var allTTCases: [TranslationType] {
|
||||
TranslationType.allCases.filter { type in
|
||||
if type != .useApple {
|
||||
return true
|
||||
}
|
||||
#if canImport(_Translation_SwiftUI)
|
||||
if #available(iOS 17.4, *) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var deepLPicker: some View {
|
||||
@Bindable var preferences = preferences
|
||||
|
@ -80,6 +98,35 @@ struct TranslationSettingsView: View {
|
|||
} footer: {
|
||||
Text("settings.translation.auto-detect-post-language-footer")
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var backgroundAPIKey: some View {
|
||||
if preferences.preferredTranslationType != .useDeepl,
|
||||
!apiKey.isEmpty
|
||||
{
|
||||
Section {
|
||||
Text("The DeepL API Key is still stored!")
|
||||
if preferences.preferredTranslationType == .useServerIfPossible {
|
||||
Text(
|
||||
"It can however still be used as a fallback for your instance's translation service.")
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
withAnimation {
|
||||
writeNewValue(value: "")
|
||||
readValue()
|
||||
}
|
||||
} label: {
|
||||
Text("action.delete")
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func writeNewValue() {
|
||||
|
@ -91,11 +138,7 @@ struct TranslationSettingsView: View {
|
|||
}
|
||||
|
||||
private func readValue() {
|
||||
if let apiKey = DeepLUserAPIHandler.readIfAllowed() {
|
||||
self.apiKey = apiKey
|
||||
} else {
|
||||
apiKey = ""
|
||||
}
|
||||
apiKey = DeepLUserAPIHandler.readKey()
|
||||
}
|
||||
|
||||
private func updatePrefs() {
|
||||
|
|
8
IceCubesApp/App/Tabs/Settings/WishlistView.swift
Normal file
|
@ -0,0 +1,8 @@
|
|||
import SwiftUI
|
||||
import WishKit
|
||||
|
||||
struct WishlistView: View {
|
||||
var body: some View {
|
||||
WishKit.FeedbackListView()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import Account
|
||||
import AppIntents
|
||||
import DesignSystem
|
||||
import Explore
|
||||
import Foundation
|
||||
|
@ -6,7 +7,7 @@ import StatusKit
|
|||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||
enum AppTab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||
case timeline, notifications, mentions, explore, messages, settings, other
|
||||
case trending, federated, local
|
||||
case profile
|
||||
|
@ -21,37 +22,37 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
rawValue
|
||||
}
|
||||
|
||||
static func loggedOutTab() -> [Tab] {
|
||||
static func loggedOutTab() -> [AppTab] {
|
||||
[.timeline, .settings]
|
||||
}
|
||||
|
||||
static func visionOSTab() -> [Tab] {
|
||||
static func visionOSTab() -> [AppTab] {
|
||||
[.profile, .timeline, .notifications, .mentions, .explore, .post, .settings]
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeContentView(selectedTab: Binding<Tab>, popToRootTab: Binding<Tab>) -> some View {
|
||||
func makeContentView(selectedTab: Binding<AppTab>) -> some View {
|
||||
switch self {
|
||||
case .timeline:
|
||||
TimelineTab(popToRootTab: popToRootTab)
|
||||
TimelineTab()
|
||||
case .trending:
|
||||
TimelineTab(popToRootTab: popToRootTab, timeline: .trending)
|
||||
TimelineTab(timeline: .trending)
|
||||
case .local:
|
||||
TimelineTab(popToRootTab: popToRootTab, timeline: .local)
|
||||
TimelineTab(timeline: .local)
|
||||
case .federated:
|
||||
TimelineTab(popToRootTab: popToRootTab, timeline: .federated)
|
||||
TimelineTab(timeline: .federated)
|
||||
case .notifications:
|
||||
NotificationsTab(selectedTab: selectedTab, popToRootTab: popToRootTab, lockedType: nil)
|
||||
NotificationsTab(selectedTab: selectedTab, lockedType: nil)
|
||||
case .mentions:
|
||||
NotificationsTab(selectedTab: selectedTab, popToRootTab: popToRootTab, lockedType: .mention)
|
||||
NotificationsTab(selectedTab: selectedTab, lockedType: .mention)
|
||||
case .explore:
|
||||
ExploreTab(popToRootTab: popToRootTab)
|
||||
ExploreTab()
|
||||
case .messages:
|
||||
MessagesTab(popToRootTab: popToRootTab)
|
||||
MessagesTab()
|
||||
case .settings:
|
||||
SettingsTabs(popToRootTab: popToRootTab, isModal: false)
|
||||
SettingsTabs(isModal: false)
|
||||
case .profile:
|
||||
ProfileTab(popToRootTab: popToRootTab)
|
||||
ProfileTab()
|
||||
case .bookmarks:
|
||||
NavigationTab {
|
||||
AccountStatusesListView(mode: .bookmarks)
|
||||
|
@ -79,41 +80,47 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
|
||||
@ViewBuilder
|
||||
var label: some View {
|
||||
if self != .other {
|
||||
Label(title, systemImage: iconName)
|
||||
}
|
||||
}
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .timeline:
|
||||
Label("tab.timeline", systemImage: iconName)
|
||||
"tab.timeline"
|
||||
case .trending:
|
||||
Label("tab.trending", systemImage: iconName)
|
||||
"tab.trending"
|
||||
case .local:
|
||||
Label("tab.local", systemImage: iconName)
|
||||
"tab.local"
|
||||
case .federated:
|
||||
Label("tab.federated", systemImage: iconName)
|
||||
"tab.federated"
|
||||
case .notifications:
|
||||
Label("tab.notifications", systemImage: iconName)
|
||||
"tab.notifications"
|
||||
case .mentions:
|
||||
Label("tab.mentions", systemImage: iconName)
|
||||
"tab.mentions"
|
||||
case .explore:
|
||||
Label("tab.explore", systemImage: iconName)
|
||||
"tab.explore"
|
||||
case .messages:
|
||||
Label("tab.messages", systemImage: iconName)
|
||||
"tab.messages"
|
||||
case .settings:
|
||||
Label("tab.settings", systemImage: iconName)
|
||||
"tab.settings"
|
||||
case .profile:
|
||||
Label("tab.profile", systemImage: iconName)
|
||||
"tab.profile"
|
||||
case .bookmarks:
|
||||
Label("accessibility.tabs.profile.picker.bookmarks", systemImage: iconName)
|
||||
"accessibility.tabs.profile.picker.bookmarks"
|
||||
case .favorites:
|
||||
Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName)
|
||||
"accessibility.tabs.profile.picker.favorites"
|
||||
case .post:
|
||||
Label("menu.new-post", systemImage: iconName)
|
||||
"menu.new-post"
|
||||
case .followedTags:
|
||||
Label("timeline.filter.tags", systemImage: iconName)
|
||||
"timeline.filter.tags"
|
||||
case .lists:
|
||||
Label("timeline.filter.lists", systemImage: iconName)
|
||||
"timeline.filter.lists"
|
||||
case .links:
|
||||
Label("explore.section.trending.links", systemImage: iconName)
|
||||
"explore.section.trending.links"
|
||||
case .other:
|
||||
EmptyView()
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +168,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
|||
@Observable
|
||||
class SidebarTabs {
|
||||
struct SidedebarTab: Hashable, Codable {
|
||||
let tab: Tab
|
||||
let tab: AppTab
|
||||
var enabled: Bool
|
||||
}
|
||||
|
||||
|
@ -179,6 +186,7 @@ class SidebarTabs {
|
|||
.init(tab: .favorites, enabled: true),
|
||||
.init(tab: .followedTags, enabled: true),
|
||||
.init(tab: .lists, enabled: true),
|
||||
.init(tab: .links, enabled: true),
|
||||
|
||||
.init(tab: .settings, enabled: true),
|
||||
.init(tab: .profile, enabled: true),
|
||||
|
@ -194,7 +202,7 @@ class SidebarTabs {
|
|||
}
|
||||
}
|
||||
|
||||
func isEnabled(_ tab: Tab) -> Bool {
|
||||
func isEnabled(_ tab: AppTab) -> Bool {
|
||||
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
|
||||
}
|
||||
|
||||
|
@ -211,45 +219,45 @@ class iOSTabs {
|
|||
}
|
||||
|
||||
class Storage {
|
||||
@AppStorage(TabEntries.first.rawValue) var firstTab = Tab.timeline
|
||||
@AppStorage(TabEntries.second.rawValue) var secondTab = Tab.notifications
|
||||
@AppStorage(TabEntries.third.rawValue) var thirdTab = Tab.explore
|
||||
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = Tab.messages
|
||||
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = Tab.profile
|
||||
@AppStorage(TabEntries.first.rawValue) var firstTab = AppTab.timeline
|
||||
@AppStorage(TabEntries.second.rawValue) var secondTab = AppTab.notifications
|
||||
@AppStorage(TabEntries.third.rawValue) var thirdTab = AppTab.explore
|
||||
@AppStorage(TabEntries.fourth.rawValue) var fourthTab = AppTab.links
|
||||
@AppStorage(TabEntries.fifth.rawValue) var fifthTab = AppTab.profile
|
||||
}
|
||||
|
||||
private let storage = Storage()
|
||||
public static let shared = iOSTabs()
|
||||
|
||||
var tabs: [Tab] {
|
||||
var tabs: [AppTab] {
|
||||
[firstTab, secondTab, thirdTab, fourthTab, fifthTab]
|
||||
}
|
||||
|
||||
var firstTab: Tab {
|
||||
var firstTab: AppTab {
|
||||
didSet {
|
||||
storage.firstTab = firstTab
|
||||
}
|
||||
}
|
||||
|
||||
var secondTab: Tab {
|
||||
var secondTab: AppTab {
|
||||
didSet {
|
||||
storage.secondTab = secondTab
|
||||
}
|
||||
}
|
||||
|
||||
var thirdTab: Tab {
|
||||
var thirdTab: AppTab {
|
||||
didSet {
|
||||
storage.thirdTab = thirdTab
|
||||
}
|
||||
}
|
||||
|
||||
var fourthTab: Tab {
|
||||
var fourthTab: AppTab {
|
||||
didSet {
|
||||
storage.fourthTab = fourthTab
|
||||
}
|
||||
}
|
||||
|
||||
var fifthTab: Tab {
|
||||
var fifthTab: AppTab {
|
||||
didSet {
|
||||
storage.fifthTab = fifthTab
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ struct EditTagGroupView: View {
|
|||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
|
||||
Section("add-tag-groups.edit.tags") {
|
||||
|
@ -50,7 +50,7 @@ struct EditTagGroupView: View {
|
|||
)
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
#endif
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
|
@ -65,16 +65,16 @@ struct EditTagGroupView: View {
|
|||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
#endif
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!tagGroup.isValid)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("action.save", action: { save() })
|
||||
.disabled(!tagGroup.isValid)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
focusedField = .title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,8 +140,7 @@ private struct TitleInputView: View {
|
|||
var warningText: LocalizedStringKey {
|
||||
if case let .invalid(description) = titleValidationStatus {
|
||||
return description
|
||||
} else if
|
||||
isNewGroup,
|
||||
} else if isNewGroup,
|
||||
tagGroups.contains(where: { $0.title == title })
|
||||
{
|
||||
return "\(title) add-tag-groups.edit.title.field.warning.already-exists"
|
||||
|
@ -180,7 +179,7 @@ private struct SymbolInputView: View {
|
|||
}
|
||||
|
||||
if case let .invalid(description) = selectedSymbolValidationStatus,
|
||||
focusedField == .symbol
|
||||
focusedField == .symbol
|
||||
{
|
||||
Text(description).warningLabel()
|
||||
}
|
||||
|
@ -210,7 +209,9 @@ private struct TagsInputView: View {
|
|||
HStack {
|
||||
Text(tag)
|
||||
Spacer()
|
||||
Button { deleteTag(tag) } label: {
|
||||
Button {
|
||||
deleteTag(tag)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
|
@ -240,7 +241,9 @@ private struct TagsInputView: View {
|
|||
Spacer()
|
||||
|
||||
if !newTag.isEmpty, !tags.contains(newTag) {
|
||||
Button { addNewTag() } label: {
|
||||
Button {
|
||||
addNewTag()
|
||||
} label: {
|
||||
Image(systemName: "checkmark.circle.fill").tint(.green)
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +284,7 @@ private struct TagsInputView: View {
|
|||
|
||||
private func addTag(_ tag: String) {
|
||||
guard !tag.isEmpty,
|
||||
!tags.contains(tag)
|
||||
!tags.contains(tag)
|
||||
else { return }
|
||||
|
||||
tags.append(tag)
|
||||
|
@ -347,12 +350,15 @@ private struct SymbolSearchResultsView: View {
|
|||
var validationStatus: ValidationStatus {
|
||||
if results.isEmpty {
|
||||
if symbolQuery == selectedSymbol,
|
||||
!symbolQuery.isEmpty,
|
||||
results.count == 0
|
||||
!symbolQuery.isEmpty,
|
||||
results.count == 0
|
||||
{
|
||||
.invalid(description: "\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected")
|
||||
.invalid(
|
||||
description:
|
||||
"\(symbolQuery) add-tag-groups.edit.tags.field.warning.search-results.already-selected")
|
||||
} else {
|
||||
.invalid(description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found")
|
||||
.invalid(
|
||||
description: "add-tag-groups.edit.tags.field.warning.search-results.no-symbol-found")
|
||||
}
|
||||
} else {
|
||||
.valid
|
||||
|
@ -385,7 +391,8 @@ extension TagGroup {
|
|||
if symbolName.isEmpty {
|
||||
return .invalid(description: "add-tag-groups.edit.title.field.warning.no-symbol-selected")
|
||||
} else if !Self.allSymbols.contains(symbolName) {
|
||||
return .invalid(description: "\(symbolName) add-tag-groups.edit.title.field.warning.invalid-sfsymbol-name")
|
||||
return .invalid(
|
||||
description: "\(symbolName) add-tag-groups.edit.title.field.warning.invalid-sfsymbol-name")
|
||||
}
|
||||
|
||||
return .valid
|
||||
|
@ -430,8 +437,7 @@ extension TagGroup {
|
|||
guard !query.isEmpty else { return [] }
|
||||
|
||||
return allSymbols.filter {
|
||||
$0.contains(query) &&
|
||||
$0 != excludedSymbol
|
||||
$0.contains(query) && $0 != excludedSymbol
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,11 @@ struct AddRemoteTimelineView: View {
|
|||
.foregroundColor(.green)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
if !instanceName.isEmpty && instance == nil {
|
||||
Label("timeline.\(instanceName)-not-valid", systemImage: "xmark.seal.fill")
|
||||
.foregroundColor(.red)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
Button {
|
||||
guard instance != nil else { return }
|
||||
context.insert(LocalTimeline(instance: instanceName))
|
||||
|
@ -45,6 +50,7 @@ struct AddRemoteTimelineView: View {
|
|||
Text("timeline.add.action.add")
|
||||
}
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
.disabled(instance == nil)
|
||||
|
||||
instancesListView
|
||||
}
|
||||
|
@ -56,25 +62,28 @@ struct AddRemoteTimelineView: View {
|
|||
.background(theme.secondaryBackgroundColor)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
#endif
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
.toolbar {
|
||||
CancelToolbarItem()
|
||||
}
|
||||
.onChange(of: instanceName) { _, newValue in
|
||||
instanceNamePublisher.send(newValue)
|
||||
}
|
||||
.onReceive(
|
||||
instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
|
||||
) { newValue in
|
||||
Task {
|
||||
let client = Client(server: newValue)
|
||||
instance = try? await client.get(endpoint: Instances.instance)
|
||||
}
|
||||
.onChange(of: instanceName) { _, newValue in
|
||||
instanceNamePublisher.send(newValue)
|
||||
}
|
||||
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
|
||||
Task {
|
||||
let client = Client(server: newValue)
|
||||
instance = try? await client.get(endpoint: Instances.instance)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
let client = InstanceSocialClient()
|
||||
Task {
|
||||
instances = await client.fetchInstances(keyword: instanceName)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isInstanceURLFieldFocused = true
|
||||
let client = InstanceSocialClient()
|
||||
let instanceName = instanceName
|
||||
Task {
|
||||
instances = await client.fetchInstances(keyword: instanceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +93,10 @@ struct AddRemoteTimelineView: View {
|
|||
ProgressView()
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
} else {
|
||||
ForEach(instanceName.isEmpty ? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }) { instance in
|
||||
ForEach(
|
||||
instanceName.isEmpty
|
||||
? instances : instances.filter { $0.name.contains(instanceName.lowercased()) }
|
||||
) { instance in
|
||||
Button {
|
||||
instanceName = instance.name
|
||||
} label: {
|
||||
|
|
|
@ -18,12 +18,10 @@ struct TimelineTab: View {
|
|||
@Environment(UserPreferences.self) private var preferences
|
||||
@Environment(Client.self) private var client
|
||||
@State private var routerPath = RouterPath()
|
||||
@Binding var popToRootTab: Tab
|
||||
|
||||
@State private var didAppear: Bool = false
|
||||
@State private var timeline: TimelineFilter = .home
|
||||
@State private var selectedTagGroup: TagGroup?
|
||||
@State private var scrollToTopSignal: Int = 0
|
||||
|
||||
@Query(sort: \LocalTimeline.creationDate, order: .reverse) var localTimelines: [LocalTimeline]
|
||||
@Query(sort: \TagGroup.creationDate, order: .reverse) var tagGroups: [TagGroup]
|
||||
|
@ -33,26 +31,26 @@ struct TimelineTab: View {
|
|||
|
||||
private let canFilterTimeline: Bool
|
||||
|
||||
init(popToRootTab: Binding<Tab>, timeline: TimelineFilter? = nil) {
|
||||
init(timeline: TimelineFilter? = nil) {
|
||||
canFilterTimeline = timeline == nil
|
||||
_popToRootTab = popToRootTab
|
||||
_timeline = .init(initialValue: timeline ?? .home)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
TimelineView(timeline: $timeline,
|
||||
pinnedFilters: $pinnedFilters,
|
||||
selectedTagGroup: $selectedTagGroup,
|
||||
scrollToTopSignal: $scrollToTopSignal,
|
||||
canFilterTimeline: canFilterTimeline)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbar {
|
||||
toolbarView
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
TimelineView(
|
||||
timeline: $timeline,
|
||||
pinnedFilters: $pinnedFilters,
|
||||
selectedTagGroup: $selectedTagGroup,
|
||||
canFilterTimeline: canFilterTimeline
|
||||
)
|
||||
.withAppRouter()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
.toolbar {
|
||||
toolbarView
|
||||
}
|
||||
.toolbarBackground(theme.primaryBackgroundColor.opacity(0.30), for: .navigationBar)
|
||||
.id(client.id)
|
||||
}
|
||||
.onAppear {
|
||||
routerPath.client = client
|
||||
|
@ -61,31 +59,22 @@ struct TimelineTab: View {
|
|||
if client.isAuth {
|
||||
timeline = lastTimelineFilter
|
||||
} else {
|
||||
timeline = .federated
|
||||
timeline = .trending
|
||||
}
|
||||
}
|
||||
Task {
|
||||
await currentAccount.fetchLists()
|
||||
}
|
||||
if !client.isAuth {
|
||||
routerPath.presentedSheet = .addAccount
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await currentAccount.fetchLists()
|
||||
}
|
||||
.onChange(of: client.isAuth) {
|
||||
resetTimelineFilter()
|
||||
}
|
||||
.onChange(of: currentAccount.account?.id) {
|
||||
resetTimelineFilter()
|
||||
}
|
||||
.onChange(of: $popToRootTab.wrappedValue) { _, newValue in
|
||||
if newValue == .timeline {
|
||||
if routerPath.path.isEmpty {
|
||||
scrollToTopSignal += 1
|
||||
} else {
|
||||
routerPath.path = []
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: client.id) {
|
||||
routerPath.path = []
|
||||
}
|
||||
|
@ -125,8 +114,10 @@ struct TimelineTab: View {
|
|||
private var timelineFilterButton: some View {
|
||||
headerGroup
|
||||
timelineFiltersButtons
|
||||
listsFiltersButons
|
||||
tagsFiltersButtons
|
||||
if client.isAuth {
|
||||
listsFiltersButons
|
||||
tagsFiltersButtons
|
||||
}
|
||||
localTimelinesFiltersButtons
|
||||
tagGroupsFiltersButtons
|
||||
Divider()
|
||||
|
@ -193,7 +184,8 @@ struct TimelineTab: View {
|
|||
Button {
|
||||
timeline = .latest
|
||||
} label: {
|
||||
Label(TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName())
|
||||
Label(
|
||||
TimelineFilter.latest.localizedTitle(), systemImage: TimelineFilter.latest.iconName())
|
||||
}
|
||||
}
|
||||
if timeline == .home {
|
||||
|
@ -201,8 +193,9 @@ struct TimelineTab: View {
|
|||
timeline = .resume
|
||||
} label: {
|
||||
VStack {
|
||||
Label(TimelineFilter.resume.localizedTitle(),
|
||||
systemImage: TimelineFilter.resume.iconName())
|
||||
Label(
|
||||
TimelineFilter.resume.localizedTitle(),
|
||||
systemImage: TimelineFilter.resume.iconName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,9 +209,11 @@ struct TimelineTab: View {
|
|||
Button {
|
||||
withAnimation {
|
||||
if let index {
|
||||
pinnedFilters.remove(at: index)
|
||||
let timeline = pinnedFilters.remove(at: index)
|
||||
Telemetry.signal("timeline.pin.removed", parameters: ["timeline": timeline.rawValue])
|
||||
} else {
|
||||
pinnedFilters.append(timeline)
|
||||
Telemetry.signal("timeline.pin.added", parameters: ["timeline": timeline.rawValue])
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
@ -314,18 +309,20 @@ struct TimelineTab: View {
|
|||
}
|
||||
|
||||
private var contentFilterButton: some View {
|
||||
Button(action: {
|
||||
routerPath.presentedSheet = .timelineContentFilter
|
||||
}, label: {
|
||||
Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease)
|
||||
})
|
||||
Button(
|
||||
action: {
|
||||
routerPath.presentedSheet = .timelineContentFilter
|
||||
},
|
||||
label: {
|
||||
Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease)
|
||||
})
|
||||
}
|
||||
|
||||
private func resetTimelineFilter() {
|
||||
if client.isAuth, canFilterTimeline {
|
||||
timeline = lastTimelineFilter
|
||||
} else if !client.isAuth {
|
||||
timeline = .federated
|
||||
timeline = .trending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,18 +9,41 @@ struct ToolbarTab: ToolbarContent {
|
|||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@Environment(UserPreferences.self) private var userPreferences
|
||||
@Environment(Theme.self) private var theme
|
||||
|
||||
@Binding var routerPath: RouterPath
|
||||
|
||||
var body: some ToolbarContent {
|
||||
if !isSecondaryColumn {
|
||||
statusEditorToolbarItem(routerPath: routerPath,
|
||||
visibility: userPreferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad ||
|
||||
(UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||
if horizontalSizeClass == .regular {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad
|
||||
|| UIDevice.current.userInterfaceIdiom == .mac
|
||||
{
|
||||
Button {
|
||||
withAnimation {
|
||||
userPreferences.isSidebarExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
if userPreferences.isSidebarExpanded {
|
||||
Image(systemName: "sidebar.squares.left")
|
||||
} else {
|
||||
Image(systemName: "sidebar.left")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statusEditorToolbarItem(
|
||||
routerPath: routerPath,
|
||||
visibility: userPreferences.postVisibility)
|
||||
if UIDevice.current.userInterfaceIdiom != .pad
|
||||
|| (UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .compact)
|
||||
{
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
AppAccountsSelectorView(routerPath: routerPath)
|
||||
AppAccountsSelectorView(
|
||||
routerPath: routerPath,
|
||||
avatarConfig: theme.avatarShape == .circle ? .badge : .badgeRounded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/16.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/256 1.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/256.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/32 1.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/32.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/512 1.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/512.png
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/64.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/Content.png
Normal file
After Width: | Height: | Size: 501 KiB |
|
@ -1,67 +1,91 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "macos16.png",
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "dark.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "tinted.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "macos32 1.png",
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "macos32.png",
|
||||
"filename" : "32 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "macos64.png",
|
||||
"filename" : "64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "macos128.png",
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "mac256 1.png",
|
||||
"filename" : "256 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "mac256.png",
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS512 1.png",
|
||||
"filename" : "512 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS512.png",
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS1024.png",
|
||||
"filename" : "Content.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
|
|
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/dark.png
Normal file
After Width: | Height: | Size: 370 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 973 KiB |
Before Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 8.3 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIcon.appiconset/tinted.png
Normal file
After Width: | Height: | Size: 545 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/AppIcon-fs8.png
vendored
Normal file
After Width: | Height: | Size: 79 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate0-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 991 KiB |
After Width: | Height: | Size: 156 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"filename" : "AppIcon-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/1024.png
vendored
Normal file
After Width: | Height: | Size: 85 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate1-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/AppIconAlternate10.png
vendored
Normal file
After Width: | Height: | Size: 151 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate10-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate10.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon15.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate11-image.imageset/icon15.png
vendored
Normal file
After Width: | Height: | Size: 120 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon16.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate12-image.imageset/icon16.png
vendored
Normal file
After Width: | Height: | Size: 108 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon17.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate13-image.imageset/icon17.png
vendored
Normal file
After Width: | Height: | Size: 72 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon18.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate14-image.imageset/icon18.png
vendored
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/AppIconAlternate15.png
vendored
Normal file
After Width: | Height: | Size: 91 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate15-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIconAlternate15.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "icon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate16-image.imageset/icon.png
vendored
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Alternate43-fs8.png
vendored
Normal file
After Width: | Height: | Size: 102 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate17-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Alternate43-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Alternate44-fs8.png
vendored
Normal file
After Width: | Height: | Size: 165 KiB |
21
IceCubesApp/Assets.xcassets/AppIconAlternate18-image.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Alternate44-fs8.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
IceCubesApp/Assets.xcassets/AppIconAlternate19-image.imageset/Alternate45-fs8.png
vendored
Normal file
After Width: | Height: | Size: 172 KiB |