Compare commits

...

36 commits

Author SHA1 Message Date
Adem Özsayın d36930b7af
Updated TR translations (#2088) 2024-06-01 13:55:26 +02:00
David Whetstone c06e3b59e4
Add "Settings ..." menu and ⌘, hotkey (#2079) 2024-06-01 13:55:06 +02:00
Thomas Ricouard 8cca261e43 Bump version to 1.10.42 2024-06-01 13:54:44 +02:00
Thomas Ricouard f40aeb9cac Add followers count widget 2024-05-17 14:22:00 +02:00
Thomas Ricouard 1578896b3e Immersive short modal 2024-05-17 13:56:03 +02:00
Thomas Ricouard ba3d8b1882 Composer: disable predictive type on all platforms 2024-05-17 13:55:55 +02:00
Thomas Ricouard 04af087c4b Bump version to 1.10.41 2024-05-16 07:05:17 +02:00
Thomas Ricouard a9398c25af fix #2064 2024-05-15 10:52:41 +02:00
Thomas Ricouard 13d721912b Add lists widget 2024-05-15 09:27:35 +02:00
Thomas Ricouard e3d4e693d2 More improvement to alt edit 2024-05-15 08:30:57 +02:00
Thomas Ricouard 86c053344b Improve media alt edit 2024-05-15 08:28:05 +02:00
Thomas Ricouard a996aace80 Add translate for image alt 2024-05-14 19:43:52 +02:00
Jesús Jiménez Sánchez 18a1d17230
Update ES localization (#2076) 2024-05-14 19:37:40 +02:00
Thomas Ricouard 69cb9a20f9 Add native translate for media description edit + profile bio 2024-05-14 19:36:25 +02:00
Thomas Ricouard bab2b4be9c Fix localization 2024-05-14 12:20:25 +02:00
Thomas Ricouard bb005386df Merge branch 'main' of https://github.com/Dimillian/IceCubesApp 2024-05-13 22:20:45 +02:00
Thomas Ricouard c77bb992b4 Update OpenAI models to gpt-4o 2024-05-13 22:20:43 +02:00
Paul Schuetz 7caf00d07d
Resolve escaped characters in a status (#2071)
* Resolve escaped characters in a status

Escaped characters are now returned to their original form for
HTMLString.asRawText.

* Unescape the markdown version too

The HTMLString.asMarkdown string is now also unescaped, & and
similar are resolved.

* Fix a internal fallback

If one of the unescape(...) commands fails, the original, unescaped
text is used instead of an empty string.
2024-05-13 21:32:38 +02:00
Thomas Ricouard 6ed760a775 Bump version to 1.10.40 2024-05-13 20:20:24 +02:00
Thomas Ricouard ecd149b3d2 Fix a crash in quote post editor on macOS 2024-05-13 19:26:48 +02:00
Xabi 9aaf0b2350
Update EU localisation (#2062)
Round 2
2024-05-13 13:28:24 +02:00
Cthulhux 2d6cce6b01
de: translated one more string (#2063) 2024-05-13 13:27:58 +02:00
Paul Schuetz 48faddebea
Implement Apple Translate (#2065)
* Implement a first version of Apple's Translation

The user can now choose between his instance's server, DeepL (with API
key) and Apple's Translation framework. A translation is cleared if
the translation type is changed. The strings aren't yet written, but
the translations settings view's inconsistent background is now fixed.

* Transfer the old "always_use_deepl" setting

The "always_use_deepl"-setting is now deleted, but its content is
transferred to the equivalent value in "preferred_translation_type".

* Show the user if the DeepL-API key is still stored

The user is now shown a prompt if they've switched away from
.useDeepl, but there's still an API key stored. The API key is not
deleted if the user doesn't instruct the app to do so, so this change
makes it more transparent, since a user might not expect the key to
be stored and might not want this to be the case.

* Localize Labels

The labels for the buttons and options are now localized. "DeepL API Key" is written consistently (with uppercase Key)

* Run all the strings through localization

The strings "DeepL" and "Apple Translate" are now also saved in
localizable.strings and addressed through keys. They were taken
directly previously, which was inconsistent.

* Fix storage

The selected value for preferredTranslationType wasn't stored, the
synchronization between UserPreferences and Storage is now in place.

* Hide Apple Translate if not yet on iOS 17.4

The Apple Translate option is hidden if the user hasn't updated their
phone to at least iOS 17.4. If the Apple Translate option is selected
but the user has downgraded to before iOS 17.4, the standard instance
option is selected.

* Consistently show Apple Translate

Apple Translate was previously only shown if the standard translate
button was visible, that is now fixed. It's now attached to the
StatusRowView, which is always present.

* Animate the removal of translations

The reset of a translation when the translation type is changed is now
animated, which is important for iPad users if they've translated a
post in the sidebar.

* Add support for the Mac Catalyst build

The Mac Catalyst Version doesn't allow the import of the api, so
compiler flags now check if the import isn't allowed and then remove
all references to Apple Translate.

* Swift Format

* Revert "Run all the strings through localization"

This reverts commit 86c5099662.

# Conflicts:
#	Packages/Env/Sources/Env/TranslationType.swift

* Remove the DeepL fallback

The DeepL fallback for the instance translation service is removed,
error messages are shown if a translation fails.

* Allow for the use of an User API Key as fallback

The DeepL fallback is reinstated if the user has put in their own API
Key

* Make the localization keys clear strings

* Make Apple and the instance a fallback

Apple Translate is now a fallback for both other translation types,
the instance service is a fallback for DeepL.
2024-05-13 13:27:21 +02:00
Thomas Ricouard a8039df22d Don't open link on secondary column 2024-05-13 09:27:24 +02:00
Thomas Ricouard e21ec0bd1f Add expanded sidebar layout 2024-05-08 11:51:28 +02:00
Thomas Ricouard 9c42a3d7cc Add copy button for alt text 2024-05-08 11:03:25 +02:00
Thomas Ricouard 54a16b2c9a Fix unboost icon 2024-05-08 11:00:40 +02:00
Thomas Ricouard a6f3068728 Add accounts list placeholder 2024-05-08 10:59:31 +02:00
Thomas Ricouard f04258ec04 Revert "Delete unused functions in TimelineDatasource.swift (#2037)"
This reverts commit e9a2d3e151.
2024-05-08 10:50:22 +02:00
Cthulhux 8468e51c17
de: Update Localizable.xcstrings (#2057)
(Not entirely sure whether to translate "TimelineFilter" et al.)
2024-05-08 10:39:09 +02:00
Noah Martin e9a2d3e151
Delete unused functions in TimelineDatasource.swift (#2037) 2024-05-08 10:38:36 +02:00
Igor Camilo 1f56fa1b9b
Add tooltip to sidebar buttons. (#2040) 2024-05-08 10:38:27 +02:00
Jerry Zhang ccad00a094
Update Simplified Chinese localization (#2052)
* Update Simplified Chinese localization

* Fix

---------

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
2024-05-08 10:32:26 +02:00
Thomas Ricouard 51fecb01f5 Bump version to 1.10.39 2024-05-06 09:25:36 +02:00
Thomas Ricouard c29de44d8c Widget: Mentions only allow large size 2024-05-06 09:20:01 +02:00
Thomas Ricouard 1d79832544 Bump version to 1.10.38 2024-05-06 08:41:33 +02:00
44 changed files with 2488 additions and 393 deletions

View file

@ -57,6 +57,9 @@
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4A48182976B21900A1A038 /* ProfileTab.swift */; };
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; };
9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; };
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6252BF48FE10074387E /* ListEntity.swift */; };
9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */; };
9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5BE6212BF48FBA0074387E /* ListsWidget.swift */; };
9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; };
9F6028562B3F36AE00476078 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028552B3F36AE00476078 /* AppView.swift */; };
9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6028572B3F3B7600476078 /* ToolbarTab.swift */; };
@ -86,8 +89,10 @@
9F7788F02BE78E77004E6BEF /* Timeline in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7788EF2BE78E77004E6BEF /* Timeline */; };
9F7D93942980063100EE6B7A /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9F7D93932980063100EE6B7A /* AppAccount */; };
9F7D939A29805DBD00EE6B7A /* AccountSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */; };
9F8B92122BF77DBE003D37A2 /* AccountWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */; };
9F8B92132BF77DBE003D37A2 /* AccountWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */; };
9F8B92162BF77F0B003D37A2 /* AccountWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */; };
9FA6FD6229C04A8800E2312C /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */; };
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9FAD85822971BF7200496AB1 /* Secret.plist */; };
9FAD858B29743F7400496AB1 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAD858A29743F7400496AB1 /* ShareViewController.swift */; };
9FAD858E29743F7400496AB1 /* (null) in Resources */ = {isa = PBXBuildFile; };
9FAD859229743F7400496AB1 /* IceCubesShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */; platformFilters = (ios, maccatalyst, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -246,6 +251,9 @@
9F4A48182976B21900A1A038 /* ProfileTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTab.swift; sourceTree = "<group>"; };
9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = "<group>"; };
9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = "<group>"; };
9F5BE6212BF48FBA0074387E /* ListsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidget.swift; sourceTree = "<group>"; };
9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsWidgetConfiguration.swift; sourceTree = "<group>"; };
9F5BE6252BF48FE10074387E /* ListEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEntity.swift; sourceTree = "<group>"; };
9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; };
9F6028552B3F36AE00476078 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
9F6028572B3F3B7600476078 /* ToolbarTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarTab.swift; sourceTree = "<group>"; };
@ -270,8 +278,10 @@
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilterEntity.swift; sourceTree = "<group>"; };
9F7D939529800B0300EE6B7A /* IceCubesApp-release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "IceCubesApp-release.xcconfig"; sourceTree = "<group>"; };
9F7D939929805DBD00EE6B7A /* AccountSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSettingView.swift; sourceTree = "<group>"; };
9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidgetConfiguration.swift; sourceTree = "<group>"; };
9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidget.swift; sourceTree = "<group>"; };
9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountWidgetView.swift; sourceTree = "<group>"; };
9FA6FD6129C04A8800E2312C /* TranslationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
9FAD85822971BF7200496AB1 /* Secret.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secret.plist; sourceTree = "<group>"; };
9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCubesShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9FAD858A29743F7400496AB1 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
9FAD858F29743F7400496AB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -435,6 +445,7 @@
9F37BDDA2BE36E22007F28AD /* PostIntent.swift */,
9F37BDDE2BE37C35007F28AD /* TabIntent.swift */,
9F7788EC2BE78D75004E6BEF /* TimelineFilterEntity.swift */,
9F5BE6252BF48FE10074387E /* ListEntity.swift */,
);
path = IceCubesAppIntents;
sourceTree = "<group>";
@ -463,6 +474,15 @@
path = Resources;
sourceTree = "<group>";
};
9F5BE6202BF48FB20074387E /* ListsWidget */ = {
isa = PBXGroup;
children = (
9F5BE6212BF48FBA0074387E /* ListsWidget.swift */,
9F5BE6232BF48FC40074387E /* ListsWidgetConfiguration.swift */,
);
path = ListsWidget;
sourceTree = "<group>";
};
9F654BF0299AC46200D27FA5 /* Report */ = {
isa = PBXGroup;
children = (
@ -483,6 +503,8 @@
9F7788CA2BE652B1004E6BEF /* IceCubesAppWidgetsExtension */ = {
isa = PBXGroup;
children = (
9F8B920D2BF77DA8003D37A2 /* AccountWidget */,
9F5BE6202BF48FB20074387E /* ListsWidget */,
9FF2FB6B2BE8AE78001560CE /* MentionWidget */,
9FF2FB642BE7F7FA001560CE /* Shared */,
9FF2FB5D2BE7F559001560CE /* HashtagPostsWidget */,
@ -495,6 +517,16 @@
path = IceCubesAppWidgetsExtension;
sourceTree = "<group>";
};
9F8B920D2BF77DA8003D37A2 /* AccountWidget */ = {
isa = PBXGroup;
children = (
9F8B920E2BF77DB4003D37A2 /* AccountWidgetConfiguration.swift */,
9F8B92102BF77DBB003D37A2 /* AccountWidget.swift */,
9F8B92142BF77F05003D37A2 /* AccountWidgetView.swift */,
);
path = AccountWidget;
sourceTree = "<group>";
};
9FA0D2AC29921C1F008A143B /* Embeds */ = {
isa = PBXGroup;
children = (
@ -602,7 +634,6 @@
9FAE4AC8293774FF00772766 /* Info.plist */,
9F398AB429360A5800A889F2 /* App */,
9F398AB529360A6100A889F2 /* Resources */,
9FAD85822971BF7200496AB1 /* Secret.plist */,
);
path = IceCubesApp;
sourceTree = "<group>";
@ -960,7 +991,6 @@
9F18801829AE477F00D85459 /* favorite.wav in Resources */,
9F24EEB829360C330042359D /* Preview Assets.xcassets in Resources */,
069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */,
9FAD85832971BF7200496AB1 /* Secret.plist in Resources */,
9F18801229AE477F00D85459 /* tabSelection.wav in Resources */,
9F18801429AE477F00D85459 /* bookmark.wav in Resources */,
9F18801629AE477F00D85459 /* refresh.wav in Resources */,
@ -995,17 +1025,23 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9F5BE6272BF492CF0074387E /* ListEntity.swift in Sources */,
9FF2FB622BE7F5D5001560CE /* HashtagPostsWidget.swift in Sources */,
9FF2FB712BE8AEA0001560CE /* MentionWidget.swift in Sources */,
9F8B92162BF77F0B003D37A2 /* AccountWidgetView.swift in Sources */,
9F8B92122BF77DBE003D37A2 /* AccountWidgetConfiguration.swift in Sources */,
9F8B92132BF77DBE003D37A2 /* AccountWidget.swift in Sources */,
9F7788EA2BE65585004E6BEF /* AppAccountEntity.swift in Sources */,
9FF2FB6A2BE7F84E001560CE /* SharedUtils.swift in Sources */,
9F7788CE2BE652B1004E6BEF /* LatestPostsWidget.swift in Sources */,
9F7788EE2BE78D7B004E6BEF /* TimelineFilterEntity.swift in Sources */,
9F5BE6292BF492D40074387E /* ListsWidget.swift in Sources */,
9F7788CC2BE652B1004E6BEF /* IceCubesAppWidgetsExtensionBundle.swift in Sources */,
9FF2FB702BE8AE9D001560CE /* MentionWidgetConfiguration.swift in Sources */,
9F7788D02BE652B1004E6BEF /* LatestPostsWidgetConfiguration.swift in Sources */,
9FF2FB672BE7F816001560CE /* PostsWidgetView.swift in Sources */,
9FF2FB632BE7F5D9001560CE /* HashtagPostsWidgetConfiguration.swift in Sources */,
9F5BE6282BF492D10074387E /* ListsWidgetConfiguration.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1157,7 +1193,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1192,7 +1228,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1300,7 +1336,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1334,7 +1370,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1482,6 +1518,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO;
CODE_SIGN_ENTITLEMENTS = IceCubesApp/App/IceCubesApp.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
@ -1515,7 +1552,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto;
@ -1537,6 +1574,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO;
CODE_SIGN_ENTITLEMENTS = "IceCubesApp/App/IceCubesApp-release.entitlements";
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
@ -1570,7 +1608,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp";
PRODUCT_NAME = "Ice Cubes";
SDKROOT = auto;
@ -1605,7 +1643,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@ -1640,7 +1678,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.10.37;
MARKETING_VERSION = 1.10.42;
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;

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

View file

@ -100,6 +100,7 @@ extension IceCubesApp {
}
}
.withEnvironments()
.environment(RouterPath())
.withModelContainer()
.applyTheme(theme)
.frame(minWidth: 300, minHeight: 400)

View file

@ -13,6 +13,7 @@ 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
@ -25,10 +26,12 @@ private struct SafariRouter: ViewModifier {
content
.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://)
guard !isSecondaryColumn else { return }
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
guard let url = URL(string: urlString), url.host != nil else { return }
_ = routerPath.handleDeepLink(url: url)

View file

@ -35,15 +35,23 @@ struct SideBarView<Content: View>: View {
}
private func makeIconForTab(tab: Tab) -> some View {
ZStack(alignment: .topTrailing) {
SideBarIcon(systemIconName: tab.iconName,
isSelected: tab == selectedTab)
let badge = badgeFor(tab: tab)
if badge > 0 {
makeBadgeView(count: badge)
HStack {
ZStack(alignment: .topTrailing) {
SideBarIcon(systemIconName: tab.iconName,
isSelected: tab == selectedTab)
let badge = badgeFor(tab: tab)
if badge > 0 {
makeBadgeView(count: badge)
}
}
if userPreferences.isSidebarExpanded {
Text(tab.title)
.font(.headline)
.foregroundColor(tab == selectedTab ? theme.tintColor : theme.labelColor)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.frame(width: .sidebarWidth - 24, height: 50)
.frame(width: (userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth) - 24, height: 50)
.background(tab == selectedTab ? theme.secondaryBackgroundColor : .clear,
in: RoundedRectangle(cornerRadius: 8))
}
@ -75,6 +83,7 @@ struct SideBarView<Content: View>: View {
.offset(x: 2, y: -2)
}
.buttonStyle(.borderedProminent)
.help(Tab.post.title)
}
private func makeAccountButton(account: AppAccount, showBadge: Bool) -> some View {
@ -91,9 +100,19 @@ struct SideBarView<Content: View>: View {
}
} label: {
ZStack(alignment: .topTrailing) {
AppAccountView(viewModel: .init(appAccount: account, isCompact: true),
isParentPresented: .constant(false))
if showBadge,
if userPreferences.isSidebarExpanded {
AppAccountView(viewModel: .init(appAccount: account,
isCompact: false,
isInSettings: false),
isParentPresented: .constant(false))
} else {
AppAccountView(viewModel: .init(appAccount: account,
isCompact: true,
isInSettings: false),
isParentPresented: .constant(false))
}
if !userPreferences.isSidebarExpanded,
showBadge,
let token = account.oauthToken,
let notificationsCount = userPreferences.notificationsCount[token],
notificationsCount > 0
@ -101,13 +120,23 @@ 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)
}
private func accountButtonTitle(accountName: String?) -> LocalizedStringKey {
if let accountName {
"tab.profile-account-\(accountName)"
} else {
Tab.profile.title
}
}
private var tabsView: some View {
ForEach(tabs) { tab in
if tab != .profile && sidebarTabs.isEnabled(tab) {
@ -132,6 +161,7 @@ struct SideBarView<Content: View>: View {
} label: {
makeIconForTab(tab: tab)
}
.help(tab.title)
}
}
}
@ -155,15 +185,22 @@ struct SideBarView<Content: View>: View {
}
}
}
.frame(width: .sidebarWidth)
.frame(width: userPreferences.isSidebarExpanded ? .sidebarWidthExpanded : .sidebarWidth)
.scrollContentBackground(.hidden)
.background(.thinMaterial)
.safeAreaInset(edge: .bottom, content: {
HStack {
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 +233,7 @@ private struct SideBarIcon: View {
self.isHovered = isHovered
}
}
.frame(width: 50, height: 40)
}
}

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)

View file

@ -31,7 +31,9 @@ struct SettingsTabs: View {
@Binding var popToRootTab: Tab
let isModal: Bool
@State private var startingPoint: SettingsStartingPoint? = nil
var body: some View {
NavigationStack(path: $routerPath.path) {
Form {
@ -64,6 +66,32 @@ struct SettingsTabs: View {
}
.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

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,34 @@ 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 +137,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

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

View file

@ -14,6 +14,21 @@ struct ToolbarTab: ToolbarContent {
var body: some ToolbarContent {
if !isSecondaryColumn {
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 ||

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>DEEPL_SECRET</key>
<string>NICE_TRY_AGAIN</string>
</dict>
</plist>

View file

@ -0,0 +1,55 @@
import AppAccount
import AppIntents
import Env
import Foundation
import Models
import Network
import Timeline
public struct ListEntity: Identifiable, AppEntity {
public var id: String { list.id }
public let list: Models.List
public static let defaultQuery = DefaultListEntityQuery()
public static let typeDisplayRepresentation: TypeDisplayRepresentation = "List"
public var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(list.title)")
}
}
public struct DefaultListEntityQuery: EntityQuery {
public init() {}
@IntentParameterDependency<ListsWidgetConfiguration>(
\.$account
)
var account
public func entities(for _: [ListEntity.ID]) async throws -> [ListEntity] {
await fetchLists().map{ .init(list: $0 )}
}
public func suggestedEntities() async throws -> [ListEntity] {
await fetchLists().map{ .init(list: $0 )}
}
public func defaultResult() async -> ListEntity? {
nil
}
private func fetchLists() async -> [Models.List] {
guard let account = account?.account.account else {
return []
}
let client = Client(server: account.server, oauthToken: account.oauthToken)
do {
let lists: [Models.List] = try await client.get(endpoint: Lists.lists)
return lists
} catch {
return []
}
}
}

View file

@ -0,0 +1,64 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct AccountWidgetProvider: AppIntentTimelineProvider {
func placeholder(in _: Context) -> AccountWidgetEntry {
.init(date: Date(), account: .placeholder(), avatar: nil)
}
func snapshot(for configuration: AccountWidgetConfiguration, in context: Context) async -> AccountWidgetEntry {
let account = await fetchAccount(configuration: configuration)
return .init(date: Date(), account: account, avatar: nil)
}
func timeline(for configuration: AccountWidgetConfiguration, in context: Context) async -> Timeline<AccountWidgetEntry> {
let account = await fetchAccount(configuration: configuration)
let images = try? await loadImages(urls: [account.avatar])
return .init(entries: [.init(date: Date(), account: account, avatar: images?.first?.value)],
policy: .atEnd)
}
private func fetchAccount(configuration: AccountWidgetConfiguration) async -> Account {
let client = Client(server: configuration.account.account.server,
oauthToken: configuration.account.account.oauthToken)
do {
let account: Account = try await client.get(endpoint: Accounts.verifyCredentials)
return account
} catch {
return .placeholder()
}
}
}
struct AccountWidgetEntry: TimelineEntry {
let date: Date
let account: Account
let avatar: UIImage?
}
struct AccountWidget: Widget {
let kind: String = "AccountWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
intent: AccountWidgetConfiguration.self,
provider: AccountWidgetProvider())
{ entry in
AccountWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
}
.configurationDisplayName("Account")
.description("Show information about your Mastodon account")
.supportedFamilies([.systemSmall])
}
}
#Preview(as: .systemSmall) {
AccountWidget()
} timeline: {
AccountWidgetEntry(date: Date(), account: .placeholder(), avatar: nil)
}

View file

@ -0,0 +1,18 @@
import AppIntents
import WidgetKit
struct AccountWidgetConfiguration: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Configuration"
static let description = IntentDescription("Choose the account for this widget")
@Parameter(title: "Account")
var account: AppAccountEntity
}
extension AccountWidgetConfiguration {
static var previewAccount: AccountWidgetConfiguration {
let intent = AccountWidgetConfiguration()
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
return intent
}
}

View file

@ -0,0 +1,34 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct AccountWidgetView: View {
var entry: AccountWidgetProvider.Entry
@Environment(\.widgetFamily) var family
@Environment(\.redactionReasons) var redacted
var body: some View {
VStack(alignment: .center, spacing: 4) {
if let avatar = entry.avatar {
Image(uiImage: avatar)
.resizable()
.frame(width: 64, height: 64)
.clipShape(Circle())
Text("\(entry.account.followersCount ?? 0)")
.font(.title)
.fontDesign(.rounded)
.fontWeight(.bold)
.monospacedDigit()
Text("Followers")
.font(.headline)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity)
}
}

View file

@ -6,6 +6,8 @@ struct IceCubesAppWidgetsExtensionBundle: WidgetBundle {
var body: some Widget {
LatestPostsWidget()
HashtagPostsWidget()
ListsPostWidget()
MentionsWidget()
AccountWidget()
}
}

View file

@ -0,0 +1,75 @@
import DesignSystem
import Models
import Network
import SwiftUI
import Timeline
import WidgetKit
struct ListsWidgetProvider: AppIntentTimelineProvider {
func placeholder(in _: Context) -> PostsWidgetEntry {
.init(date: Date(),
title: "List name",
statuses: [.placeholder()],
images: [:])
}
func snapshot(for configuration: ListsWidgetConfiguration, in context: Context) async -> PostsWidgetEntry {
if let entry = await timeline(for: configuration, context: context).entries.first {
return entry
}
return .init(date: Date(),
title: "List name",
statuses: [],
images: [:])
}
func timeline(for configuration: ListsWidgetConfiguration, in context: Context) async -> Timeline<PostsWidgetEntry> {
await timeline(for: configuration, context: context)
}
private func timeline(for configuration: ListsWidgetConfiguration, context: Context) async -> Timeline<PostsWidgetEntry> {
do {
let timeline: TimelineFilter = .list(list: configuration.timeline.list)
let statuses = await loadStatuses(for: timeline,
account: configuration.account,
widgetFamily: context.family)
let images = try await loadImages(urls: statuses.map { $0.account.avatar })
return Timeline(entries: [.init(date: Date(),
title: timeline.title,
statuses: statuses,
images: images)], policy: .atEnd)
} catch {
return Timeline(entries: [.init(date: Date(),
title: "List name",
statuses: [],
images: [:])],
policy: .atEnd)
}
}
}
struct ListsPostWidget: Widget {
let kind: String = "ListsPostWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind,
intent: ListsWidgetConfiguration.self,
provider: ListsWidgetProvider())
{ entry in
PostsWidgetView(entry: entry)
.containerBackground(Color("WidgetBackground").gradient, for: .widget)
}
.configurationDisplayName("List timeline")
.description("Show the latest post for the selected list")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}
#Preview(as: .systemMedium) {
ListsPostWidget()
} timeline: {
PostsWidgetEntry(date: .now,
title: "List name",
statuses: [.placeholder(), .placeholder(), .placeholder(), .placeholder()],
images: [:])
}

View file

@ -0,0 +1,21 @@
import AppIntents
import WidgetKit
struct ListsWidgetConfiguration: WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Configuration"
static let description = IntentDescription("Choose the account and list for this widget")
@Parameter(title: "Account")
var account: AppAccountEntity
@Parameter(title: "List")
var timeline: ListEntity
}
extension ListsWidgetConfiguration {
static var previewAccount: LatestPostsWidgetConfiguration {
let intent = LatestPostsWidgetConfiguration()
intent.account = .init(account: .init(server: "Test", accountName: "Test account"))
return intent
}
}

View file

@ -33,7 +33,7 @@ struct MentionsWidgetProvider: AppIntentTimelineProvider {
oauthToken: configuration.account.account.oauthToken)
var excludedTypes = Models.Notification.NotificationType.allCases
excludedTypes.removeAll(where: { $0 == .mention })
var notifications: [Models.Notification] =
let notifications: [Models.Notification] =
try await client.get(endpoint: Notifications.notifications(minId: nil,
maxId: nil,
types: excludedTypes.map(\.rawValue),
@ -67,7 +67,7 @@ struct MentionsWidget: Widget {
}
.configurationDisplayName("Mentions")
.description("Show the latest mentions for the selected account.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
.supportedFamilies([.systemLarge, .systemExtraLarge])
}
}

View file

@ -9,6 +9,7 @@ public struct AccountDetailContextMenu: View {
@Environment(UserPreferences.self) private var preferences
@Binding var showBlockConfirmation: Bool
@Binding var showTranslateView: Bool
var viewModel: AccountDetailViewModel
@ -136,15 +137,15 @@ public struct AccountDetailContextMenu: View {
#endif
}
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier {
Button {
Task {
await viewModel.translate(userLang: lang)
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) {
Button {
showTranslateView = true
} label: {
Label("status.action.translate", systemImage: "captions.bubble")
}
}
} label: {
Label("status.action.translate", systemImage: "captions.bubble")
}
}
#endif
if viewModel.relationship?.following == true {
Button {

View file

@ -24,6 +24,7 @@ public struct AccountDetailView: View {
@State private var isCurrentUser: Bool = false
@State private var showBlockConfirmation: Bool = false
@State private var isEditingRelationshipNote: Bool = false
@State private var showTranslateView: Bool = false
@State private var displayTitle: Bool = false
@ -285,7 +286,9 @@ public struct AccountDetailView: View {
}
Menu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: viewModel)
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView,
viewModel: viewModel)
if !viewModel.isCurrentUser {
Button {
@ -380,6 +383,9 @@ public struct AccountDetailView: View {
} message: {
Text("account.action.block-user-confirmation")
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account?.note.asRawText ?? "")
#endif
}
}
}

View file

@ -273,22 +273,4 @@ import SwiftUI
func statusDidAppear(status _: Models.Status) {}
func statusDidDisappear(status _: Status) {}
func translate(userLang: String) async {
guard let account else { return }
withAnimation {
isLoadingTranslation = true
}
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
let translation = try? await deeplClient.request(target: userLang, text: account.note.asRawText)
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
}
}

View file

@ -32,6 +32,7 @@ public struct AccountsListRow: View {
@State private var isEditingRelationshipNote: Bool = false
@State private var showBlockConfirmation: Bool = false
@State private var showTranslateView: Bool = false
let isFollowRequest: Bool
let requestUpdated: (() -> Void)?
@ -108,8 +109,13 @@ public struct AccountsListRow: View {
.onTapGesture {
routerPath.navigate(to: .accountDetailWithAccount(account: viewModel.account))
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: viewModel.account.note.asRawText)
#endif
.contextMenu {
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation, viewModel: .init(account: viewModel.account))
AccountDetailContextMenu(showBlockConfirmation: $showBlockConfirmation,
showTranslateView: $showTranslateView,
viewModel: .init(account: viewModel.account))
} preview: {
List {
AccountDetailHeaderView(viewModel: .init(account: viewModel.account),

View file

@ -124,16 +124,23 @@ public struct AccountsListView: View {
}
}
Section {
ForEach(accounts) { account in
if let relationship = relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account,
relationShip: relationship))
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
if accounts.isEmpty {
PlaceholderView(iconName: "person.icloud",
title: "No accounts found",
message: "This list of accounts is empty")
.listRowSeparator(.hidden)
} else {
ForEach(accounts) { account in
if let relationship = relationships.first(where: { $0.id == account.id }) {
AccountsListRow(viewModel: .init(account: account,
relationShip: relationship))
}
}
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
switch nextPageState {
case .hasNextPage:

View file

@ -50,7 +50,7 @@ public struct AppAccountsSelectorView: View {
}
.sheet(isPresented: $isPresented, content: {
accountsView.presentationDetents([.height(preferredHeight), .large])
.presentationBackground(.thinMaterial)
.presentationBackground(.ultraThinMaterial)
.presentationCornerRadius(16)
.onAppear {
refreshAccounts()
@ -101,7 +101,7 @@ public struct AppAccountsSelectorView: View {
#endif
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor.opacity(0.4))
#endif
if accountCreationEnabled {
@ -113,7 +113,7 @@ public struct AppAccountsSelectorView: View {
#if os(visionOS)
.foregroundStyle(theme.labelColor)
#else
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor.opacity(0.4))
#endif
}
}

View file

@ -9,5 +9,6 @@ public extension CGFloat {
static let statusComponentSpacing: CGFloat = 6
static let secondaryColumnWidth: CGFloat = 400
static let sidebarWidth: CGFloat = 90
static let sidebarWidthExpanded: CGFloat = 220
static let pollBarHeight: CGFloat = 30
}

View file

@ -23,22 +23,30 @@ public enum DeepLUserAPIHandler {
}
}
public static func readIfAllowed() -> String? {
guard UserPreferences.shared.alwaysUseDeepl else { return nil }
public static func readKeyIfAllowed() -> String? {
guard UserPreferences.shared.preferredTranslationType == .useDeepl else { return nil }
return readValue()
return readKeyInternal()
}
private static func readValue() -> String? {
public static func readKey() -> String {
return readKeyInternal() ?? ""
}
private static func readKeyInternal() -> String? {
keychain.synchronizable = true
return keychain.get(key)
}
public static func deactivateToggleIfNoKey() {
UserPreferences.shared.alwaysUseDeepl = shouldAlwaysUseDeepl
if UserPreferences.shared.preferredTranslationType == .useDeepl {
if readKeyInternal() == nil {
UserPreferences.shared.preferredTranslationType = .useServerIfPossible
}
}
}
public static var shouldAlwaysUseDeepl: Bool {
readIfAllowed() != nil
readKeyIfAllowed() != nil
}
}

View file

@ -0,0 +1,15 @@
import SwiftUI
#if canImport(_Translation_SwiftUI)
import Translation
extension View {
public func addTranslateView(isPresented: Binding<Bool>, text: String) -> some View {
if #available(iOS 17.4, *) {
return self.translationPresentation(isPresented: isPresented, text: text)
} else {
return self
}
}
}
#endif

View file

@ -115,6 +115,18 @@ public enum SheetDestination: Identifiable, Hashable {
}
}
public enum SettingsStartingPoint {
case display
case haptic
case remoteTimelines
case tagGroups
case recentTags
case content
case swipeActions
case tabAndSidebarEntries
case translation
}
@MainActor
@Observable public class RouterPath {
public var client: Client?
@ -123,6 +135,8 @@ public enum SheetDestination: Identifiable, Hashable {
public var path: [RouterDestination] = []
public var presentedSheet: SheetDestination?
public static var settingsStartingPoint: SettingsStartingPoint? = nil
public init() {}
public func navigate(to: RouterDestination) {

View file

@ -0,0 +1,18 @@
import SwiftUI
public enum TranslationType: String, CaseIterable {
case useServerIfPossible
case useDeepl
case useApple
public var description: LocalizedStringKey {
switch self {
case .useServerIfPossible:
"Instance"
case .useDeepl:
"DeepL"
case .useApple:
"Apple Translate"
}
}
}

View file

@ -25,7 +25,7 @@ import SwiftUI
@AppStorage("app_require_alt_text") public var appRequireAltText = false
@AppStorage("autoplay_video") public var autoPlayVideo = true
@AppStorage("mute_video") public var muteVideo = true
@AppStorage("always_use_deepl") public var alwaysUseDeepl = false
@AppStorage("preferred_translation_type") public var preferredTranslationType = TranslationType.useServerIfPossible
@AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
@ -61,7 +61,32 @@ import SwiftUI
@AppStorage("show_account_popover") public var showAccountPopover: Bool = true
init() {}
@AppStorage("sidebar_expanded") public var isSidebarExpanded: Bool = false
init() {
prepareTranslationType()
}
private func prepareTranslationType() {
let sharedDefault = UserDefaults.standard
if let alwaysUseDeepl = (sharedDefault.object(forKey: "always_use_deepl") as? Bool) {
if alwaysUseDeepl {
preferredTranslationType = .useDeepl
}
sharedDefault.removeObject(forKey: "always_use_deepl")
}
#if canImport(_Translation_SwiftUI)
if #unavailable(iOS 17.4),
preferredTranslationType == .useApple
{
preferredTranslationType = .useServerIfPossible
}
#else
if preferredTranslationType == .useApple {
preferredTranslationType = .useServerIfPossible
}
#endif
}
}
public static let sharedDefault = UserDefaults(suiteName: "group.com.thomasricouard.IceCubesApp")
@ -183,9 +208,9 @@ import SwiftUI
}
}
public var alwaysUseDeepl: Bool {
public var preferredTranslationType: TranslationType {
didSet {
storage.alwaysUseDeepl = alwaysUseDeepl
storage.preferredTranslationType = preferredTranslationType
}
}
@ -327,6 +352,12 @@ import SwiftUI
}
}
public var isSidebarExpanded: Bool {
didSet {
storage.isSidebarExpanded = isSidebarExpanded
}
}
public func getRealMaxIndent() -> UInt {
showReplyIndentation ? maxReplyIndentation : 0
}
@ -474,7 +505,7 @@ import SwiftUI
appDefaultPostsSensitive = storage.appDefaultPostsSensitive
appRequireAltText = storage.appRequireAltText
autoPlayVideo = storage.autoPlayVideo
alwaysUseDeepl = storage.alwaysUseDeepl
preferredTranslationType = storage.preferredTranslationType
userDeeplAPIFree = storage.userDeeplAPIFree
autoDetectPostLanguage = storage.autoDetectPostLanguage
inAppBrowserReaderView = storage.inAppBrowserReaderView
@ -501,6 +532,7 @@ import SwiftUI
showReplyIndentation = storage.showReplyIndentation
showAccountPopover = storage.showAccountPopover
muteVideo = storage.muteVideo
isSidebarExpanded = storage.isSidebarExpanded
}
}

View file

@ -61,7 +61,7 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
_ = text.removeLast()
_ = text.removeLast()
}
asRawText = text
asRawText = (try? Entities.unescape(text)) ?? text
if asMarkdown.hasPrefix("\n") {
_ = asMarkdown.removeFirst()
@ -175,7 +175,9 @@ public struct HTMLString: Codable, Equatable, Hashable, @unchecked Sendable {
return
} else if node.nodeName() == "#text" {
var txt = node.description
txt = (try? Entities.unescape(txt)) ?? txt
if let underscore_regex, let main_regex {
// This is the markdown escaper
txt = main_regex.stringByReplacingMatches(in: txt, options: [], range: NSRange(location: 0, length: txt.count), withTemplate: "\\\\$1")

View file

@ -12,20 +12,8 @@ public struct DeepLClient: Sendable {
"https://api\(deeplUserAPIFree && (deeplUserAPIKey != nil) ? "-free" : "").deepl.com/v2/translate"
}
private var APIKey: String {
if let deeplUserAPIKey {
return deeplUserAPIKey
}
if let path = Bundle.main.path(forResource: "Secret", ofType: "plist") {
let secret = NSDictionary(contentsOfFile: path)
return secret?["DEEPL_SECRET"] as? String ?? ""
}
return ""
}
private var authorizationHeaderValue: String {
"DeepL-Auth-Key \(APIKey)"
"DeepL-Auth-Key \(deeplUserAPIKey ?? "")"
}
public struct Response: Decodable {
@ -49,26 +37,22 @@ public struct DeepLClient: Sendable {
}
public func request(target: String, text: String) async throws -> Translation {
do {
var components = URLComponents(string: endpoint)!
var queryItems: [URLQueryItem] = []
queryItems.append(.init(name: "text", value: text))
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let (result, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: result)
if let translation = response.translations.first {
return .init(content: translation.text.removingPercentEncoding ?? "",
detectedSourceLanguage: translation.detectedSourceLanguage,
provider: "DeepL.com")
}
throw DeepLError.notFound
} catch {
throw error
var components = URLComponents(string: endpoint)!
var queryItems: [URLQueryItem] = []
queryItems.append(.init(name: "text", value: text))
queryItems.append(.init(name: "target_lang", value: target.uppercased()))
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let (result, _) = try await URLSession.shared.data(for: request)
let response = try decoder.decode(Response.self, from: result)
if let translation = response.translations.first {
return .init(content: translation.text.removingPercentEncoding ?? "",
detectedSourceLanguage: translation.detectedSourceLanguage,
provider: "DeepL.com")
}
throw DeepLError.notFound
}
}

View file

@ -25,7 +25,7 @@ public struct OpenAIClient {
public let content: String
}
let model = "gpt-3.5-turbo"
let model = "gpt-4o"
let messages: [Message]
let temperature: CGFloat
@ -52,7 +52,7 @@ public struct OpenAIClient {
public let content: [MessageContent]
}
let model = "gpt-4-vision-preview"
let model = "gpt-4o"
let messages: [Message]
let maxTokens = 50
}

View file

@ -47,7 +47,7 @@ struct NotificationsPolicyView: View {
})
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor.opacity(0.3))
#endif
}
.formStyle(.grouped)

View file

@ -23,7 +23,7 @@ extension StatusEditor {
@State private var didAppear: Bool = false
@State private var isGeneratingDescription: Bool = false
@State private var showTranslateButton: Bool = false
@State private var showTranslateView: Bool = false
@State private var isTranslating: Bool = false
var body: some View {
@ -34,8 +34,14 @@ extension StatusEditor {
text: $imageDescription,
axis: .vertical)
.focused($isFieldFocused)
generateButton
translateButton
if imageDescription.isEmpty {
generateButton
}
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *), !imageDescription.isEmpty {
translateButton
}
#endif
}
.listRowBackground(theme.primaryBackgroundColor)
Section {
@ -111,12 +117,6 @@ extension StatusEditor {
Task {
if let description = await generateDescription(url: url) {
imageDescription = description
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
if lang != nil, lang != "en" {
withAnimation {
showTranslateButton = true
}
}
}
}
} label: {
@ -131,24 +131,18 @@ extension StatusEditor {
@ViewBuilder
private var translateButton: some View {
if showTranslateButton {
Button {
Task {
if let description = await translateDescription() {
imageDescription = description
withAnimation {
showTranslateButton = false
}
}
}
} label: {
if isTranslating {
ProgressView()
} else {
Text("status.action.translate")
}
Button {
showTranslateView = true
} label: {
if isTranslating {
ProgressView()
} else {
Text("status.action.translate")
}
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $showTranslateView, text: imageDescription)
#endif
}
private func generateDescription(url: URL) async -> String? {
@ -158,17 +152,5 @@ extension StatusEditor {
isGeneratingDescription = false
return response?.trimmedText
}
private func translateDescription() async -> String? {
isTranslating = true
let userAPIKey = DeepLUserAPIHandler.readIfAllowed()
let userAPIFree = UserPreferences.shared.userDeeplAPIFree
let deeplClient = DeepLClient(userAPIKey: userAPIKey, userAPIFree: userAPIFree)
let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier
guard let lang else { return nil }
let translation = try? await deeplClient.request(target: lang, text: imageDescription)
isTranslating = false
return translation?.content.asRawText
}
}
}

View file

@ -45,9 +45,7 @@ extension TextView.Representable {
textView.allowsEditingTextAttributes = false
textView.returnKeyType = .default
textView.allowsEditingTextAttributes = true
#if targetEnvironment(macCatalyst)
textView.inlinePredictionType = .no
#endif
textView.inlinePredictionType = .no
self.getTextView?(textView)
}

View file

@ -15,6 +15,7 @@ public struct StatusRowView: View {
@Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled
@Environment(\.isStatusFocused) private var isFocused
@Environment(\.indentationLevel) private var indentationLevel
@Environment(RouterPath.self) private var routerPath: RouterPath
@Environment(QuickLook.self) private var quickLook
@Environment(Theme.self) private var theme
@ -219,6 +220,23 @@ public struct StatusRowView: View {
StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus,
client: viewModel.client)
)
.alert("DeepL couldn't be reached!\nIs the API Key correct?", isPresented: $viewModel.deeplTranslationError) {
Button("alert.button.ok", role: .cancel) {}
Button("settings.general.translate") {
RouterPath.settingsStartingPoint = .translation
routerPath.presentedSheet = .settings
}
}
.alert("The Translation Service of your Instance couldn't be reached!", isPresented: $viewModel.instanceTranslationError) {
Button("alert.button.ok", role: .cancel) {}
Button("settings.general.translate") {
RouterPath.settingsStartingPoint = .translation
routerPath.presentedSheet = .settings
}
}
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $viewModel.showAppleTranslation, text: viewModel.finalStatus.content.asRawText)
#endif
}
@ViewBuilder

View file

@ -31,6 +31,18 @@ import SwiftUI
var translation: Translation?
var isLoadingTranslation: Bool = false
var showDeleteAlert: Bool = false
var showAppleTranslation = false
var preferredTranslationType = TranslationType.useServerIfPossible {
didSet {
if oldValue != preferredTranslationType {
translation = nil
showAppleTranslation = false
}
}
}
var deeplTranslationError = false
var instanceTranslationError = false
private(set) var actionsAccountsFetched: Bool = false
var favoriters: [Account] = []
@ -297,25 +309,43 @@ import SwiftUI
}
func translate(userLang: String) async {
withAnimation {
isLoadingTranslation = true
}
if !alwaysTranslateWithDeepl {
do {
// We first use instance translation API if available.
let translation: Translation = try await client.post(endpoint: Statuses.translate(id: finalStatus.id,
lang: userLang))
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
return
} catch {}
updatePreferredTranslation()
if preferredTranslationType == .useApple {
showAppleTranslation = true
return
}
// If not or fail we use Ice Cubes own DeepL client.
await translateWithDeepL(userLang: userLang)
if preferredTranslationType != .useDeepl {
await translateWithInstance(userLang: userLang)
if translation == nil {
await translateWithDeepL(userLang: userLang)
}
} else {
await translateWithDeepL(userLang: userLang)
if translation == nil {
await translateWithInstance(userLang: userLang)
}
}
var hasShown = false
#if canImport(_Translation_SwiftUI)
if translation == nil,
#available(iOS 17.4, *) {
showAppleTranslation = true
hasShown = true
}
#endif
if !hasShown,
translation == nil {
if preferredTranslationType == .useDeepl {
deeplTranslationError = true
} else {
instanceTranslationError = true
}
}
}
func translateWithDeepL(userLang: String) async {
@ -331,6 +361,20 @@ import SwiftUI
isLoadingTranslation = false
}
}
func translateWithInstance(userLang: String) async {
withAnimation {
isLoadingTranslation = true
}
let translation: Translation? = try? await client.post(endpoint: Statuses.translate(id: finalStatus.id,
lang: userLang))
withAnimation {
self.translation = translation
isLoadingTranslation = false
}
}
private func getDeepLClient() -> DeepLClient {
let userAPIfree = UserPreferences.shared.userDeeplAPIFree
@ -339,11 +383,17 @@ import SwiftUI
}
private var userAPIKey: String? {
DeepLUserAPIHandler.readIfAllowed()
DeepLUserAPIHandler.readKey()
}
var alwaysTranslateWithDeepl: Bool {
DeepLUserAPIHandler.shouldAlwaysUseDeepl
func updatePreferredTranslation() {
if DeepLUserAPIHandler.shouldAlwaysUseDeepl {
preferredTranslationType = .useDeepl
} else if UserPreferences.shared.preferredTranslationType == .useApple {
preferredTranslationType = .useApple
} else {
preferredTranslationType = .useServerIfPossible
}
}
func fetchRemoteStatus() async -> Bool {

View file

@ -31,7 +31,7 @@ struct StatusRowContextMenu: View {
}
if statusDataController.isReblogged {
return Label("status.action.unboost", image: "Rocket.fill")
return Label("status.action.unboost", image: "Rocket.Fill")
}
return Label("status.action.boost", image: "Rocket")
}

View file

@ -258,6 +258,7 @@ struct AltTextButton: View {
@Environment(Theme.self) private var theme
@State private var isDisplayingAlert = false
@State private var isDisplayingTranslation = false
var body: some View {
if !isInCaptureMode,
@ -278,6 +279,9 @@ struct AltTextButton: View {
.buttonStyle(.borderless)
.padding(EdgeInsets(top: 5, leading: 7, bottom: 5, trailing: 7))
.background(.thinMaterial)
#if canImport(_Translation_SwiftUI)
.addTranslateView(isPresented: $isDisplayingTranslation, text: text)
#endif
#if os(visionOS)
.clipShape(Capsule())
#endif
@ -288,6 +292,12 @@ struct AltTextButton: View {
isPresented: $isDisplayingAlert
) {
Button("alert.button.ok", action: {})
Button("status.action.copy-text", action: { UIPasteboard.general.string = text })
#if canImport(_Translation_SwiftUI)
if #available(iOS 17.4, *) {
Button("status.action.translate", action: { isDisplayingTranslation = true })
}
#endif
} message: {
Text(text)
}

View file

@ -35,7 +35,8 @@ struct StatusRowTranslateView: View {
}
}
var body: some View {
@ViewBuilder
var translateButton: some View {
if !isInCaptureMode,
!isCompact,
let userLang = preferences.serverPreferences?.postLanguage,
@ -54,8 +55,22 @@ struct StatusRowTranslateView: View {
}
.buttonStyle(.borderless)
}
}
if let translation = viewModel.translation, !viewModel.isLoadingTranslation {
@ViewBuilder
var generalTranslateButton: some View {
translateButton
}
var body: some View {
generalTranslateButton
.onChange(of: preferences.preferredTranslationType) { _, _ in
withAnimation {
_ = viewModel.updatePreferredTranslation()
}
}
if let translation = viewModel.translation, !viewModel.isLoadingTranslation, preferences.preferredTranslationType != .useApple {
GroupBox {
VStack(alignment: .leading, spacing: 4) {
Text(translation.content.asSafeMarkdownAttributedString)

View file

@ -30,7 +30,7 @@ public struct TimelineContentFilterView: View {
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor.opacity(0.3))
#endif
Section {
@ -46,7 +46,7 @@ public struct TimelineContentFilterView: View {
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
.listRowBackground(theme.primaryBackgroundColor.opacity(0.3))
#endif
}
.navigationTitle("timeline.content-filter.title")