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"

2
.gitignore vendored
View file

@ -90,3 +90,5 @@ fastlane/test_output
iOSInjectionProject/
.DS_Store
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
default:
tabBarView
}
#endif
}
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: {
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
})) {
},
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,9 +83,37 @@ 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
{
@ -107,25 +124,28 @@ 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,
@ -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,7 +128,8 @@ extension IceCubesApp {
Group {
switch destination.wrappedValue {
case let .mediaViewer(attachments, selectedAttachment):
MediaUIView(selectedAttachment: selectedAttachment,
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,14 +18,13 @@ 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
{
@ -34,18 +33,38 @@ struct SideBarView<Content: View>: View {
return 0
}
private func makeIconForTab(tab: Tab) -> some View {
private func makeIconForTab(tab: AppTab) -> some View {
ZStack(alignment: .topTrailing) {
SideBarIcon(systemIconName: tab.iconName,
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,9 +113,23 @@ struct SideBarView<Content: View>: View {
}
} label: {
ZStack(alignment: .topTrailing) {
AppAccountView(viewModel: .init(appAccount: account, isCompact: true),
if userPreferences.isSidebarExpanded {
AppAccountView(
viewModel: .init(
appAccount: account,
isCompact: false,
isInSettings: false),
isParentPresented: .constant(false))
if showBadge,
} 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
@ -101,11 +137,24 @@ struct SideBarView<Content: View>: View {
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,7 +189,8 @@ struct SideBarView<Content: View>: View {
tabsView
} else {
ForEach(appAccounts.availableAccounts) { account in
makeAccountButton(account: account,
makeAccountButton(
account: account,
showBadge: account.id != appAccounts.currentAccount.id)
if account.id == appAccounts.currentAccount.id {
tabsView
@ -155,15 +199,24 @@ struct SideBarView<Content: View>: View {
}
}
}
.frame(width: .sidebarWidth)
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.scrollContentBackground(.hidden)
.background(.thinMaterial)
.safeAreaInset(edge: .bottom, content: {
HStack {
.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: .sidebarWidth)
}
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.background(.thinMaterial)
})
Divider().edgesIgnoringSafeArea(.all)
@ -196,6 +249,7 @@ private struct SideBarIcon: View {
self.isHovered = isHovered
}
}
.frame(width: 50, height: 40)
}
}

View file

@ -23,7 +23,8 @@ public struct ReportView: View {
NavigationStack {
Form {
Section {
TextField("report.comment.placeholder",
TextField(
"report.comment.placeholder",
text: $commentText,
axis: .vertical)
}
@ -47,7 +48,9 @@ public struct ReportView: View {
Task {
do {
let _: ReportSent =
try await client.post(endpoint: Statuses.report(accountId: status.account.id,
try await client.post(
endpoint: Statuses.report(
accountId: status.account.id,
statusId: status.id,
comment: commentText))
dismiss()

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
.environment(
\.openURL,
OpenURLAction { url in
// Open internal URL.
routerPath.handle(url: 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 }
@ -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
}
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,34 +30,38 @@ 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)
@ -65,8 +69,21 @@ struct AboutView: View {
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("""
Text(
"""
[EmojiText](https://github.com/divadretlaw/EmojiText)
[HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown)
@ -90,7 +107,8 @@ struct AboutView: View {
[RevenueCat](https://github.com/RevenueCat/purchases-ios)
[SFSafeSymbols](https://github.com/SFSafeSymbols/SFSafeSymbols)
""")
"""
)
.multilineTextAlignment(.leading)
.font(.scaledSubheadline)
.foregroundStyle(.secondary)
@ -112,7 +130,9 @@ struct AboutView: View {
#endif
.navigationTitle(Text("settings.about.title"))
.navigationBarTitleDisplayMode(.large)
.environment(\.openURL, OpenURLAction { url in
.environment(
\.openURL,
OpenURLAction { url in
routerPath.handle(url: url)
})
}
@ -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,7 +35,8 @@ 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: "")
@ -87,12 +89,11 @@ struct AddAccountView: View {
.scrollDismissesKeyboard(.immediately)
#endif
.toolbar {
if !appAccountsManager.availableAccounts.isEmpty {
CancelToolbarItem()
}
}
.onAppear {
isInstanceURLFieldFocused = true
let instanceName = instanceName
Task {
let instances = await instanceSocialClient.fetchInstances(keyword: instanceName)
withAnimation {
@ -103,6 +104,8 @@ struct AddAccountView: View {
}
.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 }
@ -121,25 +124,26 @@ struct AddAccountView: View {
do {
// bare bones preflight for domain validity
let instanceDetailClient = Client(server: sanitizedName)
if
instanceDetailClient.server.contains("."),
if instanceDetailClient.server.contains("."),
instanceDetailClient.server.last != "."
{
let instance: Instance = try await instanceDetailClient.get(endpoint: Instances.instance)
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
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
}
instanceFetchError = nil
} else {
instance = nil
instanceFetchError = nil
}
} catch _ as DecodingError {
} catch _ as ServerError {
instance = nil
instanceFetchError = "account.add.error.instance-not-supported"
} catch {
instance = nil
instanceFetchError = nil
}
}
}
@ -177,7 +181,7 @@ struct AddAccountView: View {
Spacer()
}
}
.buttonStyle(.borderedProminent)
.buttonStyle(PlainButtonStyle())
}
#if !os(visionOS)
.listRowBackground(theme.tintColor)
@ -219,7 +223,10 @@ struct AddAccountView: View {
.foregroundStyle(theme.tintColor)
}
.padding(.bottom, 5)
Text(instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "")
Text(
instance.info?.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
?? ""
)
.foregroundStyle(Color.secondary)
.lineLimit(10)
}
@ -270,7 +277,8 @@ 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,
let url = try? await webAuthenticationSession.authenticate(
using: oauthURL,
callbackURLScheme: AppInfo.scheme.replacingOccurrences(of: "://", with: ""))
{
await continueSignIn(url: url)
@ -288,7 +296,10 @@ 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,
Telemetry.signal("account.added")
appAccountsManager.add(
account: AppAccount(
server: client.server,
accountName: "\(account.acct)@\(client.server)",
oauthToken: oauthToken))
Task {

View file

@ -34,7 +34,10 @@ struct ContentSettingsView: View {
#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)
@ -89,17 +92,23 @@ struct ContentSettingsView: View {
#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)
}

View file

@ -29,7 +29,8 @@ struct DisplaySettingsView: View {
@State private var isFontSelectorPresented = false
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
private let previewStatusViewModel = StatusRowViewModel(
status: Status.placeholder(forSettings: true, language: "la"),
client: Client(server: ""),
routerPath: RouterPath()) // translate from latin button
@ -37,7 +38,7 @@ struct DisplaySettingsView: View {
ZStack(alignment: .top) {
Form {
#if !os(visionOS)
StatusRowView(viewModel: previewStatusViewModel)
StatusRowExternalView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.opacity(0)
.hidden()
@ -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,7 +97,9 @@ struct DisplaySettingsView: View {
Rectangle()
.fill(theme.secondaryBackgroundColor)
.frame(height: 30)
.mask(LinearGradient(gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]),
.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)
@ -135,7 +141,10 @@ struct DisplaySettingsView: View {
private var fontSection: some View {
Section("settings.display.section.font") {
Picker("settings.display.font", selection: .init(get: { () -> FontState in
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" {
@ -144,7 +153,8 @@ struct DisplaySettingsView: View {
return FontState.SFRounded
}
return theme.chosenFontData != nil ? FontState.custom : FontState.system
}, set: { newValue in
},
set: { newValue in
switch newValue {
case .system:
theme.chosenFont = nil
@ -157,7 +167,8 @@ struct DisplaySettingsView: View {
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,8 +185,10 @@ 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))")
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
@ -224,12 +237,17 @@ struct DisplaySettingsView: View {
Toggle("settings.display.show-reply-indentation", isOn: $userPreferences.showReplyIndentation)
if userPreferences.showReplyIndentation {
VStack {
Slider(value: .init(get: {
Slider(
value: .init(
get: {
Double(userPreferences.maxReplyIndentation)
}, set: { newVal in
},
set: { newVal in
userPreferences.maxReplyIndentation = UInt(newVal)
}), in: 1 ... 20, step: 1)
Text("settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))")
}), in: 1...20, step: 1)
Text(
"settings.display.max-reply-indentation-\(String(userPreferences.maxReplyIndentation))"
)
.font(.scaledBody)
}
.alignmentGuide(.listRowSeparatorLeading) { d in
@ -237,6 +255,8 @@ struct DisplaySettingsView: View {
}
}
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)

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: [
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]),
.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))]
@ -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

@ -18,16 +18,20 @@ struct PushNotificationsView: View {
var body: some View {
Form {
Section {
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isEnabled = newValue
if newValue {
updateSubscription()
} else {
deleteSubscription()
}
})) {
})
) {
Text("settings.push.main-toggle")
}
} footer: {
@ -39,52 +43,76 @@ struct PushNotificationsView: View {
if subscription.isEnabled {
Section {
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isMentionNotificationEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isMentionNotificationEnabled = newValue
updateSubscription()
})) {
})
) {
Label("settings.push.mentions", systemImage: "at")
}
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isFollowNotificationEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isFollowNotificationEnabled = newValue
updateSubscription()
})) {
})
) {
Label("settings.push.follows", systemImage: "person.badge.plus")
}
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isFavoriteNotificationEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isFavoriteNotificationEnabled = newValue
updateSubscription()
})) {
})
) {
Label("settings.push.favorites", systemImage: "star")
}
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isReblogNotificationEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isReblogNotificationEnabled = newValue
updateSubscription()
})) {
})
) {
Label("settings.push.boosts", image: "Rocket")
}
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isPollNotificationEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isPollNotificationEnabled = newValue
updateSubscription()
})) {
})
) {
Label("settings.push.polls", systemImage: "chart.bar")
}
Toggle(isOn: .init(get: {
Toggle(
isOn: .init(
get: {
subscription.isNewPostsNotificationEnabled
}, set: { newValue in
},
set: { newValue in
subscription.isNewPostsNotificationEnabled = newValue
updateSubscription()
})) {
})
) {
Label("settings.push.new-posts", systemImage: "bubble.right")
}
}

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,6 +39,8 @@ struct SettingsTabs: View {
accountsSection
generalSection
otherSections
postStreamingSection
AISection
cacheSection
}
.scrollContentBackground(.hidden)
@ -58,12 +60,40 @@ struct SettingsTabs: View {
}
}
}
if UIDevice.current.userInterfaceIdiom == .pad, !preferences.showiPadSecondaryColumn, !isModal {
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,10 +133,10 @@ struct SettingsTabs: View {
}
}
}
addAccountButton
if !appAccountsManager.availableAccounts.isEmpty {
editAccountButton
}
addAccountButton
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
@ -126,6 +151,7 @@ struct SettingsTabs: View {
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")
}
@ -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")
}
@ -228,6 +253,44 @@ struct SettingsTabs: View {
#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
}
private var appSection: some View {
Section {
#if !targetEnvironment(macCatalyst) && !os(visionOS)
@ -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)!)
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,15 +332,24 @@ 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)
@ -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,6 +397,10 @@ struct SettingsTabs: View {
}
}
}
} header: {
Text("settings.section.cache")
} footer: {
Text("Remove all cached images and videos")
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)

View file

@ -72,16 +72,32 @@ 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: {
}
)
.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()
@ -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
}
@ -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)

