mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-01-06 05:02:54 +00:00
Version 11.5.3
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
# push:
|
||||
# branches: [ master ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,9 @@
|
||||
submodules/**/.build/*
|
||||
swiftgram-scripts
|
||||
Swiftgram/Playground/custom_bazel_path.bzl
|
||||
Swiftgram/Playground/codesigning
|
||||
buildServer.json
|
||||
|
||||
fastlane/README.md
|
||||
fastlane/report.xml
|
||||
fastlane/test_output/*
|
||||
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -1,7 +1,6 @@
|
||||
|
||||
[submodule "submodules/rlottie/rlottie"]
|
||||
path = submodules/rlottie/rlottie
|
||||
url=../rlottie.git
|
||||
url=https://github.com/TelegramMessenger/rlottie.git
|
||||
[submodule "build-system/bazel-rules/rules_apple"]
|
||||
path = build-system/bazel-rules/rules_apple
|
||||
url=https://github.com/ali-fareed/rules_apple.git
|
||||
@@ -16,7 +15,7 @@ url=https://github.com/bazelbuild/rules_swift.git
|
||||
url = https://github.com/telegramdesktop/libtgvoip.git
|
||||
[submodule "submodules/TgVoipWebrtc/tgcalls"]
|
||||
path = submodules/TgVoipWebrtc/tgcalls
|
||||
url=../tgcalls.git
|
||||
url=https://github.com/TelegramMessenger/tgcalls.git
|
||||
[submodule "third-party/libvpx/libvpx"]
|
||||
path = third-party/libvpx/libvpx
|
||||
url = https://github.com/webmproject/libvpx.git
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"sweetpad.build.xcodeWorkspacePath": "Telegram/Swiftgram.xcodeproj/project.xcworkspace"
|
||||
}
|
||||
17
README.md
17
README.md
@@ -1,3 +1,16 @@
|
||||
# Swiftgram
|
||||
|
||||
Supercharged Telegram fork for iOS
|
||||
|
||||
[<img src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" height="50">](https://apps.apple.com/app/apple-store/id6471879502?pt=126511626&ct=gh&mt=8)
|
||||
|
||||
- Download: [App Store](https://apps.apple.com/app/apple-store/id6471879502?pt=126511626&ct=gh&mt=8)
|
||||
- Telegram channel: https://t.me/swiftgram
|
||||
- Telegram chat: https://t.me/swiftgramchat
|
||||
- TestFlight beta, local chats, translations and other [@SwiftgramLinks](https://t.me/s/SwiftgramLinks)
|
||||
|
||||
Swiftgram's compilation steps are the same as for the official app. Below you'll find a complete compilation guide based on the official app.
|
||||
|
||||
# Telegram iOS Source Code Compilation Guide
|
||||
|
||||
We welcome all developers to use our API and source code to create applications on our platform.
|
||||
@@ -16,7 +29,7 @@ There are several things we require from **all developers** for the moment.
|
||||
## Get the Code
|
||||
|
||||
```
|
||||
git clone --recursive -j8 https://github.com/TelegramMessenger/Telegram-iOS.git
|
||||
git clone --recursive -j8 https://github.com/Swiftgram/Telegram-iOS.git
|
||||
```
|
||||
|
||||
## Setup Xcode
|
||||
@@ -29,7 +42,7 @@ Install Xcode (directly from https://developer.apple.com/download/applications o
|
||||
```
|
||||
openssl rand -hex 8
|
||||
```
|
||||
2. Create a new Xcode project. Use `Telegram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier.
|
||||
2. Create a new Xcode project. Use `Swiftgram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier.
|
||||
3. Open `Keychain Access` and navigate to `Certificates`. Locate `Apple Development: your@email.address (XXXXXXXXXX)` and double tap the certificate. Under `Details`, locate `Organizational Unit`. This is the Team ID.
|
||||
4. Edit `build-system/template_minimal_development_configuration.json`. Use data from the previous steps.
|
||||
|
||||
|
||||
9
Swiftgram/AppleStyleFolders/BUILD
Normal file
9
Swiftgram/AppleStyleFolders/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "AppleStyleFolders",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
1034
Swiftgram/AppleStyleFolders/Sources/File.swift
Normal file
1034
Swiftgram/AppleStyleFolders/Sources/File.swift
Normal file
File diff suppressed because it is too large
Load Diff
9
Swiftgram/ChatControllerImplExtension/BUILD
Normal file
9
Swiftgram/ChatControllerImplExtension/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "ChatControllerImplExtension",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,225 @@
|
||||
import SGSimpleSettings
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SafariServices
|
||||
import MobileCoreServices
|
||||
import Intents
|
||||
import LegacyComponents
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import DeviceAccess
|
||||
import TextFormat
|
||||
import TelegramBaseController
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import OverlayStatusController
|
||||
import DeviceLocationManager
|
||||
import ShareController
|
||||
import UrlEscaping
|
||||
import ContextUI
|
||||
import ComposePollUI
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
import TelegramCallsUI
|
||||
import TelegramNotices
|
||||
import GameUI
|
||||
import ScreenCaptureDetection
|
||||
import GalleryUI
|
||||
import OpenInExternalAppUI
|
||||
import LegacyUI
|
||||
import InstantPageUI
|
||||
import LocationUI
|
||||
import BotPaymentsUI
|
||||
import DeleteChatPeerActionSheetItem
|
||||
import HashtagSearchUI
|
||||
import LegacyMediaPickerUI
|
||||
import Emoji
|
||||
import PeerAvatarGalleryUI
|
||||
import PeerInfoUI
|
||||
import RaiseToListen
|
||||
import UrlHandling
|
||||
import AvatarNode
|
||||
import AppBundle
|
||||
import LocalizedPeerData
|
||||
import PhoneNumberFormat
|
||||
import SettingsUI
|
||||
import UrlWhitelist
|
||||
import TelegramIntents
|
||||
import TooltipUI
|
||||
import StatisticsUI
|
||||
import MediaResources
|
||||
import GalleryData
|
||||
import ChatInterfaceState
|
||||
import InviteLinksUI
|
||||
import Markdown
|
||||
import TelegramPermissionsUI
|
||||
import Speak
|
||||
import TranslateUI
|
||||
import UniversalMediaPlayer
|
||||
import WallpaperBackgroundNode
|
||||
import ChatListUI
|
||||
import CalendarMessageScreen
|
||||
import ReactionSelectionNode
|
||||
import ReactionListContextMenuContent
|
||||
import AttachmentUI
|
||||
import AttachmentTextInputPanelNode
|
||||
import MediaPickerUI
|
||||
import ChatPresentationInterfaceState
|
||||
import Pasteboard
|
||||
import ChatSendMessageActionUI
|
||||
import ChatTextLinkEditUI
|
||||
import WebUI
|
||||
import PremiumUI
|
||||
import ImageTransparency
|
||||
import StickerPackPreviewUI
|
||||
import TextNodeWithEntities
|
||||
import EntityKeyboard
|
||||
import ChatTitleView
|
||||
import EmojiStatusComponent
|
||||
import ChatTimerScreen
|
||||
import MediaPasteboardUI
|
||||
import ChatListHeaderComponent
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
import ChatEntityKeyboardInputNode
|
||||
import StorageUsageScreen
|
||||
import AvatarEditorScreen
|
||||
import ChatScheduleTimeController
|
||||
import ICloudResources
|
||||
import StoryContainerScreen
|
||||
import MoreHeaderButton
|
||||
import VolumeButtons
|
||||
import ChatAvatarNavigationNode
|
||||
import ChatContextQuery
|
||||
import PeerReportScreen
|
||||
import PeerSelectionController
|
||||
import SaveToCameraRoll
|
||||
import ChatMessageDateAndStatusNode
|
||||
import ReplyAccessoryPanelNode
|
||||
import TextSelectionNode
|
||||
import ChatMessagePollBubbleContentNode
|
||||
import ChatMessageItem
|
||||
import ChatMessageItemImpl
|
||||
import ChatMessageItemView
|
||||
import ChatMessageItemCommon
|
||||
import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
import WallpaperGridScreen
|
||||
import VideoMessageCameraScreen
|
||||
import TopMessageReactions
|
||||
import AudioWaveform
|
||||
import PeerNameColorScreen
|
||||
import ChatEmptyNode
|
||||
import ChatMediaInputStickerGridItem
|
||||
import AdsInfoScreen
|
||||
|
||||
extension ChatControllerImpl {
|
||||
|
||||
func forwardMessagesToCloud(messageIds: [MessageId], removeNames: Bool, openCloud: Bool, resetCurrent: Bool = false) {
|
||||
let _ = (self.context.engine.data.get(EngineDataMap(
|
||||
messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init)
|
||||
))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] messages in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if resetCurrent {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withoutSelectionState() }) })
|
||||
}
|
||||
|
||||
let sortedMessages = messages.values.compactMap { $0?._asMessage() }.sorted { lhs, rhs in
|
||||
return lhs.id < rhs.id
|
||||
}
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
if removeNames {
|
||||
attributes.append(ForwardOptionsMessageAttribute(hideNames: true, hideCaptions: false))
|
||||
}
|
||||
|
||||
if !openCloud {
|
||||
Queue.mainQueue().after(0.88) {
|
||||
strongSelf.chatDisplayNode.hapticFeedback.success()
|
||||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: true, text: messages.count == 1 ? presentationData.strings.Conversation_ForwardTooltip_SavedMessages_One : presentationData.strings.Conversation_ForwardTooltip_SavedMessages_Many), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] value in
|
||||
if case .info = value, let strongSelf = self {
|
||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { peer in
|
||||
guard let strongSelf = self, let peer = peer, let navigationController = strongSelf.effectiveNavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil))
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}), in: .current)
|
||||
}
|
||||
|
||||
let _ = (enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.context.account.peerId, messages: sortedMessages.map { message -> EnqueueMessage in
|
||||
return .forward(source: message.id, threadId: nil, grouping: .auto, attributes: attributes, correlationId: nil)
|
||||
})
|
||||
|> deliverOnMainQueue).startStandalone(next: { messageIds in
|
||||
guard openCloud else {
|
||||
return
|
||||
}
|
||||
if let strongSelf = self {
|
||||
let signals: [Signal<Bool, NoError>] = messageIds.compactMap({ id -> Signal<Bool, NoError>? in
|
||||
guard let id = id else {
|
||||
return nil
|
||||
}
|
||||
return strongSelf.context.account.pendingMessageManager.pendingMessageStatus(id)
|
||||
|> mapToSignal { status, _ -> Signal<Bool, NoError> in
|
||||
if status != nil {
|
||||
return .never()
|
||||
} else {
|
||||
return .single(true)
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
})
|
||||
if strongSelf.shareStatusDisposable == nil {
|
||||
strongSelf.shareStatusDisposable = MetaDisposable()
|
||||
}
|
||||
strongSelf.shareStatusDisposable?.set((combineLatest(signals)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak strongSelf] _ in
|
||||
guard let strongSelf = strongSelf else {
|
||||
return
|
||||
}
|
||||
strongSelf.chatDisplayNode.hapticFeedback.success()
|
||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak strongSelf] peer in
|
||||
guard let strongSelf = strongSelf, let peer = peer, let navigationController = strongSelf.effectiveNavigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
var navigationSubject: ChatControllerSubject? = nil
|
||||
for messageId in messageIds {
|
||||
if let messageId = messageId {
|
||||
navigationSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false)
|
||||
break
|
||||
}
|
||||
}
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: navigationSubject, keepStack: .always, purposefulAction: {}, peekData: nil))
|
||||
})
|
||||
} ))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
0
Swiftgram/FLEX/BUILD
Normal file
0
Swiftgram/FLEX/BUILD
Normal file
68
Swiftgram/FLEX/FLEX.BUILD
Normal file
68
Swiftgram/FLEX/FLEX.BUILD
Normal file
@@ -0,0 +1,68 @@
|
||||
objc_library(
|
||||
name = "FLEX",
|
||||
module_name = "FLEX",
|
||||
srcs = glob(
|
||||
["Classes/**/*"],
|
||||
exclude = [
|
||||
"Classes/Info.plist",
|
||||
"Classes/Utility/APPLE_LICENSE",
|
||||
"Classes/Network/OSCache/LICENSE.md",
|
||||
"Classes/Network/PonyDebugger/LICENSE",
|
||||
"Classes/GlobalStateExplorers/DatabaseBrowser/LICENSE",
|
||||
"Classes/GlobalStateExplorers/Keychain/SSKeychain_LICENSE",
|
||||
"Classes/GlobalStateExplorers/SystemLog/LLVM_LICENSE.TXT",
|
||||
]
|
||||
),
|
||||
hdrs = glob([
|
||||
"Classes/**/*.h"
|
||||
]),
|
||||
includes = [
|
||||
"Classes",
|
||||
"Classes/Core",
|
||||
"Classes/Core/Controllers",
|
||||
"Classes/Core/Views",
|
||||
"Classes/Core/Views/Cells",
|
||||
"Classes/Core/Views/Carousel",
|
||||
"Classes/ObjectExplorers",
|
||||
"Classes/ObjectExplorers/Sections",
|
||||
"Classes/ObjectExplorers/Sections/Shortcuts",
|
||||
"Classes/Network",
|
||||
"Classes/Network/PonyDebugger",
|
||||
"Classes/Network/OSCache",
|
||||
"Classes/Toolbar",
|
||||
"Classes/Manager",
|
||||
"Classes/Manager/Private",
|
||||
"Classes/Editing",
|
||||
"Classes/Editing/ArgumentInputViews",
|
||||
"Classes/Headers",
|
||||
"Classes/ExplorerInterface",
|
||||
"Classes/ExplorerInterface/Tabs",
|
||||
"Classes/ExplorerInterface/Bookmarks",
|
||||
"Classes/GlobalStateExplorers",
|
||||
"Classes/GlobalStateExplorers/Globals",
|
||||
"Classes/GlobalStateExplorers/Keychain",
|
||||
"Classes/GlobalStateExplorers/FileBrowser",
|
||||
"Classes/GlobalStateExplorers/SystemLog",
|
||||
"Classes/GlobalStateExplorers/DatabaseBrowser",
|
||||
"Classes/GlobalStateExplorers/RuntimeBrowser",
|
||||
"Classes/GlobalStateExplorers/RuntimeBrowser/DataSources",
|
||||
"Classes/ViewHierarchy",
|
||||
"Classes/ViewHierarchy/SnapshotExplorer",
|
||||
"Classes/ViewHierarchy/SnapshotExplorer/Scene",
|
||||
"Classes/ViewHierarchy/TreeExplorer",
|
||||
"Classes/Utility",
|
||||
"Classes/Utility/Runtime",
|
||||
"Classes/Utility/Runtime/Objc",
|
||||
"Classes/Utility/Runtime/Objc/Reflection",
|
||||
"Classes/Utility/Categories",
|
||||
"Classes/Utility/Categories/Private",
|
||||
"Classes/Utility/Keyboard"
|
||||
],
|
||||
copts = [
|
||||
"-Wno-deprecated-declarations",
|
||||
"-Wno-strict-prototypes",
|
||||
"-Wno-unsupported-availability-guard",
|
||||
],
|
||||
deps = [],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
3
Swiftgram/Playground/.swiftformat
Normal file
3
Swiftgram/Playground/.swiftformat
Normal file
@@ -0,0 +1,3 @@
|
||||
--maxwidth 100
|
||||
--indent 4
|
||||
--disable redundantSelf
|
||||
87
Swiftgram/Playground/BUILD
Normal file
87
Swiftgram/Playground/BUILD
Normal file
@@ -0,0 +1,87 @@
|
||||
load("@bazel_skylib//rules:common_settings.bzl",
|
||||
"bool_flag",
|
||||
)
|
||||
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
load(
|
||||
"@rules_xcodeproj//xcodeproj:defs.bzl",
|
||||
"top_level_targets",
|
||||
"xcodeproj",
|
||||
)
|
||||
load(
|
||||
"@build_configuration//:variables.bzl", "telegram_bazel_path"
|
||||
)
|
||||
|
||||
bool_flag(
|
||||
name = "disableProvisioningProfiles",
|
||||
build_setting_default = False,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "disableProvisioningProfilesSetting",
|
||||
flag_values = {
|
||||
":disableProvisioningProfiles": "True",
|
||||
},
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "PlaygroundMain",
|
||||
srcs = [
|
||||
"Sources/main.m"
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
swift_library(
|
||||
name = "PlaygroundLib",
|
||||
srcs = glob(["Sources/**/*.swift"]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/LegacyUI:LegacyUI",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//Swiftgram/SGSwiftUI:SGSwiftUI",
|
||||
],
|
||||
data = [
|
||||
"//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
ios_application(
|
||||
name = "Playground",
|
||||
bundle_id = "app.swiftgram.ios.Playground",
|
||||
families = [
|
||||
"iphone",
|
||||
"ipad",
|
||||
],
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "codesigning/Playground.mobileprovision",
|
||||
}),
|
||||
infoplists = ["Resources/Info.plist"],
|
||||
minimum_os_version = "14.0",
|
||||
visibility = ["//visibility:public"],
|
||||
strings = [
|
||||
"//Telegram:AppStringResources",
|
||||
],
|
||||
launch_storyboard = "Resources/LaunchScreen.storyboard",
|
||||
deps = [":PlaygroundMain", ":PlaygroundLib"],
|
||||
)
|
||||
|
||||
xcodeproj(
|
||||
bazel_path = telegram_bazel_path,
|
||||
name = "Playground_xcodeproj",
|
||||
build_mode = "bazel",
|
||||
project_name = "Playground",
|
||||
tags = ["manual"],
|
||||
top_level_targets = top_level_targets(
|
||||
labels = [
|
||||
":Playground",
|
||||
],
|
||||
target_environments = ["device", "simulator"],
|
||||
),
|
||||
)
|
||||
25
Swiftgram/Playground/README.md
Normal file
25
Swiftgram/Playground/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Swiftgram Playground
|
||||
|
||||
Small app to quickly iterate on components testing without building an entire messenger.
|
||||
|
||||
## (Optional) Setup Codesigning
|
||||
|
||||
Create simple `codesigning/Playground.mobileprovision`. It is only required for non-simulator builds and can be skipped with `--disableProvisioningProfiles`.
|
||||
|
||||
## Generate Xcode project
|
||||
|
||||
Same as main project described in [../../Readme.md](../../Readme.md), but with `--target="Swiftgram/Playground"` parameter.
|
||||
|
||||
## Run generated project on simulator
|
||||
|
||||
### From root
|
||||
|
||||
```shell
|
||||
./Swiftgram/Playground/launch_on_simulator.py
|
||||
```
|
||||
|
||||
### From current directory
|
||||
|
||||
```shell
|
||||
./launch_on_simulator.py
|
||||
```
|
||||
39
Swiftgram/Playground/Resources/Info.plist
Normal file
39
Swiftgram/Playground/Resources/Info.plist
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
25
Swiftgram/Playground/Resources/LaunchScreen.storyboard
Normal file
25
Swiftgram/Playground/Resources/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
82
Swiftgram/Playground/Sources/AppDelegate.swift
Normal file
82
Swiftgram/Playground/Sources/AppDelegate.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import LegacyUI
|
||||
|
||||
let SHOW_SAFE_AREA = false
|
||||
|
||||
@objc(AppDelegate)
|
||||
final class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
private var mainWindow: Window1?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
let statusBarHost = ApplicationStatusBarHost()
|
||||
let (window, hostView) = nativeWindowHostView()
|
||||
let mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost)
|
||||
self.mainWindow = mainWindow
|
||||
hostView.containerView.backgroundColor = UIColor.white
|
||||
self.window = window
|
||||
|
||||
let navigationController = NavigationController(
|
||||
mode: .single,
|
||||
theme: NavigationControllerTheme(
|
||||
statusBar: .black,
|
||||
navigationBar: THEME.navigationBar,
|
||||
emptyAreaColor: .white
|
||||
)
|
||||
)
|
||||
|
||||
mainWindow.viewController = navigationController
|
||||
|
||||
let rootViewController = mySwiftUIViewController(0)
|
||||
|
||||
if SHOW_SAFE_AREA {
|
||||
// Add insets visualization
|
||||
rootViewController.view.layoutMargins = .zero
|
||||
rootViewController.view.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
let topInsetView = UIView()
|
||||
let leftInsetView = UIView()
|
||||
let rightInsetView = UIView()
|
||||
let bottomInsetView = UIView()
|
||||
|
||||
[topInsetView, leftInsetView, rightInsetView, bottomInsetView].forEach {
|
||||
$0.backgroundColor = .systemRed
|
||||
$0.alpha = 0.3
|
||||
rootViewController.view.addSubview($0)
|
||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
topInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor),
|
||||
topInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor),
|
||||
topInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor),
|
||||
topInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.topAnchor),
|
||||
|
||||
leftInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor),
|
||||
leftInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor),
|
||||
leftInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor),
|
||||
leftInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.leadingAnchor),
|
||||
|
||||
rightInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor),
|
||||
rightInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor),
|
||||
rightInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor),
|
||||
rightInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.trailingAnchor),
|
||||
|
||||
bottomInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor),
|
||||
bottomInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor),
|
||||
bottomInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor),
|
||||
bottomInsetView.topAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
navigationController.setViewControllers([rootViewController], animated: false)
|
||||
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
100
Swiftgram/Playground/Sources/AppNavigationSetup.swift
Normal file
100
Swiftgram/Playground/Sources/AppNavigationSetup.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
public func isKeyboardWindow(window: NSObject) -> Bool {
|
||||
let typeName = NSStringFromClass(type(of: window))
|
||||
if #available(iOS 9.0, *) {
|
||||
if typeName.hasPrefix("UI") && typeName.hasSuffix("RemoteKeyboardWindow") {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if typeName.hasPrefix("UI") && typeName.hasSuffix("TextEffectsWindow") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func isKeyboardView(view: NSObject) -> Bool {
|
||||
let typeName = NSStringFromClass(type(of: view))
|
||||
if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetHostView") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func isKeyboardViewContainer(view: NSObject) -> Bool {
|
||||
let typeName = NSStringFromClass(type(of: view))
|
||||
if typeName.hasPrefix("UI") && typeName.hasSuffix("InputSetContainerView") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public class ApplicationStatusBarHost: StatusBarHost {
|
||||
private let application = UIApplication.shared
|
||||
|
||||
public var isApplicationInForeground: Bool {
|
||||
switch self.application.applicationState {
|
||||
case .background:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public var statusBarFrame: CGRect {
|
||||
return self.application.statusBarFrame
|
||||
}
|
||||
public var statusBarStyle: UIStatusBarStyle {
|
||||
get {
|
||||
return self.application.statusBarStyle
|
||||
} set(value) {
|
||||
self.setStatusBarStyle(value, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
public func setStatusBarStyle(_ style: UIStatusBarStyle, animated: Bool) {
|
||||
if self.shouldChangeStatusBarStyle?(style) ?? true {
|
||||
self.application.internalSetStatusBarStyle(style, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
public var shouldChangeStatusBarStyle: ((UIStatusBarStyle) -> Bool)?
|
||||
|
||||
public func setStatusBarHidden(_ value: Bool, animated: Bool) {
|
||||
self.application.internalSetStatusBarHidden(value, animation: animated ? .fade : .none)
|
||||
}
|
||||
|
||||
public var keyboardWindow: UIWindow? {
|
||||
if #available(iOS 16.0, *) {
|
||||
return UIApplication.shared.internalGetKeyboard()
|
||||
}
|
||||
|
||||
for window in UIApplication.shared.windows {
|
||||
if isKeyboardWindow(window: window) {
|
||||
return window
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var keyboardView: UIView? {
|
||||
guard let keyboardWindow = self.keyboardWindow else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for view in keyboardWindow.subviews {
|
||||
if isKeyboardViewContainer(view: view) {
|
||||
for subview in view.subviews {
|
||||
if isKeyboardView(view: subview) {
|
||||
return subview
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
5
Swiftgram/Playground/Sources/Application.swift
Normal file
5
Swiftgram/Playground/Sources/Application.swift
Normal file
@@ -0,0 +1,5 @@
|
||||
import UIKit
|
||||
|
||||
@objc(Application) class Application: UIApplication {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
private final class PlaygroundSplashScreenNode: ASDisplayNode {
|
||||
private let headerBackgroundNode: ASDisplayNode
|
||||
private let headerCornerNode: ASImageNode
|
||||
|
||||
private var isDismissed = false
|
||||
|
||||
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||
|
||||
override init() {
|
||||
self.headerBackgroundNode = ASDisplayNode()
|
||||
self.headerBackgroundNode.backgroundColor = .black
|
||||
|
||||
self.headerCornerNode = ASImageNode()
|
||||
self.headerCornerNode.displaysAsynchronously = false
|
||||
self.headerCornerNode.displayWithoutProcessing = true
|
||||
self.headerCornerNode.image = generateImage(CGSize(width: 20.0, height: 10.0), rotatedContext: { size, context in
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setBlendMode(.copy)
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 20.0, height: 20.0)))
|
||||
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1)
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = THEME.list.itemBlocksBackgroundColor
|
||||
|
||||
self.addSubnode(self.headerBackgroundNode)
|
||||
self.addSubnode(self.headerCornerNode)
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.isDismissed {
|
||||
return
|
||||
}
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
let headerHeight = navigationHeight + 260.0
|
||||
|
||||
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: 0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight)))
|
||||
transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: 10.0)))
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
guard let (layout, navigationHeight) = self.validLayout else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
self.isDismissed = true
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
|
||||
let headerHeight = navigationHeight + 260.0
|
||||
|
||||
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: -1.0, y: -headerHeight - 10.0), size: CGSize(width: layout.size.width + 2.0, height: headerHeight)), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
transition.updateFrame(node: self.headerCornerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: CGSize(width: layout.size.width, height: 10.0)))
|
||||
}
|
||||
}
|
||||
|
||||
public final class PlaygroundSplashScreen: ViewController {
|
||||
|
||||
public init() {
|
||||
|
||||
let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, enableBackgroundBlur: true, separatorColor: .clear, badgeBackgroundColor: THEME.navigationBar.badgeBackgroundColor, badgeStrokeColor: THEME.navigationBar.badgeStrokeColor, badgeTextColor: THEME.navigationBar.badgeTextColor)
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: "", close: "")))
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = PlaygroundSplashScreenNode()
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
(self.displayNode as! PlaygroundSplashScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
|
||||
}
|
||||
|
||||
public func animateOut(completion: @escaping () -> Void) {
|
||||
self.statusBar.statusBarStyle = .Black
|
||||
(self.displayNode as! PlaygroundSplashScreenNode).animateOut(completion: completion)
|
||||
}
|
||||
}
|
||||
362
Swiftgram/Playground/Sources/PlaygroundTheme.swift
Normal file
362
Swiftgram/Playground/Sources/PlaygroundTheme.swift
Normal file
@@ -0,0 +1,362 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
|
||||
|
||||
public final class PlaygroundInfoTheme {
|
||||
public let buttonBackgroundColor: UIColor
|
||||
public let buttonTextColor: UIColor
|
||||
public let incomingFundsTitleColor: UIColor
|
||||
public let outgoingFundsTitleColor: UIColor
|
||||
|
||||
public init(
|
||||
buttonBackgroundColor: UIColor,
|
||||
buttonTextColor: UIColor,
|
||||
incomingFundsTitleColor: UIColor,
|
||||
outgoingFundsTitleColor: UIColor
|
||||
) {
|
||||
self.buttonBackgroundColor = buttonBackgroundColor
|
||||
self.buttonTextColor = buttonTextColor
|
||||
self.incomingFundsTitleColor = incomingFundsTitleColor
|
||||
self.outgoingFundsTitleColor = outgoingFundsTitleColor
|
||||
}
|
||||
}
|
||||
|
||||
public final class PlaygroundTransactionTheme {
|
||||
public let descriptionBackgroundColor: UIColor
|
||||
public let descriptionTextColor: UIColor
|
||||
|
||||
public init(
|
||||
descriptionBackgroundColor: UIColor,
|
||||
descriptionTextColor: UIColor
|
||||
) {
|
||||
self.descriptionBackgroundColor = descriptionBackgroundColor
|
||||
self.descriptionTextColor = descriptionTextColor
|
||||
}
|
||||
}
|
||||
|
||||
public final class PlaygroundSetupTheme {
|
||||
public let buttonFillColor: UIColor
|
||||
public let buttonForegroundColor: UIColor
|
||||
public let inputBackgroundColor: UIColor
|
||||
public let inputPlaceholderColor: UIColor
|
||||
public let inputTextColor: UIColor
|
||||
public let inputClearButtonColor: UIColor
|
||||
|
||||
public init(
|
||||
buttonFillColor: UIColor,
|
||||
buttonForegroundColor: UIColor,
|
||||
inputBackgroundColor: UIColor,
|
||||
inputPlaceholderColor: UIColor,
|
||||
inputTextColor: UIColor,
|
||||
inputClearButtonColor: UIColor
|
||||
) {
|
||||
self.buttonFillColor = buttonFillColor
|
||||
self.buttonForegroundColor = buttonForegroundColor
|
||||
self.inputBackgroundColor = inputBackgroundColor
|
||||
self.inputPlaceholderColor = inputPlaceholderColor
|
||||
self.inputTextColor = inputTextColor
|
||||
self.inputClearButtonColor = inputClearButtonColor
|
||||
}
|
||||
}
|
||||
|
||||
public final class PlaygroundListTheme {
|
||||
public let itemPrimaryTextColor: UIColor
|
||||
public let itemSecondaryTextColor: UIColor
|
||||
public let itemPlaceholderTextColor: UIColor
|
||||
public let itemDestructiveColor: UIColor
|
||||
public let itemAccentColor: UIColor
|
||||
public let itemDisabledTextColor: UIColor
|
||||
public let plainBackgroundColor: UIColor
|
||||
public let blocksBackgroundColor: UIColor
|
||||
public let itemPlainSeparatorColor: UIColor
|
||||
public let itemBlocksBackgroundColor: UIColor
|
||||
public let itemBlocksSeparatorColor: UIColor
|
||||
public let itemHighlightedBackgroundColor: UIColor
|
||||
public let sectionHeaderTextColor: UIColor
|
||||
public let freeTextColor: UIColor
|
||||
public let freeTextErrorColor: UIColor
|
||||
public let inputClearButtonColor: UIColor
|
||||
|
||||
public init(
|
||||
itemPrimaryTextColor: UIColor,
|
||||
itemSecondaryTextColor: UIColor,
|
||||
itemPlaceholderTextColor: UIColor,
|
||||
itemDestructiveColor: UIColor,
|
||||
itemAccentColor: UIColor,
|
||||
itemDisabledTextColor: UIColor,
|
||||
plainBackgroundColor: UIColor,
|
||||
blocksBackgroundColor: UIColor,
|
||||
itemPlainSeparatorColor: UIColor,
|
||||
itemBlocksBackgroundColor: UIColor,
|
||||
itemBlocksSeparatorColor: UIColor,
|
||||
itemHighlightedBackgroundColor: UIColor,
|
||||
sectionHeaderTextColor: UIColor,
|
||||
freeTextColor: UIColor,
|
||||
freeTextErrorColor: UIColor,
|
||||
inputClearButtonColor: UIColor
|
||||
) {
|
||||
self.itemPrimaryTextColor = itemPrimaryTextColor
|
||||
self.itemSecondaryTextColor = itemSecondaryTextColor
|
||||
self.itemPlaceholderTextColor = itemPlaceholderTextColor
|
||||
self.itemDestructiveColor = itemDestructiveColor
|
||||
self.itemAccentColor = itemAccentColor
|
||||
self.itemDisabledTextColor = itemDisabledTextColor
|
||||
self.plainBackgroundColor = plainBackgroundColor
|
||||
self.blocksBackgroundColor = blocksBackgroundColor
|
||||
self.itemPlainSeparatorColor = itemPlainSeparatorColor
|
||||
self.itemBlocksBackgroundColor = itemBlocksBackgroundColor
|
||||
self.itemBlocksSeparatorColor = itemBlocksSeparatorColor
|
||||
self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor
|
||||
self.sectionHeaderTextColor = sectionHeaderTextColor
|
||||
self.freeTextColor = freeTextColor
|
||||
self.freeTextErrorColor = freeTextErrorColor
|
||||
self.inputClearButtonColor = inputClearButtonColor
|
||||
}
|
||||
}
|
||||
|
||||
public final class PlaygroundTheme: Equatable {
|
||||
public let info: PlaygroundInfoTheme
|
||||
public let transaction: PlaygroundTransactionTheme
|
||||
public let setup: PlaygroundSetupTheme
|
||||
public let list: PlaygroundListTheme
|
||||
public let statusBarStyle: StatusBarStyle
|
||||
public let navigationBar: NavigationBarTheme
|
||||
public let keyboardAppearance: UIKeyboardAppearance
|
||||
public let alert: AlertControllerTheme
|
||||
public let actionSheet: ActionSheetControllerTheme
|
||||
|
||||
private let resourceCache = PlaygroundThemeResourceCache()
|
||||
|
||||
public init(info: PlaygroundInfoTheme, transaction: PlaygroundTransactionTheme, setup: PlaygroundSetupTheme, list: PlaygroundListTheme, statusBarStyle: StatusBarStyle, navigationBar: NavigationBarTheme, keyboardAppearance: UIKeyboardAppearance, alert: AlertControllerTheme, actionSheet: ActionSheetControllerTheme) {
|
||||
self.info = info
|
||||
self.transaction = transaction
|
||||
self.setup = setup
|
||||
self.list = list
|
||||
self.statusBarStyle = statusBarStyle
|
||||
self.navigationBar = navigationBar
|
||||
self.keyboardAppearance = keyboardAppearance
|
||||
self.alert = alert
|
||||
self.actionSheet = actionSheet
|
||||
}
|
||||
|
||||
func image(_ key: Int32, _ generate: (PlaygroundTheme) -> UIImage?) -> UIImage? {
|
||||
return self.resourceCache.image(key, self, generate)
|
||||
}
|
||||
|
||||
public static func ==(lhs: PlaygroundTheme, rhs: PlaygroundTheme) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class PlaygroundThemeResourceCacheHolder {
|
||||
var images: [Int32: UIImage] = [:]
|
||||
}
|
||||
|
||||
private final class PlaygroundThemeResourceCache {
|
||||
private let imageCache = Atomic<PlaygroundThemeResourceCacheHolder>(value: PlaygroundThemeResourceCacheHolder())
|
||||
|
||||
public func image(_ key: Int32, _ theme: PlaygroundTheme, _ generate: (PlaygroundTheme) -> UIImage?) -> UIImage? {
|
||||
let result = self.imageCache.with { holder -> UIImage? in
|
||||
return holder.images[key]
|
||||
}
|
||||
if let result = result {
|
||||
return result
|
||||
} else {
|
||||
if let image = generate(theme) {
|
||||
self.imageCache.with { holder -> Void in
|
||||
holder.images[key] = image
|
||||
}
|
||||
return image
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PlaygroundThemeResourceKey: Int32 {
|
||||
case itemListCornersBoth
|
||||
case itemListCornersTop
|
||||
case itemListCornersBottom
|
||||
case itemListClearInputIcon
|
||||
case itemListDisclosureArrow
|
||||
case navigationShareIcon
|
||||
case transactionLockIcon
|
||||
|
||||
case clockMin
|
||||
case clockFrame
|
||||
}
|
||||
|
||||
func cornersImage(_ theme: PlaygroundTheme, top: Bool, bottom: Bool) -> UIImage? {
|
||||
if !top && !bottom {
|
||||
return nil
|
||||
}
|
||||
let key: PlaygroundThemeResourceKey
|
||||
if top && bottom {
|
||||
key = .itemListCornersBoth
|
||||
} else if top {
|
||||
key = .itemListCornersTop
|
||||
} else {
|
||||
key = .itemListCornersBottom
|
||||
}
|
||||
return theme.image(key.rawValue, { theme in
|
||||
return generateImage(CGSize(width: 50.0, height: 50.0), rotatedContext: { (size, context) in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.setFillColor(theme.list.blocksBackgroundColor.cgColor)
|
||||
context.fill(bounds)
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
|
||||
var corners: UIRectCorner = []
|
||||
if top {
|
||||
corners.insert(.topLeft)
|
||||
corners.insert(.topRight)
|
||||
}
|
||||
if bottom {
|
||||
corners.insert(.bottomLeft)
|
||||
corners.insert(.bottomRight)
|
||||
}
|
||||
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 11.0, height: 11.0))
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: 25, topCapHeight: 25)
|
||||
})
|
||||
}
|
||||
|
||||
func itemListClearInputIcon(_ theme: PlaygroundTheme) -> UIImage? {
|
||||
return theme.image(PlaygroundThemeResourceKey.itemListClearInputIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Playground/ClearInput"), color: theme.list.inputClearButtonColor)
|
||||
})
|
||||
}
|
||||
|
||||
func navigationShareIcon(_ theme: PlaygroundTheme) -> UIImage? {
|
||||
return theme.image(PlaygroundThemeResourceKey.navigationShareIcon.rawValue, { theme in
|
||||
generateTintedImage(image: UIImage(bundleImageName: "Playground/NavigationShare"), color: theme.navigationBar.buttonColor)
|
||||
})
|
||||
}
|
||||
|
||||
func disclosureArrowImage(_ theme: PlaygroundTheme) -> UIImage? {
|
||||
return theme.image(PlaygroundThemeResourceKey.itemListDisclosureArrow.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Playground/DisclosureArrow"), color: theme.list.itemSecondaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
func clockFrameImage(_ theme: PlaygroundTheme) -> UIImage? {
|
||||
return theme.image(PlaygroundThemeResourceKey.clockFrame.rawValue, { theme in
|
||||
let color = theme.list.itemSecondaryTextColor
|
||||
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(color.cgColor)
|
||||
context.setFillColor(color.cgColor)
|
||||
let strokeWidth: CGFloat = 1.0
|
||||
context.setLineWidth(strokeWidth)
|
||||
context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth))
|
||||
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func clockMinImage(_ theme: PlaygroundTheme) -> UIImage? {
|
||||
return theme.image(PlaygroundThemeResourceKey.clockMin.rawValue, { theme in
|
||||
let color = theme.list.itemSecondaryTextColor
|
||||
return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
let strokeWidth: CGFloat = 1.0
|
||||
context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func PlaygroundTransactionLockIcon(_ theme: PlaygroundTheme) -> UIImage? {
|
||||
return theme.image(PlaygroundThemeResourceKey.transactionLockIcon.rawValue, { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Playground/EncryptedComment"), color: theme.list.itemSecondaryTextColor)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
public let ACCENT_COLOR = UIColor(rgb: 0x007ee5)
|
||||
public let NAVIGATION_BAR_THEME = NavigationBarTheme(
|
||||
buttonColor: ACCENT_COLOR,
|
||||
disabledButtonColor: UIColor(rgb: 0xd0d0d0),
|
||||
primaryTextColor: .black,
|
||||
backgroundColor: UIColor(rgb: 0xf7f7f7),
|
||||
enableBackgroundBlur: true,
|
||||
separatorColor: UIColor(rgb: 0xb1b1b1),
|
||||
badgeBackgroundColor: UIColor(rgb: 0xff3b30),
|
||||
badgeStrokeColor: UIColor(rgb: 0xff3b30),
|
||||
badgeTextColor: .white
|
||||
)
|
||||
public let THEME = PlaygroundTheme(
|
||||
info: PlaygroundInfoTheme(
|
||||
buttonBackgroundColor: UIColor(rgb: 0x32aafe),
|
||||
buttonTextColor: .white,
|
||||
incomingFundsTitleColor: UIColor(rgb: 0x00b12c),
|
||||
outgoingFundsTitleColor: UIColor(rgb: 0xff3b30)
|
||||
), transaction: PlaygroundTransactionTheme(
|
||||
descriptionBackgroundColor: UIColor(rgb: 0xf1f1f4),
|
||||
descriptionTextColor: .black
|
||||
), setup: PlaygroundSetupTheme(
|
||||
buttonFillColor: ACCENT_COLOR,
|
||||
buttonForegroundColor: .white,
|
||||
inputBackgroundColor: UIColor(rgb: 0xe9e9e9),
|
||||
inputPlaceholderColor: UIColor(rgb: 0x818086),
|
||||
inputTextColor: .black,
|
||||
inputClearButtonColor: UIColor(rgb: 0x7b7b81).withAlphaComponent(0.8)
|
||||
),
|
||||
list: PlaygroundListTheme(
|
||||
itemPrimaryTextColor: .black,
|
||||
itemSecondaryTextColor: UIColor(rgb: 0x8e8e93),
|
||||
itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce),
|
||||
itemDestructiveColor: UIColor(rgb: 0xff3b30),
|
||||
itemAccentColor: ACCENT_COLOR,
|
||||
itemDisabledTextColor: UIColor(rgb: 0x8e8e93),
|
||||
plainBackgroundColor: .white,
|
||||
blocksBackgroundColor: UIColor(rgb: 0xefeff4),
|
||||
itemPlainSeparatorColor: UIColor(rgb: 0xc8c7cc),
|
||||
itemBlocksBackgroundColor: .white,
|
||||
itemBlocksSeparatorColor: UIColor(rgb: 0xc8c7cc),
|
||||
itemHighlightedBackgroundColor: UIColor(rgb: 0xe5e5ea),
|
||||
sectionHeaderTextColor: UIColor(rgb: 0x6d6d72),
|
||||
freeTextColor: UIColor(rgb: 0x6d6d72),
|
||||
freeTextErrorColor: UIColor(rgb: 0xcf3030),
|
||||
inputClearButtonColor: UIColor(rgb: 0xcccccc)
|
||||
),
|
||||
statusBarStyle: .Black,
|
||||
navigationBar: NAVIGATION_BAR_THEME,
|
||||
keyboardAppearance: .light,
|
||||
alert: AlertControllerTheme(
|
||||
backgroundType: .light,
|
||||
backgroundColor: .white,
|
||||
separatorColor: UIColor(white: 0.9, alpha: 1.0),
|
||||
highlightedItemColor: UIColor(rgb: 0xe5e5ea),
|
||||
primaryColor: .black,
|
||||
secondaryColor: UIColor(rgb: 0x5e5e5e),
|
||||
accentColor: ACCENT_COLOR,
|
||||
contrastColor: .green,
|
||||
destructiveColor: UIColor(rgb: 0xff3b30),
|
||||
disabledColor: UIColor(rgb: 0xd0d0d0),
|
||||
controlBorderColor: .green,
|
||||
baseFontSize: 17.0
|
||||
),
|
||||
actionSheet: ActionSheetControllerTheme(
|
||||
dimColor: UIColor(white: 0.0, alpha: 0.4),
|
||||
backgroundType: .light,
|
||||
itemBackgroundColor: .white,
|
||||
itemHighlightedBackgroundColor: UIColor(white: 0.9, alpha: 1.0),
|
||||
standardActionTextColor: ACCENT_COLOR,
|
||||
destructiveActionTextColor: UIColor(rgb: 0xff3b30),
|
||||
disabledActionTextColor: UIColor(rgb: 0xb3b3b3),
|
||||
primaryTextColor: .black,
|
||||
secondaryTextColor: UIColor(rgb: 0x5e5e5e),
|
||||
controlAccentColor: ACCENT_COLOR,
|
||||
controlColor: UIColor(rgb: 0x7e8791),
|
||||
switchFrameColor: UIColor(rgb: 0xe0e0e0),
|
||||
switchContentColor: UIColor(rgb: 0x77d572),
|
||||
switchHandleColor: UIColor(rgb: 0xffffff),
|
||||
baseFontSize: 17.0
|
||||
)
|
||||
)
|
||||
85
Swiftgram/Playground/Sources/SwiftUIViewController.swift
Normal file
85
Swiftgram/Playground/Sources/SwiftUIViewController.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Foundation
|
||||
import LegacyUI
|
||||
import SGSwiftUI
|
||||
import SwiftUI
|
||||
import TelegramPresentationData
|
||||
import UIKit
|
||||
|
||||
struct MySwiftUIView: View {
|
||||
weak var wrapperController: LegacyController?
|
||||
|
||||
var num: Int64
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
Text("Hello, World!")
|
||||
.font(.title)
|
||||
.foregroundColor(.black)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button("Push") {
|
||||
self.wrapperController?.push(mySwiftUIViewController(num + 1))
|
||||
}.buttonStyle(AppleButtonStyle())
|
||||
Spacer()
|
||||
Button("Modal") {
|
||||
self.wrapperController?.present(
|
||||
mySwiftUIViewController(num + 1),
|
||||
in: .window(.root),
|
||||
with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
|
||||
)
|
||||
}.buttonStyle(AppleButtonStyle())
|
||||
Spacer()
|
||||
if num > 0 {
|
||||
Button("Dismiss") {
|
||||
self.wrapperController?.dismiss()
|
||||
}.buttonStyle(AppleButtonStyle())
|
||||
Spacer()
|
||||
}
|
||||
ForEach(1..<20, id: \.self) { i in
|
||||
Button("TAP: \(i)") {
|
||||
print("Tapped \(i)")
|
||||
}.buttonStyle(AppleButtonStyle())
|
||||
}
|
||||
|
||||
}
|
||||
.background(Color.green)
|
||||
}
|
||||
}
|
||||
|
||||
struct AppleButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.padding()
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(10)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
.opacity(configuration.isPressed ? 0.9 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
public func mySwiftUIViewController(_ num: Int64) -> ViewController {
|
||||
let legacyController = LegacySwiftUIController(
|
||||
presentation: .modal(animateIn: true),
|
||||
theme: defaultPresentationTheme,
|
||||
strings: defaultPresentationStrings
|
||||
)
|
||||
legacyController.statusBar.statusBarStyle = defaultPresentationTheme.rootController
|
||||
.statusBarStyle.style
|
||||
legacyController.title = "Controller: \(num)"
|
||||
|
||||
let swiftUIView = SGSwiftUIView<MySwiftUIView>(
|
||||
navigationBarHeight: legacyController.navigationBarHeightModel,
|
||||
containerViewLayout: legacyController.containerViewLayoutModel,
|
||||
content: { MySwiftUIView(wrapperController: legacyController, num: num) }
|
||||
)
|
||||
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
||||
legacyController.bind(controller: controller)
|
||||
|
||||
return legacyController
|
||||
}
|
||||
7
Swiftgram/Playground/Sources/main.m
Normal file
7
Swiftgram/Playground/Sources/main.m
Normal file
@@ -0,0 +1,7 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, @"Application", @"AppDelegate");
|
||||
}
|
||||
}
|
||||
78
Swiftgram/Playground/generate_project.py
Executable file
78
Swiftgram/Playground/generate_project.py
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
import textwrap
|
||||
|
||||
# Import the locate_bazel function
|
||||
sys.path.append(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "build-system", "Make")
|
||||
)
|
||||
from BazelLocation import locate_bazel
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cwd(path):
|
||||
oldpwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
os.chdir(oldpwd)
|
||||
|
||||
|
||||
def main():
|
||||
# Get the current script directory
|
||||
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with cwd(os.path.join(current_script_dir, "..", "..")):
|
||||
bazel_path = locate_bazel(os.getcwd(), cache_host=None)
|
||||
# 1. Kill all Xcode processes
|
||||
subprocess.run(["killall", "Xcode"], check=False)
|
||||
|
||||
# 2. Delete xcodeproj.bazelrc if it exists and write a new one
|
||||
bazelrc_path = os.path.join(current_script_dir, "..", "..", "xcodeproj.bazelrc")
|
||||
if os.path.exists(bazelrc_path):
|
||||
os.remove(bazelrc_path)
|
||||
|
||||
with open(bazelrc_path, "w") as f:
|
||||
f.write(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
build --announce_rc
|
||||
build --features=swift.use_global_module_cache
|
||||
build --verbose_failures
|
||||
build --features=swift.enable_batch_mode
|
||||
build --features=-swift.debug_prefix_map
|
||||
# build --disk_cache=
|
||||
|
||||
build --swiftcopt=-no-warnings-as-errors
|
||||
build --copt=-Wno-error
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
# 3. Delete the Xcode project if it exists
|
||||
xcode_project_path = os.path.join(current_script_dir, "Playground.xcodeproj")
|
||||
if os.path.exists(xcode_project_path):
|
||||
shutil.rmtree(xcode_project_path)
|
||||
|
||||
# 4. Write content to generate_project.py
|
||||
generate_project_path = os.path.join(current_script_dir, "custom_bazel_path.bzl")
|
||||
with open(generate_project_path, "w") as f:
|
||||
f.write("def custom_bazel_path():\n")
|
||||
f.write(f' return "{bazel_path}"\n')
|
||||
|
||||
# 5. Run xcodeproj generator
|
||||
working_dir = os.path.join(current_script_dir, "..", "..")
|
||||
bazel_command = f'"{bazel_path}" run //Swiftgram/Playground:Playground_xcodeproj'
|
||||
subprocess.run(bazel_command, shell=True, cwd=working_dir, check=True)
|
||||
|
||||
# 5. Open Xcode project
|
||||
subprocess.run(["open", xcode_project_path], check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
170
Swiftgram/Playground/launch_on_simulator.py
Executable file
170
Swiftgram/Playground/launch_on_simulator.py
Executable file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def find_app(start_path):
|
||||
for root, dirs, _ in os.walk(start_path):
|
||||
for dir in dirs:
|
||||
if dir.endswith(".app"):
|
||||
return os.path.join(root, dir)
|
||||
return None
|
||||
|
||||
|
||||
def ensure_simulator_booted(device_name) -> str:
|
||||
# List all devices
|
||||
devices_json = subprocess.check_output(
|
||||
["xcrun", "simctl", "list", "devices", "--json"]
|
||||
).decode()
|
||||
devices = json.loads(devices_json)
|
||||
for runtime in devices["devices"]:
|
||||
for device in devices["devices"][runtime]:
|
||||
if device["name"] == device_name:
|
||||
device_udid = device["udid"]
|
||||
if device["state"] == "Booted":
|
||||
print(f"Simulator {device_name} is already booted.")
|
||||
return device_udid
|
||||
break
|
||||
if device_udid:
|
||||
break
|
||||
|
||||
if not device_udid:
|
||||
raise Exception(f"Simulator {device_name} not found")
|
||||
|
||||
# Boot the device
|
||||
print(f"Booting simulator {device_name}...")
|
||||
subprocess.run(["xcrun", "simctl", "boot", device_udid], check=True)
|
||||
|
||||
# Wait for the device to finish booting
|
||||
print("Waiting for simulator to finish booting...")
|
||||
while True:
|
||||
boot_status = subprocess.check_output(
|
||||
["xcrun", "simctl", "list", "devices"]
|
||||
).decode()
|
||||
if f"{device_name} ({device_udid}) (Booted)" in boot_status:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
print(f"Simulator {device_name} is now booted.")
|
||||
return device_udid
|
||||
|
||||
|
||||
def build_and_run_xcode_project(project_path, scheme_name, destination):
|
||||
# Change to the directory containing the .xcodeproj file
|
||||
os.chdir(os.path.dirname(project_path))
|
||||
|
||||
# Build the project
|
||||
build_command = [
|
||||
"xcodebuild",
|
||||
"-project",
|
||||
project_path,
|
||||
"-scheme",
|
||||
scheme_name,
|
||||
"-destination",
|
||||
destination,
|
||||
"-sdk",
|
||||
"iphonesimulator",
|
||||
"build",
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(build_command, check=True)
|
||||
print("Build successful!")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Build failed with error: {e}")
|
||||
return
|
||||
|
||||
# Get the bundle identifier and app path
|
||||
settings_command = [
|
||||
"xcodebuild",
|
||||
"-project",
|
||||
project_path,
|
||||
"-scheme",
|
||||
scheme_name,
|
||||
"-sdk",
|
||||
"iphonesimulator",
|
||||
"-showBuildSettings",
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
settings_command, capture_output=True, text=True, check=True
|
||||
)
|
||||
settings = result.stdout.split("\n")
|
||||
bundle_id = next(
|
||||
line.split("=")[1].strip()
|
||||
for line in settings
|
||||
if "PRODUCT_BUNDLE_IDENTIFIER" in line
|
||||
)
|
||||
build_dir = next(
|
||||
line.split("=")[1].strip()
|
||||
for line in settings
|
||||
if "TARGET_BUILD_DIR" in line
|
||||
)
|
||||
|
||||
app_path = find_app(build_dir)
|
||||
if not app_path:
|
||||
print(f"Could not find .app file in {build_dir}")
|
||||
return
|
||||
print(f"Found app at: {app_path}")
|
||||
print(f"Bundle identifier: {bundle_id}")
|
||||
print(f"App path: {app_path}")
|
||||
except (subprocess.CalledProcessError, StopIteration) as e:
|
||||
print(f"Failed to get build settings: {e}")
|
||||
return
|
||||
|
||||
device_udid = ensure_simulator_booted(simulator_name)
|
||||
|
||||
# Install the app on the simulator
|
||||
install_command = ["xcrun", "simctl", "install", device_udid, app_path]
|
||||
|
||||
try:
|
||||
subprocess.run(install_command, check=True)
|
||||
print("App installed on simulator successfully!")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to install app on simulator: {e}")
|
||||
return
|
||||
|
||||
# List installed apps
|
||||
try:
|
||||
listapps_cmd = "/usr/bin/xcrun simctl listapps booted | /usr/bin/plutil -convert json -r -o - -- -"
|
||||
result = subprocess.run(
|
||||
listapps_cmd, shell=True, capture_output=True, text=True, check=True
|
||||
)
|
||||
apps = json.loads(result.stdout)
|
||||
|
||||
if bundle_id in apps:
|
||||
print(f"App {bundle_id} is installed on the simulator")
|
||||
else:
|
||||
print(f"App {bundle_id} is not installed on the simulator")
|
||||
print("Installed apps:", list(apps.keys()))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to list apps: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Failed to parse app list: {e}")
|
||||
|
||||
# Focus simulator
|
||||
subprocess.run(["open", "-a", "Simulator"], check=True)
|
||||
|
||||
# Run the project on the simulator
|
||||
run_command = ["xcrun", "simctl", "launch", "booted", bundle_id]
|
||||
|
||||
try:
|
||||
subprocess.run(run_command, check=True)
|
||||
print("Application launched in simulator!")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to launch application in simulator: {e}")
|
||||
|
||||
|
||||
# Usage
|
||||
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_path = os.path.join(current_script_dir, "Playground.xcodeproj")
|
||||
scheme_name = "Playground"
|
||||
simulator_name = "iPhone 15"
|
||||
destination = f"platform=iOS Simulator,name={simulator_name},OS=latest"
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_and_run_xcode_project(project_path, scheme_name, destination)
|
||||
17
Swiftgram/SFSafariViewControllerPlus/BUILD
Normal file
17
Swiftgram/SFSafariViewControllerPlus/BUILD
Normal file
@@ -0,0 +1,17 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SFSafariViewControllerPlus",
|
||||
module_name = "SFSafariViewControllerPlus",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
import SafariServices
|
||||
|
||||
public class SFSafariViewControllerPlusDidFinish: SFSafariViewController, SFSafariViewControllerDelegate {
|
||||
public var onDidFinish: (() -> Void)?
|
||||
|
||||
public override init(url URL: URL, configuration: SFSafariViewController.Configuration = SFSafariViewController.Configuration()) {
|
||||
super.init(url: URL, configuration: configuration)
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
public func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
|
||||
onDidFinish?()
|
||||
}
|
||||
}
|
||||
25
Swiftgram/SGAPI/BUILD
Normal file
25
Swiftgram/SGAPI/BUILD
Normal file
@@ -0,0 +1,25 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGAPI",
|
||||
module_name = "SGAPI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//Swiftgram/SGLogging:SGLogging",
|
||||
"//Swiftgram/SGWebAppExtensions:SGWebAppExtensions",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme",
|
||||
"//Swiftgram/SGRegDateScheme:SGRegDateScheme",
|
||||
"//Swiftgram/SGRequests:SGRequests",
|
||||
"//Swiftgram/SGConfig:SGConfig"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
145
Swiftgram/SGAPI/Sources/SGAPI.swift
Normal file
145
Swiftgram/SGAPI/Sources/SGAPI.swift
Normal file
@@ -0,0 +1,145 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
|
||||
import SGConfig
|
||||
import SGLogging
|
||||
import SGSimpleSettings
|
||||
import SGWebAppExtensions
|
||||
import SGWebSettingsScheme
|
||||
import SGRequests
|
||||
import SGRegDateScheme
|
||||
|
||||
private let API_VERSION: String = "0"
|
||||
|
||||
private func buildApiUrl(_ endpoint: String) -> String {
|
||||
return "\(SG_CONFIG.apiUrl)/v\(API_VERSION)/\(endpoint)"
|
||||
}
|
||||
|
||||
public let SG_API_AUTHORIZATION_HEADER = "Authorization"
|
||||
public let SG_API_DEVICE_TOKEN_HEADER = "Device-Token"
|
||||
|
||||
private enum HTTPRequestError {
|
||||
case network
|
||||
}
|
||||
|
||||
public enum SGAPIError {
|
||||
case generic(String? = nil)
|
||||
}
|
||||
|
||||
public func getSGSettings(token: String) -> Signal<SGWebSettings, SGAPIError> {
|
||||
return Signal { subscriber in
|
||||
|
||||
let url = URL(string: buildApiUrl("settings"))!
|
||||
let headers = [SG_API_AUTHORIZATION_HEADER: "Token \(token)"]
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
headers.forEach { key, value in
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
let downloadSignal = requestsCustom(request: request).start(next: { data, urlResponse in
|
||||
let _ = completed.swap(true)
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let settings = try decoder.decode(SGWebSettings.self, from: data)
|
||||
subscriber.putNext(settings)
|
||||
subscriber.putCompletion()
|
||||
} catch {
|
||||
subscriber.putError(.generic("Can't parse user settings: \(error). Response: \(String(data: data, encoding: .utf8) ?? "")"))
|
||||
}
|
||||
}, error: { error in
|
||||
subscriber.putError(.generic("Error requesting user settings: \(String(describing: error))"))
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
downloadSignal.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public func postSGSettings(token: String, data: [String:Any]) -> Signal<Void, SGAPIError> {
|
||||
return Signal { subscriber in
|
||||
|
||||
let url = URL(string: buildApiUrl("settings"))!
|
||||
let headers = [SG_API_AUTHORIZATION_HEADER: "Token \(token)"]
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
headers.forEach { key, value in
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let jsonData = try? JSONSerialization.data(withJSONObject: data, options: [])
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = jsonData
|
||||
|
||||
let dataSignal = requestsCustom(request: request).start(next: { data, urlResponse in
|
||||
let _ = completed.swap(true)
|
||||
|
||||
if let httpResponse = urlResponse as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200...299:
|
||||
subscriber.putCompletion()
|
||||
default:
|
||||
subscriber.putError(.generic("Can't update settings: \(httpResponse.statusCode). Response: \(String(data: data, encoding: .utf8) ?? "")"))
|
||||
}
|
||||
} else {
|
||||
subscriber.putError(.generic("Not an HTTP response: \(String(describing: urlResponse))"))
|
||||
}
|
||||
}, error: { error in
|
||||
subscriber.putError(.generic("Error updating settings: \(String(describing: error))"))
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
dataSignal.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func getSGAPIRegDate(token: String, deviceToken: String, userId: Int64) -> Signal<RegDate, SGAPIError> {
|
||||
return Signal { subscriber in
|
||||
|
||||
let url = URL(string: buildApiUrl("regdate/\(userId)"))!
|
||||
let headers = [
|
||||
SG_API_AUTHORIZATION_HEADER: "Token \(token)",
|
||||
SG_API_DEVICE_TOKEN_HEADER: deviceToken
|
||||
]
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
headers.forEach { key, value in
|
||||
request.addValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
request.timeoutInterval = 10
|
||||
|
||||
let downloadSignal = requestsCustom(request: request).start(next: { data, urlResponse in
|
||||
let _ = completed.swap(true)
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let settings = try decoder.decode(RegDate.self, from: data)
|
||||
subscriber.putNext(settings)
|
||||
subscriber.putCompletion()
|
||||
} catch {
|
||||
subscriber.putError(.generic("Can't parse regDate: \(error). Response: \(String(data: data, encoding: .utf8) ?? "")"))
|
||||
}
|
||||
}, error: { error in
|
||||
subscriber.putError(.generic("Error requesting regDate: \(String(describing: error))"))
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
downloadSignal.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Swiftgram/SGAPIToken/BUILD
Normal file
24
Swiftgram/SGAPIToken/BUILD
Normal file
@@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGAPIToken",
|
||||
module_name = "SGAPIToken",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//Swiftgram/SGLogging:SGLogging",
|
||||
"//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme",
|
||||
"//Swiftgram/SGConfig:SGConfig",
|
||||
"//Swiftgram/SGWebAppExtensions:SGWebAppExtensions",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
133
Swiftgram/SGAPIToken/Sources/SGAPIToken.swift
Normal file
133
Swiftgram/SGAPIToken/Sources/SGAPIToken.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import SGLogging
|
||||
import SGConfig
|
||||
import SGWebAppExtensions
|
||||
|
||||
private let tokenExpirationTime: TimeInterval = 30 * 60 // 30 minutes
|
||||
|
||||
private var tokenCache: [Int64: (token: String, expiration: Date)] = [:]
|
||||
|
||||
public enum SGAPITokenError {
|
||||
case generic(String? = nil)
|
||||
}
|
||||
|
||||
public func getSGApiToken(context: AccountContext, botUsername: String = SG_CONFIG.botUsername) -> Signal<String, SGAPITokenError> {
|
||||
let userId = context.account.peerId.id._internalGetInt64Value()
|
||||
|
||||
if let (token, expiration) = tokenCache[userId], Date() < expiration {
|
||||
// SGLogger.shared.log("SGAPI", "Using cached token. Expiring at: \(expiration)")
|
||||
return Signal { subscriber in
|
||||
subscriber.putNext(token)
|
||||
subscriber.putCompletion()
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
SGLogger.shared.log("SGAPI", "Requesting new token")
|
||||
// Workaround for Apple Review
|
||||
if context.account.testingEnvironment {
|
||||
return context.account.postbox.transaction { transaction -> String? in
|
||||
if let testUserPeer = transaction.getPeer(context.account.peerId) as? TelegramUser, let testPhone = testUserPeer.phone {
|
||||
return testPhone
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|> mapToSignalPromotingError { phone -> Signal<String, SGAPITokenError> in
|
||||
if let phone = phone {
|
||||
// https://core.telegram.org/api/auth#test-accounts
|
||||
if phone.starts(with: String(99966)) {
|
||||
SGLogger.shared.log("SGAPI", "Using demo token")
|
||||
tokenCache[userId] = (phone, Date().addingTimeInterval(tokenExpirationTime))
|
||||
return .single(phone)
|
||||
} else {
|
||||
return .fail(.generic("Non-demo phone number on test DC"))
|
||||
}
|
||||
} else {
|
||||
return .fail(.generic("Missing test account peer or it's number (how?)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Signal { subscriber in
|
||||
let getSettingsURLSignal = getSGSettingsURL(context: context, botUsername: botUsername).start(next: { url in
|
||||
if let hashPart = url.components(separatedBy: "#").last {
|
||||
let parsedParams = urlParseHashParams(hashPart)
|
||||
if let token = parsedParams["tgWebAppData"], let token = token {
|
||||
tokenCache[userId] = (token, Date().addingTimeInterval(tokenExpirationTime))
|
||||
#if DEBUG
|
||||
print("[SGAPI]", "API Token: \(token)")
|
||||
#endif
|
||||
subscriber.putNext(token)
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(.generic("Invalid or missing token in response url! \(url)"))
|
||||
}
|
||||
} else {
|
||||
subscriber.putError(.generic("No hash part in URL \(url)"))
|
||||
}
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
getSettingsURLSignal.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func getSGSettingsURL(context: AccountContext, botUsername: String = SG_CONFIG.botUsername, url: String = SG_CONFIG.webappUrl, themeParams: [String: Any]? = nil) -> Signal<String, SGAPITokenError> {
|
||||
return Signal { subscriber in
|
||||
// themeParams = generateWebAppThemeParams(
|
||||
// context.sharedContext.currentPresentationData.with { $0 }.theme
|
||||
// )
|
||||
var requestWebViewSignalDisposable: Disposable? = nil
|
||||
var requestUpdatePeerIsBlocked: Disposable? = nil
|
||||
let resolvePeerSignal = (
|
||||
context.engine.peers.resolvePeerByName(name: botUsername, referrer: nil)
|
||||
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
return .complete()
|
||||
}
|
||||
return .single(result)
|
||||
}).start(next: { botPeer in
|
||||
if let botPeer = botPeer {
|
||||
SGLogger.shared.log("SGAPI", "Botpeer found for \(botUsername)")
|
||||
let requestWebViewSignal = context.engine.messages.requestWebView(peerId: botPeer.id, botId: botPeer.id, url: url, payload: nil, themeParams: themeParams, fromMenu: true, replyToMessageId: nil, threadId: nil)
|
||||
|
||||
requestWebViewSignalDisposable = requestWebViewSignal.start(next: { webViewResult in
|
||||
subscriber.putNext(webViewResult.url)
|
||||
subscriber.putCompletion()
|
||||
}, error: { e in
|
||||
SGLogger.shared.log("SGAPI", "Webview request error, retrying with unblock")
|
||||
// if e.errorDescription == "YOU_BLOCKED_USER" {
|
||||
requestUpdatePeerIsBlocked = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botPeer.id, isBlocked: false)
|
||||
|> afterDisposed(
|
||||
{
|
||||
requestWebViewSignalDisposable?.dispose()
|
||||
requestWebViewSignalDisposable = requestWebViewSignal.start(next: { webViewResult in
|
||||
SGLogger.shared.log("SGAPI", "Webview retry success \(webViewResult)")
|
||||
subscriber.putNext(webViewResult.url)
|
||||
subscriber.putCompletion()
|
||||
}, error: { e in
|
||||
SGLogger.shared.log("SGAPI", "Webview retry failure \(e)")
|
||||
subscriber.putError(.generic("Webview retry failure \(e)"))
|
||||
})
|
||||
})).start()
|
||||
// }
|
||||
})
|
||||
|
||||
} else {
|
||||
SGLogger.shared.log("SGAPI", "Botpeer not found for \(botUsername)")
|
||||
subscriber.putError(.generic())
|
||||
}
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
resolvePeerSignal.dispose()
|
||||
requestUpdatePeerIsBlocked?.dispose()
|
||||
requestWebViewSignalDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Swiftgram/SGAPIWebSettings/BUILD
Normal file
23
Swiftgram/SGAPIWebSettings/BUILD
Normal file
@@ -0,0 +1,23 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGAPIWebSettings",
|
||||
module_name = "SGAPIWebSettings",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//Swiftgram/SGAPI:SGAPI",
|
||||
"//Swiftgram/SGAPIToken:SGAPIToken",
|
||||
"//Swiftgram/SGLogging:SGLogging",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
49
Swiftgram/SGAPIWebSettings/Sources/File.swift
Normal file
49
Swiftgram/SGAPIWebSettings/Sources/File.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
|
||||
import SGAPIToken
|
||||
import SGAPI
|
||||
import SGLogging
|
||||
|
||||
import AccountContext
|
||||
|
||||
import SGSimpleSettings
|
||||
import TelegramCore
|
||||
|
||||
public func updateSGWebSettingsInteractivelly(context: AccountContext) {
|
||||
let _ = getSGApiToken(context: context).startStandalone(next: { token in
|
||||
let _ = getSGSettings(token: token).startStandalone(next: { webSettings in
|
||||
SGLogger.shared.log("SGAPI", "New SGWebSettings for id \(context.account.peerId.id._internalGetInt64Value()): \(webSettings) ")
|
||||
SGSimpleSettings.shared.canUseStealthMode = webSettings.global.storiesAvailable
|
||||
let _ = (context.account.postbox.transaction { transaction in
|
||||
updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in
|
||||
var configuration = configuration
|
||||
configuration.sgWebSettings = webSettings
|
||||
return configuration
|
||||
})
|
||||
}).startStandalone()
|
||||
}, error: { e in
|
||||
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
|
||||
SGLogger.shared.log("SGAPI", errorMessage)
|
||||
}
|
||||
})
|
||||
}, error: { e in
|
||||
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
|
||||
SGLogger.shared.log("SGAPI", errorMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
public func postSGWebSettingsInteractivelly(context: AccountContext, data: [String: Any]) {
|
||||
let _ = getSGApiToken(context: context).startStandalone(next: { token in
|
||||
let _ = postSGSettings(token: token, data: data).startStandalone(error: { e in
|
||||
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
|
||||
SGLogger.shared.log("SGAPI", errorMessage)
|
||||
}
|
||||
})
|
||||
}, error: { e in
|
||||
if case let .generic(errorMessage) = e, let errorMessage = errorMessage {
|
||||
SGLogger.shared.log("SGAPI", errorMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
17
Swiftgram/SGActionRequestHandlerSanitizer/BUILD
Normal file
17
Swiftgram/SGActionRequestHandlerSanitizer/BUILD
Normal file
@@ -0,0 +1,17 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGActionRequestHandlerSanitizer",
|
||||
module_name = "SGActionRequestHandlerSanitizer",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
15
Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift
Normal file
15
Swiftgram/SGActionRequestHandlerSanitizer/Sources/File.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
public func sgActionRequestHandlerSanitizer(_ url: URL) -> URL {
|
||||
var url = url
|
||||
if let scheme = url.scheme {
|
||||
let openInPrefix = "\(scheme)://parseurl?url="
|
||||
let urlString = url.absoluteString
|
||||
if urlString.hasPrefix(openInPrefix) {
|
||||
if let unwrappedUrlString = String(urlString.dropFirst(openInPrefix.count)).removingPercentEncoding, let newUrl = URL(string: unwrappedUrlString) {
|
||||
url = newUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
18
Swiftgram/SGConfig/BUILD
Normal file
18
Swiftgram/SGConfig/BUILD
Normal file
@@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGConfig",
|
||||
module_name = "SGConfig",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/BuildConfig:BuildConfig"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
20
Swiftgram/SGConfig/Sources/File.swift
Normal file
20
Swiftgram/SGConfig/Sources/File.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import BuildConfig
|
||||
|
||||
public struct SGConfig: Codable {
|
||||
public var apiUrl: String = "https://api.swiftgram.app"
|
||||
public var webappUrl: String = "https://my.swiftgram.app"
|
||||
public var botUsername: String = "SwiftgramBot"
|
||||
}
|
||||
|
||||
private func parseSGConfig(_ jsonString: String) -> SGConfig {
|
||||
let jsonData = Data(jsonString.utf8)
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
return (try? decoder.decode(SGConfig.self, from: jsonData)) ?? SGConfig()
|
||||
}
|
||||
|
||||
private let baseAppBundleId = Bundle.main.bundleIdentifier!
|
||||
private let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
||||
public let SG_CONFIG: SGConfig = parseSGConfig(buildConfig.sgConfig)
|
||||
public let SG_API_WEBAPP_URL_PARSED = URL(string: SG_CONFIG.webappUrl)!
|
||||
18
Swiftgram/SGContentAnalysis/BUILD
Normal file
18
Swiftgram/SGContentAnalysis/BUILD
Normal file
@@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGContentAnalysis",
|
||||
module_name = "SGContentAnalysis",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
64
Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift
Normal file
64
Swiftgram/SGContentAnalysis/Sources/ContentAnalysis.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import SensitiveContentAnalysis
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ContentAnalysisError: Error {
|
||||
case generic(_ message: String)
|
||||
}
|
||||
|
||||
public enum ContentAnalysisMediaType {
|
||||
case image
|
||||
case video
|
||||
}
|
||||
|
||||
public func canAnalyzeMedia() -> Bool {
|
||||
if #available(iOS 17, *) {
|
||||
let analyzer = SCSensitivityAnalyzer()
|
||||
let policy = analyzer.analysisPolicy
|
||||
return policy != .disabled
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func analyzeMediaSignal(_ url: URL, mediaType: ContentAnalysisMediaType = .image) -> Signal<Bool, Error> {
|
||||
return Signal { subscriber in
|
||||
analyzeMedia(url: url, mediaType: mediaType, completion: { result, error in
|
||||
if let result = result {
|
||||
subscriber.putNext(result)
|
||||
subscriber.putCompletion()
|
||||
} else if let error = error {
|
||||
subscriber.putError(error)
|
||||
} else {
|
||||
subscriber.putError(ContentAnalysisError.generic("Unknown response"))
|
||||
}
|
||||
})
|
||||
|
||||
return ActionDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func analyzeMedia(url: URL, mediaType: ContentAnalysisMediaType, completion: @escaping (Bool?, Error?) -> Void) {
|
||||
if #available(iOS 17, *) {
|
||||
let analyzer = SCSensitivityAnalyzer()
|
||||
switch mediaType {
|
||||
case .image:
|
||||
analyzer.analyzeImage(at: url) { analysisResult, analysisError in
|
||||
completion(analysisResult?.isSensitive, analysisError)
|
||||
}
|
||||
case .video:
|
||||
Task {
|
||||
do {
|
||||
let handler = analyzer.videoAnalysis(forFileAt: url)
|
||||
let response = try await handler.hasSensitiveContent()
|
||||
completion(response.isSensitive, nil)
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(false, nil)
|
||||
}
|
||||
}
|
||||
9
Swiftgram/SGDBReset/BUILD
Normal file
9
Swiftgram/SGDBReset/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "SGDBReset",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
47
Swiftgram/SGDBReset/Sources/File.swift
Normal file
47
Swiftgram/SGDBReset/Sources/File.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
import UIKit
|
||||
import Foundation
|
||||
import SGLogging
|
||||
|
||||
private let dbResetKey = "sg_db_reset"
|
||||
|
||||
public func sgDBResetIfNeeded(databasePath: String, present: ((UIViewController) -> ())?) {
|
||||
guard UserDefaults.standard.bool(forKey: dbResetKey) else {
|
||||
return
|
||||
}
|
||||
NSLog("[SG.DBReset] Resetting DB with system settings")
|
||||
let alert = UIAlertController(
|
||||
title: "Database reset.\nPlease wait...",
|
||||
message: nil,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
present?(alert)
|
||||
do {
|
||||
let _ = try FileManager.default.removeItem(atPath: databasePath)
|
||||
NSLog("[SG.DBReset] Done. Reset completed")
|
||||
let successAlert = UIAlertController(
|
||||
title: "Database reset completed",
|
||||
message: nil,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
successAlert.addAction(UIAlertAction(title: "Restart App", style: .cancel) { _ in
|
||||
exit(0)
|
||||
})
|
||||
successAlert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
alert.dismiss(animated: false) {
|
||||
present?(successAlert)
|
||||
}
|
||||
} catch {
|
||||
NSLog("[SG.DBReset] ERROR. Failed to reset database: \(error)")
|
||||
let failAlert = UIAlertController(
|
||||
title: "ERROR. Failed to reset database",
|
||||
message: "\(error)",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.dismiss(animated: false) {
|
||||
present?(failAlert)
|
||||
}
|
||||
}
|
||||
UserDefaults.standard.set(false, forKey: dbResetKey)
|
||||
// let semaphore = DispatchSemaphore(value: 0)
|
||||
// semaphore.wait()
|
||||
}
|
||||
48
Swiftgram/SGDebugUI/BUILD
Normal file
48
Swiftgram/SGDebugUI/BUILD
Normal file
@@ -0,0 +1,48 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
config_setting(
|
||||
name = "debug_build",
|
||||
values = {
|
||||
"compilation_mode": "dbg",
|
||||
},
|
||||
)
|
||||
|
||||
flex_dependency = select({
|
||||
":debug_build": [
|
||||
"@flex_sdk//:FLEX"
|
||||
],
|
||||
"//conditions:default": [],
|
||||
})
|
||||
|
||||
|
||||
swift_library(
|
||||
name = "SGDebugUI",
|
||||
module_name = "SGDebugUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//Swiftgram/SGItemListUI:SGItemListUI",
|
||||
"//Swiftgram/SGLogging:SGLogging",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//Swiftgram/SGStrings:SGStrings",
|
||||
"//Swiftgram/SGSwiftUI:SGSwiftUI",
|
||||
"//submodules/LegacyUI:LegacyUI",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
] + flex_dependency,
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
986
Swiftgram/SGDebugUI/Sources/SGDebugUI.swift
Normal file
986
Swiftgram/SGDebugUI/Sources/SGDebugUI.swift
Normal file
@@ -0,0 +1,986 @@
|
||||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
import SGItemListUI
|
||||
import UndoUI
|
||||
import AccountContext
|
||||
import Display
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import ItemListUI
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
|
||||
// Optional
|
||||
import SGSimpleSettings
|
||||
import SGLogging
|
||||
import OverlayStatusController
|
||||
#if DEBUG
|
||||
import FLEX
|
||||
#endif
|
||||
import Security
|
||||
|
||||
|
||||
let BACKUP_SERVICE: String = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
|
||||
|
||||
enum KeychainError: Error {
|
||||
case duplicateEntry
|
||||
case unknown(OSStatus)
|
||||
case itemNotFound
|
||||
case invalidItemFormat
|
||||
}
|
||||
|
||||
class KeychainBackupManager {
|
||||
static let shared = KeychainBackupManager()
|
||||
private let service = "\(Bundle.main.bundleIdentifier!).sessionsbackup"
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Save Credentials
|
||||
func saveSession(id: String, _ session: Data) throws {
|
||||
// Create query dictionary
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: id,
|
||||
kSecValueData as String: session,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
|
||||
]
|
||||
|
||||
// Add to keychain
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
|
||||
if status == errSecDuplicateItem {
|
||||
// Item already exists, update it
|
||||
let updateQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: id
|
||||
]
|
||||
|
||||
let attributesToUpdate: [String: Any] = [
|
||||
kSecValueData as String: session
|
||||
]
|
||||
|
||||
let updateStatus = SecItemUpdate(updateQuery as CFDictionary,
|
||||
attributesToUpdate as CFDictionary)
|
||||
|
||||
if updateStatus != errSecSuccess {
|
||||
throw KeychainError.unknown(updateStatus)
|
||||
}
|
||||
} else if status != errSecSuccess {
|
||||
throw KeychainError.unknown(status)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Retrieve Credentials
|
||||
func retrieveSession(for id: String) throws -> Data {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: id,
|
||||
kSecReturnData as String: true
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
guard status == errSecSuccess, let sessionData = result as? Data else {
|
||||
throw KeychainError.itemNotFound
|
||||
}
|
||||
|
||||
return sessionData
|
||||
}
|
||||
|
||||
// MARK: - Delete Credentials
|
||||
func deleteSession(for id: String) throws {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: id
|
||||
]
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
if status != errSecSuccess && status != errSecItemNotFound {
|
||||
throw KeychainError.unknown(status)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Retrieve All Accounts
|
||||
func getAllSessons() throws -> [Data] {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
|
||||
if status == errSecItemNotFound {
|
||||
return []
|
||||
}
|
||||
|
||||
guard status == errSecSuccess,
|
||||
let credentialsDataArray = result as? [Data] else {
|
||||
throw KeychainError.unknown(status)
|
||||
}
|
||||
|
||||
return credentialsDataArray
|
||||
}
|
||||
|
||||
// MARK: - Delete All Sessions
|
||||
func deleteAllSessions() throws {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service
|
||||
]
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
|
||||
// If no items were found, that's fine - just return
|
||||
if status == errSecItemNotFound {
|
||||
return
|
||||
}
|
||||
|
||||
// For any other error, throw
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError.unknown(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionBackup: Codable {
|
||||
var name: String? = nil
|
||||
var date: Date = Date()
|
||||
let accountRecord: AccountRecord<TelegramAccountManagerTypes.Attribute>
|
||||
|
||||
var peerIdInternal: Int64 {
|
||||
var userId: Int64 = 0
|
||||
for attribute in accountRecord.attributes {
|
||||
if case let .backupData(backupData) = attribute, let backupPeerID = backupData.data?.peerId {
|
||||
userId = backupPeerID
|
||||
break
|
||||
}
|
||||
}
|
||||
return userId
|
||||
}
|
||||
|
||||
var userId: Int64 {
|
||||
return PeerId(peerIdInternal).id._internalGetInt64Value()
|
||||
}
|
||||
}
|
||||
|
||||
import SwiftUI
|
||||
import SGSwiftUI
|
||||
import LegacyUI
|
||||
import SGStrings
|
||||
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct SessionBackupRow: View {
|
||||
let backup: SessionBackup
|
||||
let isLoggedIn: Bool
|
||||
|
||||
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .short
|
||||
return formatter
|
||||
}()
|
||||
|
||||
var formattedDate: String {
|
||||
if #available(iOS 15.0, *) {
|
||||
return backup.date.formatted(date: .abbreviated, time: .shortened)
|
||||
} else {
|
||||
return dateFormatter.string(from: backup.date)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(backup.name ?? String(backup.userId))
|
||||
.font(.body)
|
||||
|
||||
Text("ID: \(backup.userId)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Last Backup: \(formattedDate)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(isLoggedIn ? "Logged In" : "Logged Out")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct BorderedButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.accentColor, lineWidth: 1)
|
||||
)
|
||||
.opacity(configuration.isPressed ? 0.7 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct SessionBackupManagerView: View {
|
||||
weak var wrapperController: LegacyController?
|
||||
let context: AccountContext
|
||||
|
||||
@State private var sessions: [SessionBackup] = []
|
||||
@State private var loggedInPeerIDs: [Int64] = []
|
||||
@State private var loggedInAccountsDisposable: Disposable? = nil
|
||||
|
||||
private func performBackup() {
|
||||
let controller = OverlayStatusController(theme: context.sharedContext.currentPresentationData.with { $0 }.theme, type: .loading(cancelled: nil))
|
||||
|
||||
let signal = context.sharedContext.accountManager.accountRecords()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let signal2 = context.sharedContext.activeAccountsWithInfo
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
|
||||
Task {
|
||||
let (view, accountsWithInfo) = await combineLatest(signal, signal2).awaitable()
|
||||
backupSessionsFromView(view, accountsWithInfo: accountsWithInfo.1)
|
||||
withAnimation {
|
||||
sessions = getBackedSessions()
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func performRestore() {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
|
||||
let _ = (context.sharedContext.accountManager.accountRecords()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controller] view in
|
||||
|
||||
let backupSessions = getBackedSessions()
|
||||
var restoredSessions: Int64 = 0
|
||||
|
||||
func importNextBackup(index: Int) {
|
||||
// Check if we're done
|
||||
if index >= backupSessions.count {
|
||||
// All done, update UI
|
||||
withAnimation {
|
||||
sessions = getBackedSessions()
|
||||
}
|
||||
controller?.dismiss()
|
||||
wrapperController?.present(
|
||||
okUndoController("OK: \(restoredSessions) Sessions restored", presentationData),
|
||||
in: .current
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let backup = backupSessions[index]
|
||||
|
||||
// Check for existing record
|
||||
let existingRecord = view.records.first { record in
|
||||
var userId: Int64 = 0
|
||||
for attribute in record.attributes {
|
||||
if case let .backupData(backupData) = attribute {
|
||||
userId = backupData.data?.peerId ?? 0
|
||||
}
|
||||
}
|
||||
return userId == backup.peerIdInternal
|
||||
}
|
||||
|
||||
if existingRecord != nil {
|
||||
print("Record \(backup.userId) already exists, skipping")
|
||||
importNextBackup(index: index + 1)
|
||||
return
|
||||
}
|
||||
|
||||
var importAttributes = backup.accountRecord.attributes
|
||||
importAttributes.removeAll { attribute in
|
||||
if case .sortOrder = attribute {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let importBackupSignal = context.sharedContext.accountManager.transaction { transaction -> Void in
|
||||
let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in
|
||||
for attribute in record.attributes {
|
||||
if case let .sortOrder(sortOrder) = attribute {
|
||||
return sortOrder.order
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}).max() ?? 0) + 1
|
||||
importAttributes.append(.sortOrder(AccountSortOrderAttribute(order: nextSortOrder)))
|
||||
let accountRecordId = transaction.createRecord(importAttributes)
|
||||
print("Imported record \(accountRecordId) for \(backup.userId)")
|
||||
restoredSessions += 1
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let _ = importBackupSignal.start(completed: {
|
||||
importNextBackup(index: index + 1)
|
||||
})
|
||||
}
|
||||
|
||||
// Start the import chain
|
||||
importNextBackup(index: 0)
|
||||
})
|
||||
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
}
|
||||
|
||||
private func performDeleteAll() {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = textAlertController(context: context, title: "Delete All Backups?", text: "All sessions will be removed from Keychain.\n\nAccounts will not be logged out from Swiftgram.", actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
do {
|
||||
try KeychainBackupManager.shared.deleteAllSessions()
|
||||
withAnimation {
|
||||
sessions = getBackedSessions()
|
||||
}
|
||||
controller.dismiss()
|
||||
} catch let e {
|
||||
print("Error deleting all sessions: \(e)")
|
||||
}
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
|
||||
])
|
||||
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
}
|
||||
|
||||
private func performDelete(_ session: SessionBackup) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = textAlertController(context: context, title: "Delete 1 Backup?", text: "\(session.name ?? "\(session.userId)") session will be removed from Keychain.\n\nAccount will not be logged out from Swiftgram.", actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
do {
|
||||
try KeychainBackupManager.shared.deleteSession(for: "\(session.peerIdInternal)")
|
||||
withAnimation {
|
||||
sessions = getBackedSessions()
|
||||
}
|
||||
controller.dismiss()
|
||||
} catch let e {
|
||||
print("Error deleting session: \(e)")
|
||||
}
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
|
||||
])
|
||||
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
private func performRemoveSessionFromApp(session: SessionBackup) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let controller = textAlertController(context: context, title: "Remove session from App?", text: "\(session.name ?? "\(session.userId)") session will be removed from app? Account WILL BE logged out of Swiftgram.", actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
|
||||
let signal = context.sharedContext.accountManager.accountRecords()
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let _ = signal.start(next: { [weak controller] view in
|
||||
|
||||
// Find record to delete
|
||||
let accountRecord = view.records.first { record in
|
||||
var userId: Int64 = 0
|
||||
for attribute in record.attributes {
|
||||
if case let .backupData(backupData) = attribute {
|
||||
userId = backupData.data?.peerId ?? 0
|
||||
}
|
||||
}
|
||||
return userId == session.peerIdInternal
|
||||
}
|
||||
|
||||
if let record = accountRecord {
|
||||
let deleteSignal = context.sharedContext.accountManager.transaction { transaction -> Void in
|
||||
transaction.updateRecord(record.id, { _ in return nil})
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let _ = deleteSignal.start(next: {
|
||||
withAnimation {
|
||||
sessions = getBackedSessions()
|
||||
}
|
||||
controller?.dismiss()
|
||||
})
|
||||
} else {
|
||||
controller?.dismiss()
|
||||
}
|
||||
})
|
||||
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
|
||||
])
|
||||
|
||||
wrapperController?.present(controller, in: .window(.root), with: nil)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Actions")) {
|
||||
Button(action: performBackup) {
|
||||
HStack {
|
||||
Image(systemName: "key.fill")
|
||||
.frame(width: 30)
|
||||
Text("Backup to Keychain")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: performRestore) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.2.circlepath")
|
||||
.frame(width: 30)
|
||||
Text("Restore from Keychain")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: performDeleteAll) {
|
||||
HStack {
|
||||
Image(systemName: "trash")
|
||||
.frame(width: 30)
|
||||
Text("Delete Keychain Backup")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
// Text("Removing sessions from Keychain. This will not affect logged-in accounts.")
|
||||
// .font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Backups")) {
|
||||
ForEach(sessions, id: \.peerIdInternal) { session in
|
||||
SessionBackupRow(
|
||||
backup: session,
|
||||
isLoggedIn: loggedInPeerIDs.contains(session.peerIdInternal)
|
||||
)
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
performDelete(session)
|
||||
}, label: {
|
||||
HStack(spacing: 4) {
|
||||
Text("Delete from Backup")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
})
|
||||
#if DEBUG
|
||||
Button(action: {
|
||||
performRemoveSessionFromApp(session: session)
|
||||
}, label: {
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text("Remove from App")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
})
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// .onDelete { indexSet in
|
||||
// performDelete(indexSet)
|
||||
// }
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
sessions = getBackedSessions()
|
||||
}
|
||||
|
||||
let accountsSignal = context.sharedContext.accountManager.accountRecords()
|
||||
|> deliverOnMainQueue
|
||||
|
||||
loggedInAccountsDisposable = accountsSignal.start(next: { view in
|
||||
var result: [Int64] = []
|
||||
for record in view.records {
|
||||
var isLoggedOut: Bool = false
|
||||
var userId: Int64 = 0
|
||||
for attribute in record.attributes {
|
||||
if case .loggedOut = attribute {
|
||||
isLoggedOut = true
|
||||
} else if case let .backupData(backupData) = attribute {
|
||||
userId = backupData.data?.peerId ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
if !isLoggedOut && userId != 0 {
|
||||
result.append(userId)
|
||||
}
|
||||
}
|
||||
|
||||
print("Will check logged in accounts")
|
||||
if loggedInPeerIDs != result {
|
||||
print("Updating logged in accounts", result)
|
||||
loggedInPeerIDs = result
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
.onDisappear {
|
||||
loggedInAccountsDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func getBackedSessions() -> [SessionBackup] {
|
||||
var sessions: [SessionBackup] = []
|
||||
do {
|
||||
let backupSessionsData = try KeychainBackupManager.shared.getAllSessons()
|
||||
for sessionBackupData in backupSessionsData {
|
||||
do {
|
||||
let backup = try JSONDecoder().decode(SessionBackup.self, from: sessionBackupData)
|
||||
sessions.append(backup)
|
||||
} catch let e {
|
||||
print("IMPORT ERROR: \(e)")
|
||||
}
|
||||
}
|
||||
} catch let e {
|
||||
print("Error getting all sessions: \(e)")
|
||||
}
|
||||
return sessions
|
||||
}
|
||||
|
||||
|
||||
func backupSessionsFromView(_ view: AccountRecordsView<TelegramAccountManagerTypes>, accountsWithInfo: [AccountWithInfo] = []) {
|
||||
var recordsToBackup: [Int64: AccountRecord<TelegramAccountManagerTypes.Attribute>] = [:]
|
||||
for record in view.records {
|
||||
var sortOrder: Int32 = 0
|
||||
var isLoggedOut: Bool = false
|
||||
var isTestingEnvironment: Bool = false
|
||||
var peerId: Int64 = 0
|
||||
for attribute in record.attributes {
|
||||
if case let .sortOrder(value) = attribute {
|
||||
sortOrder = value.order
|
||||
} else if case .loggedOut = attribute {
|
||||
isLoggedOut = true
|
||||
} else if case let .environment(environment) = attribute, case .test = environment.environment {
|
||||
isTestingEnvironment = true
|
||||
} else if case let .backupData(backupData) = attribute {
|
||||
peerId = backupData.data?.peerId ?? 0
|
||||
}
|
||||
}
|
||||
let _ = sortOrder
|
||||
let _ = isTestingEnvironment
|
||||
|
||||
if !isLoggedOut && peerId != 0 {
|
||||
recordsToBackup[peerId] = record
|
||||
}
|
||||
}
|
||||
|
||||
for (peerId, record) in recordsToBackup {
|
||||
var backupName: String? = nil
|
||||
if let accountWithInfo = accountsWithInfo.first(where: { $0.peer.id == PeerId(peerId) }) {
|
||||
if let user = accountWithInfo.peer as? TelegramUser {
|
||||
if let username = user.username {
|
||||
backupName = "@\(username)"
|
||||
} else {
|
||||
backupName = user.nameOrPhone
|
||||
}
|
||||
}
|
||||
}
|
||||
let backup = SessionBackup(name: backupName, accountRecord: record)
|
||||
do {
|
||||
let data = try JSONEncoder().encode(backup)
|
||||
try KeychainBackupManager.shared.saveSession(id: "\(backup.peerIdInternal)", data)
|
||||
} catch let e {
|
||||
print("BACKUP ERROR: \(e)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func sgSessionBackupManagerController(context: AccountContext, presentationData: PresentationData? = nil) -> ViewController {
|
||||
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
|
||||
let strings = presentationData?.strings ?? defaultPresentationStrings
|
||||
|
||||
let legacyController = LegacySwiftUIController(
|
||||
presentation: .navigation,
|
||||
theme: theme,
|
||||
strings: strings
|
||||
)
|
||||
legacyController.statusBar.statusBarStyle = theme.rootController
|
||||
.statusBarStyle.style
|
||||
legacyController.title = "Session Backup" //i18n("BackupManager.Title", strings.baseLanguageCode)
|
||||
|
||||
let swiftUIView = SGSwiftUIView<SessionBackupManagerView>(
|
||||
navigationBarHeight: legacyController.navigationBarHeightModel,
|
||||
containerViewLayout: legacyController.containerViewLayoutModel,
|
||||
content: {
|
||||
SessionBackupManagerView(wrapperController: legacyController, context: context)
|
||||
}
|
||||
)
|
||||
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
||||
legacyController.bind(controller: controller)
|
||||
|
||||
return legacyController
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct MessageFilterKeywordInputFieldModifier: ViewModifier {
|
||||
@Binding var newKeyword: String
|
||||
let onAdd: () -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 15.0, *) {
|
||||
content
|
||||
.submitLabel(.return)
|
||||
.submitScope(false) // TODO(swiftgram): Keyboard still closing
|
||||
.interactiveDismissDisabled()
|
||||
.onSubmit {
|
||||
onAdd()
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct MessageFilterKeywordInputView: View {
|
||||
@Binding var newKeyword: String
|
||||
let onAdd: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
TextField("Enter keyword", text: $newKeyword)
|
||||
.autocorrectionDisabled(true)
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.default)
|
||||
.modifier(MessageFilterKeywordInputFieldModifier(newKeyword: $newKeyword, onAdd: onAdd))
|
||||
|
||||
|
||||
Button(action: onAdd) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundColor(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .secondary : .accentColor)
|
||||
.imageScale(.large)
|
||||
}
|
||||
.disabled(newKeyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
struct MessageFilterView: View {
|
||||
weak var wrapperController: LegacyController?
|
||||
|
||||
@State private var newKeyword: String = ""
|
||||
@State private var keywords: [String] {
|
||||
didSet {
|
||||
SGSimpleSettings.shared.messageFilterKeywords = keywords
|
||||
}
|
||||
}
|
||||
|
||||
init(wrapperController: LegacyController?) {
|
||||
self.wrapperController = wrapperController
|
||||
_keywords = State(initialValue: SGSimpleSettings.shared.messageFilterKeywords)
|
||||
}
|
||||
|
||||
var bodyContent: some View {
|
||||
List {
|
||||
Section {
|
||||
// Icon and title
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "nosign.app.fill")
|
||||
.font(.system(size: 50))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Message Filter")
|
||||
.font(.title)
|
||||
.bold()
|
||||
|
||||
Text("Remove distraction and reduce visibility of messages containing keywords below.\nKeywords are case-sensitive.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.listRowInsets(EdgeInsets())
|
||||
|
||||
}
|
||||
|
||||
Section {
|
||||
MessageFilterKeywordInputView(newKeyword: $newKeyword, onAdd: addKeyword)
|
||||
}
|
||||
|
||||
Section(header: Text("Keywords")) {
|
||||
ForEach(keywords.reversed(), id: \.self) { keyword in
|
||||
Text(keyword)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
let originalIndices = IndexSet(
|
||||
indexSet.map { keywords.count - 1 - $0 }
|
||||
)
|
||||
deleteKeywords(at: originalIndices)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tgNavigationBackButton(wrapperController: wrapperController)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
if #available(iOS 14.0, *) {
|
||||
bodyContent
|
||||
.toolbar {
|
||||
EditButton()
|
||||
}
|
||||
} else {
|
||||
bodyContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addKeyword() {
|
||||
let trimmedKeyword = newKeyword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedKeyword.isEmpty else { return }
|
||||
|
||||
let keywordExists = keywords.contains {
|
||||
$0 == trimmedKeyword
|
||||
}
|
||||
|
||||
guard !keywordExists else {
|
||||
return
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
keywords.append(trimmedKeyword)
|
||||
}
|
||||
newKeyword = ""
|
||||
|
||||
}
|
||||
|
||||
private func deleteKeywords(at offsets: IndexSet) {
|
||||
withAnimation {
|
||||
keywords.remove(atOffsets: offsets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
public func sgMessageFilterController(presentationData: PresentationData? = nil) -> ViewController {
|
||||
let theme = presentationData?.theme ?? (UITraitCollection.current.userInterfaceStyle == .dark ? defaultDarkColorPresentationTheme : defaultPresentationTheme)
|
||||
let strings = presentationData?.strings ?? defaultPresentationStrings
|
||||
|
||||
let legacyController = LegacySwiftUIController(
|
||||
presentation: .navigation,
|
||||
theme: theme,
|
||||
strings: strings
|
||||
)
|
||||
// Status bar color will break if theme changed
|
||||
legacyController.statusBar.statusBarStyle = theme.rootController
|
||||
.statusBarStyle.style
|
||||
legacyController.displayNavigationBar = false
|
||||
let swiftUIView = MessageFilterView(wrapperController: legacyController)
|
||||
let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true)
|
||||
legacyController.bind(controller: controller)
|
||||
|
||||
return legacyController
|
||||
}
|
||||
|
||||
|
||||
private enum SGDebugControllerSection: Int32, SGItemListSection {
|
||||
case base
|
||||
}
|
||||
|
||||
private enum SGDebugDisclosureLink: String {
|
||||
case sessionBackupManager
|
||||
case messageFilter
|
||||
}
|
||||
|
||||
private enum SGDebugActions: String {
|
||||
case flexing
|
||||
case fileManager
|
||||
case clearRegDateCache
|
||||
}
|
||||
|
||||
private enum SGDebugToggles: String {
|
||||
case forceImmediateShareSheet
|
||||
case legacyNotificationsFix
|
||||
}
|
||||
|
||||
|
||||
private typealias SGDebugControllerEntry = SGItemListUIEntry<SGDebugControllerSection, SGDebugToggles, AnyHashable, AnyHashable, SGDebugDisclosureLink, SGDebugActions>
|
||||
|
||||
private func SGDebugControllerEntries(presentationData: PresentationData) -> [SGDebugControllerEntry] {
|
||||
var entries: [SGDebugControllerEntry] = []
|
||||
|
||||
let id = SGItemListCounter()
|
||||
#if DEBUG
|
||||
entries.append(.action(id: id.count, section: .base, actionType: .flexing, text: "FLEX", kind: .generic))
|
||||
entries.append(.action(id: id.count, section: .base, actionType: .fileManager, text: "FileManager", kind: .generic))
|
||||
#endif
|
||||
|
||||
if SGSimpleSettings.shared.b {
|
||||
entries.append(.disclosure(id: id.count, section: .base, link: .sessionBackupManager, text: "Session Backup"))
|
||||
entries.append(.disclosure(id: id.count, section: .base, link: .messageFilter, text: "Message Filter"))
|
||||
}
|
||||
entries.append(.action(id: id.count, section: .base, actionType: .clearRegDateCache, text: "Clear Regdate cache", kind: .generic))
|
||||
entries.append(.toggle(id: id.count, section: .base, settingName: .forceImmediateShareSheet, value: SGSimpleSettings.shared.forceSystemSharing, text: "Force System Share Sheet", enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .base, settingName: .legacyNotificationsFix, value: SGSimpleSettings.shared.legacyNotificationsFix, text: "[Legacy] Fix empty notifications", enabled: true))
|
||||
|
||||
return entries
|
||||
}
|
||||
private func okUndoController(_ text: String, _ presentationData: PresentationData) -> UndoOverlayController {
|
||||
return UndoOverlayController(presentationData: presentationData, content: .succeed(text: text, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })
|
||||
}
|
||||
|
||||
|
||||
public func sgDebugController(context: AccountContext) -> ViewController {
|
||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
let simplePromise = ValuePromise(true, ignoreRepeated: false)
|
||||
|
||||
let arguments = SGItemListArguments<SGDebugToggles, AnyHashable, AnyHashable, SGDebugDisclosureLink, SGDebugActions>(context: context, setBoolValue: { toggleName, value in
|
||||
switch toggleName {
|
||||
case .forceImmediateShareSheet:
|
||||
SGSimpleSettings.shared.forceSystemSharing = value
|
||||
case .legacyNotificationsFix:
|
||||
SGSimpleSettings.shared.legacyNotificationsFix = value
|
||||
}
|
||||
}, openDisclosureLink: { link in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
switch (link) {
|
||||
case .sessionBackupManager:
|
||||
if #available(iOS 13.0, *) {
|
||||
pushControllerImpl?(sgSessionBackupManagerController(context: context, presentationData: presentationData))
|
||||
} else {
|
||||
presentControllerImpl?(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
|
||||
elevatedLayout: false,
|
||||
action: { _ in return false }
|
||||
), nil)
|
||||
}
|
||||
case .messageFilter:
|
||||
if #available(iOS 13.0, *) {
|
||||
pushControllerImpl?(sgMessageFilterController(presentationData: presentationData))
|
||||
} else {
|
||||
presentControllerImpl?(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: nil, text: "Update OS to access this feature", timeout: nil, customUndoText: nil),
|
||||
elevatedLayout: false,
|
||||
action: { _ in return false }
|
||||
), nil)
|
||||
}
|
||||
}
|
||||
}, action: { actionType in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
switch actionType {
|
||||
case .clearRegDateCache:
|
||||
SGLogger.shared.log("SGDebug", "Regdate cache cleanup init")
|
||||
|
||||
/*
|
||||
let spinner = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||
|
||||
presentControllerImpl?(spinner, nil)
|
||||
*/
|
||||
SGSimpleSettings.shared.regDateCache.drop()
|
||||
SGLogger.shared.log("SGDebug", "Regdate cache cleanup succesfull")
|
||||
presentControllerImpl?(okUndoController("OK: Regdate cache cleaned", presentationData), nil)
|
||||
/*
|
||||
Queue.mainQueue().async() { [weak spinner] in
|
||||
spinner?.dismiss()
|
||||
}
|
||||
*/
|
||||
case .flexing:
|
||||
#if DEBUG
|
||||
FLEXManager.shared.toggleExplorer()
|
||||
#endif
|
||||
case .fileManager:
|
||||
#if DEBUG
|
||||
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
if let maybeAppGroupUrl = maybeAppGroupUrl {
|
||||
if let fileManager = FLEXFileBrowserController(path: maybeAppGroupUrl.path) {
|
||||
FLEXManager.shared.showExplorer()
|
||||
let flexNavigation = FLEXNavigationController(rootViewController: fileManager)
|
||||
FLEXManager.shared.presentTool({ return flexNavigation })
|
||||
}
|
||||
} else {
|
||||
presentControllerImpl?(UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: nil, text: "Empty path", timeout: nil, customUndoText: nil),
|
||||
elevatedLayout: false,
|
||||
action: { _ in return false }
|
||||
),
|
||||
nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
})
|
||||
|
||||
let signal = combineLatest(context.sharedContext.presentationData, simplePromise.get())
|
||||
|> map { presentationData, _ -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
|
||||
let entries = SGDebugControllerEntries(presentationData: presentationData)
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ )
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
presentControllerImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
(controller?.navigationController as? NavigationController)?.pushViewController(c)
|
||||
}
|
||||
// Workaround
|
||||
let _ = pushControllerImpl
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
|
||||
18
Swiftgram/SGDeviceToken/BUILD
Normal file
18
Swiftgram/SGDeviceToken/BUILD
Normal file
@@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGDeviceToken",
|
||||
module_name = "SGDeviceToken",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
31
Swiftgram/SGDeviceToken/Sources/File.swift
Normal file
31
Swiftgram/SGDeviceToken/Sources/File.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import SwiftSignalKit
|
||||
import DeviceCheck
|
||||
|
||||
public enum SGDeviceTokenError {
|
||||
case unsupportedDevice
|
||||
case generic(String)
|
||||
}
|
||||
|
||||
public func getDeviceToken() -> Signal<String, SGDeviceTokenError> {
|
||||
return Signal { subscriber in
|
||||
let currentDevice = DCDevice.current
|
||||
if currentDevice.isSupported {
|
||||
currentDevice.generateToken { (data, error) in
|
||||
guard error == nil else {
|
||||
subscriber.putError(.generic(error!.localizedDescription))
|
||||
return
|
||||
}
|
||||
if let tokenData = data {
|
||||
subscriber.putNext(tokenData.base64EncodedString())
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(.generic("Empty Token"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscriber.putError(.unsupportedDevice)
|
||||
}
|
||||
return ActionDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Swiftgram/SGDoubleTapMessageAction/BUILD
Normal file
9
Swiftgram/SGDoubleTapMessageAction/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "SGDoubleTapMessageAction",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
import SGSimpleSettings
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
|
||||
func sgDoubleTapMessageAction(incoming: Bool, message: Message) -> String {
|
||||
if incoming {
|
||||
return SGSimpleSettings.MessageDoubleTapAction.default.rawValue
|
||||
} else {
|
||||
return SGSimpleSettings.shared.messageDoubleTapActionOutgoing
|
||||
}
|
||||
}
|
||||
9
Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD
Normal file
9
Swiftgram/SGEmojiKeyboardDefaultFirst/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "SGEmojiKeyboardDefaultFirst",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
func sgPatchEmojiKeyboardItems(_ items: [EmojiPagerContentComponent.ItemGroup]) -> [EmojiPagerContentComponent.ItemGroup] {
|
||||
var items = items
|
||||
let staticEmojisIndex = items.firstIndex { item in
|
||||
if let groupId = item.groupId.base as? String, groupId == "static" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
let recentEmojisIndex = items.firstIndex { item in
|
||||
if let groupId = item.groupId.base as? String, groupId == "recent" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if let staticEmojisIndex = staticEmojisIndex {
|
||||
let staticEmojiItem = items.remove(at: staticEmojisIndex)
|
||||
items.insert(staticEmojiItem, at: (recentEmojisIndex ?? -1) + 1 )
|
||||
}
|
||||
return items
|
||||
}
|
||||
9
Swiftgram/SGIQTP/BUILD
Normal file
9
Swiftgram/SGIQTP/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "SGIQTP",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
76
Swiftgram/SGIQTP/Sources/SGIQTP.swift
Normal file
76
Swiftgram/SGIQTP/Sources/SGIQTP.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
import SGConfig
|
||||
import SGLogging
|
||||
|
||||
|
||||
public struct SGIQTPResponse {
|
||||
public let status: Int
|
||||
public let description: String?
|
||||
public let text: String?
|
||||
}
|
||||
|
||||
public func makeIqtpQuery(_ api: Int, _ method: String, _ args: [String] = []) -> String {
|
||||
let buildNumber = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] ?? ""
|
||||
let baseQuery = "tp:\(api):\(buildNumber):\(method)"
|
||||
if args.isEmpty {
|
||||
return baseQuery
|
||||
}
|
||||
return baseQuery + ":" + args.joined(separator: ":")
|
||||
}
|
||||
|
||||
public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<SGIQTPResponse?, NoError> {
|
||||
#if DEBUG
|
||||
SGLogger.shared.log("SGIQTP", "Query: \(query)")
|
||||
#else
|
||||
SGLogger.shared.log("SGIQTP", "Query")
|
||||
#endif
|
||||
return engine.peers.resolvePeerByName(name: SG_CONFIG.botUsername, referrer: nil)
|
||||
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
|
||||
guard case let .result(result) = result else {
|
||||
SGLogger.shared.log("SGIQTP", "Failed to resolve peer")
|
||||
return .complete()
|
||||
}
|
||||
return .single(result)
|
||||
}
|
||||
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
|
||||
guard let peer = peer else {
|
||||
SGLogger.shared.log("SGIQTP", "Empty peer")
|
||||
return .single(nil)
|
||||
}
|
||||
return engine.messages.requestChatContextResults(botId: peer.id, peerId: engine.account.peerId, query: query, offset: "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
|
||||
|> map { results -> ChatContextResultCollection? in
|
||||
return results?.results
|
||||
}
|
||||
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
|
||||
SGLogger.shared.log("SGIQTP", "Failed to request inline results")
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> map { contextResult -> SGIQTPResponse? in
|
||||
guard let contextResult, let firstResult = contextResult.results.first else {
|
||||
SGLogger.shared.log("SGIQTP", "Empty inline result")
|
||||
return nil
|
||||
}
|
||||
|
||||
var t: String?
|
||||
if case let .text(text, _, _, _, _) = firstResult.message {
|
||||
t = text
|
||||
}
|
||||
|
||||
var status = 400
|
||||
if let title = firstResult.title {
|
||||
status = Int(title) ?? 400
|
||||
}
|
||||
let response = SGIQTPResponse(
|
||||
status: status,
|
||||
description: firstResult.description,
|
||||
text: t
|
||||
)
|
||||
SGLogger.shared.log("SGIQTP", "Response: \(response)")
|
||||
return response
|
||||
}
|
||||
}
|
||||
30
Swiftgram/SGItemListUI/BUILD
Normal file
30
Swiftgram/SGItemListUI/BUILD
Normal file
@@ -0,0 +1,30 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGItemListUI",
|
||||
module_name = "SGItemListUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/MtProtoKit:MtProtoKit",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
333
Swiftgram/SGItemListUI/Sources/SGItemListUI.swift
Normal file
333
Swiftgram/SGItemListUI/Sources/SGItemListUI.swift
Normal file
@@ -0,0 +1,333 @@
|
||||
// MARK: Swiftgram
|
||||
import SGLogging
|
||||
import SGSimpleSettings
|
||||
import SGStrings
|
||||
import SGAPIToken
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import MtProtoKit
|
||||
import MessageUI
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import OverlayStatusController
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import WebKit
|
||||
import PeerNameColorScreen
|
||||
|
||||
public class SGItemListCounter {
|
||||
private var _count = 0
|
||||
|
||||
public init() {}
|
||||
|
||||
public var count: Int {
|
||||
_count += 1
|
||||
return _count
|
||||
}
|
||||
|
||||
public func increment(_ amount: Int) {
|
||||
_count += amount
|
||||
}
|
||||
|
||||
public func countWith(_ amount: Int) -> Int {
|
||||
_count += amount
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public protocol SGItemListSection: Equatable {
|
||||
var rawValue: Int32 { get }
|
||||
}
|
||||
|
||||
public final class SGItemListArguments<BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable> {
|
||||
let context: AccountContext
|
||||
//
|
||||
let setBoolValue: (BoolSetting, Bool) -> Void
|
||||
let updateSliderValue: (SliderSetting, Int32) -> Void
|
||||
let setOneFromManyValue: (OneFromManySetting) -> Void
|
||||
let openDisclosureLink: (DisclosureLink) -> Void
|
||||
let action: (ActionType) -> Void
|
||||
let searchInput: (String) -> Void
|
||||
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
//
|
||||
setBoolValue: @escaping (BoolSetting, Bool) -> Void = { _,_ in },
|
||||
updateSliderValue: @escaping (SliderSetting, Int32) -> Void = { _,_ in },
|
||||
setOneFromManyValue: @escaping (OneFromManySetting) -> Void = { _ in },
|
||||
openDisclosureLink: @escaping (DisclosureLink) -> Void = { _ in},
|
||||
action: @escaping (ActionType) -> Void = { _ in },
|
||||
searchInput: @escaping (String) -> Void = { _ in }
|
||||
) {
|
||||
self.context = context
|
||||
//
|
||||
self.setBoolValue = setBoolValue
|
||||
self.updateSliderValue = updateSliderValue
|
||||
self.setOneFromManyValue = setOneFromManyValue
|
||||
self.openDisclosureLink = openDisclosureLink
|
||||
self.action = action
|
||||
self.searchInput = searchInput
|
||||
}
|
||||
}
|
||||
|
||||
public enum SGItemListUIEntry<Section: SGItemListSection, BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable>: ItemListNodeEntry {
|
||||
case header(id: Int, section: Section, text: String, badge: String?)
|
||||
case toggle(id: Int, section: Section, settingName: BoolSetting, value: Bool, text: String, enabled: Bool)
|
||||
case notice(id: Int, section: Section, text: String)
|
||||
case percentageSlider(id: Int, section: Section, settingName: SliderSetting, value: Int32)
|
||||
case oneFromManySelector(id: Int, section: Section, settingName: OneFromManySetting, text: String, value: String, enabled: Bool)
|
||||
case disclosure(id: Int, section: Section, link: DisclosureLink, text: String)
|
||||
case peerColorDisclosurePreview(id: Int, section: Section, name: String, color: UIColor)
|
||||
case action(id: Int, section: Section, actionType: ActionType, text: String, kind: ItemListActionKind)
|
||||
case searchInput(id: Int, section: Section, title: NSAttributedString, text: String, placeholder: String)
|
||||
|
||||
public var section: ItemListSectionId {
|
||||
switch self {
|
||||
case let .header(_, sectionId, _, _):
|
||||
return sectionId.rawValue
|
||||
case let .toggle(_, sectionId, _, _, _, _):
|
||||
return sectionId.rawValue
|
||||
case let .notice(_, sectionId, _):
|
||||
return sectionId.rawValue
|
||||
|
||||
case let .disclosure(_, sectionId, _, _):
|
||||
return sectionId.rawValue
|
||||
|
||||
case let .percentageSlider(_, sectionId, _, _):
|
||||
return sectionId.rawValue
|
||||
|
||||
case let .peerColorDisclosurePreview(_, sectionId, _, _):
|
||||
return sectionId.rawValue
|
||||
case let .oneFromManySelector(_, sectionId, _, _, _, _):
|
||||
return sectionId.rawValue
|
||||
|
||||
case let .action(_, sectionId, _, _, _):
|
||||
return sectionId.rawValue
|
||||
|
||||
case let .searchInput(_, sectionId, _, _, _):
|
||||
return sectionId.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public var stableId: Int {
|
||||
switch self {
|
||||
case let .header(stableIdValue, _, _, _):
|
||||
return stableIdValue
|
||||
case let .toggle(stableIdValue, _, _, _, _, _):
|
||||
return stableIdValue
|
||||
case let .notice(stableIdValue, _, _):
|
||||
return stableIdValue
|
||||
case let .disclosure(stableIdValue, _, _, _):
|
||||
return stableIdValue
|
||||
case let .percentageSlider(stableIdValue, _, _, _):
|
||||
return stableIdValue
|
||||
case let .peerColorDisclosurePreview(stableIdValue, _, _, _):
|
||||
return stableIdValue
|
||||
case let .oneFromManySelector(stableIdValue, _, _, _, _, _):
|
||||
return stableIdValue
|
||||
case let .action(stableIdValue, _, _, _, _):
|
||||
return stableIdValue
|
||||
case let .searchInput(stableIdValue, _, _, _, _):
|
||||
return stableIdValue
|
||||
}
|
||||
}
|
||||
|
||||
public static func <(lhs: SGItemListUIEntry, rhs: SGItemListUIEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
public static func ==(lhs: SGItemListUIEntry, rhs: SGItemListUIEntry) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.header(id1, section1, text1, badge1), .header(id2, section2, text2, badge2)):
|
||||
return id1 == id2 && section1 == section2 && text1 == text2 && badge1 == badge2
|
||||
|
||||
case let (.toggle(id1, section1, settingName1, value1, text1, enabled1), .toggle(id2, section2, settingName2, value2, text2, enabled2)):
|
||||
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && value1 == value2 && text1 == text2 && enabled1 == enabled2
|
||||
|
||||
case let (.notice(id1, section1, text1), .notice(id2, section2, text2)):
|
||||
return id1 == id2 && section1 == section2 && text1 == text2
|
||||
|
||||
case let (.percentageSlider(id1, section1, settingName1, value1), .percentageSlider(id2, section2, settingName2, value2)):
|
||||
return id1 == id2 && section1 == section2 && value1 == value2 && settingName1 == settingName2
|
||||
|
||||
case let (.disclosure(id1, section1, link1, text1), .disclosure(id2, section2, link2, text2)):
|
||||
return id1 == id2 && section1 == section2 && link1 == link2 && text1 == text2
|
||||
|
||||
case let (.peerColorDisclosurePreview(id1, section1, name1, currentColor1), .peerColorDisclosurePreview(id2, section2, name2, currentColor2)):
|
||||
return id1 == id2 && section1 == section2 && name1 == name2 && currentColor1 == currentColor2
|
||||
|
||||
case let (.oneFromManySelector(id1, section1, settingName1, text1, value1, enabled1), .oneFromManySelector(id2, section2, settingName2, text2, value2, enabled2)):
|
||||
return id1 == id2 && section1 == section2 && settingName1 == settingName2 && text1 == text2 && value1 == value2 && enabled1 == enabled2
|
||||
case let (.action(id1, section1, actionType1, text1, kind1), .action(id2, section2, actionType2, text2, kind2)):
|
||||
return id1 == id2 && section1 == section2 && actionType1 == actionType2 && text1 == text2 && kind1 == kind2
|
||||
|
||||
case let (.searchInput(id1, lhsValue1, lhsValue2, lhsValue3, lhsValue4), .searchInput(id2, rhsValue1, rhsValue2, rhsValue3, rhsValue4)):
|
||||
return id1 == id2 && lhsValue1 == rhsValue1 && lhsValue2 == rhsValue2 && lhsValue3 == rhsValue3 && lhsValue4 == rhsValue4
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! SGItemListArguments<BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>
|
||||
switch self {
|
||||
case let .header(_, _, string, badge):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: string, badge: badge, sectionId: self.section)
|
||||
|
||||
case let .toggle(_, _, setting, value, text, enabled):
|
||||
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in
|
||||
arguments.setBoolValue(setting, value)
|
||||
})
|
||||
case let .notice(_, _, string):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(string), sectionId: self.section)
|
||||
case let .disclosure(_, _, link, text):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks) {
|
||||
arguments.openDisclosureLink(link)
|
||||
}
|
||||
case let .percentageSlider(_, _, setting, value):
|
||||
return SliderPercentageItem(
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
value: value,
|
||||
sectionId: self.section,
|
||||
updated: { value in
|
||||
arguments.updateSliderValue(setting, value)
|
||||
}
|
||||
)
|
||||
|
||||
case let .peerColorDisclosurePreview(_, _, name, color):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: " ", enabled: false, label: name, labelStyle: .semitransparentBadge(color), centerLabelAlignment: true, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: {
|
||||
})
|
||||
|
||||
case let .oneFromManySelector(_, _, settingName, text, value, enabled):
|
||||
return ItemListDisclosureItem(presentationData: presentationData, title: text, enabled: enabled, label: value, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.setOneFromManyValue(settingName)
|
||||
})
|
||||
case let .action(_, _, actionType, text, kind):
|
||||
return ItemListActionItem(presentationData: presentationData, title: text, kind: kind, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.action(actionType)
|
||||
})
|
||||
case let .searchInput(_, _, title, text, placeholder):
|
||||
return ItemListSingleLineInputItem(presentationData: presentationData, title: title, text: text, placeholder: placeholder, returnKeyType: .done, spacing: 3.0, clearType: .always, selectAllOnFocus: true, secondaryStyle: true, sectionId: self.section, textUpdated: { input in arguments.searchInput(input) }, action: {}, dismissKeyboardOnEnter: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func filterSGItemListUIEntrires<Section: SGItemListSection & Hashable, BoolSetting: Hashable, SliderSetting: Hashable, OneFromManySetting: Hashable, DisclosureLink: Hashable, ActionType: Hashable>(
|
||||
entries: [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>],
|
||||
by searchQuery: String?
|
||||
) -> [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>] {
|
||||
|
||||
guard let query = searchQuery?.lowercased(), !query.isEmpty else {
|
||||
return entries
|
||||
}
|
||||
|
||||
var sectionIdsForEntireIncludion: Set<ItemListSectionId> = []
|
||||
var sectionIdsWithMatches: Set<ItemListSectionId> = []
|
||||
var filteredEntries: [SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>] = []
|
||||
|
||||
func entryMatches(_ entry: SGItemListUIEntry<Section, BoolSetting, SliderSetting, OneFromManySetting, DisclosureLink, ActionType>, query: String) -> Bool {
|
||||
switch entry {
|
||||
case .header(_, _, let text, _):
|
||||
return text.lowercased().contains(query)
|
||||
case .toggle(_, _, _, _, let text, _):
|
||||
return text.lowercased().contains(query)
|
||||
case .notice(_, _, let text):
|
||||
return text.lowercased().contains(query)
|
||||
case .percentageSlider:
|
||||
return false // Assuming percentage sliders don't have searchable text
|
||||
case .oneFromManySelector(_, _, _, let text, let value, _):
|
||||
return text.lowercased().contains(query) || value.lowercased().contains(query)
|
||||
case .disclosure(_, _, _, let text):
|
||||
return text.lowercased().contains(query)
|
||||
case .peerColorDisclosurePreview:
|
||||
return false // Never indexed during search
|
||||
case .action(_, _, _, let text, _):
|
||||
return text.lowercased().contains(query)
|
||||
case .searchInput:
|
||||
return true // Never hiding search input
|
||||
}
|
||||
}
|
||||
|
||||
// First pass: identify sections with matches
|
||||
for entry in entries {
|
||||
if entryMatches(entry, query: query) {
|
||||
switch entry {
|
||||
case .searchInput:
|
||||
continue
|
||||
default:
|
||||
sectionIdsWithMatches.insert(entry.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: keep matching entries and headers of sections with matches
|
||||
for (index, entry) in entries.enumerated() {
|
||||
switch entry {
|
||||
case .header:
|
||||
if entryMatches(entry, query: query) {
|
||||
// Will show all entries for the same section
|
||||
sectionIdsForEntireIncludion.insert(entry.section)
|
||||
if !filteredEntries.contains(entry) {
|
||||
filteredEntries.append(entry)
|
||||
}
|
||||
}
|
||||
// Or show header if something from the section already matched
|
||||
if sectionIdsWithMatches.contains(entry.section) {
|
||||
if !filteredEntries.contains(entry) {
|
||||
filteredEntries.append(entry)
|
||||
}
|
||||
}
|
||||
default:
|
||||
if entryMatches(entry, query: query) {
|
||||
if case .notice = entry {
|
||||
// add previous entry to if it's not another notice and if it's not already here
|
||||
// possibly targeting related toggle / setting if we've matched it's description (notice) in search
|
||||
if index > 0 {
|
||||
let previousEntry = entries[index - 1]
|
||||
if case .notice = previousEntry {} else {
|
||||
if !filteredEntries.contains(previousEntry) {
|
||||
filteredEntries.append(previousEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !filteredEntries.contains(entry) {
|
||||
filteredEntries.append(entry)
|
||||
}
|
||||
} else {
|
||||
if !filteredEntries.contains(entry) {
|
||||
filteredEntries.append(entry)
|
||||
}
|
||||
// add next entry if it's notice
|
||||
// possibly targeting description (notice) for the currently search-matched toggle/setting
|
||||
if index < entries.count - 1 {
|
||||
let nextEntry = entries[index + 1]
|
||||
if case .notice = nextEntry {
|
||||
if !filteredEntries.contains(nextEntry) {
|
||||
filteredEntries.append(nextEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if sectionIdsForEntireIncludion.contains(entry.section) {
|
||||
if !filteredEntries.contains(entry) {
|
||||
filteredEntries.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEntries
|
||||
}
|
||||
353
Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift
Normal file
353
Swiftgram/SGItemListUI/Sources/SliderPercentageItem.swift
Normal file
@@ -0,0 +1,353 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import LegacyComponents
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AppBundle
|
||||
|
||||
public class SliderPercentageItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let value: Int32
|
||||
public let sectionId: ItemListSectionId
|
||||
let updated: (Int32) -> Void
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.value = value
|
||||
self.sectionId = sectionId
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = SliderPercentageItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? SliderPercentageItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rescalePercentageValueToSlider(_ value: CGFloat) -> CGFloat {
|
||||
return max(0.0, min(1.0, value))
|
||||
}
|
||||
|
||||
private func rescaleSliderValueToPercentageValue(_ value: CGFloat) -> CGFloat {
|
||||
return max(0.0, min(1.0, value))
|
||||
}
|
||||
|
||||
class SliderPercentageItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private var sliderView: TGPhotoEditorSliderView?
|
||||
private let leftTextNode: ImmediateTextNode
|
||||
private let rightTextNode: ImmediateTextNode
|
||||
private let centerTextNode: ImmediateTextNode
|
||||
private let centerMeasureTextNode: ImmediateTextNode
|
||||
|
||||
private let batteryImage: UIImage?
|
||||
private let batteryBackgroundNode: ASImageNode
|
||||
private let batteryForegroundNode: ASImageNode
|
||||
|
||||
private var item: SliderPercentageItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
// MARK: Swiftgram
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.leftTextNode = ImmediateTextNode()
|
||||
self.rightTextNode = ImmediateTextNode()
|
||||
self.centerTextNode = ImmediateTextNode()
|
||||
self.centerMeasureTextNode = ImmediateTextNode()
|
||||
|
||||
self.batteryImage = nil //UIImage(bundleImageName: "Settings/UsageBatteryFrame")
|
||||
self.batteryBackgroundNode = ASImageNode()
|
||||
self.batteryForegroundNode = ASImageNode()
|
||||
|
||||
// MARK: Swiftgram
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.leftTextNode)
|
||||
self.addSubnode(self.rightTextNode)
|
||||
self.addSubnode(self.centerTextNode)
|
||||
self.addSubnode(self.batteryBackgroundNode)
|
||||
self.addSubnode(self.batteryForegroundNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
|
||||
// MARK: Swiftgram
|
||||
self.activateArea.increment = { [weak self] in
|
||||
if let self {
|
||||
self.sliderView?.increase(by: 0.10)
|
||||
}
|
||||
}
|
||||
|
||||
self.activateArea.decrement = { [weak self] in
|
||||
if let self {
|
||||
self.sliderView?.decrease(by: 0.10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let sliderView = TGPhotoEditorSliderView()
|
||||
sliderView.enableEdgeTap = true
|
||||
sliderView.enablePanHandling = true
|
||||
sliderView.trackCornerRadius = 1.0
|
||||
sliderView.lineSize = 4.0
|
||||
sliderView.minimumValue = 0.0
|
||||
sliderView.startValue = 0.0
|
||||
sliderView.maximumValue = 1.0
|
||||
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
||||
sliderView.displayEdges = true
|
||||
if let item = self.item, let params = self.layoutParams {
|
||||
sliderView.value = rescalePercentageValueToSlider(CGFloat(item.value) / 100.0)
|
||||
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
sliderView.backColor = item.theme.list.itemSwitchColors.frameColor
|
||||
sliderView.trackColor = item.theme.list.itemAccentColor
|
||||
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
|
||||
|
||||
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
|
||||
}
|
||||
self.view.addSubview(sliderView)
|
||||
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
|
||||
self.sliderView = sliderView
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: SliderPercentageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, neighbors in
|
||||
var themeUpdated = false
|
||||
if currentItem?.theme !== item.theme {
|
||||
themeUpdated = true
|
||||
}
|
||||
|
||||
let contentSize: CGSize
|
||||
let insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
contentSize = CGSize(width: params.width, height: 88.0)
|
||||
insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = params.leftInset + 16.0
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "0%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
|
||||
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "100%", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor)
|
||||
|
||||
let centralText: String = "\(item.value)%"
|
||||
let centralMeasureText: String = centralText
|
||||
strongSelf.batteryBackgroundNode.isHidden = true
|
||||
strongSelf.batteryForegroundNode.isHidden = strongSelf.batteryBackgroundNode.isHidden
|
||||
strongSelf.centerTextNode.attributedText = NSAttributedString(string: centralText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
strongSelf.centerMeasureTextNode.attributedText = NSAttributedString(string: centralMeasureText, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
|
||||
strongSelf.leftTextNode.isAccessibilityElement = true
|
||||
strongSelf.leftTextNode.accessibilityLabel = "Minimum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.minimumValue ?? 0.0) * 100.0))%"
|
||||
strongSelf.rightTextNode.isAccessibilityElement = true
|
||||
strongSelf.rightTextNode.accessibilityLabel = "Maximum: \(Int32(rescaleSliderValueToPercentageValue(strongSelf.sliderView?.maximumValue ?? 1.0) * 100.0))%"
|
||||
|
||||
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
|
||||
let centerMeasureTextSize = strongSelf.centerMeasureTextNode.updateLayout(CGSize(width: 200.0, height: 100.0))
|
||||
|
||||
let sideInset: CGFloat = 18.0
|
||||
|
||||
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize)
|
||||
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize)
|
||||
|
||||
var centerFrame = CGRect(origin: CGPoint(x: floor((params.width - centerMeasureTextSize.width) / 2.0), y: 11.0), size: centerTextSize)
|
||||
if !strongSelf.batteryBackgroundNode.isHidden {
|
||||
centerFrame.origin.x -= 12.0
|
||||
}
|
||||
strongSelf.centerTextNode.frame = centerFrame
|
||||
|
||||
if let frameImage = strongSelf.batteryImage {
|
||||
strongSelf.batteryBackgroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let image = generateTintedImage(image: frameImage, color: item.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.9)) {
|
||||
image.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0))
|
||||
context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath)
|
||||
context.clip()
|
||||
}
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
strongSelf.batteryForegroundNode.image = generateImage(frameImage.size, rotatedContext: { size, context in
|
||||
UIGraphicsPushContext(context)
|
||||
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let contentRect = CGRect(origin: CGPoint(x: 3.0, y: (size.height - 9.0) * 0.5), size: CGSize(width: 20.8, height: 9.0))
|
||||
context.addPath(UIBezierPath(roundedRect: contentRect, cornerRadius: 2.0).cgPath)
|
||||
context.clip()
|
||||
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: contentRect.origin, size: CGSize(width: contentRect.width * CGFloat(item.value) / 100.0, height: contentRect.height)), cornerRadius: 1.0).cgPath)
|
||||
context.fillPath()
|
||||
|
||||
UIGraphicsPopContext()
|
||||
})
|
||||
|
||||
let batteryColor: UIColor
|
||||
if item.value <= 20 {
|
||||
batteryColor = UIColor(rgb: 0xFF3B30)
|
||||
} else {
|
||||
batteryColor = item.theme.list.itemSwitchColors.positiveColor
|
||||
}
|
||||
|
||||
if strongSelf.batteryForegroundNode.layer.layerTintColor == nil {
|
||||
strongSelf.batteryForegroundNode.layer.layerTintColor = batteryColor.cgColor
|
||||
} else {
|
||||
ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateTintColor(layer: strongSelf.batteryForegroundNode.layer, color: batteryColor)
|
||||
}
|
||||
|
||||
strongSelf.batteryBackgroundNode.frame = CGRect(origin: CGPoint(x: centerFrame.minX + centerMeasureTextSize.width + 4.0, y: floor(centerFrame.midY - frameImage.size.height * 0.5)), size: frameImage.size)
|
||||
strongSelf.batteryForegroundNode.frame = strongSelf.batteryBackgroundNode.frame
|
||||
}
|
||||
|
||||
if let sliderView = strongSelf.sliderView {
|
||||
if themeUpdated {
|
||||
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
sliderView.backColor = item.theme.list.itemSecondaryTextColor
|
||||
sliderView.trackColor = item.theme.list.itemAccentColor.withAlphaComponent(0.45)
|
||||
sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme)
|
||||
}
|
||||
|
||||
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0))
|
||||
}
|
||||
|
||||
strongSelf.activateArea.accessibilityLabel = "Slider"
|
||||
strongSelf.activateArea.accessibilityValue = centralMeasureText
|
||||
strongSelf.activateArea.accessibilityTraits = .adjustable
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc func sliderValueChanged() {
|
||||
guard let sliderView = self.sliderView else {
|
||||
return
|
||||
}
|
||||
self.item?.updated(Int32(rescaleSliderValueToPercentageValue(sliderView.value) * 100.0))
|
||||
}
|
||||
}
|
||||
|
||||
19
Swiftgram/SGLogging/BUILD
Normal file
19
Swiftgram/SGLogging/BUILD
Normal file
@@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGLogging",
|
||||
module_name = "SGLogging",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/ManagedFile:ManagedFile"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
236
Swiftgram/SGLogging/Sources/SGLogger.swift
Normal file
236
Swiftgram/SGLogging/Sources/SGLogger.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import ManagedFile
|
||||
|
||||
private let queue = DispatchQueue(label: "app.swiftgram.ios.trace", qos: .utility)
|
||||
|
||||
private var sharedLogger: SGLogger?
|
||||
|
||||
private let binaryEventMarker: UInt64 = 0xcadebabef00dcafe
|
||||
|
||||
private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
||||
return appGroupPath + "/telegram-data"
|
||||
}
|
||||
|
||||
public final class SGLogger {
|
||||
private let queue = Queue(name: "app.swiftgram.ios.log", qos: .utility)
|
||||
private let maxLength: Int = 2 * 1024 * 1024
|
||||
private let maxShortLength: Int = 1 * 1024 * 1024
|
||||
private let maxFiles: Int = 20
|
||||
|
||||
private let rootPath: String
|
||||
private let basePath: String
|
||||
private var file: (ManagedFile, Int)?
|
||||
private var shortFile: (ManagedFile, Int)?
|
||||
|
||||
public static let sgLogsPath = "/logs/app-logs-sg"
|
||||
|
||||
public var logToFile: Bool = true
|
||||
public var logToConsole: Bool = true
|
||||
public var redactSensitiveData: Bool = true
|
||||
|
||||
public static func setSharedLogger(_ logger: SGLogger) {
|
||||
sharedLogger = logger
|
||||
}
|
||||
|
||||
public static var shared: SGLogger {
|
||||
if let sharedLogger = sharedLogger {
|
||||
return sharedLogger
|
||||
} else {
|
||||
print("SGLogger setup...")
|
||||
guard let baseAppBundleId = Bundle.main.bundleIdentifier else {
|
||||
print("Can't setup logger (1)!")
|
||||
return SGLogger(rootPath: "", basePath: "")
|
||||
}
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
print("Can't setup logger (2)!")
|
||||
return SGLogger(rootPath: "", basePath: "")
|
||||
}
|
||||
let newRootPath = rootPathForBasePath(appGroupUrl.path)
|
||||
let newLogsPath = newRootPath + sgLogsPath
|
||||
let _ = try? FileManager.default.createDirectory(atPath: newLogsPath, withIntermediateDirectories: true, attributes: nil)
|
||||
self.setSharedLogger(SGLogger(rootPath: newRootPath, basePath: newLogsPath))
|
||||
if let sharedLogger = sharedLogger {
|
||||
return sharedLogger
|
||||
} else {
|
||||
print("Can't setup logger (3)!")
|
||||
return SGLogger(rootPath: "", basePath: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(rootPath: String, basePath: String) {
|
||||
self.rootPath = rootPath
|
||||
self.basePath = basePath
|
||||
}
|
||||
|
||||
public func collectLogs(prefix: String? = nil) -> Signal<[(String, String)], NoError> {
|
||||
return Signal { subscriber in
|
||||
self.queue.async {
|
||||
let logsPath: String
|
||||
if let prefix = prefix {
|
||||
logsPath = self.rootPath + prefix
|
||||
} else {
|
||||
logsPath = self.basePath
|
||||
}
|
||||
|
||||
var result: [(Date, String, String)] = []
|
||||
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) {
|
||||
for url in files {
|
||||
if url.lastPathComponent.hasPrefix("log-") {
|
||||
if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate {
|
||||
result.append((creationDate, url.lastPathComponent, url.path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort(by: { $0.0 < $1.0 })
|
||||
subscriber.putNext(result.map { ($0.1, $0.2) })
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func collectLogs(basePath: String) -> Signal<[(String, String)], NoError> {
|
||||
return Signal { subscriber in
|
||||
self.queue.async {
|
||||
let logsPath: String = basePath
|
||||
|
||||
var result: [(Date, String, String)] = []
|
||||
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logsPath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) {
|
||||
for url in files {
|
||||
if url.lastPathComponent.hasPrefix("log-") {
|
||||
if let creationDate = (try? url.resourceValues(forKeys: Set([.creationDateKey])))?.creationDate {
|
||||
result.append((creationDate, url.lastPathComponent, url.path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort(by: { $0.0 < $1.0 })
|
||||
subscriber.putNext(result.map { ($0.1, $0.2) })
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public func log(_ tag: String, _ what: @autoclosure () -> String) {
|
||||
if !self.logToFile && !self.logToConsole {
|
||||
return
|
||||
}
|
||||
|
||||
let string = what()
|
||||
|
||||
var rawTime = time_t()
|
||||
time(&rawTime)
|
||||
var timeinfo = tm()
|
||||
localtime_r(&rawTime, &timeinfo)
|
||||
|
||||
var curTime = timeval()
|
||||
gettimeofday(&curTime, nil)
|
||||
let milliseconds = curTime.tv_usec / 1000
|
||||
|
||||
var consoleContent: String?
|
||||
if self.logToConsole {
|
||||
let content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string])
|
||||
consoleContent = content
|
||||
print(content)
|
||||
}
|
||||
|
||||
if self.logToFile {
|
||||
self.queue.async {
|
||||
let content: String
|
||||
if let consoleContent = consoleContent {
|
||||
content = consoleContent
|
||||
} else {
|
||||
content = String(format: "[SG.%@] %d-%d-%d %02d:%02d:%02d.%03d %@", arguments: [tag, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds), string])
|
||||
}
|
||||
|
||||
var currentFile: ManagedFile?
|
||||
var openNew = false
|
||||
if let (file, length) = self.file {
|
||||
if length >= self.maxLength {
|
||||
self.file = nil
|
||||
openNew = true
|
||||
} else {
|
||||
currentFile = file
|
||||
}
|
||||
} else {
|
||||
openNew = true
|
||||
}
|
||||
if openNew {
|
||||
let _ = try? FileManager.default.createDirectory(atPath: self.basePath, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
var createNew = false
|
||||
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [URLResourceKey.creationDateKey], options: []) {
|
||||
var minCreationDate: (Date, URL)?
|
||||
var maxCreationDate: (Date, URL)?
|
||||
var count = 0
|
||||
for url in files {
|
||||
if url.lastPathComponent.hasPrefix("log-") {
|
||||
if let values = try? url.resourceValues(forKeys: Set([URLResourceKey.creationDateKey])), let creationDate = values.creationDate {
|
||||
count += 1
|
||||
if minCreationDate == nil || minCreationDate!.0 > creationDate {
|
||||
minCreationDate = (creationDate, url)
|
||||
}
|
||||
if maxCreationDate == nil || maxCreationDate!.0 < creationDate {
|
||||
maxCreationDate = (creationDate, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (_, url) = minCreationDate, count >= self.maxFiles {
|
||||
let _ = try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
if let (_, url) = maxCreationDate {
|
||||
var value = stat()
|
||||
if stat(url.path, &value) == 0 && Int(value.st_size) < self.maxLength {
|
||||
if let file = ManagedFile(queue: self.queue, path: url.path, mode: .append) {
|
||||
self.file = (file, Int(value.st_size))
|
||||
currentFile = file
|
||||
}
|
||||
} else {
|
||||
createNew = true
|
||||
}
|
||||
} else {
|
||||
createNew = true
|
||||
}
|
||||
}
|
||||
|
||||
if createNew {
|
||||
let fileName = String(format: "log-%d-%d-%d_%02d-%02d-%02d.%03d.txt", arguments: [Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), Int(timeinfo.tm_sec), Int(milliseconds)])
|
||||
|
||||
let path = self.basePath + "/" + fileName
|
||||
|
||||
if let file = ManagedFile(queue: self.queue, path: path, mode: .append) {
|
||||
self.file = (file, 0)
|
||||
currentFile = file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let currentFile = currentFile {
|
||||
if let data = content.data(using: .utf8) {
|
||||
data.withUnsafeBytes { rawBytes -> Void in
|
||||
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
let _ = currentFile.write(bytes, count: data.count)
|
||||
}
|
||||
var newline: UInt8 = 0x0a
|
||||
let _ = currentFile.write(&newline, count: 1)
|
||||
if let file = self.file {
|
||||
self.file = (file.0, file.1 + data.count + 1)
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Swiftgram/SGLogging/Sources/Utils.swift
Normal file
6
Swiftgram/SGLogging/Sources/Utils.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
//import Foundation
|
||||
//
|
||||
//public func extractNameFromPath(_ path: String) -> String {
|
||||
// let fileName = URL(fileURLWithPath: path).lastPathComponent
|
||||
// return String(fileName.prefix(upTo: fileName.lastIndex { $0 == "." } ?? fileName.endIndex))
|
||||
//}
|
||||
27
Swiftgram/SGRegDate/BUILD
Normal file
27
Swiftgram/SGRegDate/BUILD
Normal file
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGRegDate",
|
||||
module_name = "SGRegDate",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//Swiftgram/SGRegDateScheme:SGRegDateScheme",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//Swiftgram/SGAPI:SGAPI",
|
||||
"//Swiftgram/SGAPIToken:SGAPIToken",
|
||||
"//Swiftgram/SGDeviceToken:SGDeviceToken",
|
||||
"//Swiftgram/SGStrings:SGStrings",
|
||||
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
45
Swiftgram/SGRegDate/Sources/SGRegDate.swift
Normal file
45
Swiftgram/SGRegDate/Sources/SGRegDate.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
|
||||
import SGLogging
|
||||
import SGStrings
|
||||
import SGRegDateScheme
|
||||
import AccountContext
|
||||
import SGSimpleSettings
|
||||
import SGAPI
|
||||
import SGAPIToken
|
||||
import SGDeviceToken
|
||||
|
||||
public enum RegDateError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public func getRegDate(context: AccountContext, peerId: Int64) -> Signal<RegDate?, NoError> {
|
||||
return Signal { subscriber in
|
||||
var tokensRequestSignal: Disposable? = nil
|
||||
var apiRequestSignal: Disposable? = nil
|
||||
if let regDateData = SGSimpleSettings.shared.regDateCache[String(peerId)], let regDate = try? JSONDecoder().decode(RegDate.self, from: regDateData), regDate.validUntil == 0 || regDate.validUntil > Int64(Date().timeIntervalSince1970) {
|
||||
subscriber.putNext(regDate)
|
||||
subscriber.putCompletion()
|
||||
} else if SGSimpleSettings.shared.showRegDate {
|
||||
tokensRequestSignal = combineLatest(getDeviceToken() |> mapError { error -> Void in SGLogger.shared.log("SGDeviceToken", "Error generating token: \(error)"); return Void() } , getSGApiToken(context: context) |> mapError { _ -> Void in return Void() }).start(next: { deviceToken, apiToken in
|
||||
apiRequestSignal = getSGAPIRegDate(token: apiToken, deviceToken: deviceToken, userId: peerId).start(next: { regDate in
|
||||
if let data = try? JSONEncoder().encode(regDate) {
|
||||
SGSimpleSettings.shared.regDateCache[String(peerId)] = data
|
||||
}
|
||||
subscriber.putNext(regDate)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
subscriber.putNext(nil)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
|
||||
return ActionDisposable {
|
||||
tokensRequestSignal?.dispose()
|
||||
apiRequestSignal?.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Swiftgram/SGRegDateScheme/BUILD
Normal file
17
Swiftgram/SGRegDateScheme/BUILD
Normal file
@@ -0,0 +1,17 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGRegDateScheme",
|
||||
module_name = "SGRegDateScheme",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
7
Swiftgram/SGRegDateScheme/Sources/File.swift
Normal file
7
Swiftgram/SGRegDateScheme/Sources/File.swift
Normal file
@@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
public struct RegDate: Codable {
|
||||
public let from: Int64
|
||||
public let to: Int64
|
||||
public let validUntil: Int64
|
||||
}
|
||||
18
Swiftgram/SGRequests/BUILD
Normal file
18
Swiftgram/SGRequests/BUILD
Normal file
@@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGRequests",
|
||||
module_name = "SGRequests",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
72
Swiftgram/SGRequests/Sources/File.swift
Normal file
72
Swiftgram/SGRequests/Sources/File.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
|
||||
|
||||
public func requestsDownload(url: URL) -> Signal<(Data, URLResponse?), Error?> {
|
||||
return Signal { subscriber in
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { location, response, error in
|
||||
let _ = completed.swap(true)
|
||||
if let location = location, let data = try? Data(contentsOf: location) {
|
||||
subscriber.putNext((data, response))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(error)
|
||||
}
|
||||
})
|
||||
downloadTask.resume()
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
downloadTask.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func requestsGet(url: URL) -> Signal<(Data, URLResponse?), Error?> {
|
||||
return Signal { subscriber in
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
|
||||
let urlTask = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
|
||||
let _ = completed.swap(true)
|
||||
if let strongData = data {
|
||||
subscriber.putNext((strongData, response))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(error)
|
||||
}
|
||||
})
|
||||
urlTask.resume()
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
urlTask.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func requestsCustom(request: URLRequest) -> Signal<(Data, URLResponse?), Error?> {
|
||||
return Signal { subscriber in
|
||||
let completed = Atomic<Bool>(value: false)
|
||||
let urlTask = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
|
||||
_ = completed.swap(true)
|
||||
if let strongData = data {
|
||||
subscriber.putNext((strongData, response))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(error)
|
||||
}
|
||||
})
|
||||
urlTask.resume()
|
||||
|
||||
return ActionDisposable {
|
||||
if !completed.with({ $0 }) {
|
||||
urlTask.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Swiftgram/SGSettingsBundle/BUILD
Normal file
10
Swiftgram/SGSettingsBundle/BUILD
Normal file
@@ -0,0 +1,10 @@
|
||||
load("@build_bazel_rules_apple//apple:resources.bzl", "apple_bundle_import")
|
||||
|
||||
apple_bundle_import(
|
||||
name = "SGSettingsBundle",
|
||||
bundle_imports = glob([
|
||||
"Settings.bundle/*",
|
||||
"Settings.bundle/**/*",
|
||||
]),
|
||||
visibility = ["//visibility:public"]
|
||||
)
|
||||
29
Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist
Normal file
29
Swiftgram/SGSettingsBundle/Settings.bundle/Root.plist
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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>StringsTable</key>
|
||||
<string>Root</string>
|
||||
<key>PreferenceSpecifiers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
<key>FooterText</key>
|
||||
<string>Reset.Notice</string>
|
||||
<key>Title</key>
|
||||
<string>Reset.Title</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
<string>PSToggleSwitchSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Reset.Toggle</string>
|
||||
<key>Key</key>
|
||||
<string>sg_db_reset</string>
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
/* A single strings file, whose title is specified in your preferences schema. The strings files provide the localized content to display to the user for each of your preferences. */
|
||||
|
||||
"Reset.Title" = "TROUBLESHOOTING";
|
||||
"Reset.Toggle" = "Reset caches on next launch";
|
||||
"Reset.Notice" = "Use in case you're stuck and can't open the app. This WILL NOT logout your accounts, but all secret chats will be lost.";
|
||||
@@ -0,0 +1,3 @@
|
||||
"Reset.Title" = "РЕШЕНИЕ ПРОБЛЕМ";
|
||||
"Reset.Toggle" = "Сбросить кэш при следующем запуске";
|
||||
"Reset.Notice" = "Используйте, если приложение вылетает или не загружается. Эта опция НЕ СБРАСЫВАЕТ ваши аккаунты, но удалит все секретные чаты.";
|
||||
43
Swiftgram/SGSettingsUI/BUILD
Normal file
43
Swiftgram/SGSettingsUI/BUILD
Normal file
@@ -0,0 +1,43 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
filegroup(
|
||||
name = "SGUIAssets",
|
||||
srcs = glob(["Images.xcassets/**"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "SGSettingsUI",
|
||||
module_name = "SGSettingsUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//Swiftgram/SGItemListUI:SGItemListUI",
|
||||
"//Swiftgram/SGLogging:SGLogging",
|
||||
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
|
||||
"//Swiftgram/SGStrings:SGStrings",
|
||||
# "//Swiftgram/SGAPI:SGAPI",
|
||||
"//Swiftgram/SGAPIToken:SGAPIToken",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/MtProtoKit:MtProtoKit",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TelegramUI/Components/Settings/PeerNameColorScreen",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
6
Swiftgram/SGSettingsUI/Images.xcassets/Contents.json
Normal file
6
Swiftgram/SGSettingsUI/Images.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json
vendored
Normal file
12
Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_lt_savetocloud.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
BIN
Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf
vendored
Normal file
BIN
Swiftgram/SGSettingsUI/Images.xcassets/SaveToCloud.imageset/ic_lt_savetocloud.pdf
vendored
Normal file
Binary file not shown.
12
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json
vendored
Normal file
12
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "swiftgram_context_menu.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
81
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf
vendored
Normal file
81
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramContextMenu.imageset/swiftgram_context_menu.pdf
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 2.964844 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
15.076375 10.766671 m
|
||||
15.473662 11.399487 14.937258 12.204764 14.200223 12.081993 c
|
||||
9.059459 11.225675 l
|
||||
8.855769 11.191745 8.670359 11.348825 8.670359 11.555322 c
|
||||
8.670359 18.524288 l
|
||||
8.670359 19.289572 7.652856 19.554642 7.279467 18.886631 c
|
||||
1.036950 7.718488 l
|
||||
0.658048 7.040615 1.293577 6.244993 2.038416 6.464749 c
|
||||
9.378864 8.630468 l
|
||||
9.637225 8.706696 9.814250 8.373775 9.606588 8.202201 c
|
||||
6.918006 5.980853 l
|
||||
6.462659 5.604639 6.199009 5.044809 6.199009 4.454151 c
|
||||
6.199009 -0.793964 l
|
||||
6.199009 -1.539309 7.174314 -1.820084 7.570620 -1.188831 c
|
||||
15.076375 10.766671 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
702
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000000792 00000 n
|
||||
0000000814 00000 n
|
||||
0000000987 00000 n
|
||||
0000001061 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1120
|
||||
%%EOF
|
||||
13
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Contents.json
vendored
Normal file
13
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Swiftgram.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
||||
242
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf
vendored
Normal file
242
Swiftgram/SGSettingsUI/Images.xcassets/SwiftgramSettings.imageset/Swiftgram.pdf
vendored
Normal file
@@ -0,0 +1,242 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< /Length 2 0 R
|
||||
/Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ]
|
||||
/Domain [ 0.000000 1.000000 ]
|
||||
/FunctionType 4
|
||||
>>
|
||||
stream
|
||||
{ 1.000000 exch 0.764706 exch 0.415686 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.098039 mul 1.000000 add exch dup 0.000000 sub -0.764706 mul 0.764706 add exch dup 0.000000 sub -0.415686 mul 0.415686 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.901961 exch 0.000000 exch 0.000000 exch } if pop }
|
||||
endstream
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
339
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 4 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << /Pattern << /P1 << /Matrix [ -625.250061 -1215.250000 1215.250000 -625.250061 -946.303711 1659.980225 ]
|
||||
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
|
||||
/ColorSpace /DeviceRGB
|
||||
/Function 1 0 R
|
||||
/Domain [ 0.000000 1.000000 ]
|
||||
/ShadingType 2
|
||||
/Extend [ true true ]
|
||||
>>
|
||||
/PatternType 2
|
||||
/Type /Pattern
|
||||
>> >> >>
|
||||
/BBox [ 0.000000 0.000000 512.000000 512.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
/Pattern cs
|
||||
/P1 scn
|
||||
0.000000 320.853333 m
|
||||
0.000000 387.754669 0.000000 421.205322 12.970667 446.805328 c
|
||||
24.405334 469.333344 42.666668 487.594666 65.194672 499.029327 c
|
||||
90.794670 512.000000 124.245338 512.000000 191.146667 512.000000 c
|
||||
320.853333 512.000000 l
|
||||
387.754669 512.000000 421.205353 512.000000 446.805359 499.029327 c
|
||||
469.333374 487.594666 487.594696 469.333344 499.029358 446.805328 c
|
||||
512.000000 421.205322 512.000000 387.754669 512.000000 320.853333 c
|
||||
512.000000 191.146667 l
|
||||
512.000000 124.245331 512.000000 90.794647 499.029358 65.194641 c
|
||||
487.594696 42.666626 469.333374 24.405304 446.805359 12.970642 c
|
||||
421.205353 0.000000 387.754669 0.000000 320.853333 0.000000 c
|
||||
191.146667 0.000000 l
|
||||
124.245338 0.000000 90.794670 0.000000 65.194672 12.970642 c
|
||||
42.666668 24.405304 24.405334 42.666626 12.970667 65.194641 c
|
||||
0.000000 90.794647 0.000000 124.245331 0.000000 191.146667 c
|
||||
0.000000 320.853333 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 119.500000 103.400391 cm
|
||||
1.000000 1.000000 1.000000 scn
|
||||
256.015533 182.826599 m
|
||||
262.761963 193.572601 253.653152 207.247192 241.137390 205.162399 c
|
||||
153.840836 190.621048 l
|
||||
150.381927 190.044891 147.233429 192.712296 147.233429 196.218872 c
|
||||
147.233429 314.560455 l
|
||||
147.233429 327.555908 129.954987 332.057098 123.614365 320.713440 c
|
||||
17.608702 131.064743 l
|
||||
11.174477 119.553635 21.966566 106.042999 34.614845 109.774734 c
|
||||
159.264740 146.551285 l
|
||||
163.652023 147.845703 166.658112 142.192291 163.131760 139.278763 c
|
||||
117.476318 101.557587 l
|
||||
109.743965 95.169006 105.266861 85.662384 105.266861 75.632263 c
|
||||
105.266861 -13.487152 l
|
||||
105.266861 -26.143982 121.828712 -30.911926 128.558456 -20.192505 c
|
||||
256.015533 182.826599 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
1771
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Length 6 0 R
|
||||
/Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ]
|
||||
/Domain [ 0.000000 1.000000 ]
|
||||
/FunctionType 4
|
||||
>>
|
||||
stream
|
||||
{ 1.000000 exch 0.764706 exch 0.415686 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub -0.098039 mul 1.000000 add exch dup 0.000000 sub -0.764706 mul 0.764706 add exch dup 0.000000 sub -0.415686 mul 0.415686 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.901961 exch 0.000000 exch 0.000000 exch } if pop }
|
||||
endstream
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
339
|
||||
endobj
|
||||
|
||||
7 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 8 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << /Pattern << /P1 << /Matrix [ -625.250061 -1215.250000 1215.250000 -625.250061 -946.303711 1659.980225 ]
|
||||
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
|
||||
/ColorSpace /DeviceRGB
|
||||
/Function 5 0 R
|
||||
/Domain [ 0.000000 1.000000 ]
|
||||
/ShadingType 2
|
||||
/Extend [ true true ]
|
||||
>>
|
||||
/PatternType 2
|
||||
/Type /Pattern
|
||||
>> >> >>
|
||||
/BBox [ 0.000000 0.000000 512.000000 512.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
/Pattern cs
|
||||
/P1 scn
|
||||
0.000000 320.853333 m
|
||||
0.000000 387.754669 0.000000 421.205322 12.970667 446.805328 c
|
||||
24.405334 469.333344 42.666668 487.594666 65.194672 499.029327 c
|
||||
90.794670 512.000000 124.245338 512.000000 191.146667 512.000000 c
|
||||
320.853333 512.000000 l
|
||||
387.754669 512.000000 421.205353 512.000000 446.805359 499.029327 c
|
||||
469.333374 487.594666 487.594696 469.333344 499.029358 446.805328 c
|
||||
512.000000 421.205322 512.000000 387.754669 512.000000 320.853333 c
|
||||
512.000000 191.146667 l
|
||||
512.000000 124.245331 512.000000 90.794647 499.029358 65.194641 c
|
||||
487.594696 42.666626 469.333374 24.405304 446.805359 12.970642 c
|
||||
421.205353 0.000000 387.754669 0.000000 320.853333 0.000000 c
|
||||
191.146667 0.000000 l
|
||||
124.245338 0.000000 90.794670 0.000000 65.194672 12.970642 c
|
||||
42.666668 24.405304 24.405334 42.666626 12.970667 65.194641 c
|
||||
0.000000 90.794647 0.000000 124.245331 0.000000 191.146667 c
|
||||
0.000000 320.853333 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
8 0 obj
|
||||
1006
|
||||
endobj
|
||||
|
||||
9 0 obj
|
||||
<< /XObject << /X1 3 0 R >>
|
||||
/ExtGState << /E1 << /SMask << /Type /Mask
|
||||
/G 7 0 R
|
||||
/S /Alpha
|
||||
>>
|
||||
/Type /ExtGState
|
||||
>> >>
|
||||
>>
|
||||
endobj
|
||||
|
||||
10 0 obj
|
||||
<< /Length 11 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
/E1 gs
|
||||
/X1 Do
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
11 0 obj
|
||||
46
|
||||
endobj
|
||||
|
||||
12 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 512.000000 512.000000 ]
|
||||
/Resources 9 0 R
|
||||
/Contents 10 0 R
|
||||
/Parent 13 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
13 0 obj
|
||||
<< /Kids [ 12 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
14 0 obj
|
||||
<< /Pages 13 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 15
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000533 00000 n
|
||||
0000000555 00000 n
|
||||
0000003331 00000 n
|
||||
0000003354 00000 n
|
||||
0000003877 00000 n
|
||||
0000003899 00000 n
|
||||
0000005910 00000 n
|
||||
0000005933 00000 n
|
||||
0000006231 00000 n
|
||||
0000006335 00000 n
|
||||
0000006357 00000 n
|
||||
0000006535 00000 n
|
||||
0000006611 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 14 0 R
|
||||
/Size 15
|
||||
>>
|
||||
startxref
|
||||
6672
|
||||
%%EOF
|
||||
672
Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift
Normal file
672
Swiftgram/SGSettingsUI/Sources/SGSettingsController.swift
Normal file
@@ -0,0 +1,672 @@
|
||||
// MARK: Swiftgram
|
||||
import SGLogging
|
||||
import SGSimpleSettings
|
||||
import SGStrings
|
||||
import SGAPIToken
|
||||
|
||||
import SGItemListUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import MtProtoKit
|
||||
import MessageUI
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import OverlayStatusController
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import WebKit
|
||||
import PeerNameColorScreen
|
||||
import UndoUI
|
||||
|
||||
|
||||
private enum SGControllerSection: Int32, SGItemListSection {
|
||||
case search
|
||||
case content
|
||||
case tabs
|
||||
case folders
|
||||
case chatList
|
||||
case profiles
|
||||
case stories
|
||||
case translation
|
||||
case photo
|
||||
case stickers
|
||||
case videoNotes
|
||||
case contextMenu
|
||||
case accountColors
|
||||
case other
|
||||
}
|
||||
|
||||
private enum SGBoolSetting: String {
|
||||
case hidePhoneInSettings
|
||||
case showTabNames
|
||||
case showContactsTab
|
||||
case showCallsTab
|
||||
case foldersAtBottom
|
||||
case startTelescopeWithRearCam
|
||||
case hideStories
|
||||
case uploadSpeedBoost
|
||||
case showProfileId
|
||||
case warnOnStoriesOpen
|
||||
case sendWithReturnKey
|
||||
case rememberLastFolder
|
||||
case sendLargePhotos
|
||||
case storyStealthMode
|
||||
case disableSwipeToRecordStory
|
||||
case disableDeleteChatSwipeOption
|
||||
case quickTranslateButton
|
||||
case hideReactions
|
||||
case showRepostToStory
|
||||
case contextShowSelectFromUser
|
||||
case contextShowSaveToCloud
|
||||
case contextShowHideForwardName
|
||||
case contextShowRestrict
|
||||
case contextShowReport
|
||||
case contextShowReply
|
||||
case contextShowPin
|
||||
case contextShowSaveMedia
|
||||
case contextShowMessageReplies
|
||||
case contextShowJson
|
||||
case disableScrollToNextChannel
|
||||
case disableScrollToNextTopic
|
||||
case disableChatSwipeOptions
|
||||
case disableGalleryCamera
|
||||
case disableSendAsButton
|
||||
case disableSnapDeletionEffect
|
||||
case stickerTimestamp
|
||||
case hideRecordingButton
|
||||
case hideTabBar
|
||||
case showDC
|
||||
case showCreationDate
|
||||
case showRegDate
|
||||
case compactChatList
|
||||
case compactFolderNames
|
||||
case allChatsHidden
|
||||
case defaultEmojisFirst
|
||||
case messageDoubleTapActionOutgoingEdit
|
||||
case wideChannelPosts
|
||||
case forceEmojiTab
|
||||
case forceBuiltInMic
|
||||
case hideChannelBottomButton
|
||||
case confirmCalls
|
||||
case swipeForVideoPIP
|
||||
}
|
||||
|
||||
private enum SGOneFromManySetting: String {
|
||||
case bottomTabStyle
|
||||
case downloadSpeedBoost
|
||||
case allChatsTitleLengthOverride
|
||||
// case allChatsFolderPositionOverride
|
||||
}
|
||||
|
||||
private enum SGSliderSetting: String {
|
||||
case accountColorsSaturation
|
||||
case outgoingPhotoQuality
|
||||
case stickerSize
|
||||
}
|
||||
|
||||
private enum SGDisclosureLink: String {
|
||||
case contentSettings
|
||||
case languageSettings
|
||||
}
|
||||
|
||||
private struct PeerNameColorScreenState: Equatable {
|
||||
var updatedNameColor: PeerNameColor?
|
||||
var updatedBackgroundEmojiId: Int64?
|
||||
}
|
||||
|
||||
private struct SGSettingsControllerState: Equatable {
|
||||
var searchQuery: String?
|
||||
}
|
||||
|
||||
private typealias SGControllerEntry = SGItemListUIEntry<SGControllerSection, SGBoolSetting, SGSliderSetting, SGOneFromManySetting, SGDisclosureLink, AnyHashable>
|
||||
|
||||
private func SGControllerEntries(presentationData: PresentationData, callListSettings: CallListSettings, experimentalUISettings: ExperimentalUISettings, SGSettings: SGUISettings, appConfiguration: AppConfiguration, nameColors: PeerNameColors, state: SGSettingsControllerState) -> [SGControllerEntry] {
|
||||
|
||||
let lang = presentationData.strings.baseLanguageCode
|
||||
var entries: [SGControllerEntry] = []
|
||||
|
||||
let id = SGItemListCounter()
|
||||
|
||||
entries.append(.searchInput(id: id.count, section: .search, title: NSAttributedString(string: "🔍"), text: state.searchQuery ?? "", placeholder: presentationData.strings.Common_Search))
|
||||
if appConfiguration.sgWebSettings.global.canEditSettings {
|
||||
entries.append(.disclosure(id: id.count, section: .content, link: .contentSettings, text: i18n("Settings.ContentSettings", lang)))
|
||||
} else {
|
||||
id.increment(1)
|
||||
}
|
||||
|
||||
entries.append(.header(id: id.count, section: .tabs, text: i18n("Settings.Tabs.Header", lang), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .tabs, settingName: .hideTabBar, value: SGSimpleSettings.shared.hideTabBar, text: i18n("Settings.Tabs.HideTabBar", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .tabs, settingName: .showContactsTab, value: callListSettings.showContactsTab, text: i18n("Settings.Tabs.ShowContacts", lang), enabled: !SGSimpleSettings.shared.hideTabBar))
|
||||
entries.append(.toggle(id: id.count, section: .tabs, settingName: .showCallsTab, value: callListSettings.showTab, text: presentationData.strings.CallSettings_TabIcon, enabled: !SGSimpleSettings.shared.hideTabBar))
|
||||
entries.append(.toggle(id: id.count, section: .tabs, settingName: .showTabNames, value: SGSimpleSettings.shared.showTabNames, text: i18n("Settings.Tabs.ShowNames", lang), enabled: !SGSimpleSettings.shared.hideTabBar))
|
||||
|
||||
entries.append(.header(id: id.count, section: .folders, text: presentationData.strings.Settings_ChatFolders.uppercased(), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .folders, settingName: .foldersAtBottom, value: experimentalUISettings.foldersTabAtBottom, text: i18n("Settings.Folders.BottomTab", lang), enabled: true))
|
||||
entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .bottomTabStyle, text: i18n("Settings.Folders.BottomTabStyle", lang), value: i18n("Settings.Folders.BottomTabStyle.\(SGSimpleSettings.shared.bottomTabStyle)", lang), enabled: experimentalUISettings.foldersTabAtBottom))
|
||||
entries.append(.toggle(id: id.count, section: .folders, settingName: .allChatsHidden, value: SGSimpleSettings.shared.allChatsHidden, text: i18n("Settings.Folders.AllChatsHidden", lang, presentationData.strings.ChatList_Tabs_AllChats), enabled: true))
|
||||
#if DEBUG
|
||||
// entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .allChatsFolderPositionOverride, text: i18n("Settings.Folders.AllChatsPlacement", lang), value: i18n("Settings.Folders.AllChatsPlacement.\(SGSimpleSettings.shared.allChatsFolderPositionOverride)", lang), enabled: true))
|
||||
#endif
|
||||
entries.append(.toggle(id: id.count, section: .folders, settingName: .compactFolderNames, value: SGSimpleSettings.shared.compactFolderNames, text: i18n("Settings.Folders.CompactNames", lang), enabled: SGSimpleSettings.shared.bottomTabStyle != SGSimpleSettings.BottomTabStyleValues.ios.rawValue))
|
||||
entries.append(.oneFromManySelector(id: id.count, section: .folders, settingName: .allChatsTitleLengthOverride, text: i18n("Settings.Folders.AllChatsTitle", lang), value: i18n("Settings.Folders.AllChatsTitle.\(SGSimpleSettings.shared.allChatsTitleLengthOverride)", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .folders, settingName: .rememberLastFolder, value: SGSimpleSettings.shared.rememberLastFolder, text: i18n("Settings.Folders.RememberLast", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .folders, text: i18n("Settings.Folders.RememberLast.Notice", lang)))
|
||||
|
||||
entries.append(.header(id: id.count, section: .chatList, text: i18n("Settings.ChatList.Header", lang), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .chatList, settingName: .compactChatList, value: SGSimpleSettings.shared.compactChatList, text: i18n("Settings.CompactChatList", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .chatList, settingName: .disableChatSwipeOptions, value: !SGSimpleSettings.shared.disableChatSwipeOptions, text: i18n("Settings.ChatSwipeOptions", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .chatList, settingName: .disableDeleteChatSwipeOption, value: !SGSimpleSettings.shared.disableDeleteChatSwipeOption, text: i18n("Settings.DeleteChatSwipeOption", lang), enabled: !SGSimpleSettings.shared.disableChatSwipeOptions))
|
||||
|
||||
entries.append(.header(id: id.count, section: .profiles, text: i18n("Settings.Profiles.Header", lang), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showProfileId, value: SGSettings.showProfileId, text: i18n("Settings.ShowProfileID", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showDC, value: SGSimpleSettings.shared.showDC, text: i18n("Settings.ShowDC", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showRegDate, value: SGSimpleSettings.shared.showRegDate, text: i18n("Settings.ShowRegDate", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.ShowRegDate.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .profiles, settingName: .showCreationDate, value: SGSimpleSettings.shared.showCreationDate, text: i18n("Settings.ShowCreationDate", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.ShowCreationDate.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .profiles, settingName: .confirmCalls, value: SGSimpleSettings.shared.confirmCalls, text: i18n("Settings.CallConfirmation", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .profiles, text: i18n("Settings.CallConfirmation.Notice", lang)))
|
||||
|
||||
entries.append(.header(id: id.count, section: .stories, text: presentationData.strings.AutoDownloadSettings_Stories.uppercased(), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .stories, settingName: .hideStories, value: SGSettings.hideStories, text: i18n("Settings.Stories.Hide", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .stories, settingName: .disableSwipeToRecordStory, value: SGSimpleSettings.shared.disableSwipeToRecordStory, text: i18n("Settings.Stories.DisableSwipeToRecord", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .stories, settingName: .warnOnStoriesOpen, value: SGSettings.warnOnStoriesOpen, text: i18n("Settings.Stories.WarnBeforeView", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .stories, settingName: .showRepostToStory, value: SGSimpleSettings.shared.showRepostToStory, text: presentationData.strings.Share_RepostToStory.replacingOccurrences(of: "\n", with: " "), enabled: true))
|
||||
if SGSimpleSettings.shared.canUseStealthMode {
|
||||
entries.append(.toggle(id: id.count, section: .stories, settingName: .storyStealthMode, value: SGSimpleSettings.shared.storyStealthMode, text: presentationData.strings.Story_StealthMode_Title, enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .stories, text: presentationData.strings.Story_StealthMode_ControlText))
|
||||
} else {
|
||||
id.increment(2)
|
||||
}
|
||||
|
||||
|
||||
entries.append(.header(id: id.count, section: .translation, text: presentationData.strings.Localization_TranslateMessages.uppercased(), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .translation, settingName: .quickTranslateButton, value: SGSimpleSettings.shared.quickTranslateButton, text: i18n("Settings.Translation.QuickTranslateButton", lang), enabled: true))
|
||||
entries.append(.disclosure(id: id.count, section: .translation, link: .languageSettings, text: presentationData.strings.Localization_TranslateEntireChat))
|
||||
entries.append(.notice(id: id.count, section: .translation, text: i18n("Common.NoTelegramPremiumNeeded", lang, presentationData.strings.Settings_Premium)))
|
||||
|
||||
entries.append(.header(id: id.count, section: .photo, text: presentationData.strings.NetworkUsageSettings_MediaImageDataSection, badge: nil))
|
||||
entries.append(.header(id: id.count, section: .photo, text: presentationData.strings.PhotoEditor_QualityTool.uppercased(), badge: nil))
|
||||
entries.append(.percentageSlider(id: id.count, section: .photo, settingName: .outgoingPhotoQuality, value: SGSimpleSettings.shared.outgoingPhotoQuality))
|
||||
entries.append(.notice(id: id.count, section: .photo, text: i18n("Settings.Photo.Quality.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .photo, settingName: .sendLargePhotos, value: SGSimpleSettings.shared.sendLargePhotos, text: i18n("Settings.Photo.SendLarge", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .photo, text: i18n("Settings.Photo.SendLarge.Notice", lang)))
|
||||
|
||||
entries.append(.header(id: id.count, section: .stickers, text: presentationData.strings.StickerPacksSettings_Title.uppercased(), badge: nil))
|
||||
entries.append(.header(id: id.count, section: .stickers, text: i18n("Settings.Stickers.Size", lang), badge: nil))
|
||||
entries.append(.percentageSlider(id: id.count, section: .stickers, settingName: .stickerSize, value: SGSimpleSettings.shared.stickerSize))
|
||||
entries.append(.toggle(id: id.count, section: .stickers, settingName: .stickerTimestamp, value: SGSimpleSettings.shared.stickerTimestamp, text: i18n("Settings.Stickers.Timestamp", lang), enabled: true))
|
||||
|
||||
|
||||
entries.append(.header(id: id.count, section: .videoNotes, text: i18n("Settings.VideoNotes.Header", lang), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .videoNotes, settingName: .startTelescopeWithRearCam, value: SGSimpleSettings.shared.startTelescopeWithRearCam, text: i18n("Settings.VideoNotes.StartWithRearCam", lang), enabled: true))
|
||||
|
||||
entries.append(.header(id: id.count, section: .contextMenu, text: i18n("Settings.ContextMenu", lang), badge: nil))
|
||||
entries.append(.notice(id: id.count, section: .contextMenu, text: i18n("Settings.ContextMenu.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSaveToCloud, value: SGSimpleSettings.shared.contextShowSaveToCloud, text: i18n("ContextMenu.SaveToCloud", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowHideForwardName, value: SGSimpleSettings.shared.contextShowHideForwardName, text: presentationData.strings.Conversation_ForwardOptions_HideSendersNames, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSelectFromUser, value: SGSimpleSettings.shared.contextShowSelectFromUser, text: i18n("ContextMenu.SelectFromUser", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowRestrict, value: SGSimpleSettings.shared.contextShowRestrict, text: presentationData.strings.Conversation_ContextMenuBan, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowReport, value: SGSimpleSettings.shared.contextShowReport, text: presentationData.strings.Conversation_ContextMenuReport, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowReply, value: SGSimpleSettings.shared.contextShowReply, text: presentationData.strings.Conversation_ContextMenuReply, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowPin, value: SGSimpleSettings.shared.contextShowPin, text: presentationData.strings.Conversation_Pin, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowSaveMedia, value: SGSimpleSettings.shared.contextShowSaveMedia, text: presentationData.strings.Conversation_SaveToFiles, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowMessageReplies, value: SGSimpleSettings.shared.contextShowMessageReplies, text: presentationData.strings.Conversation_ContextViewThread, enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowJson, value: SGSimpleSettings.shared.contextShowJson, text: "JSON", enabled: true))
|
||||
/* entries.append(.toggle(id: id.count, section: .contextMenu, settingName: .contextShowRestrict, value: SGSimpleSettings.shared.contextShowRestrict, text: presentationData.strings.Conversation_ContextMenuBan)) */
|
||||
|
||||
entries.append(.header(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Header", lang), badge: nil))
|
||||
entries.append(.header(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Saturation", lang), badge: nil))
|
||||
let accountColorSaturation = SGSimpleSettings.shared.accountColorsSaturation
|
||||
entries.append(.percentageSlider(id: id.count, section: .accountColors, settingName: .accountColorsSaturation, value: accountColorSaturation))
|
||||
// let nameColor: PeerNameColor
|
||||
// if let updatedNameColor = state.updatedNameColor {
|
||||
// nameColor = updatedNameColor
|
||||
// } else {
|
||||
// nameColor = .blue
|
||||
// }
|
||||
// let _ = nameColors.get(nameColor, dark: presentationData.theme.overallDarkAppearance)
|
||||
// entries.append(.peerColorPicker(id: entries.count, section: .other,
|
||||
// colors: nameColors,
|
||||
// currentColor: nameColor, // TODO: PeerNameColor(rawValue: <#T##Int32#>)
|
||||
// currentSaturation: accountColorSaturation
|
||||
// ))
|
||||
|
||||
if accountColorSaturation == 0 {
|
||||
id.increment(100)
|
||||
entries.append(.peerColorDisclosurePreview(id: id.count, section: .accountColors, name: "\(presentationData.strings.UserInfo_FirstNamePlaceholder) \(presentationData.strings.UserInfo_LastNamePlaceholder)", color: presentationData.theme.chat.message.incoming.accentTextColor))
|
||||
} else {
|
||||
id.increment(200)
|
||||
for index in nameColors.displayOrder.prefix(3) {
|
||||
let color: PeerNameColor = PeerNameColor(rawValue: index)
|
||||
let colors = nameColors.get(color, dark: presentationData.theme.overallDarkAppearance)
|
||||
entries.append(.peerColorDisclosurePreview(id: id.count, section: .accountColors, name: "\(presentationData.strings.UserInfo_FirstNamePlaceholder) \(presentationData.strings.UserInfo_LastNamePlaceholder)", color: colors.main))
|
||||
}
|
||||
}
|
||||
entries.append(.notice(id: id.count, section: .accountColors, text: i18n("Settings.CustomColors.Saturation.Notice", lang)))
|
||||
|
||||
id.increment(10000)
|
||||
entries.append(.header(id: id.count, section: .other, text: presentationData.strings.Appearance_Other.uppercased(), badge: nil))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .swipeForVideoPIP, value: SGSimpleSettings.shared.videoPIPSwipeDirection == SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue, text: i18n("Settings.swipeForVideoPIP", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.swipeForVideoPIP.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .hideChannelBottomButton, value: SGSimpleSettings.shared.hideChannelBottomButton, text: i18n("Settings.hideChannelBottomButton", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .wideChannelPosts, value: SGSimpleSettings.shared.wideChannelPosts, text: i18n("Settings.wideChannelPosts", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .forceBuiltInMic, value: SGSimpleSettings.shared.forceBuiltInMic, text: i18n("Settings.forceBuiltInMic", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.forceBuiltInMic.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .messageDoubleTapActionOutgoingEdit, value: SGSimpleSettings.shared.messageDoubleTapActionOutgoing == SGSimpleSettings.MessageDoubleTapAction.edit.rawValue, text: i18n("Settings.messageDoubleTapActionOutgoingEdit", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .hideRecordingButton, value: !SGSimpleSettings.shared.hideRecordingButton, text: i18n("Settings.RecordingButton", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .disableSnapDeletionEffect, value: !SGSimpleSettings.shared.disableSnapDeletionEffect, text: i18n("Settings.SnapDeletionEffect", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .disableSendAsButton, value: !SGSimpleSettings.shared.disableSendAsButton, text: i18n("Settings.SendAsButton", lang, presentationData.strings.Conversation_SendMesageAs), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .disableGalleryCamera, value: !SGSimpleSettings.shared.disableGalleryCamera, text: i18n("Settings.GalleryCamera", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextChannel, value: !SGSimpleSettings.shared.disableScrollToNextChannel, text: i18n("Settings.PullToNextChannel", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .disableScrollToNextTopic, value: !SGSimpleSettings.shared.disableScrollToNextTopic, text: i18n("Settings.PullToNextTopic", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .hideReactions, value: SGSimpleSettings.shared.hideReactions, text: i18n("Settings.HideReactions", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .uploadSpeedBoost, value: SGSimpleSettings.shared.uploadSpeedBoost, text: i18n("Settings.UploadsBoost", lang), enabled: true))
|
||||
entries.append(.oneFromManySelector(id: id.count, section: .other, settingName: .downloadSpeedBoost, text: i18n("Settings.DownloadsBoost", lang), value: i18n("Settings.DownloadsBoost.\(SGSimpleSettings.shared.downloadSpeedBoost)", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.DownloadsBoost.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .sendWithReturnKey, value: SGSettings.sendWithReturnKey, text: i18n("Settings.SendWithReturnKey", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .forceEmojiTab, value: SGSimpleSettings.shared.forceEmojiTab, text: i18n("Settings.ForceEmojiTab", lang), enabled: true))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .defaultEmojisFirst, value: SGSimpleSettings.shared.defaultEmojisFirst, text: i18n("Settings.DefaultEmojisFirst", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.DefaultEmojisFirst.Notice", lang)))
|
||||
entries.append(.toggle(id: id.count, section: .other, settingName: .hidePhoneInSettings, value: SGSimpleSettings.shared.hidePhoneInSettings, text: i18n("Settings.HidePhoneInSettingsUI", lang), enabled: true))
|
||||
entries.append(.notice(id: id.count, section: .other, text: i18n("Settings.HidePhoneInSettingsUI.Notice", lang)))
|
||||
|
||||
return filterSGItemListUIEntrires(entries: entries, by: state.searchQuery)
|
||||
}
|
||||
|
||||
public func sgSettingsController(context: AccountContext/*, focusOnItemTag: Int? = nil*/) -> ViewController {
|
||||
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
// var getRootControllerImpl: (() -> UIViewController?)?
|
||||
// var getNavigationControllerImpl: (() -> NavigationController?)?
|
||||
var askForRestart: (() -> Void)?
|
||||
|
||||
let initialState = SGSettingsControllerState()
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
let stateValue = Atomic(value: initialState)
|
||||
let updateState: ((SGSettingsControllerState) -> SGSettingsControllerState) -> Void = { f in
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
// let sliderPromise = ValuePromise(SGSimpleSettings.shared.accountColorsSaturation, ignoreRepeated: true)
|
||||
// let sliderStateValue = Atomic(value: SGSimpleSettings.shared.accountColorsSaturation)
|
||||
// let _: ((Int32) -> Int32) -> Void = { f in
|
||||
// sliderPromise.set(sliderStateValue.modify( {f($0)}))
|
||||
// }
|
||||
|
||||
let simplePromise = ValuePromise(true, ignoreRepeated: false)
|
||||
|
||||
let arguments = SGItemListArguments<SGBoolSetting, SGSliderSetting, SGOneFromManySetting, SGDisclosureLink, AnyHashable>(
|
||||
context: context,
|
||||
/*updatePeerColor: { color in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.updatedNameColor = color
|
||||
return updatedState
|
||||
}
|
||||
},*/ setBoolValue: { setting, value in
|
||||
switch setting {
|
||||
case .hidePhoneInSettings:
|
||||
SGSimpleSettings.shared.hidePhoneInSettings = value
|
||||
askForRestart?()
|
||||
case .showTabNames:
|
||||
SGSimpleSettings.shared.showTabNames = value
|
||||
askForRestart?()
|
||||
case .showContactsTab:
|
||||
let _ = (
|
||||
updateCallListSettingsInteractively(
|
||||
accountManager: context.sharedContext.accountManager, { $0.withUpdatedShowContactsTab(value) }
|
||||
)
|
||||
).start()
|
||||
case .showCallsTab:
|
||||
let _ = (
|
||||
updateCallListSettingsInteractively(
|
||||
accountManager: context.sharedContext.accountManager, { $0.withUpdatedShowTab(value) }
|
||||
)
|
||||
).start()
|
||||
case .foldersAtBottom:
|
||||
let _ = (
|
||||
updateExperimentalUISettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in
|
||||
var settings = settings
|
||||
settings.foldersTabAtBottom = value
|
||||
return settings
|
||||
}
|
||||
)
|
||||
).start()
|
||||
case .startTelescopeWithRearCam:
|
||||
SGSimpleSettings.shared.startTelescopeWithRearCam = value
|
||||
case .hideStories:
|
||||
let _ = (
|
||||
updateSGUISettings(engine: context.engine, { settings in
|
||||
var settings = settings
|
||||
settings.hideStories = value
|
||||
return settings
|
||||
})
|
||||
).start()
|
||||
case .showProfileId:
|
||||
let _ = (
|
||||
updateSGUISettings(engine: context.engine, { settings in
|
||||
var settings = settings
|
||||
settings.showProfileId = value
|
||||
return settings
|
||||
})
|
||||
).start()
|
||||
case .warnOnStoriesOpen:
|
||||
let _ = (
|
||||
updateSGUISettings(engine: context.engine, { settings in
|
||||
var settings = settings
|
||||
settings.warnOnStoriesOpen = value
|
||||
return settings
|
||||
})
|
||||
).start()
|
||||
case .sendWithReturnKey:
|
||||
let _ = (
|
||||
updateSGUISettings(engine: context.engine, { settings in
|
||||
var settings = settings
|
||||
settings.sendWithReturnKey = value
|
||||
return settings
|
||||
})
|
||||
).start()
|
||||
case .rememberLastFolder:
|
||||
SGSimpleSettings.shared.rememberLastFolder = value
|
||||
case .sendLargePhotos:
|
||||
SGSimpleSettings.shared.sendLargePhotos = value
|
||||
case .storyStealthMode:
|
||||
SGSimpleSettings.shared.storyStealthMode = value
|
||||
case .disableSwipeToRecordStory:
|
||||
SGSimpleSettings.shared.disableSwipeToRecordStory = value
|
||||
case .quickTranslateButton:
|
||||
SGSimpleSettings.shared.quickTranslateButton = value
|
||||
case .uploadSpeedBoost:
|
||||
SGSimpleSettings.shared.uploadSpeedBoost = value
|
||||
case .hideReactions:
|
||||
SGSimpleSettings.shared.hideReactions = value
|
||||
case .showRepostToStory:
|
||||
SGSimpleSettings.shared.showRepostToStory = value
|
||||
case .contextShowSelectFromUser:
|
||||
SGSimpleSettings.shared.contextShowSelectFromUser = value
|
||||
case .contextShowSaveToCloud:
|
||||
SGSimpleSettings.shared.contextShowSaveToCloud = value
|
||||
case .contextShowRestrict:
|
||||
SGSimpleSettings.shared.contextShowRestrict = value
|
||||
case .contextShowHideForwardName:
|
||||
SGSimpleSettings.shared.contextShowHideForwardName = value
|
||||
case .disableScrollToNextChannel:
|
||||
SGSimpleSettings.shared.disableScrollToNextChannel = !value
|
||||
case .disableScrollToNextTopic:
|
||||
SGSimpleSettings.shared.disableScrollToNextTopic = !value
|
||||
case .disableChatSwipeOptions:
|
||||
SGSimpleSettings.shared.disableChatSwipeOptions = !value
|
||||
simplePromise.set(true) // Trigger update for 'enabled' field of other toggles
|
||||
askForRestart?()
|
||||
case .disableDeleteChatSwipeOption:
|
||||
SGSimpleSettings.shared.disableDeleteChatSwipeOption = !value
|
||||
askForRestart?()
|
||||
case .disableGalleryCamera:
|
||||
SGSimpleSettings.shared.disableGalleryCamera = !value
|
||||
case .disableSendAsButton:
|
||||
SGSimpleSettings.shared.disableSendAsButton = !value
|
||||
case .disableSnapDeletionEffect:
|
||||
SGSimpleSettings.shared.disableSnapDeletionEffect = !value
|
||||
case .contextShowReport:
|
||||
SGSimpleSettings.shared.contextShowReport = value
|
||||
case .contextShowReply:
|
||||
SGSimpleSettings.shared.contextShowReply = value
|
||||
case .contextShowPin:
|
||||
SGSimpleSettings.shared.contextShowPin = value
|
||||
case .contextShowSaveMedia:
|
||||
SGSimpleSettings.shared.contextShowSaveMedia = value
|
||||
case .contextShowMessageReplies:
|
||||
SGSimpleSettings.shared.contextShowMessageReplies = value
|
||||
case .stickerTimestamp:
|
||||
SGSimpleSettings.shared.stickerTimestamp = value
|
||||
case .contextShowJson:
|
||||
SGSimpleSettings.shared.contextShowJson = value
|
||||
case .hideRecordingButton:
|
||||
SGSimpleSettings.shared.hideRecordingButton = !value
|
||||
case .hideTabBar:
|
||||
SGSimpleSettings.shared.hideTabBar = value
|
||||
simplePromise.set(true) // Trigger update for 'enabled' field of other toggles
|
||||
askForRestart?()
|
||||
case .showDC:
|
||||
SGSimpleSettings.shared.showDC = value
|
||||
case .showCreationDate:
|
||||
SGSimpleSettings.shared.showCreationDate = value
|
||||
case .showRegDate:
|
||||
SGSimpleSettings.shared.showRegDate = value
|
||||
case .compactChatList:
|
||||
SGSimpleSettings.shared.compactChatList = value
|
||||
askForRestart?()
|
||||
case .compactFolderNames:
|
||||
SGSimpleSettings.shared.compactFolderNames = value
|
||||
case .allChatsHidden:
|
||||
SGSimpleSettings.shared.allChatsHidden = value
|
||||
askForRestart?()
|
||||
case .defaultEmojisFirst:
|
||||
SGSimpleSettings.shared.defaultEmojisFirst = value
|
||||
case .messageDoubleTapActionOutgoingEdit:
|
||||
SGSimpleSettings.shared.messageDoubleTapActionOutgoing = value ? SGSimpleSettings.MessageDoubleTapAction.edit.rawValue : SGSimpleSettings.MessageDoubleTapAction.default.rawValue
|
||||
case .wideChannelPosts:
|
||||
SGSimpleSettings.shared.wideChannelPosts = value
|
||||
case .forceEmojiTab:
|
||||
SGSimpleSettings.shared.forceEmojiTab = value
|
||||
case .forceBuiltInMic:
|
||||
SGSimpleSettings.shared.forceBuiltInMic = value
|
||||
case .hideChannelBottomButton:
|
||||
SGSimpleSettings.shared.hideChannelBottomButton = value
|
||||
case .confirmCalls:
|
||||
SGSimpleSettings.shared.confirmCalls = value
|
||||
case .swipeForVideoPIP:
|
||||
SGSimpleSettings.shared.videoPIPSwipeDirection = value ? SGSimpleSettings.VideoPIPSwipeDirection.up.rawValue : SGSimpleSettings.VideoPIPSwipeDirection.none.rawValue
|
||||
}
|
||||
}, updateSliderValue: { setting, value in
|
||||
switch (setting) {
|
||||
case .accountColorsSaturation:
|
||||
if SGSimpleSettings.shared.accountColorsSaturation != value {
|
||||
SGSimpleSettings.shared.accountColorsSaturation = value
|
||||
simplePromise.set(true)
|
||||
}
|
||||
case .outgoingPhotoQuality:
|
||||
if SGSimpleSettings.shared.outgoingPhotoQuality != value {
|
||||
SGSimpleSettings.shared.outgoingPhotoQuality = value
|
||||
simplePromise.set(true)
|
||||
}
|
||||
case .stickerSize:
|
||||
if SGSimpleSettings.shared.stickerSize != value {
|
||||
SGSimpleSettings.shared.stickerSize = value
|
||||
simplePromise.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
}, setOneFromManyValue: { setting in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
switch (setting) {
|
||||
case .downloadSpeedBoost:
|
||||
let setAction: (String) -> Void = { value in
|
||||
SGSimpleSettings.shared.downloadSpeedBoost = value
|
||||
|
||||
let enableDownloadX: Bool
|
||||
switch (value) {
|
||||
case SGSimpleSettings.DownloadSpeedBoostValues.none.rawValue:
|
||||
enableDownloadX = false
|
||||
default:
|
||||
enableDownloadX = true
|
||||
}
|
||||
|
||||
// Updating controller
|
||||
simplePromise.set(true)
|
||||
|
||||
let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in
|
||||
var settings = settings
|
||||
settings.useExperimentalDownload = enableDownloadX
|
||||
return settings
|
||||
}).start(completed: {
|
||||
Queue.mainQueue().async {
|
||||
askForRestart?()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for value in SGSimpleSettings.DownloadSpeedBoostValues.allCases {
|
||||
items.append(ActionSheetButtonItem(title: i18n("Settings.DownloadsBoost.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
setAction(value.rawValue)
|
||||
}))
|
||||
}
|
||||
case .bottomTabStyle:
|
||||
let setAction: (String) -> Void = { value in
|
||||
SGSimpleSettings.shared.bottomTabStyle = value
|
||||
simplePromise.set(true)
|
||||
}
|
||||
|
||||
for value in SGSimpleSettings.BottomTabStyleValues.allCases {
|
||||
items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.BottomTabStyle.\(value.rawValue)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
setAction(value.rawValue)
|
||||
}))
|
||||
}
|
||||
case .allChatsTitleLengthOverride:
|
||||
let setAction: (String) -> Void = { value in
|
||||
SGSimpleSettings.shared.allChatsTitleLengthOverride = value
|
||||
simplePromise.set(true)
|
||||
}
|
||||
|
||||
for value in SGSimpleSettings.AllChatsTitleLengthOverride.allCases {
|
||||
let title: String
|
||||
switch (value) {
|
||||
case SGSimpleSettings.AllChatsTitleLengthOverride.short:
|
||||
title = "\"\(presentationData.strings.ChatList_Tabs_All)\""
|
||||
case SGSimpleSettings.AllChatsTitleLengthOverride.long:
|
||||
title = "\"\(presentationData.strings.ChatList_Tabs_AllChats)\""
|
||||
default:
|
||||
title = i18n("Settings.Folders.AllChatsTitle.none", presentationData.strings.baseLanguageCode)
|
||||
}
|
||||
items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
setAction(value.rawValue)
|
||||
}))
|
||||
}
|
||||
// case .allChatsFolderPositionOverride:
|
||||
// let setAction: (String) -> Void = { value in
|
||||
// SGSimpleSettings.shared.allChatsFolderPositionOverride = value
|
||||
// simplePromise.set(true)
|
||||
// }
|
||||
//
|
||||
// for value in SGSimpleSettings.AllChatsFolderPositionOverride.allCases {
|
||||
// items.append(ActionSheetButtonItem(title: i18n("Settings.Folders.AllChatsTitle.\(value)", presentationData.strings.baseLanguageCode), color: .accent, action: { [weak actionSheet] in
|
||||
// actionSheet?.dismissAnimated()
|
||||
// setAction(value.rawValue)
|
||||
// }))
|
||||
// }
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}, openDisclosureLink: { link in
|
||||
switch (link) {
|
||||
case .languageSettings:
|
||||
pushControllerImpl?(context.sharedContext.makeLocalizationListController(context: context))
|
||||
case .contentSettings:
|
||||
let _ = (getSGSettingsURL(context: context) |> deliverOnMainQueue).start(next: { [weak context] url in
|
||||
guard let strongContext = context else {
|
||||
return
|
||||
}
|
||||
strongContext.sharedContext.applicationBindings.openUrl(url)
|
||||
})
|
||||
}
|
||||
}, searchInput: { searchQuery in
|
||||
updateState { state in
|
||||
var updatedState = state
|
||||
updatedState.searchQuery = searchQuery
|
||||
return updatedState
|
||||
}
|
||||
})
|
||||
|
||||
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings])
|
||||
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.SGUISettings, PreferencesKeys.appConfiguration])
|
||||
let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network)
|
||||
|> map(Optional.init)
|
||||
let contentSettingsConfiguration = Promise<ContentSettingsConfiguration?>()
|
||||
contentSettingsConfiguration.set(.single(nil)
|
||||
|> then(updatedContentSettingsConfiguration))
|
||||
|
||||
let signal = combineLatest(simplePromise.get(), /*sliderPromise.get(),*/ statePromise.get(), context.sharedContext.presentationData, sharedData, preferences, contentSettingsConfiguration.get(),
|
||||
context.engine.accountData.observeAvailableColorOptions(scope: .replies),
|
||||
context.engine.accountData.observeAvailableColorOptions(scope: .profile)
|
||||
)
|
||||
|> map { _, /*sliderValue,*/ state, presentationData, sharedData, view, contentSettingsConfiguration, availableReplyColors, availableProfileColors -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
|
||||
let sgUISettings: SGUISettings = view.values[ApplicationSpecificPreferencesKeys.SGUISettings]?.get(SGUISettings.self) ?? SGUISettings.default
|
||||
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
|
||||
let callListSettings: CallListSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings]?.get(CallListSettings.self) ?? CallListSettings.defaultSettings
|
||||
let experimentalUISettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings
|
||||
|
||||
let entries = SGControllerEntries(presentationData: presentationData, callListSettings: callListSettings, experimentalUISettings: experimentalUISettings, SGSettings: sgUISettings, appConfiguration: appConfiguration, nameColors: PeerNameColors.with(availableReplyColors: availableReplyColors, availableProfileColors: availableProfileColors), state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Swiftgram"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
||||
|
||||
// TODO(swiftgram): focusOnItemTag support
|
||||
/* var index = 0
|
||||
var scrollToItem: ListViewScrollToItem?
|
||||
if let focusOnItemTag = focusOnItemTag {
|
||||
for entry in entries {
|
||||
if entry.tag?.isEqual(to: focusOnItemTag) ?? false {
|
||||
scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
} */
|
||||
|
||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: /*focusOnItemTag*/ nil, initialScrollToItem: nil /* scrollToItem*/ )
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
presentControllerImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
pushControllerImpl = { [weak controller] c in
|
||||
(controller?.navigationController as? NavigationController)?.pushViewController(c)
|
||||
}
|
||||
// getRootControllerImpl = { [weak controller] in
|
||||
// return controller?.view.window?.rootViewController
|
||||
// }
|
||||
// getNavigationControllerImpl = { [weak controller] in
|
||||
// return controller?.navigationController as? NavigationController
|
||||
// }
|
||||
askForRestart = { [weak context] in
|
||||
guard let context = context else {
|
||||
return
|
||||
}
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
presentControllerImpl?(
|
||||
UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
content: .info(title: nil, // i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode),
|
||||
text: i18n("Common.RestartRequired", presentationData.strings.baseLanguageCode),
|
||||
timeout: nil,
|
||||
customUndoText: i18n("Common.RestartNow", presentationData.strings.baseLanguageCode) //presentationData.strings.Common_Yes
|
||||
),
|
||||
elevatedLayout: false,
|
||||
action: { action in if action == .undo { exit(0) }; return true }
|
||||
),
|
||||
nil
|
||||
)
|
||||
}
|
||||
return controller
|
||||
|
||||
}
|
||||
9
Swiftgram/SGShowMessageJson/BUILD
Normal file
9
Swiftgram/SGShowMessageJson/BUILD
Normal file
@@ -0,0 +1,9 @@
|
||||
filegroup(
|
||||
name = "SGShowMessageJson",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
76
Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift
Normal file
76
Swiftgram/SGShowMessageJson/Sources/SGShowMessageJson.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import Foundation
|
||||
import Wrap
|
||||
import SGLogging
|
||||
import ChatControllerInteraction
|
||||
import ChatPresentationInterfaceState
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
|
||||
public func showMessageJson(controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, message: Message, context: AccountContext) {
|
||||
if let navigationController = controllerInteraction.navigationController(), let rootController = navigationController.view.window?.rootViewController {
|
||||
var writingOptions: JSONSerialization.WritingOptions = [
|
||||
.prettyPrinted,
|
||||
//.sortedKeys,
|
||||
]
|
||||
if #available(iOS 13.0, *) {
|
||||
writingOptions.insert(.withoutEscapingSlashes)
|
||||
}
|
||||
|
||||
var messageData: Data? = nil
|
||||
do {
|
||||
messageData = try wrap(
|
||||
message,
|
||||
writingOptions: writingOptions
|
||||
)
|
||||
} catch {
|
||||
SGLogger.shared.log("ShowMessageJSON", "Error parsing data: \(error)")
|
||||
messageData = nil
|
||||
}
|
||||
|
||||
guard let messageData = messageData else { return }
|
||||
|
||||
let id = Int64.random(in: Int64.min ... Int64.max)
|
||||
let fileResource = LocalFileMediaResource(fileId: id, size: Int64(messageData.count), isSecretRelated: false)
|
||||
context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: messageData, synchronous: true)
|
||||
|
||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/json; charset=utf-8", size: Int64(messageData.count), attributes: [.FileName(fileName: "message.json")], alternativeRepresentations: [])
|
||||
|
||||
presentDocumentPreviewController(rootController: rootController, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings, postbox: context.account.postbox, file: file, canShare: !message.isCopyProtected())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension MemoryBuffer: @retroactive WrapCustomizable {
|
||||
|
||||
public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? {
|
||||
let hexString = self.description
|
||||
return ["string": hexStringToString(hexString) ?? hexString]
|
||||
}
|
||||
}
|
||||
|
||||
// There's a chacne we will need it for each empty/weird type, or it will be a runtime crash.
|
||||
extension ContentRequiresValidationMessageAttribute: @retroactive WrapCustomizable {
|
||||
|
||||
public func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? {
|
||||
return ["@type": "ContentRequiresValidationMessageAttribute"]
|
||||
}
|
||||
}
|
||||
|
||||
func hexStringToString(_ hexString: String) -> String? {
|
||||
var chars = Array(hexString)
|
||||
var result = ""
|
||||
|
||||
while chars.count > 0 {
|
||||
let c = String(chars[0...1])
|
||||
chars = Array(chars.dropFirst(2))
|
||||
if let byte = UInt8(c, radix: 16) {
|
||||
let scalar = UnicodeScalar(byte)
|
||||
result.append(String(scalar))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
17
Swiftgram/SGSimpleSettings/BUILD
Normal file
17
Swiftgram/SGSimpleSettings/BUILD
Normal file
@@ -0,0 +1,17 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGSimpleSettings",
|
||||
module_name = "SGSimpleSettings",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
58
Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift
Normal file
58
Swiftgram/SGSimpleSettings/Sources/AtomicWrapper.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//// A copy of Atomic from SwiftSignalKit
|
||||
//import Foundation
|
||||
//
|
||||
//public enum AtomicWrapperLockError: Error {
|
||||
// case isLocked
|
||||
//}
|
||||
//
|
||||
//public final class AtomicWrapper<T> {
|
||||
// private var lock: pthread_mutex_t
|
||||
// private var value: T
|
||||
//
|
||||
// public init(value: T) {
|
||||
// self.lock = pthread_mutex_t()
|
||||
// self.value = value
|
||||
//
|
||||
// pthread_mutex_init(&self.lock, nil)
|
||||
// }
|
||||
//
|
||||
// deinit {
|
||||
// pthread_mutex_destroy(&self.lock)
|
||||
// }
|
||||
//
|
||||
// public func with<R>(_ f: (T) -> R) -> R {
|
||||
// pthread_mutex_lock(&self.lock)
|
||||
// let result = f(self.value)
|
||||
// pthread_mutex_unlock(&self.lock)
|
||||
//
|
||||
// return result
|
||||
// }
|
||||
//
|
||||
// public func tryWith<R>(_ f: (T) -> R) throws -> R {
|
||||
// if pthread_mutex_trylock(&self.lock) == 0 {
|
||||
// let result = f(self.value)
|
||||
// pthread_mutex_unlock(&self.lock)
|
||||
// return result
|
||||
// } else {
|
||||
// throw AtomicWrapperLockError.isLocked
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func modify(_ f: (T) -> T) -> T {
|
||||
// pthread_mutex_lock(&self.lock)
|
||||
// let result = f(self.value)
|
||||
// self.value = result
|
||||
// pthread_mutex_unlock(&self.lock)
|
||||
//
|
||||
// return result
|
||||
// }
|
||||
//
|
||||
// public func swap(_ value: T) -> T {
|
||||
// pthread_mutex_lock(&self.lock)
|
||||
// let previous = self.value
|
||||
// self.value = value
|
||||
// pthread_mutex_unlock(&self.lock)
|
||||
//
|
||||
// return previous
|
||||
// }
|
||||
//}
|
||||
36
Swiftgram/SGSimpleSettings/Sources/RWLock.swift
Normal file
36
Swiftgram/SGSimpleSettings/Sources/RWLock.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// RWLock.swift
|
||||
// SwiftConcurrentCollections
|
||||
//
|
||||
// Created by Pete Prokop on 09/02/2020.
|
||||
// Copyright © 2020 Pete Prokop. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class RWLock {
|
||||
private var lock: pthread_rwlock_t
|
||||
|
||||
// MARK: Lifecycle
|
||||
deinit {
|
||||
pthread_rwlock_destroy(&lock)
|
||||
}
|
||||
|
||||
public init() {
|
||||
lock = pthread_rwlock_t()
|
||||
pthread_rwlock_init(&lock, nil)
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
public func writeLock() {
|
||||
pthread_rwlock_wrlock(&lock)
|
||||
}
|
||||
|
||||
public func readLock() {
|
||||
pthread_rwlock_rdlock(&lock)
|
||||
}
|
||||
|
||||
public func unlock() {
|
||||
pthread_rwlock_unlock(&lock)
|
||||
}
|
||||
}
|
||||
445
Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift
Normal file
445
Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift
Normal file
@@ -0,0 +1,445 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public class SGSimpleSettings {
|
||||
|
||||
public static let shared = SGSimpleSettings()
|
||||
|
||||
private init() {
|
||||
setDefaultValues()
|
||||
preCacheValues()
|
||||
}
|
||||
|
||||
private func setDefaultValues() {
|
||||
UserDefaults.standard.register(defaults: SGSimpleSettings.defaultValues)
|
||||
}
|
||||
|
||||
private func preCacheValues() {
|
||||
// let dispatchGroup = DispatchGroup()
|
||||
|
||||
let tasks = [
|
||||
// { let _ = self.allChatsFolderPositionOverride },
|
||||
{ let _ = self.allChatsHidden },
|
||||
{ let _ = self.hideTabBar },
|
||||
{ let _ = self.bottomTabStyle },
|
||||
{ let _ = self.compactChatList },
|
||||
{ let _ = self.compactFolderNames },
|
||||
{ let _ = self.disableSwipeToRecordStory },
|
||||
{ let _ = self.rememberLastFolder },
|
||||
{ let _ = self.quickTranslateButton },
|
||||
{ let _ = self.stickerSize },
|
||||
{ let _ = self.stickerTimestamp },
|
||||
{ let _ = self.hideReactions },
|
||||
{ let _ = self.disableGalleryCamera },
|
||||
{ let _ = self.disableSendAsButton },
|
||||
{ let _ = self.disableSnapDeletionEffect },
|
||||
{ let _ = self.startTelescopeWithRearCam },
|
||||
{ let _ = self.hideRecordingButton }
|
||||
]
|
||||
|
||||
tasks.forEach { task in
|
||||
DispatchQueue.global(qos: .background).async(/*group: dispatchGroup*/) {
|
||||
task()
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchGroup.notify(queue: DispatchQueue.main) {}
|
||||
}
|
||||
|
||||
public enum Keys: String, CaseIterable {
|
||||
case hidePhoneInSettings
|
||||
case showTabNames
|
||||
case startTelescopeWithRearCam
|
||||
case accountColorsSaturation
|
||||
case uploadSpeedBoost
|
||||
case downloadSpeedBoost
|
||||
case bottomTabStyle
|
||||
case rememberLastFolder
|
||||
case lastAccountFolders
|
||||
case localDNSForProxyHost
|
||||
case sendLargePhotos
|
||||
case outgoingPhotoQuality
|
||||
case storyStealthMode
|
||||
case canUseStealthMode
|
||||
case disableSwipeToRecordStory
|
||||
case quickTranslateButton
|
||||
case outgoingLanguageTranslation
|
||||
case hideReactions
|
||||
case showRepostToStory
|
||||
case contextShowSelectFromUser
|
||||
case contextShowSaveToCloud
|
||||
case contextShowRestrict
|
||||
// case contextShowBan
|
||||
case contextShowHideForwardName
|
||||
case contextShowReport
|
||||
case contextShowReply
|
||||
case contextShowPin
|
||||
case contextShowSaveMedia
|
||||
case contextShowMessageReplies
|
||||
case contextShowJson
|
||||
case disableScrollToNextChannel
|
||||
case disableScrollToNextTopic
|
||||
case disableChatSwipeOptions
|
||||
case disableDeleteChatSwipeOption
|
||||
case disableGalleryCamera
|
||||
case disableSendAsButton
|
||||
case disableSnapDeletionEffect
|
||||
case stickerSize
|
||||
case stickerTimestamp
|
||||
case hideRecordingButton
|
||||
case hideTabBar
|
||||
case showDC
|
||||
case showCreationDate
|
||||
case showRegDate
|
||||
case regDateCache
|
||||
case compactChatList
|
||||
case compactFolderNames
|
||||
case allChatsTitleLengthOverride
|
||||
// case allChatsFolderPositionOverride
|
||||
case allChatsHidden
|
||||
case defaultEmojisFirst
|
||||
case messageDoubleTapActionOutgoing
|
||||
case wideChannelPosts
|
||||
case forceEmojiTab
|
||||
case forceBuiltInMic
|
||||
case hideChannelBottomButton
|
||||
case forceSystemSharing
|
||||
case confirmCalls
|
||||
case videoPIPSwipeDirection
|
||||
case legacyNotificationsFix
|
||||
case messageFilterKeywords
|
||||
}
|
||||
|
||||
public enum DownloadSpeedBoostValues: String, CaseIterable {
|
||||
case none
|
||||
case medium
|
||||
case maximum
|
||||
}
|
||||
|
||||
public enum BottomTabStyleValues: String, CaseIterable {
|
||||
case telegram
|
||||
case ios
|
||||
}
|
||||
|
||||
public enum AllChatsTitleLengthOverride: String, CaseIterable {
|
||||
case none
|
||||
case short
|
||||
case long
|
||||
}
|
||||
|
||||
public enum AllChatsFolderPositionOverride: String, CaseIterable {
|
||||
case none
|
||||
case last
|
||||
case hidden
|
||||
}
|
||||
|
||||
public enum MessageDoubleTapAction: String, CaseIterable {
|
||||
case `default`
|
||||
case none
|
||||
case edit
|
||||
}
|
||||
|
||||
public enum VideoPIPSwipeDirection: String, CaseIterable {
|
||||
case up
|
||||
case down
|
||||
case none
|
||||
}
|
||||
|
||||
public static let defaultValues: [String: Any] = [
|
||||
Keys.hidePhoneInSettings.rawValue: true,
|
||||
Keys.showTabNames.rawValue: true,
|
||||
Keys.startTelescopeWithRearCam.rawValue: false,
|
||||
Keys.accountColorsSaturation.rawValue: 100,
|
||||
Keys.uploadSpeedBoost.rawValue: false,
|
||||
Keys.downloadSpeedBoost.rawValue: DownloadSpeedBoostValues.none.rawValue,
|
||||
Keys.rememberLastFolder.rawValue: false,
|
||||
Keys.bottomTabStyle.rawValue: BottomTabStyleValues.telegram.rawValue,
|
||||
Keys.lastAccountFolders.rawValue: [:],
|
||||
Keys.localDNSForProxyHost.rawValue: false,
|
||||
Keys.sendLargePhotos.rawValue: false,
|
||||
Keys.outgoingPhotoQuality.rawValue: 70,
|
||||
Keys.storyStealthMode.rawValue: false,
|
||||
Keys.canUseStealthMode.rawValue: true,
|
||||
Keys.disableSwipeToRecordStory.rawValue: false,
|
||||
Keys.quickTranslateButton.rawValue: false,
|
||||
Keys.outgoingLanguageTranslation.rawValue: [:],
|
||||
Keys.hideReactions.rawValue: false,
|
||||
Keys.showRepostToStory.rawValue: true,
|
||||
Keys.contextShowSelectFromUser.rawValue: true,
|
||||
Keys.contextShowSaveToCloud.rawValue: true,
|
||||
Keys.contextShowRestrict.rawValue: true,
|
||||
// Keys.contextShowBan.rawValue: true,
|
||||
Keys.contextShowHideForwardName.rawValue: true,
|
||||
Keys.contextShowReport.rawValue: true,
|
||||
Keys.contextShowReply.rawValue: true,
|
||||
Keys.contextShowPin.rawValue: true,
|
||||
Keys.contextShowSaveMedia.rawValue: true,
|
||||
Keys.contextShowMessageReplies.rawValue: true,
|
||||
Keys.contextShowJson.rawValue: false,
|
||||
Keys.disableScrollToNextChannel.rawValue: false,
|
||||
Keys.disableScrollToNextTopic.rawValue: false,
|
||||
Keys.disableChatSwipeOptions.rawValue: false,
|
||||
Keys.disableDeleteChatSwipeOption.rawValue: false,
|
||||
Keys.disableGalleryCamera.rawValue: false,
|
||||
Keys.disableSendAsButton.rawValue: false,
|
||||
Keys.disableSnapDeletionEffect.rawValue: false,
|
||||
Keys.stickerSize.rawValue: 100,
|
||||
Keys.stickerTimestamp.rawValue: true,
|
||||
Keys.hideRecordingButton.rawValue: false,
|
||||
Keys.hideTabBar.rawValue: false,
|
||||
Keys.showDC.rawValue: false,
|
||||
Keys.showCreationDate.rawValue: true,
|
||||
Keys.showRegDate.rawValue: true,
|
||||
Keys.regDateCache.rawValue: [:],
|
||||
Keys.compactChatList.rawValue: false,
|
||||
Keys.compactFolderNames.rawValue: false,
|
||||
Keys.allChatsTitleLengthOverride.rawValue: AllChatsTitleLengthOverride.none.rawValue,
|
||||
// Keys.allChatsFolderPositionOverride.rawValue: AllChatsFolderPositionOverride.none.rawValue
|
||||
Keys.allChatsHidden.rawValue: false,
|
||||
Keys.defaultEmojisFirst.rawValue: false,
|
||||
Keys.messageDoubleTapActionOutgoing.rawValue: MessageDoubleTapAction.default.rawValue,
|
||||
Keys.wideChannelPosts.rawValue: false,
|
||||
Keys.forceEmojiTab.rawValue: false,
|
||||
Keys.hideChannelBottomButton.rawValue: false,
|
||||
Keys.forceSystemSharing.rawValue: false,
|
||||
Keys.confirmCalls.rawValue: true,
|
||||
Keys.videoPIPSwipeDirection.rawValue: VideoPIPSwipeDirection.up.rawValue,
|
||||
Keys.legacyNotificationsFix.rawValue: false,
|
||||
Keys.messageFilterKeywords.rawValue: []
|
||||
]
|
||||
|
||||
@UserDefault(key: Keys.hidePhoneInSettings.rawValue)
|
||||
public var hidePhoneInSettings: Bool
|
||||
|
||||
@UserDefault(key: Keys.showTabNames.rawValue)
|
||||
public var showTabNames: Bool
|
||||
|
||||
@UserDefault(key: Keys.startTelescopeWithRearCam.rawValue)
|
||||
public var startTelescopeWithRearCam: Bool
|
||||
|
||||
@UserDefault(key: Keys.accountColorsSaturation.rawValue)
|
||||
public var accountColorsSaturation: Int32
|
||||
|
||||
@UserDefault(key: Keys.uploadSpeedBoost.rawValue)
|
||||
public var uploadSpeedBoost: Bool
|
||||
|
||||
@UserDefault(key: Keys.downloadSpeedBoost.rawValue)
|
||||
public var downloadSpeedBoost: String
|
||||
|
||||
@UserDefault(key: Keys.rememberLastFolder.rawValue)
|
||||
public var rememberLastFolder: Bool
|
||||
|
||||
@UserDefault(key: Keys.bottomTabStyle.rawValue)
|
||||
public var bottomTabStyle: String
|
||||
|
||||
public var lastAccountFolders = UserDefaultsBackedDictionary<String, Int32>(userDefaultsKey: Keys.lastAccountFolders.rawValue, threadSafe: false)
|
||||
|
||||
@UserDefault(key: Keys.localDNSForProxyHost.rawValue)
|
||||
public var localDNSForProxyHost: Bool
|
||||
|
||||
@UserDefault(key: Keys.sendLargePhotos.rawValue)
|
||||
public var sendLargePhotos: Bool
|
||||
|
||||
@UserDefault(key: Keys.outgoingPhotoQuality.rawValue)
|
||||
public var outgoingPhotoQuality: Int32
|
||||
|
||||
@UserDefault(key: Keys.storyStealthMode.rawValue)
|
||||
public var storyStealthMode: Bool
|
||||
|
||||
@UserDefault(key: Keys.canUseStealthMode.rawValue)
|
||||
public var canUseStealthMode: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableSwipeToRecordStory.rawValue)
|
||||
public var disableSwipeToRecordStory: Bool
|
||||
|
||||
@UserDefault(key: Keys.quickTranslateButton.rawValue)
|
||||
public var quickTranslateButton: Bool
|
||||
|
||||
public var outgoingLanguageTranslation = UserDefaultsBackedDictionary<String, String>(userDefaultsKey: Keys.outgoingLanguageTranslation.rawValue, threadSafe: false)
|
||||
|
||||
@UserDefault(key: Keys.hideReactions.rawValue)
|
||||
public var hideReactions: Bool
|
||||
|
||||
@UserDefault(key: Keys.showRepostToStory.rawValue)
|
||||
public var showRepostToStory: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowRestrict.rawValue)
|
||||
public var contextShowRestrict: Bool
|
||||
|
||||
/*@UserDefault(key: Keys.contextShowBan.rawValue)
|
||||
public var contextShowBan: Bool*/
|
||||
|
||||
@UserDefault(key: Keys.contextShowSelectFromUser.rawValue)
|
||||
public var contextShowSelectFromUser: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowSaveToCloud.rawValue)
|
||||
public var contextShowSaveToCloud: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowHideForwardName.rawValue)
|
||||
public var contextShowHideForwardName: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowReport.rawValue)
|
||||
public var contextShowReport: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowReply.rawValue)
|
||||
public var contextShowReply: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowPin.rawValue)
|
||||
public var contextShowPin: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowSaveMedia.rawValue)
|
||||
public var contextShowSaveMedia: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowMessageReplies.rawValue)
|
||||
public var contextShowMessageReplies: Bool
|
||||
|
||||
@UserDefault(key: Keys.contextShowJson.rawValue)
|
||||
public var contextShowJson: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableScrollToNextChannel.rawValue)
|
||||
public var disableScrollToNextChannel: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableScrollToNextTopic.rawValue)
|
||||
public var disableScrollToNextTopic: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableChatSwipeOptions.rawValue)
|
||||
public var disableChatSwipeOptions: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableDeleteChatSwipeOption.rawValue)
|
||||
public var disableDeleteChatSwipeOption: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableGalleryCamera.rawValue)
|
||||
public var disableGalleryCamera: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableSendAsButton.rawValue)
|
||||
public var disableSendAsButton: Bool
|
||||
|
||||
@UserDefault(key: Keys.disableSnapDeletionEffect.rawValue)
|
||||
public var disableSnapDeletionEffect: Bool
|
||||
|
||||
@UserDefault(key: Keys.stickerSize.rawValue)
|
||||
public var stickerSize: Int32
|
||||
|
||||
@UserDefault(key: Keys.stickerTimestamp.rawValue)
|
||||
public var stickerTimestamp: Bool
|
||||
|
||||
@UserDefault(key: Keys.hideRecordingButton.rawValue)
|
||||
public var hideRecordingButton: Bool
|
||||
|
||||
@UserDefault(key: Keys.hideTabBar.rawValue)
|
||||
public var hideTabBar: Bool
|
||||
|
||||
@UserDefault(key: Keys.showDC.rawValue)
|
||||
public var showDC: Bool
|
||||
|
||||
@UserDefault(key: Keys.showCreationDate.rawValue)
|
||||
public var showCreationDate: Bool
|
||||
|
||||
@UserDefault(key: Keys.showRegDate.rawValue)
|
||||
public var showRegDate: Bool
|
||||
|
||||
public var regDateCache = UserDefaultsBackedDictionary<String, Data>(userDefaultsKey: Keys.regDateCache.rawValue, threadSafe: false)
|
||||
|
||||
@UserDefault(key: Keys.compactChatList.rawValue)
|
||||
public var compactChatList: Bool
|
||||
|
||||
@UserDefault(key: Keys.compactFolderNames.rawValue)
|
||||
public var compactFolderNames: Bool
|
||||
|
||||
@UserDefault(key: Keys.allChatsTitleLengthOverride.rawValue)
|
||||
public var allChatsTitleLengthOverride: String
|
||||
//
|
||||
// @UserDefault(key: Keys.allChatsFolderPositionOverride.rawValue)
|
||||
// public var allChatsFolderPositionOverride: String
|
||||
@UserDefault(key: Keys.allChatsHidden.rawValue)
|
||||
public var allChatsHidden: Bool
|
||||
|
||||
@UserDefault(key: Keys.defaultEmojisFirst.rawValue)
|
||||
public var defaultEmojisFirst: Bool
|
||||
|
||||
@UserDefault(key: Keys.messageDoubleTapActionOutgoing.rawValue)
|
||||
public var messageDoubleTapActionOutgoing: String
|
||||
|
||||
@UserDefault(key: Keys.wideChannelPosts.rawValue)
|
||||
public var wideChannelPosts: Bool
|
||||
|
||||
@UserDefault(key: Keys.forceEmojiTab.rawValue)
|
||||
public var forceEmojiTab: Bool
|
||||
|
||||
@UserDefault(key: Keys.forceBuiltInMic.rawValue)
|
||||
public var forceBuiltInMic: Bool
|
||||
|
||||
@UserDefault(key: Keys.hideChannelBottomButton.rawValue)
|
||||
public var hideChannelBottomButton: Bool
|
||||
|
||||
@UserDefault(key: Keys.forceSystemSharing.rawValue)
|
||||
public var forceSystemSharing: Bool
|
||||
|
||||
@UserDefault(key: Keys.confirmCalls.rawValue)
|
||||
public var confirmCalls: Bool
|
||||
|
||||
@UserDefault(key: Keys.videoPIPSwipeDirection.rawValue)
|
||||
public var videoPIPSwipeDirection: String
|
||||
|
||||
@UserDefault(key: Keys.legacyNotificationsFix.rawValue)
|
||||
public var legacyNotificationsFix: Bool
|
||||
|
||||
public var b: Bool = true
|
||||
|
||||
@UserDefault(key: Keys.messageFilterKeywords.rawValue)
|
||||
public var messageFilterKeywords: [String]
|
||||
}
|
||||
|
||||
extension SGSimpleSettings {
|
||||
public var isStealthModeEnabled: Bool {
|
||||
return storyStealthMode && canUseStealthMode
|
||||
}
|
||||
|
||||
public static func makeOutgoingLanguageTranslationKey(accountId: Int64, peerId: Int64) -> String {
|
||||
return "\(accountId):\(peerId)"
|
||||
}
|
||||
}
|
||||
|
||||
public func getSGDownloadPartSize(_ default: Int64, fileSize: Int64?) -> Int64 {
|
||||
let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost
|
||||
// Increasing chunk size for small files make it worse in terms of overall download performance
|
||||
let smallFileSizeThreshold = 1 * 1024 * 1024 // 1 MB
|
||||
switch (currentDownloadSetting) {
|
||||
case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue:
|
||||
if let fileSize, fileSize <= smallFileSizeThreshold {
|
||||
return `default`
|
||||
}
|
||||
return 512 * 1024
|
||||
case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue:
|
||||
if let fileSize, fileSize <= smallFileSizeThreshold {
|
||||
return `default`
|
||||
}
|
||||
return 1024 * 1024
|
||||
default:
|
||||
return `default`
|
||||
}
|
||||
}
|
||||
|
||||
public func getSGMaxPendingParts(_ default: Int) -> Int {
|
||||
let currentDownloadSetting = SGSimpleSettings.shared.downloadSpeedBoost
|
||||
switch (currentDownloadSetting) {
|
||||
case SGSimpleSettings.DownloadSpeedBoostValues.medium.rawValue:
|
||||
return 8
|
||||
case SGSimpleSettings.DownloadSpeedBoostValues.maximum.rawValue:
|
||||
return 12
|
||||
default:
|
||||
return `default`
|
||||
}
|
||||
}
|
||||
|
||||
public func sgUseShortAllChatsTitle(_ default: Bool) -> Bool {
|
||||
let currentOverride = SGSimpleSettings.shared.allChatsTitleLengthOverride
|
||||
switch (currentOverride) {
|
||||
case SGSimpleSettings.AllChatsTitleLengthOverride.short.rawValue:
|
||||
return true
|
||||
case SGSimpleSettings.AllChatsTitleLengthOverride.long.rawValue:
|
||||
return false
|
||||
default:
|
||||
return `default`
|
||||
}
|
||||
}
|
||||
406
Swiftgram/SGSimpleSettings/Sources/UserDefaultsWrapper.swift
Normal file
406
Swiftgram/SGSimpleSettings/Sources/UserDefaultsWrapper.swift
Normal file
@@ -0,0 +1,406 @@
|
||||
import Foundation
|
||||
|
||||
public protocol AllowedUserDefaultTypes {}
|
||||
|
||||
/* // This one is more painful than helpful
|
||||
extension Bool: AllowedUserDefaultTypes {}
|
||||
extension String: AllowedUserDefaultTypes {}
|
||||
extension Int: AllowedUserDefaultTypes {}
|
||||
extension Int32: AllowedUserDefaultTypes {}
|
||||
extension Double: AllowedUserDefaultTypes {}
|
||||
extension Float: AllowedUserDefaultTypes {}
|
||||
extension Data: AllowedUserDefaultTypes {}
|
||||
extension URL: AllowedUserDefaultTypes {}
|
||||
//extension Dictionary<String, Any>: AllowedUserDefaultTypes {}
|
||||
extension Array: AllowedUserDefaultTypes where Element: AllowedUserDefaultTypes {}
|
||||
*/
|
||||
|
||||
// Does not support Optional types due to caching
|
||||
@propertyWrapper
|
||||
public class UserDefault<T> /*where T: AllowedUserDefaultTypes*/ {
|
||||
public let key: String
|
||||
public let userDefaults: UserDefaults
|
||||
private var cachedValue: T?
|
||||
|
||||
public init(key: String, userDefaults: UserDefaults = .standard) {
|
||||
self.key = key
|
||||
self.userDefaults = userDefaults
|
||||
}
|
||||
|
||||
public var wrappedValue: T {
|
||||
get {
|
||||
#if DEBUG && false
|
||||
SGtrace("UD.\(key)", what: "GET")
|
||||
#endif
|
||||
|
||||
if let strongCachedValue = cachedValue {
|
||||
#if DEBUG && false
|
||||
SGtrace("UD", what: "CACHED \(key) \(strongCachedValue)")
|
||||
#endif
|
||||
return strongCachedValue
|
||||
}
|
||||
|
||||
cachedValue = readFromUserDefaults()
|
||||
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(key)", what: "EXTRACTED: \(cachedValue!)")
|
||||
#endif
|
||||
return cachedValue!
|
||||
}
|
||||
set {
|
||||
cachedValue = newValue
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(key)", what: "CACHE UPDATED \(cachedValue!)")
|
||||
#endif
|
||||
userDefaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func readFromUserDefaults() -> T {
|
||||
switch T.self {
|
||||
case is Bool.Type:
|
||||
return (userDefaults.bool(forKey: key) as! T)
|
||||
case is String.Type:
|
||||
return (userDefaults.string(forKey: key) as! T)
|
||||
case is Int32.Type:
|
||||
return (Int32(exactly: userDefaults.integer(forKey: key)) as! T)
|
||||
case is Int.Type:
|
||||
return (userDefaults.integer(forKey: key) as! T)
|
||||
case is Double.Type:
|
||||
return (userDefaults.double(forKey: key) as! T)
|
||||
case is Float.Type:
|
||||
return (userDefaults.float(forKey: key) as! T)
|
||||
case is Data.Type:
|
||||
return (userDefaults.data(forKey: key) as! T)
|
||||
case is URL.Type:
|
||||
return (userDefaults.url(forKey: key) as! T)
|
||||
case is Array<String>.Type:
|
||||
return (userDefaults.stringArray(forKey: key) as! T)
|
||||
case is Array<Any>.Type:
|
||||
return (userDefaults.array(forKey: key) as! T)
|
||||
default:
|
||||
fatalError("Unsupported UserDefault type \(T.self)")
|
||||
// cachedValue = (userDefaults.object(forKey: key) as! T)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//public class AtomicUserDefault<T>: UserDefault<T> {
|
||||
// private let atomicCachedValue: AtomicWrapper<T?> = AtomicWrapper(value: nil)
|
||||
//
|
||||
// public override var wrappedValue: T {
|
||||
// get {
|
||||
// return atomicCachedValue.modify({ value in
|
||||
// if let strongValue = value {
|
||||
// return strongValue
|
||||
// }
|
||||
// return self.readFromUserDefaults()
|
||||
// })!
|
||||
// }
|
||||
// set {
|
||||
// let _ = atomicCachedValue.modify({ _ in
|
||||
// userDefaults.set(newValue, forKey: key)
|
||||
// return newValue
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
// Based on ConcurrentDictionary.swift from https://github.com/peterprokop/SwiftConcurrentCollections
|
||||
|
||||
/// Thread-safe UserDefaults dictionary wrapper
|
||||
/// - Important: Note that this is a `class`, i.e. reference (not value) type
|
||||
/// - Important: Key can only be String type
|
||||
public class UserDefaultsBackedDictionary<Key: Hashable, Value> {
|
||||
public let userDefaultsKey: String
|
||||
public let userDefaults: UserDefaults
|
||||
|
||||
private var container: [Key: Value]? = nil
|
||||
private let rwlock = RWLock()
|
||||
private let threadSafe: Bool
|
||||
|
||||
public var keys: [Key] {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "KEYS")
|
||||
#endif
|
||||
let result: [Key]
|
||||
if threadSafe {
|
||||
rwlock.readLock()
|
||||
}
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
|
||||
#endif
|
||||
}
|
||||
result = Array(container!.keys)
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public var values: [Value] {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "VALUES")
|
||||
#endif
|
||||
let result: [Value]
|
||||
if threadSafe {
|
||||
rwlock.readLock()
|
||||
}
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
|
||||
#endif
|
||||
}
|
||||
result = Array(container!.values)
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public init(userDefaultsKey: String, userDefaults: UserDefaults = .standard, threadSafe: Bool) {
|
||||
self.userDefaultsKey = userDefaultsKey
|
||||
self.userDefaults = userDefaults
|
||||
self.threadSafe = threadSafe
|
||||
}
|
||||
|
||||
/// Sets the value for key
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The value to set for key
|
||||
/// - key: The key to set value for
|
||||
public func set(value: Value, forKey key: Key) {
|
||||
if threadSafe {
|
||||
rwlock.writeLock()
|
||||
}
|
||||
_set(value: value, forKey: key)
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func remove(_ key: Key) -> Value? {
|
||||
let result: Value?
|
||||
if threadSafe {
|
||||
rwlock.writeLock()
|
||||
}
|
||||
result = _remove(key)
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeValue(forKey: Key) -> Value? {
|
||||
return self.remove(forKey)
|
||||
}
|
||||
|
||||
public func contains(_ key: Key) -> Bool {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CONTAINS")
|
||||
#endif
|
||||
let result: Bool
|
||||
if threadSafe {
|
||||
rwlock.readLock()
|
||||
}
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
|
||||
#endif
|
||||
}
|
||||
result = container!.index(forKey: key) != nil
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func value(forKey key: Key) -> Value? {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "VALUE")
|
||||
#endif
|
||||
let result: Value?
|
||||
if threadSafe {
|
||||
rwlock.readLock()
|
||||
}
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
|
||||
#endif
|
||||
}
|
||||
result = container![key]
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func mutateValue(forKey key: Key, mutation: (Value) -> Value) {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "MUTATE")
|
||||
#endif
|
||||
if threadSafe {
|
||||
rwlock.writeLock()
|
||||
}
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
|
||||
#endif
|
||||
}
|
||||
if let value = container![key] {
|
||||
container![key] = mutation(value)
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE \(key): \(value), \(container!)")
|
||||
#endif
|
||||
userDefaultsContainer = container!
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED \(key): \(value), \(container!)")
|
||||
#endif
|
||||
}
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
public var isEmpty: Bool {
|
||||
return self.keys.isEmpty
|
||||
}
|
||||
|
||||
// MARK: Subscript
|
||||
public subscript(key: Key) -> Value? {
|
||||
get {
|
||||
return value(forKey: key)
|
||||
}
|
||||
set {
|
||||
if threadSafe {
|
||||
rwlock.writeLock()
|
||||
}
|
||||
defer {
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
}
|
||||
guard let newValue = newValue else {
|
||||
_remove(key)
|
||||
return
|
||||
}
|
||||
_set(value: newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
@inline(__always)
|
||||
private func _set(value: Value, forKey key: Key) {
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
}
|
||||
self.container![key] = value
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE \(key): \(value), \(container!)")
|
||||
#endif
|
||||
userDefaultsContainer = container!
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED \(key): \(value), \(container!)")
|
||||
#endif
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@discardableResult
|
||||
private func _remove(_ key: Key) -> Value? {
|
||||
if container == nil {
|
||||
container = userDefaultsContainer
|
||||
}
|
||||
guard let index = container!.index(forKey: key) else { return nil }
|
||||
|
||||
let tuple = container!.remove(at: index)
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE REMOVE \(key) \(container!)")
|
||||
#endif
|
||||
userDefaultsContainer = container!
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED REMOVED \(key) \(container!)")
|
||||
#endif
|
||||
return tuple.value
|
||||
}
|
||||
|
||||
private var userDefaultsContainer: [Key: Value] {
|
||||
get {
|
||||
return userDefaults.dictionary(forKey: userDefaultsKey) as! [Key: Value]
|
||||
}
|
||||
set {
|
||||
userDefaults.set(newValue, forKey: userDefaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
public func drop() {
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "DROPPING")
|
||||
#endif
|
||||
if threadSafe {
|
||||
rwlock.writeLock()
|
||||
}
|
||||
userDefaults.removeObject(forKey: userDefaultsKey)
|
||||
container = userDefaultsContainer
|
||||
#if DEBUG
|
||||
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "DROPPED: \(container!)")
|
||||
#endif
|
||||
if threadSafe {
|
||||
rwlock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
private let queue = DispatchQueue(label: "app.swiftgram.ios.trace", qos: .utility)
|
||||
|
||||
public func SGtrace(_ domain: String, what: @autoclosure() -> String) {
|
||||
let string = what()
|
||||
var rawTime = time_t()
|
||||
time(&rawTime)
|
||||
var timeinfo = tm()
|
||||
localtime_r(&rawTime, &timeinfo)
|
||||
|
||||
var curTime = timeval()
|
||||
gettimeofday(&curTime, nil)
|
||||
let seconds = Int(curTime.tv_sec % 60) // Extracting the current second
|
||||
let microseconds = curTime.tv_usec // Full microsecond precision
|
||||
|
||||
queue.async {
|
||||
let result = String(format: "[%@] %d-%d-%d %02d:%02d:%02d.%06d %@", arguments: [domain, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), seconds, microseconds, string])
|
||||
|
||||
print(result)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
27
Swiftgram/SGStrings/BUILD
Normal file
27
Swiftgram/SGStrings/BUILD
Normal file
@@ -0,0 +1,27 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SGStrings",
|
||||
module_name = "SGStrings",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//Swiftgram/SGLogging:SGLogging"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "SGLocalizableStrings",
|
||||
srcs = glob(["Strings/*.lproj/SGLocalizable.strings"]),
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
121
Swiftgram/SGStrings/Sources/LocalizationManager.swift
Normal file
121
Swiftgram/SGStrings/Sources/LocalizationManager.swift
Normal file
@@ -0,0 +1,121 @@
|
||||
import Foundation
|
||||
|
||||
// Assuming NGLogging and AppBundle are custom modules, they are imported here.
|
||||
import SGLogging
|
||||
import AppBundle
|
||||
|
||||
|
||||
public let SGFallbackLocale = "en"
|
||||
|
||||
public class SGLocalizationManager {
|
||||
|
||||
public static let shared = SGLocalizationManager()
|
||||
|
||||
private let appBundle: Bundle
|
||||
private var localizations: [String: [String: String]] = [:]
|
||||
private var webLocalizations: [String: [String: String]] = [:]
|
||||
private let fallbackMappings: [String: String] = [
|
||||
// "from": "to"
|
||||
"zh-hant": "zh-hans",
|
||||
"be": "ru",
|
||||
"nb": "no",
|
||||
"ckb": "ku",
|
||||
"sdh": "ku"
|
||||
]
|
||||
|
||||
private init(fetchLocale: String = SGFallbackLocale) {
|
||||
self.appBundle = getAppBundle()
|
||||
// Iterating over all the app languages and loading SGLocalizable.strings
|
||||
self.appBundle.localizations.forEach { locale in
|
||||
if locale != "Base" {
|
||||
localizations[locale] = loadLocalDictionary(for: locale)
|
||||
}
|
||||
}
|
||||
// Downloading one specific locale
|
||||
self.downloadLocale(fetchLocale)
|
||||
}
|
||||
|
||||
public func localizedString(_ key: String, _ locale: String = SGFallbackLocale, args: CVarArg...) -> String {
|
||||
let sanitizedLocale = self.sanitizeLocale(locale)
|
||||
|
||||
if let localizedString = findLocalizedString(forKey: key, inLocale: sanitizedLocale) {
|
||||
if args.isEmpty {
|
||||
return String(format: localizedString)
|
||||
} else {
|
||||
return String(format: localizedString, arguments: args)
|
||||
}
|
||||
}
|
||||
|
||||
SGLogger.shared.log("Strings", "Missing string for key: \(key) in locale: \(locale)")
|
||||
return key
|
||||
}
|
||||
|
||||
private func loadLocalDictionary(for locale: String) -> [String: String] {
|
||||
guard let path = self.appBundle.path(forResource: "SGLocalizable", ofType: "strings", inDirectory: nil, forLocalization: locale) else {
|
||||
// SGLogger.shared.log("Localization", "Unable to find path for locale: \(locale)")
|
||||
return [:]
|
||||
}
|
||||
|
||||
guard let dictionary = NSDictionary(contentsOf: URL(fileURLWithPath: path)) as? [String: String] else {
|
||||
// SGLogger.shared.log("Localization", "Unable to load dictionary for locale: \(locale)")
|
||||
return [:]
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
public func downloadLocale(_ locale: String) {
|
||||
let sanitizedLocale = self.sanitizeLocale(locale)
|
||||
guard let url = URL(string: self.getStringsUrl(for: sanitizedLocale)) else {
|
||||
SGLogger.shared.log("Strings", "Invalid URL for locale: \(sanitizedLocale)")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
if let localeDict = NSDictionary(contentsOf: url) as? [String: String] {
|
||||
DispatchQueue.main.async {
|
||||
self.webLocalizations[sanitizedLocale] = localeDict
|
||||
SGLogger.shared.log("Strings", "Successfully downloaded locale \(sanitizedLocale)")
|
||||
}
|
||||
} else {
|
||||
SGLogger.shared.log("Strings", "Failed to download \(sanitizedLocale)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sanitizeLocale(_ locale: String) -> String {
|
||||
var sanitizedLocale = locale
|
||||
let rawSuffix = "-raw"
|
||||
if locale.hasSuffix(rawSuffix) {
|
||||
sanitizedLocale = String(locale.dropLast(rawSuffix.count))
|
||||
}
|
||||
|
||||
if sanitizedLocale == "pt-br" {
|
||||
sanitizedLocale = "pt"
|
||||
} else if sanitizedLocale == "nb" {
|
||||
sanitizedLocale = "no"
|
||||
}
|
||||
|
||||
return sanitizedLocale
|
||||
}
|
||||
|
||||
private func findLocalizedString(forKey key: String, inLocale locale: String) -> String? {
|
||||
if let string = self.webLocalizations[locale]?[key], !string.isEmpty {
|
||||
return string
|
||||
}
|
||||
if let string = self.localizations[locale]?[key], !string.isEmpty {
|
||||
return string
|
||||
}
|
||||
if let fallbackLocale = self.fallbackMappings[locale] {
|
||||
return self.findLocalizedString(forKey: key, inLocale: fallbackLocale)
|
||||
}
|
||||
return self.localizations[SGFallbackLocale]?[key]
|
||||
}
|
||||
|
||||
private func getStringsUrl(for locale: String) -> String {
|
||||
return "https://raw.githubusercontent.com/Swiftgram/Telegram-iOS/master/Swiftgram/SGStrings/Strings/\(locale).lproj/SGLocalizable.strings"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public let i18n = SGLocalizationManager.shared.localizedString
|
||||
152
Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/af.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Inhoudinstellings";
|
||||
|
||||
"Settings.Tabs.Header" = "OORTJIES";
|
||||
"Settings.Tabs.HideTabBar" = "Versteek Tabbalk";
|
||||
"Settings.Tabs.ShowContacts" = "Wys Kontak Oortjie";
|
||||
"Settings.Tabs.ShowNames" = "Wys oortjiename";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Lêers onderaan";
|
||||
"Settings.Folders.BottomTabStyle" = "Bodem Lêerstyl";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Versteek \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Maak laaste lêer oop";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram sal die laaste gebruikte lêer oopmaak na herbegin of rekeningwissel.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Kleiner spasie";
|
||||
"Settings.Folders.AllChatsTitle" = "\"Alle Chats\" titel";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Kort";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Lank";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Verstek";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "CHATLYS";
|
||||
"Settings.CompactChatList" = "Kompakte Chatlys";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFIELE";
|
||||
|
||||
"Settings.Stories.Hide" = "Versteek Stories";
|
||||
"Settings.Stories.WarnBeforeView" = "Vra voor besigtiging";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Deaktiveer swiep om op te neem";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Vinnige Vertaalknoppie";
|
||||
|
||||
"Stories.Warning.Author" = "Outeur";
|
||||
"Stories.Warning.ViewStory" = "Besigtig Storie?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ SAL KAN SIEN dat jy hul Storie besigtig het.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ Sal nie kan sien dat jy hul Storie besigtig het nie.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Kwaliteit van uitgaande foto's en fotostories.";
|
||||
"Settings.Photo.SendLarge" = "Stuur groot foto's";
|
||||
"Settings.Photo.SendLarge.Notice" = "Verhoog die sybeperking op saamgeperste beelde tot 2560px.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "RONDE VIDEOS";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Begin met agterkamera";
|
||||
|
||||
"Settings.CustomColors.Header" = "REKENING KLEURE";
|
||||
"Settings.CustomColors.Saturation" = "VERSATIGING";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Stel versadiging op 0%% om rekening kleure te deaktiveer.";
|
||||
|
||||
"Settings.UploadsBoost" = "Oplaai versterking";
|
||||
"Settings.DownloadsBoost" = "Aflaai versterking";
|
||||
"Settings.DownloadsBoost.Notice" = "Verhoog die aantal parallelle verbindings en die grootte van lêerstukke. As jou netwerk nie die las kan hanteer nie, probeer verskillende opsies wat by jou verbinding pas.";
|
||||
"Settings.DownloadsBoost.none" = "Gedeaktiveer";
|
||||
"Settings.DownloadsBoost.medium" = "Medium";
|
||||
"Settings.DownloadsBoost.maximum" = "Maksimum";
|
||||
|
||||
"Settings.ShowProfileID" = "Wys profiel ID";
|
||||
"Settings.ShowDC" = "Wys Data Sentrum";
|
||||
"Settings.ShowCreationDate" = "Wys Geskep Datum van Geselskap";
|
||||
"Settings.ShowCreationDate.Notice" = "Die skeppingsdatum mag onbekend wees vir sommige gesprekke.";
|
||||
|
||||
"Settings.ShowRegDate" = "Wys Registrasie Datum";
|
||||
"Settings.ShowRegDate.Notice" = "Die registrasiedatum is benaderend.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Stuur met \"terug\" sleutel";
|
||||
"Settings.HidePhoneInSettingsUI" = "Versteek telefoon in instellings";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Dit sal slegs jou telefoonnommer versteek vanaf die instellingskoppelvlak. Om dit vir ander te versteek, gaan na Privaatheid en Sekuriteit.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "As weg vir 5 sekondes";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Gebruik stelsel DNS";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Gebruik stelsel DNS om uitvaltyd te omseil as jy nie toegang tot Google DNS het nie";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Jy **het nie nodig** %@ nie!";
|
||||
"Common.RestartRequired" = "Herbegin benodig";
|
||||
"Common.RestartNow" = "Herbegin Nou";
|
||||
"Common.OpenTelegram" = "Maak Telegram oop";
|
||||
"Common.UseTelegramForPremium" = "Let daarop dat om Telegram Premium te kry, moet jy die amptelike Telegram-app gebruik. Nadat jy Telegram Premium verkry het, sal al sy funksies beskikbaar word in Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Hou vas om te Wys of te Rapporteer.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Maak seker jy het 'n rugsteun toegangsmetode. Hou 'n SIM vir SMS of 'n addisionele sessie aangemeld om te verhoed dat jy uitgesluit word.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Jy kan die kode slegs met die amptelike app kry";
|
||||
|
||||
"Settings.SmallReactions" = "Klein reaksies";
|
||||
"Settings.HideReactions" = "Verberg Reaksies";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Stoor na Wolk";
|
||||
"ContextMenu.SelectFromUser" = "Kies vanaf Outeur";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEKSMENU";
|
||||
"Settings.ContextMenu.Notice" = "Gedeaktiveerde inskrywings sal beskikbaar wees in die \"Swiftgram\" sub-menu.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Chat List Swipe Options";
|
||||
"Settings.DeleteChatSwipeOption" = "Veeg om Klets Te Verwyder";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Trek na Volgende Ongelese Kanaal";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Trek na Volgende Onderwerp";
|
||||
"Settings.GalleryCamera" = "Camera in Gallery";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Button";
|
||||
"Settings.SnapDeletionEffect" = "Message Deletion Effects";
|
||||
|
||||
"Settings.Stickers.Size" = "SIZE";
|
||||
"Settings.Stickers.Timestamp" = "Show Timestamp";
|
||||
|
||||
"Settings.RecordingButton" = "Voice Recording Button";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Prioritise standaard emojis";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Wys standaard emojis voor premium op die emoji sleutelbord";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "geskep: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Sluit aan by %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Geregistreer";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Dubbelklik om boodskap te wysig";
|
||||
|
||||
"Settings.wideChannelPosts" = "Wye pos in kanale";
|
||||
"Settings.ForceEmojiTab" = "Emoji klawerbord standaard";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Kragtoestel Mikrofoon";
|
||||
"Settings.forceBuiltInMic.Notice" = "Indien geaktiveer, sal die app slegs die toestel se mikrofoon gebruik selfs as oorfone aangesluit is.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Verberg Kanaal Onderpaneel";
|
||||
|
||||
"Settings.CallConfirmation" = "Bel Bevestiging";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram sal om jou bevestiging vra voordat 'n oproep gemaak word.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Maak 'n Oproep?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Maak 'n Video Oproep?";
|
||||
|
||||
"MutualContact.Label" = "ewige kontak";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP met Veeg";
|
||||
"Settings.swipeForVideoPIP.Notice" = "As geaktiveer, sal die veeg van die video dit in Prent-in-Prent modus oopmaak.";
|
||||
152
Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/ar.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "إعدادات المحتوى";
|
||||
|
||||
"Settings.Tabs.Header" = "تبويبات";
|
||||
"Settings.Tabs.HideTabBar" = "إخفاء شريط علامات التبويب";
|
||||
"Settings.Tabs.ShowContacts" = "إظهار تبويب جهات الاتصال";
|
||||
"Settings.Tabs.ShowNames" = "إظهار أسماء التبويبات";
|
||||
|
||||
"Settings.Folders.BottomTab" = "المجلدات في الأسفل";
|
||||
"Settings.Folders.BottomTabStyle" = "نمط المجلدات السفلية";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "إخفاء \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "فتح المجلد الأخير";
|
||||
"Settings.Folders.RememberLast.Notice" = "سيفتح Swiftgram آخر مجلد مستخدم عند إعادة تشغيل التطبيق أو تبديل الحسابات.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "مسافات أصغر";
|
||||
"Settings.Folders.AllChatsTitle" = "عنوان \"كل المحادثات\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "قصير";
|
||||
"Settings.Folders.AllChatsTitle.long" = "طويل";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "الافتراضي";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "قائمة الفواصل";
|
||||
"Settings.CompactChatList" = "قائمة الدردشة المتراصة";
|
||||
|
||||
"Settings.Profiles.Header" = "الملفات الشخصية";
|
||||
|
||||
"Settings.Stories.Hide" = "إخفاء القصص";
|
||||
"Settings.Stories.WarnBeforeView" = "اسأل قبل العرض";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "تعطيل السحب للتسجيل";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "زر الترجمة الفوري";
|
||||
|
||||
"Stories.Warning.Author" = "الكاتب";
|
||||
"Stories.Warning.ViewStory" = "عرض القصة؟";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ WILL BE يتم إخبارهم بأنك شاهدت قصتهم.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ لن يتمكن من رؤية أنك شاهدت قصته.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "جودة الصور والصور الصادرة والقصص.";
|
||||
"Settings.Photo.SendLarge" = "إرسال صور كبيرة";
|
||||
"Settings.Photo.SendLarge.Notice" = "زيادة الحد الجانبي للصور المضغوطة إلى 2560 بكسل.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "فيديوهات مستديرة";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "البدء بالكاميرا الخلفية";
|
||||
|
||||
"Settings.CustomColors.Header" = "ألوان الحساب";
|
||||
"Settings.CustomColors.Saturation" = "مستوى التشبع";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "تعيين التشبع إلى 0%% لتعطيل ألوان الحساب.";
|
||||
|
||||
"Settings.UploadsBoost" = "تعزيز التحميلات";
|
||||
"Settings.DownloadsBoost" = "تعزيز التنزيلات";
|
||||
"Settings.DownloadsBoost.Notice" = "يزيد من عدد الاتصالات المتوازية وحجم أجزاء الملفات. إذا لم يتمكن شبكتك من تحمل الحمل، حاول خيارات مختلفة تناسب اتصالك.";
|
||||
"Settings.DownloadsBoost.none" = "تعطيل";
|
||||
"Settings.DownloadsBoost.medium" = "متوسط";
|
||||
"Settings.DownloadsBoost.maximum" = "الحد الاقصى";
|
||||
|
||||
"Settings.ShowProfileID" = "إظهار معرف الملف الشخصي ID";
|
||||
"Settings.ShowDC" = "إظهار مركز البيانات";
|
||||
"Settings.ShowCreationDate" = "إظهار تاريخ إنشاء المحادثة";
|
||||
"Settings.ShowCreationDate.Notice" = "قد يكون تاريخ الإنشاء مفقوداً لبضع المحادثات.";
|
||||
|
||||
"Settings.ShowRegDate" = "إظهار تاريخ التسجيل";
|
||||
"Settings.ShowRegDate.Notice" = "تاريخ التسجيل تقريبي.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "إرسال مع مفتاح \"العودة\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "إخفاء الرقم من الإعدادات";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "سيتم اخفاء رقمك من التطبيق فقط. لأخفاءهُ من المستخدمين الآخرين، يرجى استخدام إعدادات الخصوصية.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "إذا كان بعيدا لمدة 5 ثوان";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "استخدم DNS النظام";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "استخدم نظام DNS لتجاوز المهلة إذا لم تكن لديك حق الوصول إلى Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "أنت **لا تحتاج** %@!";
|
||||
"Common.RestartRequired" = "إعادة التشغيل مطلوب";
|
||||
"Common.RestartNow" = "إعادة التشغيل الآن";
|
||||
"Common.OpenTelegram" = "افتح Telegram";
|
||||
"Common.UseTelegramForPremium" = "يُرجى ملاحظة أنه للحصول على Telegram Premium، يجب عليك استخدام تطبيق تيليجرام الرسمي. بمجرد حصولك على Telegram Premium، ستصبح جميع ميزاته متاحة في Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "اضغط للعرض أو الإبلاغ.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "تأكد من أن لديك طريقة الوصول إلى النسخ الاحتياطي. حافظ على شريحة SIM للرسائل القصيرة أو جلسة إضافية لتسجيل الدخول لتجنب أن تكون مغفلة.";
|
||||
"Auth.UnofficialAppCodeTitle" = "يمكنك الحصول على الرمز فقط من خلال التطبيق الرسمي";
|
||||
|
||||
"Settings.SmallReactions" = "ردود أفعال صغيرة";
|
||||
"Settings.HideReactions" = "إخفاء الردود";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "الحفظ في السحابة";
|
||||
"ContextMenu.SelectFromUser" = "حدد من المؤلف";
|
||||
|
||||
"Settings.ContextMenu" = "قائمة السياق";
|
||||
"Settings.ContextMenu.Notice" = "المدخلات المعطلة ستكون متوفرة في القائمة الفرعية \"Swiftgram\".";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "خيارات التمرير لقائمة المحادثة";
|
||||
"Settings.DeleteChatSwipeOption" = "اسحب لحذف المحادثة";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "اسحب للقناة الغير مقروءة التالية";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "اسحب للموضوع التالي";
|
||||
"Settings.GalleryCamera" = "الكاميرا في معرض الصور";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "زر \"%@\"";
|
||||
"Settings.SnapDeletionEffect" = "تأثيرات حذف الرسالة";
|
||||
|
||||
"Settings.Stickers.Size" = "مقاس";
|
||||
"Settings.Stickers.Timestamp" = "إظهار الطابع الزمني";
|
||||
|
||||
"Settings.RecordingButton" = "زر التسجيل الصوتي";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "الأفضلية للرموز التعبيرية الافتراضية";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "عرض الرموز التعبيرية الافتراضية قبل الرموز المتميزة في لوحة المفاتيح";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "تم إنشاؤه: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "انضم %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "مسجل";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "اضغط مزدوجًا لتحرير الرسالة";
|
||||
|
||||
"Settings.wideChannelPosts" = "المشاركات الواسعة في القنوات";
|
||||
"Settings.ForceEmojiTab" = "لوحة مفاتيح الرموز التعبيرية افتراضيًا";
|
||||
|
||||
"Settings.forceBuiltInMic" = "قوة ميكروفون الجهاز";
|
||||
"Settings.forceBuiltInMic.Notice" = "إذا تم تمكينه، سيستخدم التطبيق فقط ميكروفون الجهاز حتى لو كانت سماعات الرأس متصلة.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "إخفاء لوحة قاعدة القناة";
|
||||
|
||||
"Settings.CallConfirmation" = "تأكيد الاتصال";
|
||||
"Settings.CallConfirmation.Notice" = "سيطلب Swiftgram تأكيدك قبل إجراء مكالمة.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "هل ترغب في إجراء مكالمة؟";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "هل ترغب في إجراء مكالمة فيديو؟";
|
||||
|
||||
"MutualContact.Label" = "جهة اتصال مشتركة";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "فيديو PIP مع السحب";
|
||||
"Settings.swipeForVideoPIP.Notice" = "إذا تم تمكينه، سيفتح سحب الفيديو في وضع الصورة في الصورة.";
|
||||
152
Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/ca.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Configuració del Contingut";
|
||||
|
||||
"Settings.Tabs.Header" = "PESTANYES";
|
||||
"Settings.Tabs.HideTabBar" = "Amagar barra de pestanyes";
|
||||
"Settings.Tabs.ShowContacts" = "Mostrar Pestanya de Contactes";
|
||||
"Settings.Tabs.ShowNames" = "Mostrar noms de les pestanyes";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Carpetes a la part inferior";
|
||||
"Settings.Folders.BottomTabStyle" = "Bottom Folders Style";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Amaga \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Obrir l'última carpeta";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram obrirà l'última carpeta utilitzada després de reiniciar o canviar de compte.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Espaiat més petit";
|
||||
"Settings.Folders.AllChatsTitle" = "Títol \"Tots els xats\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Curt";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Llarg";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Per defecte";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "LLISTA DE XATS";
|
||||
"Settings.CompactChatList" = "Llista de xats compacta";
|
||||
|
||||
"Settings.Profiles.Header" = "PERFILS";
|
||||
|
||||
"Settings.Stories.Hide" = "Amagar Històries";
|
||||
"Settings.Stories.WarnBeforeView" = "Preguntar abans de veure";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Desactivar lliscar per enregistrar";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Botó de Traducció Ràpida";
|
||||
|
||||
"Stories.Warning.Author" = "Autor";
|
||||
"Stories.Warning.ViewStory" = "Veure Història?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ PODRÀ VEURE que has vist la seva Història.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ no podrà veure que has vist la seva Història.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Qualitat de les fotos sortints i històries de fotos.";
|
||||
"Settings.Photo.SendLarge" = "Enviar fotos grans";
|
||||
"Settings.Photo.SendLarge.Notice" = "Incrementar el límit de mida en imatges comprimides a 2560px.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "VÍDEOS RODONS";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Començar amb càmera posterior";
|
||||
|
||||
"Settings.CustomColors.Header" = "COLORS DEL COMPTE";
|
||||
"Settings.CustomColors.Saturation" = "SATURACIÓ";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Estableix la saturació a 0%% per desactivar els colors del compte.";
|
||||
|
||||
"Settings.UploadsBoost" = "Millora de càrregues";
|
||||
"Settings.DownloadsBoost" = "Millora de baixades";
|
||||
"Settings.DownloadsBoost.Notice" = "Augmenta el nombre de connexions paral·leles i la mida de les parts de fitxer. Si la teva xarxa no pot gestionar la càrrega, prova diferents opcions que s'adaptin a la teva connexió.";
|
||||
"Settings.DownloadsBoost.none" = "Desactivat";
|
||||
"Settings.DownloadsBoost.medium" = "Mitjà";
|
||||
"Settings.DownloadsBoost.maximum" = "Màxim";
|
||||
|
||||
"Settings.ShowProfileID" = "Mostrar ID de perfil";
|
||||
"Settings.ShowDC" = "Mostrar Data Center";
|
||||
"Settings.ShowCreationDate" = "Mostrar Data de Creació de Xat";
|
||||
"Settings.ShowCreationDate.Notice" = "La data de creació pot ser desconeguda per alguns xats.";
|
||||
|
||||
"Settings.ShowRegDate" = "Mostra la data d'inscripció";
|
||||
"Settings.ShowRegDate.Notice" = "La data d'inscripció és aproximada.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Enviar amb clau \"retorn\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "Amagar telèfon en la interfície d'ajustos";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Això només amagarà el teu número de telèfon de la interfície d'ajustos. Per amagar-lo als altres, ves a Privadesa i Seguretat.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Si no hi ha en 5 segons";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Utilitzar DNS del sistema";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Utilitzar DNS del sistema per evitar el temps d'espera si no tens accés a Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "No **necessites** %@!";
|
||||
"Common.RestartRequired" = "Reinici requerit";
|
||||
"Common.RestartNow" = "Reiniciar Ara";
|
||||
"Common.OpenTelegram" = "Obrir Telegram";
|
||||
"Common.UseTelegramForPremium" = "Recorda que per obtenir Telegram Premium, has d'utilitzar l'aplicació oficial de Telegram. Un cop hagis obtingut Telegram Premium, totes les seves funcions estaran disponibles a Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Mantingues per Mostrar o Informar.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Assegura't de tenir un mètode d'accés de reserva. Mantingues un SIM per a SMS o una sessió addicional registrada per evitar quedar bloquejat.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Només pots obtenir el codi amb l'aplicació oficial";
|
||||
|
||||
"Settings.SmallReactions" = "Petites reaccions";
|
||||
"Settings.HideReactions" = "Amaga les reaccions";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Desar al Núvol";
|
||||
"ContextMenu.SelectFromUser" = "Seleccionar de l'Autor";
|
||||
|
||||
"Settings.ContextMenu" = "MENÚ CONTEXTUAL";
|
||||
"Settings.ContextMenu.Notice" = "Les entrades desactivades estaran disponibles al submenú \"Swiftgram\".";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Opcions desplaçament de la llista de xats";
|
||||
"Settings.DeleteChatSwipeOption" = "Desplaceu-vos per esborrar la conversa";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Arrossega cap al següent canal no llegit";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Arrosega cap al següent tema";
|
||||
"Settings.GalleryCamera" = "Càmera a la galeria";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Botó";
|
||||
"Settings.SnapDeletionEffect" = "Efectes d'eliminació de missatges";
|
||||
|
||||
"Settings.Stickers.Size" = "GRANOR";
|
||||
"Settings.Stickers.Timestamp" = "Mostra l'estona";
|
||||
|
||||
"Settings.RecordingButton" = "Botó d'enregistrament de veu";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Prioritzar emojis estàndard";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Mostra emojis estàndard abans que premium al teclat emoji";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "creada: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Unida a %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Inscrit";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Toqueu dues vegades per editar el missatge";
|
||||
|
||||
"Settings.wideChannelPosts" = "Entrades àmplies als canals";
|
||||
"Settings.ForceEmojiTab" = "Teclat d'emojis per defecte";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Força el Micròfon del Dispositiu";
|
||||
"Settings.forceBuiltInMic.Notice" = "Si està habilitat, l'aplicació utilitzarà només el micròfon del dispositiu encara que estiguin connectats els auriculars.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Amaga el panell inferior del canal";
|
||||
|
||||
"Settings.CallConfirmation" = "Confirmació de trucada";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram et demanarà la teva confirmació abans de fer una trucada.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Vols fer una trucada?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Vols fer una videotrucada?";
|
||||
|
||||
"MutualContact.Label" = "contacte mutu";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Vídeo PIP amb desplaçament";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Si està habilitat, desplaçar el vídeo l'obrirà en mode Imatge en Imatge.";
|
||||
152
Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/cs.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Nastavení obsahu";
|
||||
|
||||
"Settings.Tabs.Header" = "ZÁLOŽKY";
|
||||
"Settings.Tabs.HideTabBar" = "Skrýt záložku";
|
||||
"Settings.Tabs.ShowContacts" = "Zobrazit záložku kontaktů";
|
||||
"Settings.Tabs.ShowNames" = "Zobrazit názvy záložek";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Složky dole";
|
||||
"Settings.Folders.BottomTabStyle" = "Styl dolní složky";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Skrýt \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Otevřít poslední složku";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram otevře poslední použitou složku po restartu nebo přepnutí účtu.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Menší vzdálenost";
|
||||
"Settings.Folders.AllChatsTitle" = "Název \"Všechny chaty\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Krátký";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Dlouhá";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Výchozí";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "CHAT SEZNAM";
|
||||
"Settings.CompactChatList" = "Kompaktní seznam chatu";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Skrýt příběhy";
|
||||
"Settings.Stories.WarnBeforeView" = "Upozornit před zobrazením";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Zakázat přejetí prstem pro nahrávání";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Tlačítko pro rychlý překlad";
|
||||
|
||||
"Stories.Warning.Author" = "Autor";
|
||||
"Stories.Warning.ViewStory" = "Zobrazit příběh?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ BUDE VIDĚT, že jste si prohlédl jejich příběh.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ bude moci vidět, že jste si prohlédl jejich příběh.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Kvalita odchozích fotografií a foto-příběhů.";
|
||||
"Settings.Photo.SendLarge" = "Poslat velké fotografie";
|
||||
"Settings.Photo.SendLarge.Notice" = "Zvýšit limit velikosti komprimovaných obrázků na 2560px.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "KRUHOVÁ VIDEA";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Začít s zadní kamerou";
|
||||
|
||||
"Settings.CustomColors.Header" = "BARVY ÚČTU";
|
||||
"Settings.CustomColors.Saturation" = "SYTOST";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Nastavit sytost na 0%% pro vypnutí barev účtu.";
|
||||
|
||||
"Settings.UploadsBoost" = "Zrychlení nahrávání";
|
||||
"Settings.DownloadsBoost" = "Zrychlení stahování";
|
||||
"Settings.DownloadsBoost.Notice" = "Zvyšuje počet paralelních připojení a velikost částí souboru. Pokud vaše síť nezvládá zátěž, vyzkoušejte různé možnosti, které vyhovují vašemu připojení.";
|
||||
"Settings.DownloadsBoost.none" = "Vypnuto";
|
||||
"Settings.DownloadsBoost.medium" = "Střední";
|
||||
"Settings.DownloadsBoost.maximum" = "Maximální";
|
||||
|
||||
"Settings.ShowProfileID" = "Zobrazit ID profilu";
|
||||
"Settings.ShowDC" = "Zobrazit Data Center";
|
||||
"Settings.ShowCreationDate" = "Zobrazit datum vytvoření chatu";
|
||||
"Settings.ShowCreationDate.Notice" = "Datum vytvoření chatu může být neznámé pro některé chaty.";
|
||||
|
||||
"Settings.ShowRegDate" = "Zobrazit datum registrace";
|
||||
"Settings.ShowRegDate.Notice" = "Datum registrace je přibližné.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Poslat klávesou \"enter\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "Skrýt telefon v nastavení";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Toto skryje vaše telefonní číslo pouze v nastavení rozhraní. Chcete-li je skryt před ostatními, přejděte na Soukromí a bezpečnost.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Zamknout za 5 sekund";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Použít systémové DNS";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Použít systémové DNS k obejití časového limitu, pokud nemáte přístup k Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Nepotřebujete **%@**!";
|
||||
"Common.RestartRequired" = "Vyžadován restart";
|
||||
"Common.RestartNow" = "Restartovat nyní";
|
||||
"Common.OpenTelegram" = "Otevřít Telegram";
|
||||
"Common.UseTelegramForPremium" = "Vezměte prosím na vědomí, že abyste získali Premium, musíte použít oficiální aplikaci Telegram . Jakmile získáte Telegram Premium, všechny jeho funkce budou k dispozici ve Swiftgramu.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Podržte pro zobrazení nebo nahlášení.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Ujistěte se, že máte záložní přístupovou metodu. Uchovávejte SIM pro SMS nebo další přihlášenou relaci, abyste předešli zamčení.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Kód můžete získat pouze s oficiální aplikací";
|
||||
|
||||
"Settings.SmallReactions" = "Malé reakce";
|
||||
"Settings.HideReactions" = "Skrýt reakce";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Uložit do cloudu";
|
||||
"ContextMenu.SelectFromUser" = "Vybrat od autora";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEXTOVÉ MENU";
|
||||
"Settings.ContextMenu.Notice" = "Zakázané položky budou dostupné v podmenu \"Swiftgram\".";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Možnosti potáhnutí v seznamu chatu";
|
||||
"Settings.DeleteChatSwipeOption" = "Přejeďte pro smazání chatu";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Táhnout na další nepřečtený kanál";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Přetáhněte na další téma";
|
||||
"Settings.GalleryCamera" = "Fotoaparát v galerii";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "Tlačítko \"%@\"";
|
||||
"Settings.SnapDeletionEffect" = "Účinky odstranění zpráv";
|
||||
|
||||
"Settings.Stickers.Size" = "VELIKOST";
|
||||
"Settings.Stickers.Timestamp" = "Zobrazit časové razítko";
|
||||
|
||||
"Settings.RecordingButton" = "Tlačítko nahrávání hlasu";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Upřednostněte standardní emoji";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Zobrazit standardní emoji před prémiovými na klávesnici s emoji";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "vytvořeno: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Připojeno k %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Registrováno";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Dvojitým klepnutím upravte zprávu";
|
||||
|
||||
"Settings.wideChannelPosts" = "Široké příspěvky ve skupinách";
|
||||
"Settings.ForceEmojiTab" = "Emoji klávesnice ve výchozím nastavení";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Vynutit vestavěný mikrofon zařízení";
|
||||
"Settings.forceBuiltInMic.Notice" = "Pokud je povoleno, aplikace použije pouze mikrofon zařízení, i když jsou připojeny sluchátka.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Skrýt panel dolního kanálu";
|
||||
|
||||
"Settings.CallConfirmation" = "Potvrzení hovoru";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram požádá o vaši potvrzení před uskutečněním hovoru.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Uskutečnit hovor?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Uskutečnit video hovor?";
|
||||
|
||||
"MutualContact.Label" = "vzájemný kontakt";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP s přetahováním";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Pokud je povoleno, poslání videa jej otevře v režimu Obraz v obraze.";
|
||||
152
Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/da.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Indholdindstillinger";
|
||||
|
||||
"Settings.Tabs.Header" = "Tabs";
|
||||
"Settings.Tabs.HideTabBar" = "Skjul Tabbjælke";
|
||||
"Settings.Tabs.ShowContacts" = "Kontakte Tab anzeigen";
|
||||
"Settings.Tabs.ShowNames" = "Tabnamen anzeigen";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Ordner - unten";
|
||||
"Settings.Folders.BottomTabStyle" = "Bundmapper Stil";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Skjul \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Åbn sidste mappe";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram vil åbne den sidst brugte mappe efter genstart eller konto skift.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Mindre afstand";
|
||||
"Settings.Folders.AllChatsTitle" = "\"Alle Chats\" titel";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Kort";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Lang";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Standard";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "CHAT LISTE";
|
||||
"Settings.CompactChatList" = "Kompakt Chatliste";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Skjul Historier";
|
||||
"Settings.Stories.WarnBeforeView" = "Spørg før visning";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Deaktiver swipe for at optage";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Schnellübersetzen-Schaltfläche";
|
||||
|
||||
"Stories.Warning.Author" = "Forfatter";
|
||||
"Stories.Warning.ViewStory" = "Se Historie?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ VIL KUNNE SE at du har set deres Historie.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ Vil ikke kunne se, at du har set deres Historie.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Kvalitet af udgående fotos og foto-historier.";
|
||||
"Settings.Photo.SendLarge" = "Send store fotos";
|
||||
"Settings.Photo.SendLarge.Notice" = "Forøg sidestørrelsesgrænsen på komprimerede billeder til 2560px.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "RUNDE VIDEOS";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Starte mit umgedrehter Kamera";
|
||||
|
||||
"Settings.CustomColors.Header" = "KONTOFARVER";
|
||||
"Settings.CustomColors.Saturation" = "MÆTNING";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Indstil mætning til 0%% for at deaktivere konto farver.";
|
||||
|
||||
"Settings.UploadsBoost" = "Upload Boost";
|
||||
"Settings.DownloadsBoost" = "Download Boost";
|
||||
"Settings.DownloadsBoost.Notice" = "Øger antallet af parallelle forbindelser og størrelsen på filstykker. Hvis dit netværk ikke kan håndtere belastningen, kan du prøve forskellige muligheder, der passer til din forbindelse.";
|
||||
"Settings.DownloadsBoost.none" = "Deaktiveret";
|
||||
"Settings.DownloadsBoost.medium" = "Mellem";
|
||||
"Settings.DownloadsBoost.maximum" = "Maksimum";
|
||||
|
||||
"Settings.ShowProfileID" = "Profil-ID anzeigen";
|
||||
"Settings.ShowDC" = "Vis Datacenter";
|
||||
"Settings.ShowCreationDate" = "Vis Chattens Oprettelsesdato";
|
||||
"Settings.ShowCreationDate.Notice" = "Oprettelsesdatoen kan være ukendt for nogle chats.";
|
||||
|
||||
"Settings.ShowRegDate" = "Vis Registreringsdato";
|
||||
"Settings.ShowRegDate.Notice" = "Registreringsdatoen er omtrentlig.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Send med \"return\" tasten";
|
||||
"Settings.HidePhoneInSettingsUI" = "Telefon in den Einstellungen ausblenden";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Deine Nummer wird nur in der Benutzeroberfläche versteckt. Um sie vor anderen zu verbergen, verwende bitte die Privatsphäre-Einstellungen.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Hvis væk i 5 sekunder";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Brug system DNS";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Brug system DNS for at omgå timeout hvis du ikke har adgang til Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Du **behøver ikke** %@!";
|
||||
"Common.RestartRequired" = "Genstart krævet";
|
||||
"Common.RestartNow" = "Genstart Nu";
|
||||
"Common.OpenTelegram" = "Åben Telegram";
|
||||
"Common.UseTelegramForPremium" = "Bemærk venligst, at for at få Telegram Premium skal du bruge den officielle Telegram app. Når du har fået Telegram Premium, vil alle dens funktioner blive tilgængelige i Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Hold for at Vise eller Rapportere.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Sørg for, at du har en backup adgangsmetode. Behold et SIM til SMS eller en ekstra session logget ind for at undgå at blive låst ude.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Du kan kun få koden med den officielle app";
|
||||
|
||||
"Settings.SmallReactions" = "Små reaktioner";
|
||||
"Settings.HideReactions" = "Skjul Reaktioner";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "In Cloud speichern";
|
||||
"ContextMenu.SelectFromUser" = "Vælg fra Forfatter";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEKSTMENU";
|
||||
"Settings.ContextMenu.Notice" = "Deaktiverede indgange vil være tilgængelige i \"Swiftgram\" undermenuen.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Chat List Swipe Options";
|
||||
"Settings.DeleteChatSwipeOption" = "Svejp for at slette chat";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Træk til Næste U’læst Kanal";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Træk for at gå til næste emne";
|
||||
"Settings.GalleryCamera" = "Kamera i Galleri";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Knap";
|
||||
"Settings.SnapDeletionEffect" = "Besked Sletnings Effekter";
|
||||
|
||||
"Settings.Stickers.Size" = "STØRRELSE";
|
||||
"Settings.Stickers.Timestamp" = "Vis tidsstempel";
|
||||
|
||||
"Settings.RecordingButton" = "Lydoptageknap";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Prioriter standard emojis";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Vis standard emojis før premium i emoji-tastaturet";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "oprettet: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Tilmeldt %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Registreret";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Dobbelt tryk for at redigere besked";
|
||||
|
||||
"Settings.wideChannelPosts" = "Brede indlæg i kanaler";
|
||||
"Settings.ForceEmojiTab" = "Emoji-tastatur som standard";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Tving enhedsmikrofon";
|
||||
"Settings.forceBuiltInMic.Notice" = "Hvis aktiveret, vil appen kun bruge enhedens mikrofon, selvom hovedtelefoner er tilsluttet.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Skjul Kanal Bund Panel";
|
||||
|
||||
"Settings.CallConfirmation" = "Opkaldsbekræftelse";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram vil bede om din bekræftelse, før der foretages et opkald.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Foretage et opkald?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Foretage et videoopkald?";
|
||||
|
||||
"MutualContact.Label" = "fælles kontakt";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP med Swipe";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Hvis aktiveret, vil sletning af video åbne den i billede-i-billede-tilstand.";
|
||||
152
Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/de.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Inhaltliche Einstellungen";
|
||||
|
||||
"Settings.Tabs.Header" = "Tabs";
|
||||
"Settings.Tabs.HideTabBar" = "Tab-Leiste ausblenden";
|
||||
"Settings.Tabs.ShowContacts" = "Kontakte Tab anzeigen";
|
||||
"Settings.Tabs.ShowNames" = "Tabnamen anzeigen";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Ordner unten";
|
||||
"Settings.Folders.BottomTabStyle" = "Untere Ordner-Stil";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Verberge \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Letzten Ordner öffnen";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram wird den zuletzt genutzten Order öffnen, wenn du den Account wechselst oder die App neustartest";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Kleinerer Abstand";
|
||||
"Settings.Folders.AllChatsTitle" = "Titel \"Alle Chats\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Kurze";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Lang";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Standard";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "CHAT LISTE";
|
||||
"Settings.CompactChatList" = "Kompakte Chat-Liste";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Stories verbergen";
|
||||
"Settings.Stories.WarnBeforeView" = "Vor dem Ansehen fragen";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Zum aufnehmen wischen deaktivieren";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Schnellübersetzen-Button";
|
||||
|
||||
"Stories.Warning.Author" = "Autor";
|
||||
"Stories.Warning.ViewStory" = "Story ansehen?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ wird sehen können, dass du die Story angesehen hast.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ wird nicht sehen können, dass du die Story angesehen hast.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Qualität der gesendeten Fotos und Fotostorys";
|
||||
"Settings.Photo.SendLarge" = "Sende große Fotos";
|
||||
"Settings.Photo.SendLarge.Notice" = "Seitenlimit für komprimierte Bilder auf 2560px erhöhen";
|
||||
|
||||
"Settings.VideoNotes.Header" = "RUNDE VIDEOS";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Starte mit umgedrehter Kamera";
|
||||
|
||||
"Settings.CustomColors.Header" = "ACCOUNT FARBEN";
|
||||
"Settings.CustomColors.Saturation" = "SÄTTIGUNG";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Setze Sättigung auf 0%% um Kontofarben zu deaktivieren";
|
||||
|
||||
"Settings.UploadsBoost" = "Upload Beschleuniger";
|
||||
"Settings.DownloadsBoost" = "Download Beschleuniger";
|
||||
"Settings.DownloadsBoost.Notice" = "Erhöht die Anzahl der parallelen Verbindungen und die Größe der Dateiabschnitte. Wenn Ihr Netzwerk die Last nicht bewältigen kann, versuchen Sie verschiedene Optionen, die zu Ihrer Verbindung passen.";
|
||||
"Settings.DownloadsBoost.none" = "Deaktiviert";
|
||||
"Settings.DownloadsBoost.medium" = "Mittel";
|
||||
"Settings.DownloadsBoost.maximum" = "Maximum";
|
||||
|
||||
"Settings.ShowProfileID" = "Profil-ID anzeigen";
|
||||
"Settings.ShowDC" = "Data Center anzeigen";
|
||||
"Settings.ShowCreationDate" = "Chat-Erstellungsdatum anzeigen";
|
||||
"Settings.ShowCreationDate.Notice" = "Das Erstellungsdatum kann für einige Chats unbekannt sein.";
|
||||
|
||||
"Settings.ShowRegDate" = "Anmeldedatum anzeigen";
|
||||
"Settings.ShowRegDate.Notice" = "Das Registrierungsdatum ist ungefähr.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Mit \"Enter\" senden";
|
||||
"Settings.HidePhoneInSettingsUI" = "Telefon in den Einstellungen ausblenden";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Deine Nummer wird nur in der Benutzeroberfläche versteckt. Um sie vor anderen zu verbergen, verwende bitte die Privatsphäre-Einstellungen.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Falls 5 Sekunden inaktiv";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Benutze System DNS";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Benutze System DNS um Timeout zu umgehen, wenn du keinen Zugriff auf Google DNS hast";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Du brauchst %@ nicht!";
|
||||
"Common.RestartRequired" = "Benötigt Neustart";
|
||||
"Common.RestartNow" = "Jetzt neustarten";
|
||||
"Common.OpenTelegram" = "Telegram öffnen";
|
||||
"Common.UseTelegramForPremium" = "Bitte beachten Sie, dass Sie die offizielle Telegram-App verwenden müssen, um Telegram Premium zu erhalten. Sobald Sie Telegram Premium erhalten haben, werden alle Funktionen in Swiftgram verfügbar.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Halten, zum Ansehen oder melden.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Stelle sicher, dass du eine weiter Möglichkeit hast auf den Account zuzugreifen. Behalte die SIM Karte im SMS zum Login empfangen zu können oder nutze weitere Apps/Geräte mit einer aktive Sitzung deines Accounts.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Du kannst den Code nur mit der offiziellen App erhalten";
|
||||
|
||||
"Settings.SmallReactions" = "Kleine Reaktionen";
|
||||
"Settings.HideReactions" = "Verberge Reaktionen";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "In Cloud speichern";
|
||||
"ContextMenu.SelectFromUser" = "Vom Autor auswählen";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEXTMENÜ";
|
||||
"Settings.ContextMenu.Notice" = "Deaktivierte Einträge sind im 'Swiftgram'-Untermenü verfügbar.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Chatlisten-Wisch-Optionen";
|
||||
"Settings.DeleteChatSwipeOption" = "Wischen zum Löschen des Chats";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Ziehen zum nächsten Kanal";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Ziehen Sie zum nächsten Thema";
|
||||
"Settings.GalleryCamera" = "Kamera in der Galerie";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Schaltfläche";
|
||||
"Settings.SnapDeletionEffect" = "Nachrichtenlösch-Effekte";
|
||||
|
||||
"Settings.Stickers.Size" = "GRÖSSE";
|
||||
"Settings.Stickers.Timestamp" = "Zeitstempel anzeigen";
|
||||
|
||||
"Settings.RecordingButton" = "Sprachaufnahme-Button";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Priorisieren Sie Standard-Emojis";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Zeigen Sie Standard-Emojis vor Premium-Emojis in der Emoji-Tastatur";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "erstellt: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Beigetreten am %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Registriert";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Doppeltippen, um Nachricht zu bearbeiten";
|
||||
|
||||
"Settings.wideChannelPosts" = "Breite Beiträge in Kanälen";
|
||||
"Settings.ForceEmojiTab" = "Emoji-Tastatur standardmäßig";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Erzwinge Geräte-Mikrofon";
|
||||
"Settings.forceBuiltInMic.Notice" = "Wenn aktiviert, verwendet die App nur das Geräte-Mikrofon, auch wenn Kopfhörer angeschlossen sind.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Kanalunteres Bedienfeld ausblenden";
|
||||
|
||||
"Settings.CallConfirmation" = "Anrufbestätigung";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram wird um Ihre Bestätigung bitten, bevor ein Anruf getätigt wird.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Einen Anruf tätigen?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Einen Videoanruf tätigen?";
|
||||
|
||||
"MutualContact.Label" = "gemeinsamer Kontakt";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP mit Wischen";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Wenn aktiviert, öffnet das Wischen des Videos es im Bild-in-Bild-Modus.";
|
||||
152
Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/el.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Ρυθμίσεις Περιεχομένου";
|
||||
|
||||
"Settings.Tabs.Header" = "TABS";
|
||||
"Settings.Tabs.HideTabBar" = "Απόκρυψη γραμμής καρτελών";
|
||||
"Settings.Tabs.ShowContacts" = "Εμφάνιση Καρτέλας Επαφών";
|
||||
"Settings.Tabs.ShowNames" = "Show Tab Names";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Φάκελοι στο κάτω μέρος";
|
||||
"Settings.Folders.BottomTabStyle" = "Ύφος Κάτω Φακέλων";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Απόκρυψη \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Άνοιγμα Τελευταίου Φακέλου";
|
||||
"Settings.Folders.RememberLast.Notice" = "Το Swiftgram θα ανοίξει τον τελευταίο φάκελο όταν επανεκκινήσετε την εφαρμογή ή αλλάξετε λογαριασμούς.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Μικρότερη απόσταση";
|
||||
"Settings.Folders.AllChatsTitle" = "\"Όλες οι συνομιλίες\" τίτλος";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Σύντομο";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Εκτενές";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Προεπιλογή";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "ΚΑΤΑΛΟΓΟΣ ΤΥΠΟΥ";
|
||||
"Settings.CompactChatList" = "Συμπαγής Λίστα Συνομιλίας";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Απόκρυψη Ιστοριών";
|
||||
"Settings.Stories.WarnBeforeView" = "Ερώτηση Πριν Την Προβολή";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Απενεργοποίηση ολίσθησης για εγγραφή";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Γρήγορη μετάφραση κουμπί";
|
||||
|
||||
"Stories.Warning.Author" = "Συγγραφέας";
|
||||
"Stories.Warning.ViewStory" = "Προβολή Ιστορίας?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ ΘΑ ΠΡΕΠΕΙ ΝΑ ΒΛΕΠΕ ότι έχετε δει την Ιστορία τους.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ δεν θα είναι σε θέση να δείτε ότι έχετε δει την Ιστορία τους.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Ποιότητα των ανεβασμένων φωτογραφιών και ιστοριών.";
|
||||
"Settings.Photo.SendLarge" = "Αποστολή Μεγάλων Φωτογραφιών";
|
||||
"Settings.Photo.SendLarge.Notice" = "Αυξήστε το πλευρικό όριο στις συμπιεσμένες εικόνες στα 2560px.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "ΤΡΟΠΟΣ ΒΙΝΤΕΟ";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Έναρξη με πίσω κάμερα";
|
||||
|
||||
"Settings.CustomColors.Header" = "ΧΡΩΜΑΤΑ ΛΟΓΑΡΙΑΣΜΟΥ";
|
||||
"Settings.CustomColors.Saturation" = "ΑΣΦΑΛΙΣΗ";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Ορίστε σε 0%% για να απενεργοποιήσετε τα χρώματα του λογαριασμού.";
|
||||
|
||||
"Settings.UploadsBoost" = "Ενίσχυση Αποστολής";
|
||||
"Settings.DownloadsBoost" = "Ενίσχυση Λήψης";
|
||||
"Settings.DownloadsBoost.Notice" = "Αυξάνει τον αριθμό των παράλληλων συνδέσεων και το μέγεθος των κομματιών αρχείου. Σε περίπτωση που το δίκτυό σας δεν μπορεί να διαχειριστεί το φορτίο, δοκιμάστε διαφορετικές επιλογές που ταιριάζουν στη σύνδεσή σας.";
|
||||
"Settings.DownloadsBoost.none" = "Απενεργοποιημένο";
|
||||
"Settings.DownloadsBoost.medium" = "Μεσαίο";
|
||||
"Settings.DownloadsBoost.maximum" = "Μέγιστο";
|
||||
|
||||
"Settings.ShowProfileID" = "Εμφάνιση Αναγνωριστικού Προφίλ";
|
||||
"Settings.ShowDC" = "Εμφάνιση Κέντρου Δεδομένων";
|
||||
"Settings.ShowCreationDate" = "Εμφάνιση Ημερομηνίας Δημιουργίας Συνομιλίας";
|
||||
"Settings.ShowCreationDate.Notice" = "Η ημερομηνία δημιουργίας μπορεί να είναι άγνωστη για μερικές συνομιλίες.";
|
||||
|
||||
"Settings.ShowRegDate" = "Εμφάνιση Ημερομηνίας Εγγραφής";
|
||||
"Settings.ShowRegDate.Notice" = "Η ημερομηνία εγγραφής είναι κατά προσέγγιση.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Αποστολή με κλειδί \"επιστροφή\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "Απόκρυψη τηλεφώνου στις ρυθμίσεις";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Αυτό θα κρύψει μόνο τον αριθμό τηλεφώνου σας από τη διεπαφή ρυθμίσεων. Για να τον αποκρύψετε από άλλους, μεταβείτε στο Privacy and Security.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Εάν είναι μακριά για 5 δευτερόλεπτα";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Χρήση συστήματος DNS";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Χρησιμοποιήστε το σύστημα DNS για να παρακάμψετε το χρονικό όριο αν δεν έχετε πρόσβαση στο Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Du **brauchst kein** %@!";
|
||||
"Common.RestartRequired" = "Απαιτείται επανεκκίνηση";
|
||||
"Common.RestartNow" = "Επανεκκίνηση Τώρα";
|
||||
"Common.OpenTelegram" = "Άνοιγμα Telegram";
|
||||
"Common.UseTelegramForPremium" = "Παρακαλώ σημειώστε ότι για να πάρετε Telegram Premium, θα πρέπει να χρησιμοποιήσετε την επίσημη εφαρμογή Telegram. Μόλις λάβετε Telegram Premium, όλα τα χαρακτηριστικά του θα είναι διαθέσιμα στο Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Κρατήστε για προβολή ή αναφορά.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Βεβαιωθείτε ότι έχετε μια μέθοδο πρόσβασης αντιγράφων ασφαλείας. Κρατήστε μια SIM για SMS ή μια πρόσθετη συνεδρία συνδεδεμένη για να αποφύγετε να κλειδωθεί.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Μπορείτε να πάρετε τον κωδικό μόνο με επίσημη εφαρμογή";
|
||||
|
||||
"Settings.SmallReactions" = "Μικρές Αντιδράσεις";
|
||||
"Settings.HideReactions" = "Απόκρυψη Αντιδράσεων";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Αποθήκευση στο σύννεφο";
|
||||
"ContextMenu.SelectFromUser" = "Επιλέξτε από τον Συγγραφέα";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEXTMENÜ";
|
||||
"Settings.ContextMenu.Notice" = "Deaktivierte Einträge sind im 'Swiftgram'-Untermenü verfügbar.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Επιλογές Συρσίματος Λίστας Συνομιλίας";
|
||||
"Settings.DeleteChatSwipeOption" = "Σύρετε για Διαγραφή Συνομιλίας";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Τραβήξτε στο επόμενο μη αναγνωσμένο κανάλι";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Τραβήξτε για το Επόμενο Θέμα";
|
||||
"Settings.GalleryCamera" = "Κάμερα στη Γκαλερί";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\" Κουμπί%@\"";
|
||||
"Settings.SnapDeletionEffect" = "Εφέ Διαγραφής Μηνύματος";
|
||||
|
||||
"Settings.Stickers.Size" = "ΜΕΓΕΘΟΣ";
|
||||
"Settings.Stickers.Timestamp" = "Εμφάνιση Χρονοσήμανσης";
|
||||
|
||||
"Settings.RecordingButton" = "Πλήκτρο Ηχογράφησης Φωνής";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Δώστε προτεραιότητα στα τυπικά emojis";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Εμφανίστε τυπικά emojis πριν από premium στο πληκτρολόγιο emojis";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "δημιουργήθηκε: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Εντάχθηκε στο %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Εγγεγραμμένος";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Διπλό Πάτημα για Επεξεργασία Μηνύματος";
|
||||
|
||||
"Settings.wideChannelPosts" = "Πλατείες αναρτήσεις στα κανάλια";
|
||||
"Settings.ForceEmojiTab" = "Πληκτρολόγιο Emoji από προεπιλογή";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Εξαναγκασμός Μικροφώνου Συσκευής";
|
||||
"Settings.forceBuiltInMic.Notice" = "Εάν ενεργοποιηθεί, η εφαρμογή θα χρησιμοποιεί μόνο το μικρόφωνο της συσκευής ακόμα και αν είναι συνδεδεμένα ακουστικά.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Απόκρυψη Καναλιού Κάτω Πάνελ";
|
||||
|
||||
"Settings.CallConfirmation" = "Επιβεβαίωση Κλήσης";
|
||||
"Settings.CallConfirmation.Notice" = "Η Swiftgram θα ζητήσει την επιβεβαίωσή σας πριν πραγματοποιήσει μια κλήση.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Να κάνω μια Κλήση;";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Να κάνω μια Βιντεοκλήση;";
|
||||
|
||||
"MutualContact.Label" = "αμοιβαία επαφή";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Βίντεο PIP με Swipe";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Αν είναι ενεργοποιημένο, το σ swipe video θα το ανοίξει σε λειτουργία Εικόνα μέσα στην Εικόνα.";
|
||||
152
Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/en.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Content Settings";
|
||||
|
||||
"Settings.Tabs.Header" = "TABS";
|
||||
"Settings.Tabs.HideTabBar" = "Hide Tab bar";
|
||||
"Settings.Tabs.ShowContacts" = "Show Contacts Tab";
|
||||
"Settings.Tabs.ShowNames" = "Show Tab Names";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Folders at Bottom";
|
||||
"Settings.Folders.BottomTabStyle" = "Bottom Folders Style";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Hide \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Open Last Folder";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram will open the last used folder when you restart the app or switch accounts.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Smaller spacing";
|
||||
"Settings.Folders.AllChatsTitle" = "\"All Chats\" title";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Short";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Long";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Default";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "CHAT LIST";
|
||||
"Settings.CompactChatList" = "Compact Chat List";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Hide Stories";
|
||||
"Settings.Stories.WarnBeforeView" = "Ask Before Viewing";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Disable Swipe to Record";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Quick Translate button";
|
||||
|
||||
"Stories.Warning.Author" = "Author";
|
||||
"Stories.Warning.ViewStory" = "View Story?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ WILL BE ABLE TO SEE that you viewed their Story.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ will not be able to see that you viewed their Story.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Quality of uploaded photos and stories.";
|
||||
"Settings.Photo.SendLarge" = "Send Large Photos";
|
||||
"Settings.Photo.SendLarge.Notice" = "Increase the side limit on compressed images to 2560px.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "ROUND VIDEOS";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Start with Rear Camera";
|
||||
|
||||
"Settings.CustomColors.Header" = "ACCOUNT COLORS";
|
||||
"Settings.CustomColors.Saturation" = "SATURATION";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Set to 0%% to disable account colors.";
|
||||
|
||||
"Settings.UploadsBoost" = "Upload Boost";
|
||||
"Settings.DownloadsBoost" = "Download Boost";
|
||||
"Settings.DownloadsBoost.Notice" = "Increases number of parallel connections and size of file chunks. In case your network can't handle the load, try different options that suits your connection.";
|
||||
"Settings.DownloadsBoost.none" = "Disabled";
|
||||
"Settings.DownloadsBoost.medium" = "Medium";
|
||||
"Settings.DownloadsBoost.maximum" = "Maximum";
|
||||
|
||||
"Settings.ShowProfileID" = "Show Profile ID";
|
||||
"Settings.ShowDC" = "Show Data Center";
|
||||
"Settings.ShowCreationDate" = "Show Chat Creation Date";
|
||||
"Settings.ShowCreationDate.Notice" = "The creation date may be unknown for some chats.";
|
||||
|
||||
"Settings.ShowRegDate" = "Show Registration Date";
|
||||
"Settings.ShowRegDate.Notice" = "The registration date is approximate.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Send with \"return\" key";
|
||||
"Settings.HidePhoneInSettingsUI" = "Hide Phone in Settings";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "This will only hide your phone number from the settings interface. To hide it from others, go to Privacy and Security.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "If away for 5 seconds";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Use system DNS";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Use system DNS to bypass timeout if you don't have access to Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "You **don't need** %@!";
|
||||
"Common.RestartRequired" = "Restart required";
|
||||
"Common.RestartNow" = "Restart Now";
|
||||
"Common.OpenTelegram" = "Open Telegram";
|
||||
"Common.UseTelegramForPremium" = "Please note that to get Telegram Premium, you must use the official Telegram app. Once you have obtained Telegram Premium, all its features will become available in Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Hold to Show or Report.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Make sure you have a backup access method. Keep a SIM for SMS or an additional session logged in to avoid being locked out.";
|
||||
"Auth.UnofficialAppCodeTitle" = "You can get the code only with official app";
|
||||
|
||||
"Settings.SmallReactions" = "Small Reactions";
|
||||
"Settings.HideReactions" = "Hide Reactions";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Save to Cloud";
|
||||
"ContextMenu.SelectFromUser" = "Select from Author";
|
||||
|
||||
"Settings.ContextMenu" = "CONTEXT MENU";
|
||||
"Settings.ContextMenu.Notice" = "Disabled entries will be available in \"Swiftgram\" sub-menu.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Chat List Swipe Options";
|
||||
"Settings.DeleteChatSwipeOption" = "Swipe to Delete Chat";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Pull to Next Unread Channel";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Pull to Next Topic";
|
||||
"Settings.GalleryCamera" = "Camera in Gallery";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Button";
|
||||
"Settings.SnapDeletionEffect" = "Message Deletion Effects";
|
||||
|
||||
"Settings.Stickers.Size" = "SIZE";
|
||||
"Settings.Stickers.Timestamp" = "Show Timestamp";
|
||||
|
||||
"Settings.RecordingButton" = "Voice Recording Button";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Prioritize standard emojis";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Show standard emojis before premium in emoji keyboard";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "created: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Joined %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Registered";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Double-tap to edit message";
|
||||
|
||||
"Settings.wideChannelPosts" = "Wide posts in channels";
|
||||
"Settings.ForceEmojiTab" = "Emoji keyboard by default";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Force Device Microphone";
|
||||
"Settings.forceBuiltInMic.Notice" = "If enabled, app will use only device microphone even if headphones are connected.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Hide Channel Bottom Panel";
|
||||
|
||||
"Settings.CallConfirmation" = "Call Confirmation";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram will ask for your confirmation before making a call.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Make a Call?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Make a Video Call?";
|
||||
|
||||
"MutualContact.Label" = "mutual contact";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP with Swipe";
|
||||
"Settings.swipeForVideoPIP.Notice" = "If enabled, swiping video will open it in Picture-in-Picture mode.";
|
||||
152
Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/es.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Configuración de contenido";
|
||||
|
||||
"Settings.Tabs.Header" = "PESTAÑAS";
|
||||
"Settings.Tabs.HideTabBar" = "Ocultar barra de pestaña";
|
||||
"Settings.Tabs.ShowContacts" = "Mostrar pestaña de Contactos";
|
||||
"Settings.Tabs.ShowNames" = "Mostrar nombres de pestañas";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Carpetas al fondo";
|
||||
"Settings.Folders.BottomTabStyle" = "Estilo de carpetas al fondo";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Ocultar \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Abrir última carpeta";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram abrirá la última carpeta usada después de reiniciar o cambiar de cuenta";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Espaciado más pequeño";
|
||||
"Settings.Folders.AllChatsTitle" = "Título \"Todos los Chats\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Corto";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Largo";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Por defecto";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "LISTA DE CHAT";
|
||||
"Settings.CompactChatList" = "Lista de Chat de Compacto";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Ocultar Historias";
|
||||
"Settings.Stories.WarnBeforeView" = "Preguntar antes de ver";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Desactivar deslizar para grabar";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Botón de traducción rápida";
|
||||
|
||||
"Stories.Warning.Author" = "Autor";
|
||||
"Stories.Warning.ViewStory" = "¿Ver historia?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ PODRÁ VER que viste su historia.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ no podrá ver que viste su historia.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Calidad de las fotos y foto-historias enviadas";
|
||||
"Settings.Photo.SendLarge" = "Enviar fotos grandes";
|
||||
"Settings.Photo.SendLarge.Notice" = "Aumentar el límite de tamaño de las imágenes comprimidas a 2560px";
|
||||
|
||||
"Settings.VideoNotes.Header" = "VIDEOS REDONDOS";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Comenzar con la cámara trasera";
|
||||
|
||||
"Settings.CustomColors.Header" = "COLORES DE LA CUENTA";
|
||||
"Settings.CustomColors.Saturation" = "SATURACIÓN";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Establecer saturación en 0%% para desactivar los colores de la cuenta";
|
||||
|
||||
"Settings.UploadsBoost" = "Aumento de subida";
|
||||
"Settings.DownloadsBoost" = "Aumento de descargas";
|
||||
"Settings.DownloadsBoost.Notice" = "Aumenta el número de conexiones paralelas y el tamaño de las partes del archivo. Si tu red no puede manejar la carga, prueba diferentes opciones que se adapten a tu conexión.";
|
||||
"Settings.DownloadsBoost.none" = "Desactivado";
|
||||
"Settings.DownloadsBoost.medium" = "Medio";
|
||||
"Settings.DownloadsBoost.maximum" = "Máximo";
|
||||
|
||||
"Settings.ShowProfileID" = "Mostrar ID del perfil";
|
||||
"Settings.ShowDC" = "Mostrar Centro de Datos";
|
||||
"Settings.ShowCreationDate" = "Mostrar fecha de creación del chat";
|
||||
"Settings.ShowCreationDate.Notice" = "La fecha de creación puede ser desconocida para algunos chats.";
|
||||
|
||||
"Settings.ShowRegDate" = "Mostrar fecha de registro";
|
||||
"Settings.ShowRegDate.Notice" = "La fecha de inscripción es aproximada.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Enviar con la tecla \"regresar\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "Ocultar número en Ajustes";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Tu número estará oculto en la interfaz de ajustes solamente. Ve a la configuración de privacidad para ocultarlo a otros.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Si está ausente durante 5 segundos";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Usar DNS del sistema";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Usa el DNS del sistema para omitir el tiempo de espera si no tienes acceso a Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "¡**No necesitas** %@!";
|
||||
"Common.RestartRequired" = "Es necesario reiniciar";
|
||||
"Common.RestartNow" = "Reiniciar ahora";
|
||||
"Common.OpenTelegram" = "Abrir Telegram";
|
||||
"Common.UseTelegramForPremium" = "Ten en cuenta que para obtener Telegram Premium, debes usar la aplicación oficial de Telegram. Una vez que haya obtenido Telegram Premium, todas sus características estarán disponibles en Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Mantenga presionado para mostrar o reportar.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Asegúrate de que tienes un método de acceso de copia de seguridad. Mantenga una SIM para SMS o una sesión adicional conectada para evitar ser bloqueada.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Sólo puedes obtener el código con la app oficial";
|
||||
|
||||
"Settings.SmallReactions" = "Reacciones pequeñas";
|
||||
"Settings.HideReactions" = "Ocultar Reacciones";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Guardar en la nube";
|
||||
"ContextMenu.SelectFromUser" = "Seleccionar del autor";
|
||||
|
||||
"Settings.ContextMenu" = "MENÚ CONTEXTUAL";
|
||||
"Settings.ContextMenu.Notice" = "Las entradas desactivadas estarán disponibles en el submenú \"Swiftgram\".";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Opciones de deslizamiento de la lista de chats";
|
||||
"Settings.DeleteChatSwipeOption" = "Deslizar para eliminar chat";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Saltar al siguiente canal no leído";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Deslizar para ir al siguiente tema";
|
||||
"Settings.GalleryCamera" = "Cámara en galería";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "Botón \"%@\"";
|
||||
"Settings.SnapDeletionEffect" = "Efectos de eliminación de mensajes";
|
||||
|
||||
"Settings.Stickers.Size" = "TAMAÑO";
|
||||
"Settings.Stickers.Timestamp" = "Mostrar marca de tiempo";
|
||||
|
||||
"Settings.RecordingButton" = "Botón de grabación de voz";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Priorizar emojis estándar";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Mostrar emojis estándar antes que premium en el teclado de emojis";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "creado: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Unido a %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Registrado";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Doble toque para editar mensaje";
|
||||
|
||||
"Settings.wideChannelPosts" = "Publicaciones amplias en canales";
|
||||
"Settings.ForceEmojiTab" = "Teclado de emojis por defecto";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Forzar Micrófono del Dispositivo";
|
||||
"Settings.forceBuiltInMic.Notice" = "Si está habilitado, la aplicación utilizará solo el micrófono del dispositivo incluso si se conectan auriculares.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Ocultar Panel Inferior del Canal";
|
||||
|
||||
"Settings.CallConfirmation" = "Confirmación de llamada";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram pedirá tu confirmación antes de realizar una llamada.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "¿Hacer una llamada?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "¿Hacer una videollamada?";
|
||||
|
||||
"MutualContact.Label" = "contacto mutuo";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP con deslizamiento";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Si está habilitado, deslizar el video lo abrirá en modo imagen en imagen.";
|
||||
@@ -0,0 +1,9 @@
|
||||
"Settings.Tabs.Header" = "زبانه ها";
|
||||
"Settings.Tabs.ShowContacts" = "نمایش برگه مخاطبین";
|
||||
"Settings.VideoNotes.Header" = "فیلم های round";
|
||||
"Settings.Tabs.ShowNames" = "نشان دادن برگه اسم ها";
|
||||
"Settings.HidePhoneInSettingsUI" = "پنهان کردن شماره موبایل در تنظیمات";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "شماره شما فقط در رابط کاربری پنهان خواهد شد. برای پنهان کردن آن از دید دیگران ، لطفاً از تنظیمات حریم خصوصی استفاده کنید.";
|
||||
"Settings.ShowProfileID" = "نمایش ایدی پروفایل";
|
||||
"Settings.Translation.QuickTranslateButton" = "دکمه ترجمه سریع";
|
||||
"ContextMenu.SaveToCloud" = "ذخیره در فضای ابری";
|
||||
152
Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/fi.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Sisällön Asetukset";
|
||||
|
||||
"Settings.Tabs.Header" = "VÄLILEHDET";
|
||||
"Settings.Tabs.HideTabBar" = "Piilota Välilehtipalkki";
|
||||
"Settings.Tabs.ShowContacts" = "Näytä Yhteystiedot-välilehti";
|
||||
"Settings.Tabs.ShowNames" = "Näytä välilehtien nimet";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Kansiot alhaalla";
|
||||
"Settings.Folders.BottomTabStyle" = "Alhaalla olevien kansioiden tyyli";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Piilota \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Avaa viimeisin kansio";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram avaa viimeksi käytetyn kansion uudelleenkäynnistyksen tai tilin vaihdon jälkeen.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Pienempi väli";
|
||||
"Settings.Folders.AllChatsTitle" = "\"Kaikki chatit\" otsikko";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Lyhyt";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Pitkä";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Oletus";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "CHAT LIST";
|
||||
"Settings.CompactChatList" = "Kompakti Keskustelulista";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Piilota Tarinat";
|
||||
"Settings.Stories.WarnBeforeView" = "Kysy ennen katsomista";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Poista pyyhkäisy tallennukseen käytöstä";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Pikakäännöspainike";
|
||||
|
||||
"Stories.Warning.Author" = "Tekijä";
|
||||
"Stories.Warning.ViewStory" = "Katso Tarina?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ NÄKEE, että olet katsonut heidän Tarinansa.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ ei näe, että olet katsonut heidän Tarinansa.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Lähtevien valokuvien ja valokuvatarinoiden laatu.";
|
||||
"Settings.Photo.SendLarge" = "Lähetä suuria valokuvia";
|
||||
"Settings.Photo.SendLarge.Notice" = "Suurenna pakattujen kuvien sivurajaa 2560px:ään.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "PYÖREÄT VIDEOT";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Aloita takakameralla";
|
||||
|
||||
"Settings.CustomColors.Header" = "TILIN VÄRIT";
|
||||
"Settings.CustomColors.Saturation" = "KYLLÄISYYS";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Aseta kylläisyys 0%%:iin poistaaksesi tilin värit käytöstä.";
|
||||
|
||||
"Settings.UploadsBoost" = "Latausten tehostus";
|
||||
"Settings.DownloadsBoost" = "Latausten tehostus";
|
||||
"Settings.DownloadsBoost.Notice" = "Lisää samanaikaisten yhteyksien määrää ja tiedostopalojen kokoa. Jos verkkoasi ei pysty käsittelemään kuormitusta, kokeile erilaisia vaihtoehtoja, jotka sopivat yhteyteesi.";
|
||||
"Settings.DownloadsBoost.none" = "Ei käytössä";
|
||||
"Settings.DownloadsBoost.medium" = "Keskitaso";
|
||||
"Settings.DownloadsBoost.maximum" = "Maksimi";
|
||||
|
||||
"Settings.ShowProfileID" = "Näytä profiilin ID";
|
||||
"Settings.ShowDC" = "Näytä tietokeskus";
|
||||
"Settings.ShowCreationDate" = "Näytä keskustelun luontipäivä";
|
||||
"Settings.ShowCreationDate.Notice" = "Keskustelun luontipäivä voi olla tuntematon joillekin keskusteluille.";
|
||||
|
||||
"Settings.ShowRegDate" = "Näytä Rekisteröintipäivä";
|
||||
"Settings.ShowRegDate.Notice" = "Rekisteröintipäivä on likimääräinen.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Lähetä 'paluu'-näppäimellä";
|
||||
"Settings.HidePhoneInSettingsUI" = "Piilota puhelin asetuksissa";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Tämä piilottaa puhelinnumerosi vain asetukset-käyttöliittymästä. Piilottaaksesi sen muilta, siirry kohtaan Yksityisyys ja Turvallisuus.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Jos poissa 5 sekuntia";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Käytä järjestelmän DNS:ää";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Käytä järjestelmän DNS:ää ohittaaksesi aikakatkaisun, jos sinulla ei ole pääsyä Google DNS:ään";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Et **tarvitse** %@!";
|
||||
"Common.RestartRequired" = "Uudelleenkäynnistys vaaditaan";
|
||||
"Common.RestartNow" = "Käynnistä uudelleen nyt";
|
||||
"Common.OpenTelegram" = "Avaa Telegram";
|
||||
"Common.UseTelegramForPremium" = "Huomioi, että saat Telegram Premiumin käyttämällä virallista Telegram-sovellusta. Kun olet hankkinut Telegram Premiumin, kaikki sen ominaisuudet ovat saatavilla Swiftgramissa.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Pidä esillä näyttääksesi tai ilmoittaaksesi.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Varmista, että sinulla on varmuuskopio pääsymenetelmästä. Pidä SIM tekstiviestejä varten tai ylimääräinen istunto kirjautuneena välttääksesi lukkiutumisen.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Koodin voi saada vain virallisella sovelluksella";
|
||||
|
||||
"Settings.SmallReactions" = "Pienet reaktiot";
|
||||
"Settings.HideReactions" = "Piilota reaktiot";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Tallenna Pilveen";
|
||||
"ContextMenu.SelectFromUser" = "Valitse Tekijältä";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEKSTIVALIKKO";
|
||||
"Settings.ContextMenu.Notice" = "Poistetut kohteet ovat saatavilla 'Swiftgram'-alavalikossa.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Chat List Swipe Options";
|
||||
"Settings.DeleteChatSwipeOption" = "Vedä poistaaksesi keskustelu";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Vetää seuraavaan lukemattomaan kanavaan";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Vedä seuraava aihe";
|
||||
"Settings.GalleryCamera" = "Camera in Gallery";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Button";
|
||||
"Settings.SnapDeletionEffect" = "Message Deletion Effects";
|
||||
|
||||
"Settings.Stickers.Size" = "SIZE";
|
||||
"Settings.Stickers.Timestamp" = "Show Timestamp";
|
||||
|
||||
"Settings.RecordingButton" = "Voice Recording Button";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Anna etusijalle vakiotunnuksia";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Näytä vakiotunnukset ennen premium-tunnuksia tunnusnäppäimistössä";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "created: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Joined %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Rekisteröity";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Paina kahdesti muokataksesi viestiä";
|
||||
|
||||
"Settings.wideChannelPosts" = "Leveitä viestejä kanavissa";
|
||||
"Settings.ForceEmojiTab" = "Emoji-näppäimistö oletuksena";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Pakota laitteen mikrofoni";
|
||||
"Settings.forceBuiltInMic.Notice" = "Jos otettu käyttöön, sovellus käyttää vain laitteen mikrofonia, vaikka kuulokkeet olisivatkin liitettynä.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Piilota kanavan alapalkki";
|
||||
|
||||
"Settings.CallConfirmation" = "Puhelun vahvistus";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram pyytää vahvistustasi ennen puhelun soittamista.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Soita puhelu?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Soita videopuhelu?";
|
||||
|
||||
"MutualContact.Label" = "yhteinen yhteys";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Video PIP pyyhkäisevällä toiminnolla";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Jos se on käytössä, videon pyyhkäisy avaa sen kuvassa kuvassa -tilassa.";
|
||||
137
Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings
Normal file
137
Swiftgram/SGStrings/Strings/fr.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,137 @@
|
||||
"Settings.ContentSettings" = "Paramètres du contenu";
|
||||
|
||||
"Settings.Tabs.Header" = "ONGLETS";
|
||||
"Settings.Tabs.HideTabBar" = "Masquer la barre d'onglets";
|
||||
"Settings.Tabs.ShowContacts" = "Afficher l'onglet Contacts";
|
||||
"Settings.Tabs.ShowNames" = "Afficher les noms des onglets";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Dossiers en bas";
|
||||
"Settings.Folders.BottomTabStyle" = "Style des dossiers inférieurs";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Masquer \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Ouvrir le dernier dossier";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram ouvrira le dernier dossier utilisé après le redémarrage ou changement de compte";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Espacement plus petit";
|
||||
"Settings.Folders.AllChatsTitle" = "Titre \"Tous les Chats\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Courte";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Longue";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Par défaut";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "LISTE DE CHAT";
|
||||
"Settings.CompactChatList" = "Liste de discussion compacte";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFILES";
|
||||
|
||||
"Settings.Stories.Hide" = "Cacher les histoires";
|
||||
"Settings.Stories.WarnBeforeView" = "Demander avant de visionner";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Désactiver le glissement pour enregistrer";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Bouton de traduction rapide";
|
||||
|
||||
"Stories.Warning.Author" = "Auteur";
|
||||
"Stories.Warning.ViewStory" = "Voir l'histoire?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ SERA autorisé à voir que vous avez vu son histoire.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ ne sera pas en mesure de voir que vous avez vu leur Histoire.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Qualité des photos et des récits photo sortants";
|
||||
"Settings.Photo.SendLarge" = "Envoyer de grandes photos";
|
||||
"Settings.Photo.SendLarge.Notice" = "Augmenter la limite latérale des images compressées à 2560px";
|
||||
|
||||
"Settings.VideoNotes.Header" = "VIDÉOS RONDES";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Commencer avec la caméra arrière";
|
||||
|
||||
"Settings.CustomColors.Header" = "COULEURS DU COMPTE";
|
||||
"Settings.CustomColors.Saturation" = "SATURATION";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Régler la saturation à 0%% pour désactiver les couleurs du compte";
|
||||
|
||||
"Settings.UploadsBoost" = "Chargements boost";
|
||||
"Settings.DownloadsBoost" = "Boost de téléchargements";
|
||||
"Settings.DownloadsBoost.none" = "Désactivé";
|
||||
"Settings.DownloadsBoost.medium" = "Moyenne";
|
||||
"Settings.DownloadsBoost.maximum" = "Maximum";
|
||||
|
||||
"Settings.ShowProfileID" = "Afficher l'identifiant du profil";
|
||||
"Settings.ShowDC" = "Afficher le centre de données";
|
||||
"Settings.ShowCreationDate" = "Afficher la date de création du chat";
|
||||
"Settings.ShowCreationDate.Notice" = "La date de création peut être inconnue pour certains chats.";
|
||||
|
||||
"Settings.ShowRegDate" = "Afficher la date d'inscription";
|
||||
"Settings.ShowRegDate.Notice" = "La date d'inscription est approximative.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Envoyer avec la clé \"return\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "Masquer le téléphone dans les paramètres";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Votre numéro sera masqué dans l'interface utilisateur uniquement. Pour le masquer aux autres, veuillez utiliser les paramètres de confidentialité.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Si absente pendant 5 secondes";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Utiliser le DNS du système";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Utiliser le DNS système pour contourner le délai d'attente si vous n'avez pas accès à Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Vous **n'avez pas besoin** %@!";
|
||||
"Common.RestartRequired" = "Redémarrage nécessaire";
|
||||
"Common.RestartNow" = "Redémarrer maintenant";
|
||||
"Common.OpenTelegram" = "Ouvrir Telegram";
|
||||
"Common.UseTelegramForPremium" = "Veuillez noter que pour obtenir Telegram Premium, vous devez utiliser l'application Telegram officielle. Une fois que vous avez obtenu Telegram Premium, toutes ses fonctionnalités seront disponibles dans Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Maintenir pour afficher ou rapporter.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Assurez-vous d'avoir une méthode d'accès de sauvegarde. Gardez une carte SIM pour les SMS ou une session supplémentaire connectée pour éviter d'être bloquée.";
|
||||
"Auth.UnofficialAppCodeTitle" = "Vous ne pouvez obtenir le code qu'avec l'application officielle";
|
||||
|
||||
"Settings.SmallReactions" = "Petites réactions";
|
||||
"Settings.HideReactions" = "Masquer les réactions";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Sauvegarder dans le cloud";
|
||||
"ContextMenu.SelectFromUser" = "Sélectionner de l'Auteur";
|
||||
|
||||
"Settings.ContextMenu" = "MENU CONTEXTUEL";
|
||||
"Settings.ContextMenu.Notice" = "Les entrées désactivées seront disponibles dans le sous-menu 'Swiftgram'.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Options de balayage de la liste de chat";
|
||||
"Settings.DeleteChatSwipeOption" = "Glisser pour supprimer la conversation";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Tirer vers le prochain canal non lu";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Tirer pour le sujet suivant";
|
||||
"Settings.GalleryCamera" = "Appareil photo dans la galerie";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "Bouton \"%@\"";
|
||||
"Settings.SnapDeletionEffect" = "Effets de suppression de message";
|
||||
|
||||
"Settings.Stickers.Size" = "TAILLE";
|
||||
"Settings.Stickers.Timestamp" = "Afficher l'horodatage";
|
||||
|
||||
"Settings.RecordingButton" = "Bouton d'enregistrement vocal";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Prioriser les emojis standard";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Afficher les emojis standard avant les emojis premium dans le clavier emoji";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "créé: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Rejoint %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Enregistré";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Appuyez deux fois pour modifier le message";
|
||||
|
||||
"Settings.wideChannelPosts" = "Messages larges dans les canaux";
|
||||
"Settings.ForceEmojiTab" = "Clavier emoji par défaut";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Forcer le microphone de l'appareil";
|
||||
"Settings.forceBuiltInMic.Notice" = "Si activé, l'application utilisera uniquement le microphone de l'appareil même si des écouteurs sont connectés.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Masquer le panneau inférieur du canal";
|
||||
152
Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/he.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "הגדרות תוכן";
|
||||
|
||||
"Settings.Tabs.Header" = "כרטיסיות";
|
||||
"Settings.Tabs.HideTabBar" = "הסתר סרגל לשוניים";
|
||||
"Settings.Tabs.ShowContacts" = "הצג כרטיסיית אנשי קשר";
|
||||
"Settings.Tabs.ShowNames" = "הצג שמות כרטיסיות";
|
||||
|
||||
"Settings.Folders.BottomTab" = "תיקיות בתחתית";
|
||||
"Settings.Folders.BottomTabStyle" = "סגנון תיקיות תחתון";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "טלגרם";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "להסתיר \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "פתח את התיקיה האחרונה";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram תפתח את התיקיה שנעשה בה שימוש לאחרונה לאחר הפעלה מחדש או החלפת חשבון";
|
||||
|
||||
"Settings.Folders.CompactNames" = "ריווח קטן יותר";
|
||||
"Settings.Folders.AllChatsTitle" = "כותרת \"כל הצ'אטים\"";
|
||||
"Settings.Folders.AllChatsTitle.short" = "קצר";
|
||||
"Settings.Folders.AllChatsTitle.long" = "ארוך";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "ברירת מחדל";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "רשימת צ'אטים";
|
||||
"Settings.CompactChatList" = "רשימת צ'אטים קומפקטית";
|
||||
|
||||
"Settings.Profiles.Header" = "פרופילים";
|
||||
|
||||
"Settings.Stories.Hide" = "הסתר סיפורים";
|
||||
"Settings.Stories.WarnBeforeView" = "שאל לפני צפייה";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "בטל החלקה להקלטה";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "כפתור תרגום מהיר";
|
||||
|
||||
"Stories.Warning.Author" = "מחבר";
|
||||
"Stories.Warning.ViewStory" = "לצפות בסיפור?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ יוכל לראות שצפית בסיפור שלו.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ לא יוכל לראות שצפית בסיפור שלו.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "איכות התמונות היוצאות והסיפורים בתמונות";
|
||||
"Settings.Photo.SendLarge" = "שלח תמונות גדולות";
|
||||
"Settings.Photo.SendLarge.Notice" = "הגדל את הגבול הצידי של תמונות מודחקות ל-2560px";
|
||||
|
||||
"Settings.VideoNotes.Header" = "וידאו מעוגלים";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "התחל עם מצלמה אחורית";
|
||||
|
||||
"Settings.CustomColors.Header" = "צבעי חשבון";
|
||||
"Settings.CustomColors.Saturation" = "רווי";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "קבע רווי ל-0%% כדי לבטל צבעי חשבון";
|
||||
|
||||
"Settings.UploadsBoost" = "תוספת העלאות";
|
||||
"Settings.DownloadsBoost" = "תוספת הורדות";
|
||||
"Settings.DownloadsBoost.Notice" = "מגביר את מספר החיבורים המקביליים וגודל חלקי הקבצים. אם הרשת שלך לא יכולה להתמודד עם העומס, נסה אפשרויות שונות שמתאימות לחיבור שלך.";
|
||||
"Settings.DownloadsBoost.none" = "מבוטל";
|
||||
"Settings.DownloadsBoost.medium" = "בינוני";
|
||||
"Settings.DownloadsBoost.maximum" = "מרבי";
|
||||
|
||||
"Settings.ShowProfileID" = "הצג מזהה פרופיל";
|
||||
"Settings.ShowDC" = "הצג מרכז מידע";
|
||||
"Settings.ShowCreationDate" = "הצג תאריך יצירת צ'אט";
|
||||
"Settings.ShowCreationDate.Notice" = "ייתכן שתאריך היצירה אינו ידוע עבור חלק מהצ'אטים.";
|
||||
|
||||
"Settings.ShowRegDate" = "הצג תאריך רישום";
|
||||
"Settings.ShowRegDate.Notice" = "תאריך הרישום הוא אופציונלי.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "שלח עם מקש \"חזור\"";
|
||||
"Settings.HidePhoneInSettingsUI" = "הסתר טלפון בהגדרות";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "המספר שלך יהיה מוסתר בממשק ההגדרות בלבד. עבור להגדרות פרטיות כדי להסתיר אותו מאחרים.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "נעל אוטומטית אחרי 5 שניות";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "השתמש ב-DNS של המערכת";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "השתמש ב-DNS של המערכת כדי לעקוף זמן תגובה אם אין לך גישה ל-Google DNS";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "אין **צורך** ב%@!";
|
||||
"Common.RestartRequired" = "נדרש הפעלה מחדש";
|
||||
"Common.RestartNow" = "הפעל מחדש עכשיו";
|
||||
"Common.OpenTelegram" = "פתח טלגרם";
|
||||
"Common.UseTelegramForPremium" = "שים לב כי כדי לקבל Telegram Premium, עליך להשתמש באפליקציית Telegram הרשמית. לאחר שקיבלת טלגרם פרימיום, כל התכונות שלו יהיו זמינות ב־Swiftgram.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "החזק כדי להציג או לדווח.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "ודא שיש לך שיטת גישה לגיבוי. שמור כרטיס SIM ל-SMS או פתח סשן נוסף כדי למנוע חסימה.";
|
||||
"Auth.UnofficialAppCodeTitle" = "תוכל לקבל את הקוד רק דרך האפליקציה הרשמית";
|
||||
|
||||
"Settings.SmallReactions" = "תגובות קטנות";
|
||||
"Settings.HideReactions" = "הסתר תגובות";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "שמור בענן";
|
||||
"ContextMenu.SelectFromUser" = "בחר מהמשתמש";
|
||||
|
||||
"Settings.ContextMenu" = "תפריט הקשר";
|
||||
"Settings.ContextMenu.Notice" = "פריטים מבוטלים יהיו זמינים בתת-תפריט 'Swiftgram'.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "אפשרויות גלילה ברשימת צ'אטים";
|
||||
"Settings.DeleteChatSwipeOption" = "החלק למחיקת הצ'אט";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "משוך לערוץ לא נקרא הבא";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "משוך כדי להמשיך לנושא הבא";
|
||||
"Settings.GalleryCamera" = "מצלמה בגלריה";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "כפתור \"%@\"";
|
||||
"Settings.SnapDeletionEffect" = "אפקטים של מחיקת הודעות";
|
||||
|
||||
"Settings.Stickers.Size" = "גודל";
|
||||
"Settings.Stickers.Timestamp" = "הצג חותמת זמן";
|
||||
|
||||
"Settings.RecordingButton" = "כפתור הקלטת קול";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "העדף רמזי פנים סטנדרטיים";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "הצג רמזי פנים סטנדרטיים לפני פרימיום במקלדת רמזי פנים";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "נוצר: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "הצטרף/הצטרפה ב־%@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "נרשם";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "לחץ פעמיים לעריכת הודעה";
|
||||
|
||||
"Settings.wideChannelPosts" = "פוסטים רחבים בערוצים";
|
||||
"Settings.ForceEmojiTab" = "מקלדת Emoji כברירת מחדל";
|
||||
|
||||
"Settings.forceBuiltInMic" = "כוח מיקרופון המכשיר";
|
||||
"Settings.forceBuiltInMic.Notice" = "אם מופעל, האפליקציה תשתמש רק במיקרופון המכשיר גם כאשר אוזניות מחוברות.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "הסתר פאנל תחתון של ערוץ";
|
||||
|
||||
"Settings.CallConfirmation" = "אישור שיחה";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram יבקש את אישורך לפני ביצוע שיחה.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "לבצע שיחה?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "לבצע שיחת וידאו?";
|
||||
|
||||
"MutualContact.Label" = "איש קשר משותף";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "וידאו PIP עם החלקה";
|
||||
"Settings.swipeForVideoPIP.Notice" = "אם מופעל, החלקת הווידאו תפתח אותו במצב תמונה בתוך תמונה.";
|
||||
152
Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/hi.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "कंटेंट सेटिंग्स";
|
||||
|
||||
"Settings.Tabs.Header" = "टैब";
|
||||
"Settings.Tabs.HideTabBar" = "टैब बार छिपाएं";
|
||||
"Settings.Tabs.ShowContacts" = "संपर्क टैब दिखाएँ";
|
||||
"Settings.Tabs.ShowNames" = "टैब नाम दिखाएं";
|
||||
|
||||
"Settings.Folders.BottomTab" = "निचले टैब में फोल्डर्स";
|
||||
"Settings.Folders.BottomTabStyle" = "बॉटम फोल्डर स्टाइल है";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "आईओएस";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "टेलीग्राम";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "\"%@\" छिपाएं";
|
||||
"Settings.Folders.RememberLast" = "आखिरी फोल्डर खोलें";
|
||||
"Settings.Folders.RememberLast.Notice" = "Swiftgram पुनः आरंभ या खाता स्विच करने के बाद अंतिम प्रयुक्त फोल्डर को खोलेगा";
|
||||
|
||||
"Settings.Folders.CompactNames" = "कम अंतराल";
|
||||
"Settings.Folders.AllChatsTitle" = "\"सभी चैट\" शीर्षक";
|
||||
"Settings.Folders.AllChatsTitle.short" = "संक्षिप्त";
|
||||
"Settings.Folders.AllChatsTitle.long" = "लंबा";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "डिफ़ॉल्ट";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "चैट सूची";
|
||||
"Settings.CompactChatList" = "संक्षिप्त चैट सूची";
|
||||
|
||||
"Settings.Profiles.Header" = "प्रोफाइल";
|
||||
|
||||
"Settings.Stories.Hide" = "कहानियाँ छुपाएं";
|
||||
"Settings.Stories.WarnBeforeView" = "देखने से पहले पूछें";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "रिकॉर्ड करने के लिए स्वाइप को अक्षम करें";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "त्वरित अनुवाद बटन";
|
||||
|
||||
"Stories.Warning.Author" = "लेखक";
|
||||
"Stories.Warning.ViewStory" = "कहानी देखें";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ देख सकते हैं कि आपने उनकी कहानी देखी है।";
|
||||
"Stories.Warning.NoticeStealth" = "%@ नहीं देख सकते कि आपने उनकी कहानी देखी है।";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "भेजे गए फोटो और फोटो-कहानियों की गुणवत्ता";
|
||||
"Settings.Photo.SendLarge" = "बड़े फोटो भेजें";
|
||||
"Settings.Photo.SendLarge.Notice" = "संकुचित छवियों पर साइड सीमा को 2560px तक बढ़ाएं";
|
||||
|
||||
"Settings.VideoNotes.Header" = "गोल वीडियो";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "रियर कैमरा के साथ शुरू करें";
|
||||
|
||||
"Settings.CustomColors.Header" = "खाता रंग";
|
||||
"Settings.CustomColors.Saturation" = "संतृप्ति";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "खाता रंगों को निष्क्रिय करने के लिए संतृप्ति को 0%% पर सेट करें";
|
||||
|
||||
"Settings.UploadsBoost" = "अपलोड बूस्ट";
|
||||
"Settings.DownloadsBoost" = "डाउनलोड बूस्ट";
|
||||
"Settings.DownloadsBoost.Notice" = "पैरलेल कनेक्शनों की संख्या और फ़ाइल फ़्रैगमेंट का आकार बढ़ाता है। अगर आपका नेटवर्क लोड को संभाल नहीं सकता है, तो अपने कनेक्शन के अनुरूप अलग-अलग विकल्प आजमाएं।";
|
||||
"Settings.DownloadsBoost.none" = "निष्क्रिय";
|
||||
"Settings.DownloadsBoost.medium" = "माध्यम";
|
||||
"Settings.DownloadsBoost.maximum" = "अधिकतम";
|
||||
|
||||
"Settings.ShowProfileID" = "प्रोफ़ाइल ID दिखाएं";
|
||||
"Settings.ShowDC" = "डेटा सेंटर दिखाएं";
|
||||
"Settings.ShowCreationDate" = "चैट निर्माण तिथि दिखाएं";
|
||||
"Settings.ShowCreationDate.Notice" = "कुछ चैट के लिए निर्माण तिथि अज्ञात हो सकती है।";
|
||||
|
||||
"Settings.ShowRegDate" = "पंजीकरण दिनांक दिखाएं";
|
||||
"Settings.ShowRegDate.Notice" = "पंजीकरण दिनांक अनुमानित हो सकती है।";
|
||||
|
||||
"Settings.SendWithReturnKey" = "\"वापसी\" कुंजी के साथ भेजें";
|
||||
"Settings.HidePhoneInSettingsUI" = "सेटिंग्स में फोन छिपाएं";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "आपका नंबर केवल सेटिंग्स UI में छिपा होगा। इसे दूसरों से छिपाने के लिए गोपनीयता सेटिंग्स में जाएं।";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "5 सेकंड के लिए दूर रहने पर";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "सिस्टम डीएनएस का प्रयोग करें";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "यदि आपके पास Google DNS तक पहुँच नहीं है तो टाइमआउट से बचने के लिए सिस्टम DNS का उपयोग करें";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "आपको %@ की **आवश्यकता नहीं** है!";
|
||||
"Common.RestartRequired" = "पुनः आरंभ की आवश्यकता";
|
||||
"Common.RestartNow" = "अभी रीस्टार्ट करें";
|
||||
"Common.OpenTelegram" = "टेलीग्राम खोलें";
|
||||
"Common.UseTelegramForPremium" = "कृपया ध्यान दें कि टेलीग्राम प्रीमियम प्राप्त करने के लिए आपको आधिकारिक टेलीग्राम ऐप का उपयोग करना होगा। एक बार जब आप टेलीग्राम प्रीमियम प्राप्त कर लेंगे, तो इसकी सभी सुविधाएं स्विफ्टग्राम में उपलब्ध हो जाएंगी।";
|
||||
|
||||
"Message.HoldToShowOrReport" = "दिखाने या रिपोर्ट करने के लिए दबाए रखें।";
|
||||
|
||||
"Auth.AccountBackupReminder" = "सुनिश्चित करें कि आपके पास बैकअप एक्सेस विधि है। एसएमएस के लिए एक सिम रखें या बाहर निकलने से बचने के लिए एक अतिरिक्त सत्र में लॉग इन करें।";
|
||||
"Auth.UnofficialAppCodeTitle" = "आप केवल आधिकारिक ऐप से ही कोड प्राप्त कर सकते हैं";
|
||||
|
||||
"Settings.SmallReactions" = "छोटी-छोटी प्रतिक्रियाएँ";
|
||||
"Settings.HideReactions" = "प्रतिक्रियाएँ छिपाएं";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "क्लाउड में सहेजें";
|
||||
"ContextMenu.SelectFromUser" = "लेखक में से चुनें";
|
||||
|
||||
"Settings.ContextMenu" = "संदर्भ मेनू";
|
||||
"Settings.ContextMenu.Notice" = "अक्षम प्रविष्टियाँ \"स्विफ्टग्राम\" उप-मेनू में उपलब्ध होंगी।";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "चैटलिस्ट स्वाइप विकल्प";
|
||||
"Settings.DeleteChatSwipeOption" = "चैट हटाने के लिए स्वैप करें";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "अगले अपठित चैनल पर खींचें";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "अगले विषय को खींचें";
|
||||
"Settings.GalleryCamera" = "गैलरी में कैमरा";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" बटन";
|
||||
"Settings.SnapDeletionEffect" = "संदेश विलोपन प्रभाव";
|
||||
|
||||
"Settings.Stickers.Size" = "आकार";
|
||||
"Settings.Stickers.Timestamp" = "टाइमस्टैंप दिखाएं";
|
||||
|
||||
"Settings.RecordingButton" = "वॉयस रिकॉर्डिंग बटन";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "मुख्यत: मानक इमोजी को प्राथमिकता दें";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "इमोजी कीबोर्ड में प्रीमियम से पहले मानक इमोजी दिखाएं";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "बनाया गया: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "%@ में शामिल हो गया";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "पंजीकृत";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "संदेश संपादित करने के लिए दो बार टैप करें";
|
||||
|
||||
"Settings.wideChannelPosts" = "चैनल में चौड़े पोस्ट";
|
||||
"Settings.ForceEmojiTab" = "डिफ़ॉल्ट ईमोजी कुंजीपटल";
|
||||
|
||||
"Settings.forceBuiltInMic" = "फ़ोर्स डिवाइस माइक्रोफ़ोन";
|
||||
"Settings.forceBuiltInMic.Notice" = "यदि सक्षम है, ऐप केवल उपकरण का माइक्रोफ़ोन उपयोग करेगा भले ही हेडफ़ोन कनेक्ट किए हों।";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "चैनल बॉटम पैनल छिपाएँ";
|
||||
|
||||
"Settings.CallConfirmation" = "कॉल पुष्टि";
|
||||
"Settings.CallConfirmation.Notice" = "Swiftgram कॉल करने से पहले आपकी पुष्टि मांगेगा।";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "कॉल करें?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "वीडियो कॉल करें?";
|
||||
|
||||
"MutualContact.Label" = "आपसी संपर्क";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "वीडियो PIP स्वाइप के साथ";
|
||||
"Settings.swipeForVideoPIP.Notice" = "यदि सक्षम है, तो वीडियो को स्वाइप करने से यह चित्र-इन-चित्र मोड में खोला जाएगा।";
|
||||
152
Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings
Normal file
152
Swiftgram/SGStrings/Strings/hu.lproj/SGLocalizable.strings
Normal file
@@ -0,0 +1,152 @@
|
||||
"Settings.ContentSettings" = "Tartalombeállítások";
|
||||
|
||||
"Settings.Tabs.Header" = "FÜLEK";
|
||||
"Settings.Tabs.HideTabBar" = "Feliratcsík elrejtése";
|
||||
"Settings.Tabs.ShowContacts" = "Kapcsolatok fül megjelenítése";
|
||||
"Settings.Tabs.ShowNames" = "Feliratcsík nevek megjelenítése";
|
||||
|
||||
"Settings.Folders.BottomTab" = "Könyvtárak az alján";
|
||||
"Settings.Folders.BottomTabStyle" = "Alsó könyvtár stílus";
|
||||
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.ios" = "iOS";
|
||||
/* Do not translate */
|
||||
"Settings.Folders.BottomTabStyle.telegram" = "Telegram";
|
||||
/* Example: Hide "All Chats" */
|
||||
"Settings.Folders.AllChatsHidden" = "Elrejtése \"%@\"";
|
||||
"Settings.Folders.RememberLast" = "Utolsó mappa megnyitása";
|
||||
"Settings.Folders.RememberLast.Notice" = "A Swiftgram az utoljára használt mappát fogja megnyitni, amikor újraindítja az alkalmazást vagy fiókok között vált.";
|
||||
|
||||
"Settings.Folders.CompactNames" = "Kisebb térköz";
|
||||
"Settings.Folders.AllChatsTitle" = "\"Minden Beszélgetés\" cím";
|
||||
"Settings.Folders.AllChatsTitle.short" = "Rövid";
|
||||
"Settings.Folders.AllChatsTitle.long" = "Hosszú";
|
||||
/* Default behaviour for All Chats Folder Title. "All Chats" title: Default */
|
||||
"Settings.Folders.AllChatsTitle.none" = "Alapértelmezett";
|
||||
|
||||
|
||||
"Settings.ChatList.Header" = "BESZÉLGETÉS LISTA";
|
||||
"Settings.CompactChatList" = "Kompakt Beszélgetés Lista";
|
||||
|
||||
"Settings.Profiles.Header" = "PROFIL";
|
||||
|
||||
"Settings.Stories.Hide" = "Történetek elrejtése";
|
||||
"Settings.Stories.WarnBeforeView" = "Kérdezzen megtekintés előtt";
|
||||
"Settings.Stories.DisableSwipeToRecord" = "Húzás letiltása felvételhez";
|
||||
|
||||
"Settings.Translation.QuickTranslateButton" = "Gyors Fordítás gomb";
|
||||
|
||||
"Stories.Warning.Author" = "Szerző";
|
||||
"Stories.Warning.ViewStory" = "Történet megtekintése?";
|
||||
/* Author will be able to see that you viewed their Story */
|
||||
"Stories.Warning.Notice" = "%@ LÁTNI FOGJA, hogy megtekintetted a történetüket.";
|
||||
"Stories.Warning.NoticeStealth" = "%@ nem fogja látni, hogy megtekintetted a történetüket.";
|
||||
|
||||
"Settings.Photo.Quality.Notice" = "Feltöltött fényképek és történetek minősége.";
|
||||
"Settings.Photo.SendLarge" = "Nagy fényképek küldése";
|
||||
"Settings.Photo.SendLarge.Notice" = "Növelje a tömörített képek oldalméretének határát 2560px-re.";
|
||||
|
||||
"Settings.VideoNotes.Header" = "KEREK VIDEÓK";
|
||||
"Settings.VideoNotes.StartWithRearCam" = "Kezdje a hátsó kamerával";
|
||||
|
||||
"Settings.CustomColors.Header" = "FIÓK SZÍNEI";
|
||||
"Settings.CustomColors.Saturation" = "TELÍTETTSÉG";
|
||||
/* Make sure to escape Percentage sign % */
|
||||
"Settings.CustomColors.Saturation.Notice" = "Színértéket 0%%-ra állítva az fiókszíneket letiltja.";
|
||||
|
||||
"Settings.UploadsBoost" = "Feltöltés fokozása";
|
||||
"Settings.DownloadsBoost" = "Letöltés fokozása";
|
||||
"Settings.DownloadsBoost.Notice" = "Növeli a párhuzamos kapcsolatok számát és a fájlok darabjainak méretét. Ha a hálózatod nem képes kezelni a terhelést, próbálj ki különböző opciókat, amelyek illeszkednek a kapcsolatodhoz.";
|
||||
"Settings.DownloadsBoost.none" = "Kikapcsolva";
|
||||
"Settings.DownloadsBoost.medium" = "Közepes";
|
||||
"Settings.DownloadsBoost.maximum" = "Maximális";
|
||||
|
||||
"Settings.ShowProfileID" = "Profil azonosító megjelenítése";
|
||||
"Settings.ShowDC" = "Adatközpont megjelenítése";
|
||||
"Settings.ShowCreationDate" = "Beszélgetés létrehozásának dátumának megjelenítése";
|
||||
"Settings.ShowCreationDate.Notice" = "A beszélgetés létrehozásának dátuma ismeretlen lehet néhány csevegésnél.";
|
||||
|
||||
"Settings.ShowRegDate" = "Regisztrációs Dátum Megjelenítése";
|
||||
"Settings.ShowRegDate.Notice" = "A regisztrációs dátum csak hozzávetőleges.";
|
||||
|
||||
"Settings.SendWithReturnKey" = "Küldés 'vissza' gombbal";
|
||||
"Settings.HidePhoneInSettingsUI" = "Telefonszám elrejtése a beállításokban";
|
||||
"Settings.HidePhoneInSettingsUI.Notice" = "Ezzel csak a telefonszámát rejti el a beállítások felületen. Ha mások számára is el akarja rejteni, menjen a Adatvédelem és biztonság menübe.";
|
||||
|
||||
"PasscodeSettings.AutoLock.InFiveSeconds" = "Ha 5 másodpercig távol van";
|
||||
|
||||
"ProxySettings.UseSystemDNS" = "Rendszer DNS használata";
|
||||
"ProxySettings.UseSystemDNS.Notice" = "Használja a rendszer DNS-t, ha nem fér hozzá a Google DNS-hez";
|
||||
|
||||
/* Preserve markdown asterisks! Example: You **don't** need Telegram Premium! */
|
||||
"Common.NoTelegramPremiumNeeded" = "Nem **szükséges** %@!";
|
||||
"Common.RestartRequired" = "Újraindítás szükséges";
|
||||
"Common.RestartNow" = "Újraindítás most";
|
||||
"Common.OpenTelegram" = "Telegram megnyitása";
|
||||
"Common.UseTelegramForPremium" = "Kérjük vegye figyelembe, hogy a Telegram Prémiumhoz az hivatalos Telegram appot kell használnia. Amint megkapta a Telegram Prémiumot, Swiftgram összes funkciója elérhető lesz.";
|
||||
|
||||
"Message.HoldToShowOrReport" = "Tartsa lenyomva a Megjelenítéshez vagy Jelentéshez.";
|
||||
|
||||
"Auth.AccountBackupReminder" = "Győződjön meg róla, hogy van biztonsági másolat hozzáférési módszere. Tartsa meg a SMS-hez használt SIM-et vagy egy másik bejelentkezett munkamenetet, hogy elkerülje a kizárást.";
|
||||
"Auth.UnofficialAppCodeTitle" = "A kódot csak a hivatalos alkalmazással szerezheti meg";
|
||||
|
||||
"Settings.SmallReactions" = "Kis reakciók";
|
||||
"Settings.HideReactions" = "Reakciók Elrejtése";
|
||||
|
||||
"ContextMenu.SaveToCloud" = "Mentés a Felhőbe";
|
||||
"ContextMenu.SelectFromUser" = "Kiválasztás a Szerzőtől";
|
||||
|
||||
"Settings.ContextMenu" = "KONTEXTUS MENÜ";
|
||||
"Settings.ContextMenu.Notice" = "A kikapcsolt bejegyzések elérhetők lesznek a 'Swiftgram' almenüjében.";
|
||||
|
||||
|
||||
"Settings.ChatSwipeOptions" = "Csevegőlista húzás opciók";
|
||||
"Settings.DeleteChatSwipeOption" = "Húzza át az üzenet törléséhez";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextChannelSameLocationSwipeProgress */
|
||||
"Settings.PullToNextChannel" = "Húzza a következő olvasatlan csatornához";
|
||||
/* Re-word like this string on offical app https://translations.telegram.org/en/ios/groups_and_channels/Chat.NextUnreadTopicSwipeProgress */
|
||||
"Settings.PullToNextTopic" = "Húzza le a következő témához";
|
||||
"Settings.GalleryCamera" = "Kamera a Galériában";
|
||||
/* "Send Message As..." button */
|
||||
"Settings.SendAsButton" = "\"%@\" Gomb";
|
||||
"Settings.SnapDeletionEffect" = "Üzenet törlés hatások";
|
||||
|
||||
"Settings.Stickers.Size" = "MÉRET";
|
||||
"Settings.Stickers.Timestamp" = "Időbélyeg Megjelenítése";
|
||||
|
||||
"Settings.RecordingButton" = "Hangrögzítés Gomb";
|
||||
|
||||
"Settings.DefaultEmojisFirst" = "Prioritize standard emojis";
|
||||
"Settings.DefaultEmojisFirst.Notice" = "Mutassa az alap emojisokat az emoji billentyűzet előtt a prémiumok helyett";
|
||||
|
||||
/* Date when chat was created. "created: 24 May 2016" */
|
||||
"Chat.Created" = "létrehozva: %@";
|
||||
|
||||
/* Date when user joined the chat. "Joined Swiftgram Chat" */
|
||||
"Chat.JoinedDateTitle" = "Csatlakozott %@";
|
||||
/* Date when user registered in Telegram. Will be shown like "Registered\n24 May 2016" */
|
||||
"Chat.RegDate" = "Regisztrált";
|
||||
|
||||
"Settings.messageDoubleTapActionOutgoingEdit" = "Dupla koppintás a üzenet szerkesztéséhez";
|
||||
|
||||
"Settings.wideChannelPosts" = "Széles posztok csatornákban";
|
||||
"Settings.ForceEmojiTab" = "Alapértelmezett Emoji billentyűzet";
|
||||
|
||||
"Settings.forceBuiltInMic" = "Eszköz mikrofonjának kényszerítése";
|
||||
"Settings.forceBuiltInMic.Notice" = "Ha engedélyezve van, az alkalmazás csak az eszköz mikrofonját fogja használni, még akkor is, ha a fejhallgató csatlakoztatva van.";
|
||||
|
||||
"Settings.hideChannelBottomButton" = "Kanal Alsó Panel Elrejtése";
|
||||
|
||||
"Settings.CallConfirmation" = "Hívás megerősítése";
|
||||
"Settings.CallConfirmation.Notice" = "A Swiftgram megkéri a megerősítését, mielőtt hívást indítana.";
|
||||
|
||||
/* Confirmation before making a Call */
|
||||
"CallConfirmation.Audio.Title" = "Hívást kezdeni?";
|
||||
|
||||
/* Confirmation before making a Video Call */
|
||||
"CallConfirmation.Video.Title" = "Videóhívást kezdeni?";
|
||||
|
||||
"MutualContact.Label" = "közöns kontakt";
|
||||
|
||||
"Settings.swipeForVideoPIP" = "Videó PIP a húzással";
|
||||
"Settings.swipeForVideoPIP.Notice" = "Ha engedélyezve van, a videó húzása képet-képben üzemmódban nyitja meg.";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user