Merge remote-tracking branch 'upstream/main' into zh-Hant-localization

This commit is contained in:
sh95014 2024-12-30 19:35:28 -08:00
commit 0937a8fe54
460 changed files with 17135 additions and 6164 deletions

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

@ -89,4 +89,6 @@ fastlane/test_output
iOSInjectionProject/
.DS_Store
IceCubesApp.xcconfig
IceCubesApp.xcconfig
*.resolved
buildServer.json

14
.vscode/launch.json vendored Normal file
View 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
View file

@ -0,0 +1,6 @@
{
"[swift]": {
"editor.defaultFormatter": "sweetpad.sweetpad",
"editor.formatOnSave": true,
},
}

View file

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

View file

@ -1,9 +0,0 @@
/*
InfoPlist.strings
IceCubesApp
Created by Thomas Durand on 27/01/2023.
*/
"CFBundleDisplayName" = "Apri con Ice Cubes";

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1600"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = []
}

View file

@ -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 = []
}

View file

@ -17,13 +17,7 @@ struct NavigationSheet<Content: View>: View {
NavigationStack {
content()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle")
}
}
CloseToolbarItem()
}
}
}

View file

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

View file

@ -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 = []
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ struct SidebarEntriesSettingsView: View {
.onMove(perform: move)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
.environment(\.editMode, .constant(.active))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
import SwiftUI
import WishKit
struct WishlistView: View {
var body: some View {
WishKit.FeedbackListView()
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View file

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "1024.png",
"filename" : "AppIcon-fs8.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Some files were not shown because too many files have changed in this diff Show more