View file

@ -14,31 +14,39 @@ struct SwipeActionsSettingsView: View {
Label("settings.swipeactions.status.leading", systemImage: "arrow.right")
.foregroundColor(.secondary)
createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft,
label: "settings.swipeactions.primary")
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")
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")
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")
createStatusActionPicker(
selection: $userPreferences.swipeActionsStatusTrailingLeft,
label: "settings.swipeactions.secondary"
)
.disabled(userPreferences.swipeActionsStatusTrailingRight == .none)
} header: {
@ -51,7 +59,10 @@ struct SwipeActionsSettingsView: View {
#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)
}
@ -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,31 +14,41 @@ struct TabbarEntriesSettingsView: View {
Form {
Section {
Picker("settings.tabs.first-tab", selection: $tabs.firstTab) {
ForEach(Tab.allCases) { tab in
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
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
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
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
ForEach(AppTab.allCases) { tab in
if tab == tabs.fifthTab || !tabs.tabs.contains(tab) {
tab.label.tag(tab)
}
}
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif

View file

@ -11,16 +11,13 @@ 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)
#endif
@ -37,6 +34,7 @@ struct TranslationSettingsView: View {
#endif
}
}
backgroundAPIKey
autoDetectSection
}
.navigationTitle("settings.translation.navigation-title")
@ -48,19 +46,39 @@ struct TranslationSettingsView: View {
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)
#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

@ -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"
@ -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)
}
}
@ -350,9 +353,12 @@ private struct SymbolSearchResultsView: View {
!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
}
@ -62,7 +68,9 @@ struct AddRemoteTimelineView: View {
.onChange(of: instanceName) { _, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in
.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)
@ -71,6 +79,7 @@ struct AddRemoteTimelineView: View {
.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,19 +31,19 @@ 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,
TimelineView(
timeline: $timeline,
pinnedFilters: $pinnedFilters,
selectedTagGroup: $selectedTagGroup,
scrollToTopSignal: $scrollToTopSignal,
canFilterTimeline: canFilterTimeline)
canFilterTimeline: canFilterTimeline
)
.withAppRouter()
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
.toolbar {
@ -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
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,7 +193,8 @@ struct TimelineTab: View {
timeline = .resume
} label: {
VStack {
Label(TimelineFilter.resume.localizedTitle(),
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,9 +309,11 @@ struct TimelineTab: View {
}
private var contentFilterButton: some View {
Button(action: {
Button(
action: {
routerPath.presentedSheet = .timelineContentFilter
}, label: {
},
label: {
Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease)
})
}
@ -325,7 +322,7 @@ struct TimelineTab: View {
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,
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)
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

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Alternate45-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: 96 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "AppIconAlternate2.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: 174 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Alternate46-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: 189 KiB

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