mirror of
https://github.com/yattee/yattee.git
synced 2025-12-15 12:38:15 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b0c9d3d0a | ||
|
|
371471ad81 | ||
|
|
d5464186af | ||
|
|
f4c310846a | ||
|
|
2413526d70 | ||
|
|
55f4a4a2a1 | ||
|
|
5b35c03bc5 | ||
|
|
93ea943c54 | ||
|
|
5ae6f321cd | ||
|
|
2be6f04e37 | ||
|
|
9826ee4d36 | ||
|
|
39a109216b | ||
|
|
05b25b65bc | ||
|
|
195db01602 | ||
|
|
292af65ea5 | ||
|
|
5ee46fe87a | ||
|
|
179b4358ae | ||
|
|
5be8a663e0 | ||
|
|
1d81f710a9 | ||
|
|
49e051c70d | ||
|
|
1efaed4541 | ||
|
|
3e96001511 | ||
|
|
6e8fb4a6db | ||
|
|
446ee0ac8e | ||
|
|
f6a89c7daf | ||
|
|
ad5dc8a871 | ||
|
|
afaeb754ca | ||
|
|
21fd92aea4 | ||
|
|
02e5749fc9 | ||
|
|
d44f80bd53 | ||
|
|
5c87916785 | ||
|
|
043443fb89 | ||
|
|
1bc44afde6 | ||
|
|
d80101d779 | ||
|
|
39925c390a |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,22 +1,13 @@
|
||||
## Build 173
|
||||
## Build 177
|
||||
* Added Settings Import/Export (iOS, macOS)
|
||||
* Export all settings, instances and accounts
|
||||
* Import selected elements from the file
|
||||
* Include unencrypted passwords in the export or provide them during the import
|
||||
* Added Controls setting "Action button labels" icon or icon and text
|
||||
* Added Advanced setting for MPV: "deinterlace"
|
||||
* Updated dependencies
|
||||
* Updated localizations
|
||||
* Fixed reported crashes
|
||||
* Other minor changes and improvements
|
||||
|
||||
## Previous builds
|
||||
* Description is collapsible with a button
|
||||
* Links in description are clickable on macOS
|
||||
* Aspect ratio is honored on resize on macOS
|
||||
* Added support for private Invidious instances
|
||||
* Collapsible chapters view, player setting "Open vertical chapters expanded"
|
||||
* Current chapter is highlighted
|
||||
* Disabled portrait upside down orientation on iPhone
|
||||
* Fixed issue with handling private Invidious instances requests
|
||||
* Fixed issue where Piped login token would not refresh
|
||||
* Fixed issue with MPV subtitles not working
|
||||
* Added Persian, Spanish, Turkish and Russian localizations
|
||||
* Fixed issue with displaying account username
|
||||
* Fixed reported crash
|
||||
* Other minor changes and improvements
|
||||
|
||||
**Big thanks to the past, current and future project contributors!**
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -1,6 +1,6 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
gem 'fastlane', git: 'https://github.com/nekrich/fastlane.git', branch: 'fix/match-tvos-devices-fetch'
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
||||
108
Gemfile.lock
108
Gemfile.lock
@@ -1,3 +1,50 @@
|
||||
GIT
|
||||
remote: https://github.com/nekrich/fastlane.git
|
||||
revision: d2d51a9af37f9b04a157e78fd25d147cecc89980
|
||||
branch: fix/match-tvos-devices-fetch
|
||||
specs:
|
||||
fastlane (2.219.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
@@ -8,17 +55,17 @@ GEM
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.877.0)
|
||||
aws-sdk-core (3.190.1)
|
||||
aws-partitions (1.886.0)
|
||||
aws-sdk-core (3.191.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.76.0)
|
||||
aws-sdk-core (~> 3, >= 3.188.0)
|
||||
aws-sdk-kms (1.77.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.142.0)
|
||||
aws-sdk-core (~> 3, >= 3.189.0)
|
||||
aws-sdk-s3 (1.143.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
@@ -65,51 +112,10 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.0)
|
||||
fastlane (2.219.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (>= 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.2)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -117,7 +123,6 @@ GEM
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
@@ -190,9 +195,8 @@ GEM
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
webrick (1.8.1)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.23.0)
|
||||
xcodeproj (1.24.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
@@ -211,7 +215,7 @@ PLATFORMS
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
fastlane!
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.6
|
||||
|
||||
@@ -64,6 +64,10 @@ final class AccountsModel: ObservableObject {
|
||||
)
|
||||
}
|
||||
|
||||
func find(_ id: Account.ID) -> Account? {
|
||||
all.first { $0.id == id }
|
||||
}
|
||||
|
||||
func configureAccount() {
|
||||
if let account = lastUsed ??
|
||||
InstancesModel.shared.lastUsed?.anonymousAccount ??
|
||||
@@ -108,8 +112,8 @@ final class AccountsModel: ObservableObject {
|
||||
Defaults[.accounts].first { $0.id == id }
|
||||
}
|
||||
|
||||
static func add(instance: Instance, name: String, username: String, password: String) -> Account {
|
||||
let account = Account(instanceID: instance.id, name: name, urlString: instance.apiURLString)
|
||||
static func add(instance: Instance, id: String? = UUID().uuidString, name: String, username: String, password: String) -> Account {
|
||||
let account = Account(id: id, instanceID: instance.id, name: name, urlString: instance.apiURLString)
|
||||
Defaults[.accounts].append(account)
|
||||
|
||||
setCredentials(account, username: username, password: password)
|
||||
|
||||
@@ -68,4 +68,8 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(apiURL)
|
||||
}
|
||||
|
||||
var accounts: [Account] {
|
||||
AccountsModel.shared.all.filter { $0.instanceID == id }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,15 +42,23 @@ final class InstancesModel: ObservableObject {
|
||||
Defaults[.accounts].filter { $0.instanceID == id }
|
||||
}
|
||||
|
||||
func add(app: VideosApp, name: String, url: String) -> Instance {
|
||||
func add(id: String? = UUID().uuidString, app: VideosApp, name: String, url: String) -> Instance {
|
||||
let instance = Instance(
|
||||
app: app, id: UUID().uuidString, name: name, apiURLString: standardizedURL(url)
|
||||
app: app, id: id, name: name, apiURLString: standardizedURL(url)
|
||||
)
|
||||
Defaults[.instances].append(instance)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
func insert(id: String? = UUID().uuidString, app: VideosApp, name: String, url: String) -> Instance {
|
||||
if let instance = Defaults[.instances].first(where: { $0.apiURL.absoluteString == standardizedURL(url) }) {
|
||||
return instance
|
||||
}
|
||||
|
||||
return add(id: id, app: app, name: name, url: url)
|
||||
}
|
||||
|
||||
func setFrontendURL(_ instance: Instance, _ url: String) {
|
||||
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||
var instance = Defaults[.instances][index]
|
||||
|
||||
@@ -154,7 +154,8 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
method: .post,
|
||||
parameters: ["username": username, "password": password],
|
||||
encoding: JSONEncoding.default
|
||||
).responseDecodable(of: JSON.self) { [weak self] response in
|
||||
)
|
||||
.responseDecodable(of: JSON.self) { [weak self] response in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ struct FavoritesModel {
|
||||
}
|
||||
|
||||
func add(_ item: FavoriteItem) {
|
||||
if contains(item) { return }
|
||||
all.append(item)
|
||||
}
|
||||
|
||||
@@ -122,4 +123,12 @@ struct FavoritesModel {
|
||||
func widgetSettings(_ item: FavoriteItem) -> WidgetSettings {
|
||||
widgetsSettings.first { $0.id == item.widgetSettingsKey } ?? WidgetSettings(id: item.widgetSettingsKey)
|
||||
}
|
||||
|
||||
func updateWidgetSettings(_ settings: WidgetSettings) {
|
||||
if let index = widgetsSettings.firstIndex(where: { $0.id == settings.id }) {
|
||||
widgetsSettings[index] = settings
|
||||
} else {
|
||||
widgetsSettings.append(settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu],
|
||||
"showMPVPlaybackStats": Defaults[.showMPVPlaybackStats],
|
||||
"mpvEnableLogging": Defaults[.mpvEnableLogging],
|
||||
"mpvCacheSecs": Defaults[.mpvCacheSecs],
|
||||
"mpvCachePauseWait": Defaults[.mpvCachePauseWait],
|
||||
"mpvDeinterlace": Defaults[.mpvDeinterlace],
|
||||
"showCacheStatus": Defaults[.showCacheStatus],
|
||||
"feedCacheSize": Defaults[.feedCacheSize]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class BrowsingSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"showHome": Defaults[.showHome],
|
||||
"showOpenActionsInHome": Defaults[.showOpenActionsInHome],
|
||||
"showQueueInHome": Defaults[.showQueueInHome],
|
||||
"showFavoritesInHome": Defaults[.showFavoritesInHome],
|
||||
"favorites": Defaults[.favorites].compactMap { jsonFromString(FavoriteItem.bridge.serialize($0)) },
|
||||
"widgetsSettings": Defaults[.widgetsSettings].compactMap { widgetSettingsJSON($0) },
|
||||
"startupSection": Defaults[.startupSection].rawValue,
|
||||
"visibleSections": Defaults[.visibleSections].compactMap { $0.rawValue },
|
||||
"showOpenActionsToolbarItem": Defaults[.showOpenActionsToolbarItem],
|
||||
"accountPickerDisplaysAnonymousAccounts": Defaults[.accountPickerDisplaysAnonymousAccounts],
|
||||
"showUnwatchedFeedBadges": Defaults[.showUnwatchedFeedBadges],
|
||||
"expandChannelDescription": Defaults[.expandChannelDescription],
|
||||
"keepChannelsWithUnwatchedFeedOnTop": Defaults[.keepChannelsWithUnwatchedFeedOnTop],
|
||||
"showChannelAvatarInChannelsLists": Defaults[.showChannelAvatarInChannelsLists],
|
||||
"showChannelAvatarInVideosListing": Defaults[.showChannelAvatarInVideosListing],
|
||||
"playerButtonSingleTapGesture": Defaults[.playerButtonSingleTapGesture].rawValue,
|
||||
"playerButtonDoubleTapGesture": Defaults[.playerButtonDoubleTapGesture].rawValue,
|
||||
"playerButtonShowsControlButtonsWhenMinimized": Defaults[.playerButtonShowsControlButtonsWhenMinimized],
|
||||
"playerButtonIsExpanded": Defaults[.playerButtonIsExpanded],
|
||||
"playerBarMaxWidth": Defaults[.playerBarMaxWidth],
|
||||
"channelOnThumbnail": Defaults[.channelOnThumbnail],
|
||||
"timeOnThumbnail": Defaults[.timeOnThumbnail],
|
||||
"roundedThumbnails": Defaults[.roundedThumbnails],
|
||||
"thumbnailsQuality": Defaults[.thumbnailsQuality].rawValue
|
||||
]
|
||||
}
|
||||
|
||||
override var platformJSON: JSON {
|
||||
var export = JSON()
|
||||
|
||||
#if os(iOS)
|
||||
export["showDocuments"].bool = Defaults[.showDocuments]
|
||||
export["lockPortraitWhenBrowsing"].bool = Defaults[.lockPortraitWhenBrowsing]
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
export["accountPickerDisplaysUsername"].bool = Defaults[.accountPickerDisplaysUsername]
|
||||
#endif
|
||||
|
||||
return export
|
||||
}
|
||||
|
||||
private func widgetSettingsJSON(_ settings: WidgetSettings) -> JSON {
|
||||
var json = JSON()
|
||||
json.dictionaryObject = WidgetSettingsBridge().serialize(settings)
|
||||
return json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class ConstrolsSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"avPlayerUsesSystemControls": Defaults[.avPlayerUsesSystemControls],
|
||||
"horizontalPlayerGestureEnabled": Defaults[.horizontalPlayerGestureEnabled],
|
||||
"seekGestureSensitivity": Defaults[.seekGestureSensitivity],
|
||||
"seekGestureSpeed": Defaults[.seekGestureSpeed],
|
||||
"playerControlsLayout": Defaults[.playerControlsLayout].rawValue,
|
||||
"fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue,
|
||||
"systemControlsCommands": Defaults[.systemControlsCommands].rawValue,
|
||||
"buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration],
|
||||
"buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration],
|
||||
"gestureBackwardSeekDuration": Defaults[.gestureBackwardSeekDuration],
|
||||
"gestureForwardSeekDuration": Defaults[.gestureForwardSeekDuration],
|
||||
"systemControlsSeekDuration": Defaults[.systemControlsSeekDuration],
|
||||
"playerControlsSettingsEnabled": Defaults[.playerControlsSettingsEnabled],
|
||||
"playerControlsCloseEnabled": Defaults[.playerControlsCloseEnabled],
|
||||
"playerControlsRestartEnabled": Defaults[.playerControlsRestartEnabled],
|
||||
"playerControlsAdvanceToNextEnabled": Defaults[.playerControlsAdvanceToNextEnabled],
|
||||
"playerControlsPlaybackModeEnabled": Defaults[.playerControlsPlaybackModeEnabled],
|
||||
"playerControlsMusicModeEnabled": Defaults[.playerControlsMusicModeEnabled],
|
||||
"playerActionsButtonLabelStyle": Defaults[.playerActionsButtonLabelStyle].rawValue,
|
||||
"actionButtonShareEnabled": Defaults[.actionButtonShareEnabled],
|
||||
"actionButtonAddToPlaylistEnabled": Defaults[.actionButtonAddToPlaylistEnabled],
|
||||
"actionButtonSubscribeEnabled": Defaults[.actionButtonSubscribeEnabled],
|
||||
"actionButtonSettingsEnabled": Defaults[.actionButtonSettingsEnabled],
|
||||
"actionButtonHideEnabled": Defaults[.actionButtonHideEnabled],
|
||||
"actionButtonCloseEnabled": Defaults[.actionButtonCloseEnabled],
|
||||
"actionButtonFullScreenEnabled": Defaults[.actionButtonFullScreenEnabled],
|
||||
"actionButtonPipEnabled": Defaults[.actionButtonPipEnabled],
|
||||
"actionButtonLockOrientationEnabled": Defaults[.actionButtonLockOrientationEnabled],
|
||||
"actionButtonRestartEnabled": Defaults[.actionButtonRestartEnabled],
|
||||
"actionButtonAdvanceToNextItemEnabled": Defaults[.actionButtonAdvanceToNextItemEnabled],
|
||||
"actionButtonMusicModeEnabled": Defaults[.actionButtonMusicModeEnabled]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class HistorySettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"saveRecents": Defaults[.saveRecents],
|
||||
"saveHistory": Defaults[.saveHistory],
|
||||
"showWatchingProgress": Defaults[.showWatchingProgress],
|
||||
"saveLastPlayed": Defaults[.saveLastPlayed],
|
||||
|
||||
"watchedVideoPlayNowBehavior": Defaults[.watchedVideoPlayNowBehavior].rawValue,
|
||||
"watchedThreshold": Defaults[.watchedThreshold],
|
||||
"resetWatchedStatusOnPlaying": Defaults[.resetWatchedStatusOnPlaying],
|
||||
|
||||
"watchedVideoStyle": Defaults[.watchedVideoStyle].rawValue,
|
||||
"watchedVideoBadgeColor": Defaults[.watchedVideoBadgeColor].rawValue,
|
||||
"showToggleWatchedStatusButton": Defaults[.showToggleWatchedStatusButton]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class LocationsSettingsGroupExporter: SettingsGroupExporter {
|
||||
var includePublicInstances = true
|
||||
var includeInstances = true
|
||||
var includeAccounts = true
|
||||
var includeAccountsUnencryptedPasswords = false
|
||||
|
||||
init(includePublicInstances: Bool = true, includeInstances: Bool = true, includeAccounts: Bool = true, includeAccountsUnencryptedPasswords: Bool = false) {
|
||||
self.includePublicInstances = includePublicInstances
|
||||
self.includeInstances = includeInstances
|
||||
self.includeAccounts = includeAccounts
|
||||
self.includeAccountsUnencryptedPasswords = includeAccountsUnencryptedPasswords
|
||||
}
|
||||
|
||||
override var globalJSON: JSON {
|
||||
var json = JSON()
|
||||
|
||||
if includePublicInstances {
|
||||
json["instancesManifest"].string = Defaults[.instancesManifest]
|
||||
json["countryOfPublicInstances"].string = Defaults[.countryOfPublicInstances] ?? ""
|
||||
}
|
||||
|
||||
if includeInstances {
|
||||
json["instances"].arrayObject = Defaults[.instances].compactMap { instanceJSON($0) }
|
||||
}
|
||||
|
||||
if includeAccounts {
|
||||
json["accounts"].arrayObject = Defaults[.accounts].compactMap { account in
|
||||
var account = account
|
||||
let (username, password) = AccountsModel.getCredentials(account)
|
||||
account.username = username ?? ""
|
||||
if includeAccountsUnencryptedPasswords {
|
||||
account.password = password ?? ""
|
||||
}
|
||||
|
||||
return accountJSON(account).dictionaryObject
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
private func instanceJSON(_ instance: Instance) -> JSON {
|
||||
var json = JSON()
|
||||
json.dictionaryObject = InstancesBridge().serialize(instance)
|
||||
return json
|
||||
}
|
||||
|
||||
private func accountJSON(_ account: Account) -> JSON {
|
||||
var json = JSON()
|
||||
json.dictionaryObject = AccountsBridge().serialize(account)
|
||||
return json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class OtherDataSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"lastAccountID": Defaults[.lastAccountID] ?? "",
|
||||
"lastInstanceID": Defaults[.lastInstanceID] ?? "",
|
||||
|
||||
"playerRate": Defaults[.playerRate],
|
||||
|
||||
"trendingCategory": Defaults[.trendingCategory].rawValue,
|
||||
"trendingCountry": Defaults[.trendingCountry].rawValue,
|
||||
|
||||
"subscriptionsViewPage": Defaults[.subscriptionsViewPage].rawValue,
|
||||
"subscriptionsListingStyle": Defaults[.subscriptionsListingStyle].rawValue,
|
||||
"popularListingStyle": Defaults[.popularListingStyle].rawValue,
|
||||
"trendingListingStyle": Defaults[.trendingListingStyle].rawValue,
|
||||
"playlistListingStyle": Defaults[.playlistListingStyle].rawValue,
|
||||
"channelPlaylistListingStyle": Defaults[.channelPlaylistListingStyle].rawValue,
|
||||
"searchListingStyle": Defaults[.searchListingStyle].rawValue,
|
||||
|
||||
"hideShorts": Defaults[.hideShorts],
|
||||
"hideWatched": Defaults[.hideWatched]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class PlayerSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"playerInstanceID": Defaults[.playerInstanceID] ?? "",
|
||||
"pauseOnHidingPlayer": Defaults[.pauseOnHidingPlayer],
|
||||
"closeVideoOnEOF": Defaults[.closeVideoOnEOF],
|
||||
"expandVideoDescription": Defaults[.expandVideoDescription],
|
||||
"collapsedLinesDescription": Defaults[.collapsedLinesDescription],
|
||||
"showChapters": Defaults[.showChapters],
|
||||
"expandChapters": Defaults[.expandChapters],
|
||||
"showRelated": Defaults[.showRelated],
|
||||
"showInspector": Defaults[.showInspector].rawValue,
|
||||
"playerSidebar": Defaults[.playerSidebar].rawValue,
|
||||
"showKeywords": Defaults[.showKeywords],
|
||||
"enableReturnYouTubeDislike": Defaults[.enableReturnYouTubeDislike],
|
||||
"closePiPOnNavigation": Defaults[.closePiPOnNavigation],
|
||||
"closePiPOnOpeningPlayer": Defaults[.closePiPOnOpeningPlayer],
|
||||
"closePlayerOnOpeningPiP": Defaults[.closePlayerOnOpeningPiP]
|
||||
]
|
||||
}
|
||||
|
||||
override var platformJSON: JSON {
|
||||
var export = JSON()
|
||||
|
||||
#if !os(macOS)
|
||||
export["pauseOnEnteringBackground"].bool = Defaults[.pauseOnEnteringBackground]
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
export["showScrollToTopInComments"].bool = Defaults[.showScrollToTopInComments]
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock]
|
||||
export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape]
|
||||
export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue
|
||||
#endif
|
||||
|
||||
return export
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class QualitySettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"batteryCellularProfile": Defaults[.batteryCellularProfile],
|
||||
"batteryNonCellularProfile": Defaults[.batteryNonCellularProfile],
|
||||
"chargingCellularProfile": Defaults[.chargingCellularProfile],
|
||||
"chargingNonCellularProfile": Defaults[.chargingNonCellularProfile],
|
||||
"forceAVPlayerForLiveStreams": Defaults[.forceAVPlayerForLiveStreams],
|
||||
"qualityProfiles": Defaults[.qualityProfiles].compactMap { qualityProfileJSON($0) }
|
||||
]
|
||||
}
|
||||
|
||||
func qualityProfileJSON(_ profile: QualityProfile) -> JSON {
|
||||
var json = JSON()
|
||||
json.dictionaryObject = QualityProfileBridge().serialize(profile)
|
||||
return json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class RecentlyOpenedExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"recentlyOpened": Defaults[.recentlyOpened].compactMap { recentItemJSON($0) }
|
||||
]
|
||||
}
|
||||
|
||||
private func recentItemJSON(_ recentItem: RecentItem) -> JSON {
|
||||
var json = JSON()
|
||||
json.dictionaryObject = RecentItemBridge().serialize(recentItem)
|
||||
return json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
class SettingsGroupExporter { // swiftlint:disable:this final_class
|
||||
var globalJSON: JSON {
|
||||
[]
|
||||
}
|
||||
|
||||
var platformJSON: JSON {
|
||||
[]
|
||||
}
|
||||
|
||||
var exportJSON: JSON {
|
||||
var json = globalJSON
|
||||
|
||||
if !platformJSON.isEmpty {
|
||||
try? json.merge(with: platformJSON)
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
func jsonFromString(_ string: String?) -> JSON? {
|
||||
if let data = string?.data(using: .utf8, allowLossyConversion: false),
|
||||
let json = try? JSON(data: data)
|
||||
{
|
||||
return json
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
final class SponsorBlockSettingsGroupExporter: SettingsGroupExporter {
|
||||
override var globalJSON: JSON {
|
||||
[
|
||||
"sponsorBlockInstance": Defaults[.sponsorBlockInstance],
|
||||
"sponsorBlockCategories": Array(Defaults[.sponsorBlockCategories])
|
||||
]
|
||||
}
|
||||
}
|
||||
193
Model/Import Export Settings/ImportExportSettingsModel.swift
Normal file
193
Model/Import Export Settings/ImportExportSettingsModel.swift
Normal file
@@ -0,0 +1,193 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
|
||||
final class ImportExportSettingsModel: ObservableObject {
|
||||
static let shared = ImportExportSettingsModel()
|
||||
|
||||
static var exportFile: URL {
|
||||
YatteeApp.settingsExportDirectory
|
||||
.appendingPathComponent("Yattee Settings from \(Constants.deviceName).\(settingsExtension)")
|
||||
}
|
||||
|
||||
static var settingsExtension: String {
|
||||
"yatteesettings"
|
||||
}
|
||||
|
||||
enum ExportGroup: String, Identifiable, CaseIterable {
|
||||
case browsingSettings
|
||||
case playerSettings
|
||||
case controlsSettings
|
||||
case qualitySettings
|
||||
case historySettings
|
||||
case sponsorBlockSettings
|
||||
case advancedSettings
|
||||
|
||||
case locationsSettings
|
||||
case instances
|
||||
case accounts
|
||||
case accountsUnencryptedPasswords
|
||||
|
||||
case recentlyOpened
|
||||
case otherData
|
||||
|
||||
static var settingsGroups: [Self] {
|
||||
[.browsingSettings, .playerSettings, .controlsSettings, .qualitySettings, .historySettings, .sponsorBlockSettings, .advancedSettings]
|
||||
}
|
||||
|
||||
static var locationsGroups: [Self] {
|
||||
[.locationsSettings, .instances, .accounts, .accountsUnencryptedPasswords]
|
||||
}
|
||||
|
||||
static var otherGroups: [Self] {
|
||||
[.recentlyOpened, .otherData]
|
||||
}
|
||||
|
||||
var id: RawValue {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .browsingSettings:
|
||||
return "Browsing"
|
||||
case .playerSettings:
|
||||
return "Player"
|
||||
case .controlsSettings:
|
||||
return "Controls"
|
||||
case .qualitySettings:
|
||||
return "Quality"
|
||||
case .historySettings:
|
||||
return "History"
|
||||
case .sponsorBlockSettings:
|
||||
return "SponsorBlock"
|
||||
case .locationsSettings:
|
||||
return "Public Locations"
|
||||
case .instances:
|
||||
return "Custom Locations"
|
||||
case .accounts:
|
||||
return "Accounts"
|
||||
case .accountsUnencryptedPasswords:
|
||||
return "Accounts passwords (unencrypted)"
|
||||
case .advancedSettings:
|
||||
return "Advanced"
|
||||
case .recentlyOpened:
|
||||
return "Recents"
|
||||
case .otherData:
|
||||
return "Other data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var selectedExportGroups = Set<ExportGroup>()
|
||||
static var defaultExportGroups = Set<ExportGroup>([
|
||||
.browsingSettings,
|
||||
.playerSettings,
|
||||
.controlsSettings,
|
||||
.qualitySettings,
|
||||
.historySettings,
|
||||
.sponsorBlockSettings,
|
||||
.locationsSettings,
|
||||
.instances,
|
||||
.accounts,
|
||||
.advancedSettings
|
||||
])
|
||||
|
||||
@Published var isExportInProgress = false
|
||||
|
||||
private var navigation = NavigationModel.shared
|
||||
private var settings = SettingsModel.shared
|
||||
|
||||
func toggleExportGroupSelection(_ group: ExportGroup) {
|
||||
if isGroupSelected(group) {
|
||||
selectedExportGroups.remove(group)
|
||||
} else {
|
||||
selectedExportGroups.insert(group)
|
||||
}
|
||||
|
||||
removeNotEnabledSelectedGroups()
|
||||
}
|
||||
|
||||
func reset() {
|
||||
isExportInProgress = false
|
||||
selectedExportGroups = Self.defaultExportGroups
|
||||
}
|
||||
|
||||
func reset(_ model: ImportSettingsFileModel? = nil) {
|
||||
reset()
|
||||
|
||||
guard let model else { return }
|
||||
|
||||
selectedExportGroups = selectedExportGroups.filter { model.isGroupIncludedInFile($0) }
|
||||
}
|
||||
|
||||
func exportAction() {
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
var writingOptions: JSONSerialization.WritingOptions = []
|
||||
#if DEBUG
|
||||
writingOptions.insert(.prettyPrinted)
|
||||
writingOptions.insert(.sortedKeys)
|
||||
#endif
|
||||
try? self?.jsonForExport?.rawString(options: writingOptions)?.write(to: Self.exportFile, atomically: true, encoding: String.Encoding.utf8)
|
||||
#if os(macOS)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.isExportInProgress = false
|
||||
}
|
||||
NSWorkspace.shared.selectFile(Self.exportFile.path, inFileViewerRootedAtPath: YatteeApp.settingsExportDirectory.path)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private var jsonForExport: JSON? {
|
||||
[
|
||||
"metadata": metadataJSON,
|
||||
"browsingSettings": selectedExportGroups.contains(.browsingSettings) ? BrowsingSettingsGroupExporter().exportJSON : JSON(),
|
||||
"playerSettings": selectedExportGroups.contains(.playerSettings) ? PlayerSettingsGroupExporter().exportJSON : JSON(),
|
||||
"controlsSettings": selectedExportGroups.contains(.controlsSettings) ? ConstrolsSettingsGroupExporter().exportJSON : JSON(),
|
||||
"qualitySettings": selectedExportGroups.contains(.qualitySettings) ? QualitySettingsGroupExporter().exportJSON : JSON(),
|
||||
"historySettings": selectedExportGroups.contains(.historySettings) ? HistorySettingsGroupExporter().exportJSON : JSON(),
|
||||
"sponsorBlockSettings": selectedExportGroups.contains(.sponsorBlockSettings) ? SponsorBlockSettingsGroupExporter().exportJSON : JSON(),
|
||||
"locationsSettings": LocationsSettingsGroupExporter(
|
||||
includePublicInstances: isGroupSelected(.locationsSettings),
|
||||
includeInstances: isGroupSelected(.instances),
|
||||
includeAccounts: isGroupSelected(.accounts),
|
||||
includeAccountsUnencryptedPasswords: isGroupSelected(.accountsUnencryptedPasswords)
|
||||
).exportJSON,
|
||||
"advancedSettings": selectedExportGroups.contains(.advancedSettings) ? AdvancedSettingsGroupExporter().exportJSON : JSON(),
|
||||
"recentlyOpened": selectedExportGroups.contains(.recentlyOpened) ? RecentlyOpenedExporter().exportJSON : JSON(),
|
||||
"otherData": selectedExportGroups.contains(.otherData) ? OtherDataSettingsGroupExporter().exportJSON : JSON()
|
||||
]
|
||||
}
|
||||
|
||||
private var metadataJSON: JSON {
|
||||
[
|
||||
"build": YatteeApp.build,
|
||||
"timestamp": "\(Date().timeIntervalSince1970)",
|
||||
"platform": Constants.platform
|
||||
]
|
||||
}
|
||||
|
||||
func isGroupSelected(_ group: ExportGroup) -> Bool {
|
||||
selectedExportGroups.contains(group)
|
||||
}
|
||||
|
||||
func isGroupEnabled(_ group: ExportGroup) -> Bool {
|
||||
switch group {
|
||||
case .accounts:
|
||||
return selectedExportGroups.contains(.instances)
|
||||
case .accountsUnencryptedPasswords:
|
||||
return selectedExportGroups.contains(.instances) && selectedExportGroups.contains(.accounts)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func removeNotEnabledSelectedGroups() {
|
||||
selectedExportGroups = selectedExportGroups.filter { isGroupEnabled($0) }
|
||||
}
|
||||
|
||||
var isExportAvailable: Bool {
|
||||
!selectedExportGroups.isEmpty && !isExportInProgress
|
||||
}
|
||||
}
|
||||
135
Model/Import Export Settings/ImportSettingsFileModel.swift
Normal file
135
Model/Import Export Settings/ImportSettingsFileModel.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
struct ImportSettingsFileModel {
|
||||
let url: URL
|
||||
|
||||
var filename: String {
|
||||
String(url.lastPathComponent.dropLast(ImportExportSettingsModel.settingsExtension.count + 1))
|
||||
}
|
||||
|
||||
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
|
||||
if let locationsSettings = json?.dictionaryValue["locationsSettings"] {
|
||||
return LocationsSettingsGroupImporter(
|
||||
json: locationsSettings,
|
||||
includePublicLocations: importExportModel.isGroupEnabled(.locationsSettings),
|
||||
includedInstancesIDs: sheetViewModel.selectedInstances,
|
||||
includedAccountsIDs: sheetViewModel.selectedAccounts,
|
||||
includedAccountsPasswords: sheetViewModel.importableAccountsPasswords
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var importExportModel = ImportExportSettingsModel.shared
|
||||
var sheetViewModel = ImportSettingsSheetViewModel.shared
|
||||
|
||||
func isGroupIncludedInFile(_ group: ImportExportSettingsModel.ExportGroup) -> Bool {
|
||||
switch group {
|
||||
case .locationsSettings:
|
||||
return isPublicInstancesSettingsGroupInFile || instancesOrAccountsInFile
|
||||
default:
|
||||
return !groupJSON(group).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
var isPublicInstancesSettingsGroupInFile: Bool {
|
||||
guard let dict = groupJSON(.locationsSettings).dictionary else { return false }
|
||||
|
||||
return dict.keys.contains("instancesManifest") || dict.keys.contains("countryOfPublicInstances")
|
||||
}
|
||||
|
||||
var instancesOrAccountsInFile: Bool {
|
||||
guard let dict = groupJSON(.locationsSettings).dictionary else { return false }
|
||||
|
||||
return (dict.keys.contains("instances") && !(dict["instances"]?.arrayValue.isEmpty ?? true)) ||
|
||||
(dict.keys.contains("accounts") && !(dict["accounts"]?.arrayValue.isEmpty ?? true))
|
||||
}
|
||||
|
||||
func groupJSON(_ group: ImportExportSettingsModel.ExportGroup) -> JSON {
|
||||
json?.dictionaryValue[group.rawValue] ?? .init()
|
||||
}
|
||||
|
||||
func performImport() {
|
||||
if importExportModel.isGroupSelected(.browsingSettings), isGroupIncludedInFile(.browsingSettings) {
|
||||
BrowsingSettingsGroupImporter(json: groupJSON(.browsingSettings)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.playerSettings), isGroupIncludedInFile(.playerSettings) {
|
||||
PlayerSettingsGroupImporter(json: groupJSON(.playerSettings)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.controlsSettings), isGroupIncludedInFile(.controlsSettings) {
|
||||
ConstrolsSettingsGroupImporter(json: groupJSON(.controlsSettings)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.qualitySettings), isGroupIncludedInFile(.qualitySettings) {
|
||||
QualitySettingsGroupImporter(json: groupJSON(.qualitySettings)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.historySettings), isGroupIncludedInFile(.historySettings) {
|
||||
HistorySettingsGroupImporter(json: groupJSON(.historySettings)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.sponsorBlockSettings), isGroupIncludedInFile(.sponsorBlockSettings) {
|
||||
SponsorBlockSettingsGroupImporter(json: groupJSON(.sponsorBlockSettings)).performImport()
|
||||
}
|
||||
|
||||
locationsSettingsGroupImporter?.performImport()
|
||||
|
||||
if importExportModel.isGroupSelected(.advancedSettings), isGroupIncludedInFile(.advancedSettings) {
|
||||
AdvancedSettingsGroupImporter(json: groupJSON(.advancedSettings)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.recentlyOpened), isGroupIncludedInFile(.recentlyOpened) {
|
||||
RecentlyOpenedImporter(json: groupJSON(.recentlyOpened)).performImport()
|
||||
}
|
||||
|
||||
if importExportModel.isGroupSelected(.otherData), isGroupIncludedInFile(.otherData) {
|
||||
OtherDataSettingsGroupImporter(json: groupJSON(.otherData)).performImport()
|
||||
}
|
||||
}
|
||||
|
||||
var json: JSON? {
|
||||
if let fileContents = try? Data(contentsOf: url),
|
||||
let json = try? JSON(data: fileContents)
|
||||
{
|
||||
return json
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var metadataBuild: String? {
|
||||
if let build = json?.dictionaryValue["metadata"]?.dictionaryValue["build"]?.string {
|
||||
return build
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var metadataPlatform: String? {
|
||||
if let platform = json?.dictionaryValue["metadata"]?.dictionaryValue["platform"]?.string {
|
||||
return platform
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var metadataDate: String? {
|
||||
if let timestamp = json?.dictionaryValue["metadata"]?.dictionaryValue["timestamp"]?.doubleValue {
|
||||
let date = Date(timeIntervalSince1970: timestamp)
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var dateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .long
|
||||
formatter.timeStyle = .medium
|
||||
|
||||
return formatter
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct AdvancedSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let showPlayNowInBackendContextMenu = json["showPlayNowInBackendContextMenu"].bool {
|
||||
Defaults[.showPlayNowInBackendContextMenu] = showPlayNowInBackendContextMenu
|
||||
}
|
||||
|
||||
if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool {
|
||||
Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats
|
||||
}
|
||||
|
||||
if let mpvEnableLogging = json["mpvEnableLogging"].bool {
|
||||
Defaults[.mpvEnableLogging] = mpvEnableLogging
|
||||
}
|
||||
|
||||
if let mpvCacheSecs = json["mpvCacheSecs"].string {
|
||||
Defaults[.mpvCacheSecs] = mpvCacheSecs
|
||||
}
|
||||
|
||||
if let mpvCachePauseWait = json["mpvCachePauseWait"].string {
|
||||
Defaults[.mpvCachePauseWait] = mpvCachePauseWait
|
||||
}
|
||||
|
||||
if let mpvDeinterlace = json["mpvDeinterlace"].bool {
|
||||
Defaults[.mpvDeinterlace] = mpvDeinterlace
|
||||
}
|
||||
|
||||
if let showCacheStatus = json["showCacheStatus"].bool {
|
||||
Defaults[.showCacheStatus] = showCacheStatus
|
||||
}
|
||||
|
||||
if let feedCacheSize = json["feedCacheSize"].string {
|
||||
Defaults[.feedCacheSize] = feedCacheSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct BrowsingSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let showHome = json["showHome"].bool {
|
||||
Defaults[.showHome] = showHome
|
||||
}
|
||||
|
||||
if let showOpenActionsInHome = json["showOpenActionsInHome"].bool {
|
||||
Defaults[.showOpenActionsInHome] = showOpenActionsInHome
|
||||
}
|
||||
|
||||
if let showQueueInHome = json["showQueueInHome"].bool {
|
||||
Defaults[.showQueueInHome] = showQueueInHome
|
||||
}
|
||||
|
||||
if let showFavoritesInHome = json["showFavoritesInHome"].bool {
|
||||
Defaults[.showFavoritesInHome] = showFavoritesInHome
|
||||
}
|
||||
|
||||
if let favorites = json["favorites"].array {
|
||||
favorites.forEach { favoriteJSON in
|
||||
if let jsonString = favoriteJSON.rawString(options: []),
|
||||
let item = FavoriteItem.bridge.deserialize(jsonString)
|
||||
{
|
||||
FavoritesModel.shared.add(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let widgetsFavorites = json["widgetsSettings"].array {
|
||||
widgetsFavorites.forEach { widgetJSON in
|
||||
let dict = widgetJSON.dictionaryValue.mapValues { json in json.stringValue }
|
||||
if let item = WidgetSettingsBridge().deserialize(dict) {
|
||||
FavoritesModel.shared.updateWidgetSettings(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let startupSectionString = json["startupSection"].string,
|
||||
let startupSection = StartupSection(rawValue: startupSectionString)
|
||||
{
|
||||
Defaults[.startupSection] = startupSection
|
||||
}
|
||||
|
||||
if let visibleSections = json["visibleSections"].array {
|
||||
let sections = visibleSections.compactMap { visibleSectionJSON in
|
||||
if let visibleSectionString = visibleSectionJSON.rawString(options: []),
|
||||
let section = VisibleSection(rawValue: visibleSectionString)
|
||||
{
|
||||
return section
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Defaults[.visibleSections] = Set(sections)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if let showOpenActionsToolbarItem = json["showOpenActionsToolbarItem"].bool {
|
||||
Defaults[.showOpenActionsToolbarItem] = showOpenActionsToolbarItem
|
||||
}
|
||||
|
||||
if let lockPortraitWhenBrowsing = json["lockPortraitWhenBrowsing"].bool {
|
||||
Defaults[.lockPortraitWhenBrowsing] = lockPortraitWhenBrowsing
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
if let accountPickerDisplaysUsername = json["accountPickerDisplaysUsername"].bool {
|
||||
Defaults[.accountPickerDisplaysUsername] = accountPickerDisplaysUsername
|
||||
}
|
||||
#endif
|
||||
|
||||
if let accountPickerDisplaysAnonymousAccounts = json["accountPickerDisplaysAnonymousAccounts"].bool {
|
||||
Defaults[.accountPickerDisplaysAnonymousAccounts] = accountPickerDisplaysAnonymousAccounts
|
||||
}
|
||||
|
||||
if let showUnwatchedFeedBadges = json["showUnwatchedFeedBadges"].bool {
|
||||
Defaults[.showUnwatchedFeedBadges] = showUnwatchedFeedBadges
|
||||
}
|
||||
|
||||
if let expandChannelDescription = json["expandChannelDescription"].bool {
|
||||
Defaults[.expandChannelDescription] = expandChannelDescription
|
||||
}
|
||||
|
||||
if let keepChannelsWithUnwatchedFeedOnTop = json["keepChannelsWithUnwatchedFeedOnTop"].bool {
|
||||
Defaults[.keepChannelsWithUnwatchedFeedOnTop] = keepChannelsWithUnwatchedFeedOnTop
|
||||
}
|
||||
|
||||
if let showChannelAvatarInChannelsLists = json["showChannelAvatarInChannelsLists"].bool {
|
||||
Defaults[.showChannelAvatarInChannelsLists] = showChannelAvatarInChannelsLists
|
||||
}
|
||||
|
||||
if let showChannelAvatarInVideosListing = json["showChannelAvatarInVideosListing"].bool {
|
||||
Defaults[.showChannelAvatarInVideosListing] = showChannelAvatarInVideosListing
|
||||
}
|
||||
|
||||
if let playerButtonSingleTapGestureString = json["playerButtonSingleTapGesture"].string,
|
||||
let playerButtonSingleTapGesture = PlayerTapGestureAction(rawValue: playerButtonSingleTapGestureString)
|
||||
{
|
||||
Defaults[.playerButtonSingleTapGesture] = playerButtonSingleTapGesture
|
||||
}
|
||||
|
||||
if let playerButtonDoubleTapGestureString = json["playerButtonDoubleTapGesture"].string,
|
||||
let playerButtonDoubleTapGesture = PlayerTapGestureAction(rawValue: playerButtonDoubleTapGestureString)
|
||||
{
|
||||
Defaults[.playerButtonDoubleTapGesture] = playerButtonDoubleTapGesture
|
||||
}
|
||||
|
||||
if let playerButtonShowsControlButtonsWhenMinimized = json["playerButtonShowsControlButtonsWhenMinimized"].bool {
|
||||
Defaults[.playerButtonShowsControlButtonsWhenMinimized] = playerButtonShowsControlButtonsWhenMinimized
|
||||
}
|
||||
|
||||
if let playerButtonIsExpanded = json["playerButtonIsExpanded"].bool {
|
||||
Defaults[.playerButtonIsExpanded] = playerButtonIsExpanded
|
||||
}
|
||||
|
||||
if let playerBarMaxWidth = json["playerBarMaxWidth"].string {
|
||||
Defaults[.playerBarMaxWidth] = playerBarMaxWidth
|
||||
}
|
||||
|
||||
if let channelOnThumbnail = json["channelOnThumbnail"].bool {
|
||||
Defaults[.channelOnThumbnail] = channelOnThumbnail
|
||||
}
|
||||
|
||||
if let timeOnThumbnail = json["timeOnThumbnail"].bool {
|
||||
Defaults[.timeOnThumbnail] = timeOnThumbnail
|
||||
}
|
||||
|
||||
if let roundedThumbnails = json["roundedThumbnails"].bool {
|
||||
Defaults[.roundedThumbnails] = roundedThumbnails
|
||||
}
|
||||
|
||||
if let thumbnailsQualityString = json["thumbnailsQuality"].string,
|
||||
let thumbnailsQuality = ThumbnailsQuality(rawValue: thumbnailsQualityString)
|
||||
{
|
||||
Defaults[.thumbnailsQuality] = thumbnailsQuality
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct ConstrolsSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let avPlayerUsesSystemControls = json["avPlayerUsesSystemControls"].bool {
|
||||
Defaults[.avPlayerUsesSystemControls] = avPlayerUsesSystemControls
|
||||
}
|
||||
|
||||
if let horizontalPlayerGestureEnabled = json["horizontalPlayerGestureEnabled"].bool {
|
||||
Defaults[.horizontalPlayerGestureEnabled] = horizontalPlayerGestureEnabled
|
||||
}
|
||||
|
||||
if let seekGestureSensitivity = json["seekGestureSensitivity"].double {
|
||||
Defaults[.seekGestureSensitivity] = seekGestureSensitivity
|
||||
}
|
||||
|
||||
if let seekGestureSpeed = json["seekGestureSpeed"].double {
|
||||
Defaults[.seekGestureSpeed] = seekGestureSpeed
|
||||
}
|
||||
|
||||
if let playerControlsLayoutString = json["playerControlsLayout"].string,
|
||||
let playerControlsLayout = PlayerControlsLayout(rawValue: playerControlsLayoutString)
|
||||
{
|
||||
Defaults[.playerControlsLayout] = playerControlsLayout
|
||||
}
|
||||
|
||||
if let fullScreenPlayerControlsLayoutString = json["fullScreenPlayerControlsLayout"].string,
|
||||
let fullScreenPlayerControlsLayout = PlayerControlsLayout(rawValue: fullScreenPlayerControlsLayoutString)
|
||||
{
|
||||
Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout
|
||||
}
|
||||
|
||||
if let systemControlsCommandsString = json["systemControlsCommands"].string,
|
||||
let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString)
|
||||
{
|
||||
Defaults[.systemControlsCommands] = systemControlsCommands
|
||||
}
|
||||
|
||||
if let buttonBackwardSeekDuration = json["buttonBackwardSeekDuration"].string {
|
||||
Defaults[.buttonBackwardSeekDuration] = buttonBackwardSeekDuration
|
||||
}
|
||||
|
||||
if let buttonForwardSeekDuration = json["buttonForwardSeekDuration"].string {
|
||||
Defaults[.buttonForwardSeekDuration] = buttonForwardSeekDuration
|
||||
}
|
||||
|
||||
if let gestureBackwardSeekDuration = json["gestureBackwardSeekDuration"].string {
|
||||
Defaults[.gestureBackwardSeekDuration] = gestureBackwardSeekDuration
|
||||
}
|
||||
|
||||
if let gestureForwardSeekDuration = json["gestureForwardSeekDuration"].string {
|
||||
Defaults[.gestureForwardSeekDuration] = gestureForwardSeekDuration
|
||||
}
|
||||
|
||||
if let systemControlsSeekDuration = json["systemControlsSeekDuration"].string {
|
||||
Defaults[.systemControlsSeekDuration] = systemControlsSeekDuration
|
||||
}
|
||||
|
||||
if let playerControlsSettingsEnabled = json["playerControlsSettingsEnabled"].bool {
|
||||
Defaults[.playerControlsSettingsEnabled] = playerControlsSettingsEnabled
|
||||
}
|
||||
|
||||
if let playerControlsCloseEnabled = json["playerControlsCloseEnabled"].bool {
|
||||
Defaults[.playerControlsCloseEnabled] = playerControlsCloseEnabled
|
||||
}
|
||||
|
||||
if let playerControlsRestartEnabled = json["playerControlsRestartEnabled"].bool {
|
||||
Defaults[.playerControlsRestartEnabled] = playerControlsRestartEnabled
|
||||
}
|
||||
|
||||
if let playerControlsAdvanceToNextEnabled = json["playerControlsAdvanceToNextEnabled"].bool {
|
||||
Defaults[.playerControlsAdvanceToNextEnabled] = playerControlsAdvanceToNextEnabled
|
||||
}
|
||||
|
||||
if let playerControlsPlaybackModeEnabled = json["playerControlsPlaybackModeEnabled"].bool {
|
||||
Defaults[.playerControlsPlaybackModeEnabled] = playerControlsPlaybackModeEnabled
|
||||
}
|
||||
|
||||
if let playerControlsMusicModeEnabled = json["playerControlsMusicModeEnabled"].bool {
|
||||
Defaults[.playerControlsMusicModeEnabled] = playerControlsMusicModeEnabled
|
||||
}
|
||||
|
||||
if let playerActionsButtonLabelStyleString = json["playerActionsButtonLabelStyle"].string,
|
||||
let playerActionsButtonLabelStyle = ButtonLabelStyle(rawValue: playerActionsButtonLabelStyleString)
|
||||
{
|
||||
Defaults[.playerActionsButtonLabelStyle] = playerActionsButtonLabelStyle
|
||||
}
|
||||
|
||||
if let actionButtonShareEnabled = json["actionButtonShareEnabled"].bool {
|
||||
Defaults[.actionButtonShareEnabled] = actionButtonShareEnabled
|
||||
}
|
||||
|
||||
if let actionButtonAddToPlaylistEnabled = json["actionButtonAddToPlaylistEnabled"].bool {
|
||||
Defaults[.actionButtonAddToPlaylistEnabled] = actionButtonAddToPlaylistEnabled
|
||||
}
|
||||
|
||||
if let actionButtonSubscribeEnabled = json["actionButtonSubscribeEnabled"].bool {
|
||||
Defaults[.actionButtonSubscribeEnabled] = actionButtonSubscribeEnabled
|
||||
}
|
||||
|
||||
if let actionButtonSettingsEnabled = json["actionButtonSettingsEnabled"].bool {
|
||||
Defaults[.actionButtonSettingsEnabled] = actionButtonSettingsEnabled
|
||||
}
|
||||
|
||||
if let actionButtonHideEnabled = json["actionButtonHideEnabled"].bool {
|
||||
Defaults[.actionButtonHideEnabled] = actionButtonHideEnabled
|
||||
}
|
||||
|
||||
if let actionButtonCloseEnabled = json["actionButtonCloseEnabled"].bool {
|
||||
Defaults[.actionButtonCloseEnabled] = actionButtonCloseEnabled
|
||||
}
|
||||
|
||||
if let actionButtonFullScreenEnabled = json["actionButtonFullScreenEnabled"].bool {
|
||||
Defaults[.actionButtonFullScreenEnabled] = actionButtonFullScreenEnabled
|
||||
}
|
||||
|
||||
if let actionButtonPipEnabled = json["actionButtonPipEnabled"].bool {
|
||||
Defaults[.actionButtonPipEnabled] = actionButtonPipEnabled
|
||||
}
|
||||
|
||||
if let actionButtonLockOrientationEnabled = json["actionButtonLockOrientationEnabled"].bool {
|
||||
Defaults[.actionButtonLockOrientationEnabled] = actionButtonLockOrientationEnabled
|
||||
}
|
||||
|
||||
if let actionButtonRestartEnabled = json["actionButtonRestartEnabled"].bool {
|
||||
Defaults[.actionButtonRestartEnabled] = actionButtonRestartEnabled
|
||||
}
|
||||
|
||||
if let actionButtonAdvanceToNextItemEnabled = json["actionButtonAdvanceToNextItemEnabled"].bool {
|
||||
Defaults[.actionButtonAdvanceToNextItemEnabled] = actionButtonAdvanceToNextItemEnabled
|
||||
}
|
||||
|
||||
if let actionButtonMusicModeEnabled = json["actionButtonMusicModeEnabled"].bool {
|
||||
Defaults[.actionButtonMusicModeEnabled] = actionButtonMusicModeEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct HistorySettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let saveRecents = json["saveRecents"].bool {
|
||||
Defaults[.saveRecents] = saveRecents
|
||||
}
|
||||
|
||||
if let saveHistory = json["saveHistory"].bool {
|
||||
Defaults[.saveHistory] = saveHistory
|
||||
}
|
||||
|
||||
if let showWatchingProgress = json["showWatchingProgress"].bool {
|
||||
Defaults[.showWatchingProgress] = showWatchingProgress
|
||||
}
|
||||
|
||||
if let saveLastPlayed = json["saveLastPlayed"].bool {
|
||||
Defaults[.saveLastPlayed] = saveLastPlayed
|
||||
}
|
||||
|
||||
if let watchedVideoPlayNowBehaviorString = json["watchedVideoPlayNowBehavior"].string,
|
||||
let watchedVideoPlayNowBehavior = WatchedVideoPlayNowBehavior(rawValue: watchedVideoPlayNowBehaviorString)
|
||||
{
|
||||
Defaults[.watchedVideoPlayNowBehavior] = watchedVideoPlayNowBehavior
|
||||
}
|
||||
|
||||
if let watchedThreshold = json["watchedThreshold"].int {
|
||||
Defaults[.watchedThreshold] = watchedThreshold
|
||||
}
|
||||
|
||||
if let resetWatchedStatusOnPlaying = json["resetWatchedStatusOnPlaying"].bool {
|
||||
Defaults[.resetWatchedStatusOnPlaying] = resetWatchedStatusOnPlaying
|
||||
}
|
||||
|
||||
if let watchedVideoStyleString = json["watchedVideoStyle"].string,
|
||||
let watchedVideoStyle = WatchedVideoStyle(rawValue: watchedVideoStyleString)
|
||||
{
|
||||
Defaults[.watchedVideoStyle] = watchedVideoStyle
|
||||
}
|
||||
|
||||
if let watchedVideoBadgeColorString = json["watchedVideoBadgeColor"].string,
|
||||
let watchedVideoBadgeColor = WatchedVideoBadgeColor(rawValue: watchedVideoBadgeColorString)
|
||||
{
|
||||
Defaults[.watchedVideoBadgeColor] = watchedVideoBadgeColor
|
||||
}
|
||||
|
||||
if let showToggleWatchedStatusButton = json["showToggleWatchedStatusButton"].bool {
|
||||
Defaults[.showToggleWatchedStatusButton] = showToggleWatchedStatusButton
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct LocationsSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
var includePublicLocations = true
|
||||
var includedInstancesIDs = Set<Instance.ID>()
|
||||
var includedAccountsIDs = Set<Account.ID>()
|
||||
var includedAccountsPasswords = [Account.ID: String]()
|
||||
|
||||
init(
|
||||
json: JSON,
|
||||
includePublicLocations: Bool = true,
|
||||
includedInstancesIDs: Set<Instance.ID> = [],
|
||||
includedAccountsIDs: Set<Account.ID> = [],
|
||||
includedAccountsPasswords: [Account.ID: String] = [:]
|
||||
) {
|
||||
self.json = json
|
||||
self.includePublicLocations = includePublicLocations
|
||||
self.includedInstancesIDs = includedInstancesIDs
|
||||
self.includedAccountsIDs = includedAccountsIDs
|
||||
self.includedAccountsPasswords = includedAccountsPasswords
|
||||
}
|
||||
|
||||
var instances: [Instance] {
|
||||
if let instances = json["instances"].array {
|
||||
return instances.compactMap { instanceJSON in
|
||||
let dict = instanceJSON.dictionaryValue.mapValues { json in json.stringValue }
|
||||
return InstancesBridge().deserialize(dict)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
var accounts: [Account] {
|
||||
if let accounts = json["accounts"].array {
|
||||
return accounts.compactMap { accountJSON in
|
||||
let dict = accountJSON.dictionaryValue.mapValues { json in json.stringValue }
|
||||
return AccountsBridge().deserialize(dict)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
func performImport() {
|
||||
if includePublicLocations {
|
||||
Defaults[.instancesManifest] = json["instancesManifest"].string ?? ""
|
||||
Defaults[.countryOfPublicInstances] = json["countryOfPublicInstances"].string ?? ""
|
||||
}
|
||||
|
||||
instances.filter { includedInstancesIDs.contains($0.id) }.forEach { instance in
|
||||
_ = InstancesModel.shared.insert(id: instance.id, app: instance.app, name: instance.name, url: instance.apiURLString)
|
||||
}
|
||||
|
||||
if let accounts = json["accounts"].array {
|
||||
accounts.forEach { accountJSON in
|
||||
let dict = accountJSON.dictionaryValue.mapValues { json in json.stringValue }
|
||||
if let account = AccountsBridge().deserialize(dict),
|
||||
includedAccountsIDs.contains(account.id)
|
||||
{
|
||||
var password = account.password
|
||||
if password?.isEmpty ?? true {
|
||||
password = includedAccountsPasswords[account.id]
|
||||
}
|
||||
if let password,
|
||||
!password.isEmpty,
|
||||
let instanceID = account.instanceID,
|
||||
let instance = InstancesModel.shared.find(instanceID)
|
||||
{
|
||||
if !instance.accounts.contains(where: { instanceAccount in
|
||||
let (username, _) = instanceAccount.credentials
|
||||
return username == account.username
|
||||
}) {
|
||||
_ = AccountsModel.add(instance: instance, id: account.id, name: account.name, username: account.username, password: password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct OtherDataSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let lastAccountID = json["lastAccountID"].string {
|
||||
Defaults[.lastAccountID] = lastAccountID
|
||||
}
|
||||
|
||||
if let lastInstanceID = json["lastInstanceID"].string {
|
||||
Defaults[.lastInstanceID] = lastInstanceID
|
||||
}
|
||||
|
||||
if let playerRate = json["playerRate"].double {
|
||||
Defaults[.playerRate] = playerRate
|
||||
}
|
||||
|
||||
if let trendingCategoryString = json["trendingCategory"].string,
|
||||
let trendingCategory = TrendingCategory(rawValue: trendingCategoryString)
|
||||
{
|
||||
Defaults[.trendingCategory] = trendingCategory
|
||||
}
|
||||
|
||||
if let trendingCountryString = json["trendingCountry"].string,
|
||||
let trendingCountry = Country(rawValue: trendingCountryString)
|
||||
{
|
||||
Defaults[.trendingCountry] = trendingCountry
|
||||
}
|
||||
|
||||
if let subscriptionsViewPageString = json["subscriptionsViewPage"].string,
|
||||
let subscriptionsViewPage = SubscriptionsView.Page(rawValue: subscriptionsViewPageString)
|
||||
{
|
||||
Defaults[.subscriptionsViewPage] = subscriptionsViewPage
|
||||
}
|
||||
|
||||
if let subscriptionsListingStyle = json["subscriptionsListingStyle"].string {
|
||||
Defaults[.subscriptionsListingStyle] = ListingStyle(rawValue: subscriptionsListingStyle) ?? .list
|
||||
}
|
||||
|
||||
if let popularListingStyle = json["popularListingStyle"].string {
|
||||
Defaults[.popularListingStyle] = ListingStyle(rawValue: popularListingStyle) ?? .list
|
||||
}
|
||||
|
||||
if let trendingListingStyle = json["trendingListingStyle"].string {
|
||||
Defaults[.trendingListingStyle] = ListingStyle(rawValue: trendingListingStyle) ?? .list
|
||||
}
|
||||
|
||||
if let playlistListingStyle = json["playlistListingStyle"].string {
|
||||
Defaults[.playlistListingStyle] = ListingStyle(rawValue: playlistListingStyle) ?? .list
|
||||
}
|
||||
|
||||
if let channelPlaylistListingStyle = json["channelPlaylistListingStyle"].string {
|
||||
Defaults[.channelPlaylistListingStyle] = ListingStyle(rawValue: channelPlaylistListingStyle) ?? .list
|
||||
}
|
||||
|
||||
if let searchListingStyle = json["searchListingStyle"].string {
|
||||
Defaults[.searchListingStyle] = ListingStyle(rawValue: searchListingStyle) ?? .list
|
||||
}
|
||||
|
||||
if let hideShorts = json["hideShorts"].bool {
|
||||
Defaults[.hideShorts] = hideShorts
|
||||
}
|
||||
|
||||
if let hideWatched = json["hideWatched"].bool {
|
||||
Defaults[.hideWatched] = hideWatched
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct PlayerSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let playerInstanceID = json["playerInstanceID"].string {
|
||||
Defaults[.playerInstanceID] = playerInstanceID
|
||||
}
|
||||
|
||||
if let pauseOnHidingPlayer = json["pauseOnHidingPlayer"].bool {
|
||||
Defaults[.pauseOnHidingPlayer] = pauseOnHidingPlayer
|
||||
}
|
||||
|
||||
if let closeVideoOnEOF = json["closeVideoOnEOF"].bool {
|
||||
Defaults[.closeVideoOnEOF] = closeVideoOnEOF
|
||||
}
|
||||
|
||||
if let expandVideoDescription = json["expandVideoDescription"].bool {
|
||||
Defaults[.expandVideoDescription] = expandVideoDescription
|
||||
}
|
||||
|
||||
if let collapsedLinesDescription = json["collapsedLinesDescription"].int {
|
||||
Defaults[.collapsedLinesDescription] = collapsedLinesDescription
|
||||
}
|
||||
|
||||
if let showChapters = json["showChapters"].bool {
|
||||
Defaults[.showChapters] = showChapters
|
||||
}
|
||||
|
||||
if let expandChapters = json["expandChapters"].bool {
|
||||
Defaults[.expandChapters] = expandChapters
|
||||
}
|
||||
|
||||
if let showRelated = json["showRelated"].bool {
|
||||
Defaults[.showRelated] = showRelated
|
||||
}
|
||||
|
||||
if let showInspectorString = json["showInspector"].string,
|
||||
let showInspector = ShowInspectorSetting(rawValue: showInspectorString)
|
||||
{
|
||||
Defaults[.showInspector] = showInspector
|
||||
}
|
||||
|
||||
if let playerSidebarString = json["playerSidebar"].string,
|
||||
let playerSidebar = PlayerSidebarSetting(rawValue: playerSidebarString)
|
||||
{
|
||||
Defaults[.playerSidebar] = playerSidebar
|
||||
}
|
||||
|
||||
if let showKeywords = json["showKeywords"].bool {
|
||||
Defaults[.showKeywords] = showKeywords
|
||||
}
|
||||
|
||||
if let enableReturnYouTubeDislike = json["enableReturnYouTubeDislike"].bool {
|
||||
Defaults[.enableReturnYouTubeDislike] = enableReturnYouTubeDislike
|
||||
}
|
||||
|
||||
if let closePiPOnNavigation = json["closePiPOnNavigation"].bool {
|
||||
Defaults[.closePiPOnNavigation] = closePiPOnNavigation
|
||||
}
|
||||
|
||||
if let closePiPOnOpeningPlayer = json["closePiPOnOpeningPlayer"].bool {
|
||||
Defaults[.closePiPOnOpeningPlayer] = closePiPOnOpeningPlayer
|
||||
}
|
||||
|
||||
if let closePlayerOnOpeningPiP = json["closePlayerOnOpeningPiP"].bool {
|
||||
Defaults[.closePlayerOnOpeningPiP] = closePlayerOnOpeningPiP
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
if let pauseOnEnteringBackground = json["pauseOnEnteringBackground"].bool {
|
||||
Defaults[.pauseOnEnteringBackground] = pauseOnEnteringBackground
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
if let showScrollToTopInComments = json["showScrollToTopInComments"].bool {
|
||||
Defaults[.showScrollToTopInComments] = showScrollToTopInComments
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool {
|
||||
Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock
|
||||
}
|
||||
|
||||
if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool {
|
||||
Defaults[.enterFullscreenInLandscape] = enterFullscreenInLandscape
|
||||
}
|
||||
|
||||
if let rotateToLandscapeOnEnterFullScreenString = json["rotateToLandscapeOnEnterFullScreen"].string,
|
||||
let rotateToLandscapeOnEnterFullScreen = FullScreenRotationSetting(rawValue: rotateToLandscapeOnEnterFullScreenString)
|
||||
{
|
||||
Defaults[.rotateToLandscapeOnEnterFullScreen] = rotateToLandscapeOnEnterFullScreen
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct QualitySettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let batteryCellularProfileString = json["batteryCellularProfile"].string {
|
||||
Defaults[.batteryCellularProfile] = batteryCellularProfileString
|
||||
}
|
||||
|
||||
if let batteryNonCellularProfileString = json["batteryNonCellularProfile"].string {
|
||||
Defaults[.batteryNonCellularProfile] = batteryNonCellularProfileString
|
||||
}
|
||||
|
||||
if let chargingCellularProfileString = json["chargingCellularProfile"].string {
|
||||
Defaults[.chargingCellularProfile] = chargingCellularProfileString
|
||||
}
|
||||
|
||||
if let chargingNonCellularProfileString = json["chargingNonCellularProfile"].string {
|
||||
Defaults[.chargingNonCellularProfile] = chargingNonCellularProfileString
|
||||
}
|
||||
|
||||
if let forceAVPlayerForLiveStreams = json["forceAVPlayerForLiveStreams"].bool {
|
||||
Defaults[.forceAVPlayerForLiveStreams] = forceAVPlayerForLiveStreams
|
||||
}
|
||||
|
||||
if let qualityProfiles = json["qualityProfiles"].array {
|
||||
qualityProfiles.forEach { qualityProfileJSON in
|
||||
let dict = qualityProfileJSON.dictionaryValue.mapValues { json in json.stringValue }
|
||||
if let item = QualityProfileBridge().deserialize(dict) {
|
||||
QualityProfilesModel.shared.update(item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct RecentlyOpenedImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let recentlyOpened = json["recentlyOpened"].array {
|
||||
recentlyOpened.forEach { recentlyOpenedJSON in
|
||||
let dict = recentlyOpenedJSON.dictionaryValue.mapValues { json in json.stringValue }
|
||||
if let item = RecentItemBridge().deserialize(dict) {
|
||||
RecentsModel.shared.add(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Defaults
|
||||
import SwiftyJSON
|
||||
|
||||
struct SponsorBlockSettingsGroupImporter {
|
||||
var json: JSON
|
||||
|
||||
func performImport() {
|
||||
if let sponsorBlockInstance = json["sponsorBlockInstance"].string {
|
||||
Defaults[.sponsorBlockInstance] = sponsorBlockInstance
|
||||
}
|
||||
|
||||
if let sponsorBlockCategories = json["sponsorBlockCategories"].array {
|
||||
Defaults[.sponsorBlockCategories] = Set(sponsorBlockCategories.compactMap { $0.string })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,10 @@ final class NavigationModel: ObservableObject {
|
||||
|
||||
@Published var presentingFileImporter = false
|
||||
|
||||
@Published var presentingSettingsImportSheet = false
|
||||
@Published var presentingSettingsFileImporter = false
|
||||
@Published var settingsImportURL: URL?
|
||||
|
||||
func openChannel(_ channel: Channel, navigationStyle: NavigationStyle) {
|
||||
guard channel.id != Video.fixtureChannelID else {
|
||||
return
|
||||
@@ -269,6 +273,8 @@ final class NavigationModel: ObservableObject {
|
||||
presentingChannel = false
|
||||
presentingPlaylist = false
|
||||
presentingOpenVideos = false
|
||||
presentingFileImporter = false
|
||||
presentingSettingsImportSheet = false
|
||||
}
|
||||
|
||||
func hideKeyboard() {
|
||||
@@ -279,8 +285,9 @@ final class NavigationModel: ObservableObject {
|
||||
|
||||
func presentAlert(title: String, message: String? = nil) {
|
||||
let message = message.isNil ? nil : Text(message!)
|
||||
alert = Alert(title: Text(title), message: message)
|
||||
presentingAlert = true
|
||||
let alert = Alert(title: Text(title), message: message)
|
||||
|
||||
presentAlert(alert)
|
||||
}
|
||||
|
||||
func presentRequestErrorAlert(_ error: RequestError) {
|
||||
@@ -289,6 +296,11 @@ final class NavigationModel: ObservableObject {
|
||||
}
|
||||
|
||||
func presentAlert(_ alert: Alert) {
|
||||
guard !presentingSettings else {
|
||||
SettingsModel.shared.presentAlert(alert)
|
||||
return
|
||||
}
|
||||
|
||||
self.alert = alert
|
||||
presentingAlert = true
|
||||
}
|
||||
@@ -311,6 +323,16 @@ final class NavigationModel: ObservableObject {
|
||||
print("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func presentSettingsImportSheet(_ url: URL, forceSettings: Bool = false) {
|
||||
guard !presentingSettings, !forceSettings else {
|
||||
ImportExportSettingsModel.shared.reset()
|
||||
SettingsModel.shared.presentSettingsImportSheet(url)
|
||||
return
|
||||
}
|
||||
settingsImportURL = url
|
||||
presentingSettingsImportSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
typealias TabSelection = NavigationModel.TabSelection
|
||||
|
||||
@@ -67,6 +67,7 @@ final class MPVClient: ObservableObject {
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", machine == "x86_64" ? "no" : "auto-safe"))
|
||||
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
|
||||
checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no"))
|
||||
|
||||
checkError(mpv_initialize(mpv))
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ final class SettingsModel: ObservableObject {
|
||||
@Published var presentingAlert = false
|
||||
@Published var alert = Alert(title: Text("Error"))
|
||||
|
||||
@Published var presentingSettingsImportSheet = false
|
||||
@Published var settingsImportURL: URL?
|
||||
|
||||
func presentAlert(title: String, message: String? = nil) {
|
||||
let message = message.isNil ? nil : Text(message!)
|
||||
alert = Alert(title: Text(title), message: message)
|
||||
@@ -17,4 +20,9 @@ final class SettingsModel: ObservableObject {
|
||||
self.alert = alert
|
||||
presentingAlert = true
|
||||
}
|
||||
|
||||
func presentSettingsImportSheet(_ url: URL) {
|
||||
settingsImportURL = url
|
||||
presentingSettingsImportSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Constants {
|
||||
enum Constants {
|
||||
static let yatteeProtocol = "yattee://"
|
||||
static let overlayAnimation = Animation.linear(duration: 0.2)
|
||||
static var isIPhone: Bool {
|
||||
@@ -61,6 +61,26 @@ struct Constants {
|
||||
#endif
|
||||
}
|
||||
|
||||
static var deviceName: String {
|
||||
#if os(macOS)
|
||||
Host().localizedName ?? "Mac"
|
||||
#else
|
||||
UIDevice.current.name
|
||||
#endif
|
||||
}
|
||||
|
||||
static var platform: String {
|
||||
#if os(macOS)
|
||||
"macOS"
|
||||
#elseif os(iOS)
|
||||
"iOS"
|
||||
#elseif os(tvOS)
|
||||
"tvOS"
|
||||
#else
|
||||
"unknown"
|
||||
#endif
|
||||
}
|
||||
|
||||
static func seekIcon(_ type: String, _ interval: TimeInterval) -> String {
|
||||
let interval = Int(interval)
|
||||
let allVersions = [10, 15, 30, 45, 60, 75, 90]
|
||||
|
||||
@@ -6,37 +6,22 @@ import SwiftUI
|
||||
#endif
|
||||
|
||||
extension Defaults.Keys {
|
||||
static let instancesManifest = Key<String>("instancesManifest", default: "")
|
||||
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
|
||||
|
||||
static let instances = Key<[Instance]>("instances", default: [])
|
||||
static let accounts = Key<[Account]>("accounts", default: [])
|
||||
static let lastAccountID = Key<Account.ID?>("lastAccountID")
|
||||
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
||||
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
|
||||
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
|
||||
|
||||
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
|
||||
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
|
||||
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
// MARK: GROUP - Browsing
|
||||
|
||||
static let showHome = Key<Bool>("showHome", default: true)
|
||||
static let showOpenActionsInHome = Key<Bool>("showOpenActionsInHome", default: true)
|
||||
static let showQueueInHome = Key<Bool>("showQueueInHome", default: true)
|
||||
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||
static let showFavoritesInHome = Key<Bool>("showFavoritesInHome", default: true)
|
||||
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
|
||||
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
|
||||
static let startupSection = Key<StartupSection>("startupSection", default: .home)
|
||||
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
|
||||
|
||||
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||
#if os(iOS)
|
||||
static let showDocuments = Key<Bool>("showDocuments", default: false)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
#endif
|
||||
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
|
||||
static let favorites = Key<[FavoriteItem]>("favorites", default: [])
|
||||
|
||||
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
|
||||
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
||||
|
||||
#if !os(tvOS)
|
||||
#if os(macOS)
|
||||
@@ -46,21 +31,139 @@ extension Defaults.Keys {
|
||||
#endif
|
||||
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
||||
#endif
|
||||
|
||||
static let accountPickerDisplaysAnonymousAccounts = Key<Bool>("accountPickerDisplaysAnonymousAccounts", default: true)
|
||||
#if os(iOS)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
#endif
|
||||
static let showUnwatchedFeedBadges = Key<Bool>("showUnwatchedFeedBadges", default: false)
|
||||
static let keepChannelsWithUnwatchedFeedOnTop = Key<Bool>("keepChannelsWithUnwatchedFeedOnTop", default: true)
|
||||
static let showToggleWatchedStatusButton = Key<Bool>("showToggleWatchedStatusButton", default: false)
|
||||
static let expandChannelDescription = Key<Bool>("expandChannelDescription", default: false)
|
||||
|
||||
static let keepChannelsWithUnwatchedFeedOnTop = Key<Bool>("keepChannelsWithUnwatchedFeedOnTop", default: true)
|
||||
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
|
||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||
|
||||
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
|
||||
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
||||
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
|
||||
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
||||
static let roundedThumbnails = Key<Bool>("roundedThumbnails", default: true)
|
||||
static let thumbnailsQuality = Key<ThumbnailsQuality>("thumbnailsQuality", default: .highest)
|
||||
|
||||
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
|
||||
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
||||
// MARK: GROUP - Player
|
||||
|
||||
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||
|
||||
#if os(tvOS)
|
||||
static let pauseOnHidingPlayerDefault = true
|
||||
#else
|
||||
static let pauseOnHidingPlayerDefault = false
|
||||
#endif
|
||||
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
|
||||
|
||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||
|
||||
#if !os(macOS)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
static let expandVideoDescriptionDefault = Constants.isIPad
|
||||
#else
|
||||
static let expandVideoDescriptionDefault = true
|
||||
#endif
|
||||
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
|
||||
|
||||
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
|
||||
|
||||
static let showChapters = Key<Bool>("showChapters", default: true)
|
||||
static let expandChapters = Key<Bool>("expandChapters", default: true)
|
||||
static let showRelated = Key<Bool>("showRelated", default: true)
|
||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||
|
||||
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
|
||||
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
||||
#if !os(tvOS)
|
||||
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
|
||||
#endif
|
||||
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
|
||||
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
)
|
||||
#endif
|
||||
|
||||
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
|
||||
static let closePlayerOnOpeningPiP = Key<Bool>("closePlayerOnOpeningPiP", default: false)
|
||||
#if !os(macOS)
|
||||
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
|
||||
#endif
|
||||
|
||||
// MARK: GROUP - Controls
|
||||
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
|
||||
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
#elseif os(tvOS)
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
#else
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.medium
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.medium
|
||||
#endif
|
||||
|
||||
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
|
||||
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
|
||||
|
||||
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
|
||||
|
||||
static let buttonBackwardSeekDuration = Key<String>("buttonBackwardSeekDuration", default: "10")
|
||||
static let buttonForwardSeekDuration = Key<String>("buttonForwardSeekDuration", default: "10")
|
||||
static let gestureBackwardSeekDuration = Key<String>("gestureBackwardSeekDuration", default: "10")
|
||||
static let gestureForwardSeekDuration = Key<String>("gestureForwardSeekDuration", default: "10")
|
||||
static let systemControlsSeekDuration = Key<String>("systemControlsBackwardSeekDuration", default: "10")
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLockOrientationEnabled = Key<Bool>("playerControlsLockOrientationEnabled", default: true)
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
static let playerControlsSettingsEnabledDefault = true
|
||||
#else
|
||||
static let playerControlsSettingsEnabledDefault = false
|
||||
#endif
|
||||
static let playerControlsSettingsEnabled = Key<Bool>("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault)
|
||||
static let playerControlsCloseEnabled = Key<Bool>("playerControlsCloseEnabled", default: true)
|
||||
static let playerControlsRestartEnabled = Key<Bool>("playerControlsRestartEnabled", default: false)
|
||||
static let playerControlsAdvanceToNextEnabled = Key<Bool>("playerControlsAdvanceToNextEnabled", default: false)
|
||||
static let playerControlsPlaybackModeEnabled = Key<Bool>("playerControlsPlaybackModeEnabled", default: false)
|
||||
static let playerControlsMusicModeEnabled = Key<Bool>("playerControlsMusicModeEnabled", default: false)
|
||||
|
||||
static let playerActionsButtonLabelStyle = Key<ButtonLabelStyle>("playerActionsButtonLabelStyle", default: .iconAndText)
|
||||
|
||||
static let actionButtonShareEnabled = Key<Bool>("actionButtonShareEnabled", default: true)
|
||||
static let actionButtonAddToPlaylistEnabled = Key<Bool>("actionButtonAddToPlaylistEnabled", default: true)
|
||||
static let actionButtonSubscribeEnabled = Key<Bool>("actionButtonSubscribeEnabled", default: false)
|
||||
static let actionButtonSettingsEnabled = Key<Bool>("actionButtonSettingsEnabled", default: true)
|
||||
static let actionButtonHideEnabled = Key<Bool>("actionButtonHideEnabled", default: false)
|
||||
static let actionButtonCloseEnabled = Key<Bool>("actionButtonCloseEnabled", default: true)
|
||||
static let actionButtonFullScreenEnabled = Key<Bool>("actionButtonFullScreenEnabled", default: false)
|
||||
static let actionButtonPipEnabled = Key<Bool>("actionButtonPipEnabled", default: false)
|
||||
static let actionButtonLockOrientationEnabled = Key<Bool>("actionButtonLockOrientationEnabled", default: false)
|
||||
static let actionButtonRestartEnabled = Key<Bool>("actionButtonRestartEnabled", default: false)
|
||||
static let actionButtonAdvanceToNextItemEnabled = Key<Bool>("actionButtonAdvanceToNextItemEnabled", default: false)
|
||||
static let actionButtonMusicModeEnabled = Key<Bool>("actionButtonMusicModeEnabled", default: true)
|
||||
|
||||
// MARK: GROUP - Quality
|
||||
|
||||
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases)
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases)
|
||||
@@ -109,150 +212,67 @@ extension Defaults.Keys {
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
#endif
|
||||
static let playerRate = Key<Double>("playerRate", default: 1.0)
|
||||
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
|
||||
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
|
||||
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: .defaultValue)
|
||||
static let playerInstanceID = Key<Instance.ID?>("playerInstance")
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
#elseif os(tvOS)
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
#else
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.medium
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.medium
|
||||
#endif
|
||||
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
|
||||
|
||||
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
|
||||
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
|
||||
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||
static let showKeywords = Key<Bool>("showKeywords", default: false)
|
||||
#if !os(tvOS)
|
||||
static let showScrollToTopInComments = Key<Bool>("showScrollToTopInComments", default: true)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
static let expandVideoDescriptionDefault = Constants.isIPad
|
||||
#else
|
||||
static let expandVideoDescriptionDefault = true
|
||||
#endif
|
||||
static let expandVideoDescription = Key<Bool>("expandVideoDescription", default: expandVideoDescriptionDefault)
|
||||
static let collapsedLinesDescription = Key<Int>("collapsedLinesDescription", default: 5)
|
||||
|
||||
static let showChannelAvatarInChannelsLists = Key<Bool>("showChannelAvatarInChannelsLists", default: true)
|
||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||
|
||||
#if os(tvOS)
|
||||
static let pauseOnHidingPlayerDefault = true
|
||||
#else
|
||||
static let pauseOnHidingPlayerDefault = false
|
||||
#endif
|
||||
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: pauseOnHidingPlayerDefault)
|
||||
|
||||
#if !os(macOS)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||
#endif
|
||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
|
||||
#if !os(macOS)
|
||||
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
|
||||
#endif
|
||||
static let closePlayerOnOpeningPiP = Key<Bool>("closePlayerOnOpeningPiP", default: false)
|
||||
|
||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||
|
||||
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
|
||||
static let saveLastPlayed = Key<Bool>("saveLastPlayed", default: false)
|
||||
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
||||
static let playbackMode = Key<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
|
||||
// MARK: GROUP - History
|
||||
|
||||
static let saveRecents = Key<Bool>("saveRecents", default: true)
|
||||
static let saveHistory = Key<Bool>("saveHistory", default: true)
|
||||
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)
|
||||
static let saveLastPlayed = Key<Bool>("saveLastPlayed", default: false)
|
||||
|
||||
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
|
||||
static let watchedThreshold = Key<Int>("watchedThreshold", default: 90)
|
||||
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
|
||||
|
||||
static let watchedVideoStyle = Key<WatchedVideoStyle>("watchedVideoStyle", default: .badge)
|
||||
static let watchedVideoBadgeColor = Key<WatchedVideoBadgeColor>("WatchedVideoBadgeColor", default: .red)
|
||||
static let watchedVideoPlayNowBehavior = Key<WatchedVideoPlayNowBehavior>("watchedVideoPlayNowBehavior", default: .continue)
|
||||
static let resetWatchedStatusOnPlaying = Key<Bool>("resetWatchedStatusOnPlaying", default: false)
|
||||
static let saveRecents = Key<Bool>("saveRecents", default: true)
|
||||
static let showToggleWatchedStatusButton = Key<Bool>("showToggleWatchedStatusButton", default: false)
|
||||
|
||||
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
||||
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||
// MARK: GROUP - SponsorBlock
|
||||
|
||||
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.subscriptions, .trending, .playlists])
|
||||
static let startupSection = Key<StartupSection>("startupSection", default: .home)
|
||||
static let sponsorBlockInstance = Key<String>("sponsorBlockInstance", default: "https://sponsor.ajay.app")
|
||||
static let sponsorBlockCategories = Key<Set<String>>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories))
|
||||
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
)
|
||||
#endif
|
||||
// MARK: GROUP - Locations
|
||||
|
||||
static let instancesManifest = Key<String>("instancesManifest", default: "")
|
||||
static let countryOfPublicInstances = Key<String?>("countryOfPublicInstances")
|
||||
|
||||
static let instances = Key<[Instance]>("instances", default: [])
|
||||
static let accounts = Key<[Account]>("accounts", default: [])
|
||||
|
||||
// MARK: Group - Advanced
|
||||
|
||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||
static let showPlayNowInBackendContextMenu = Key<Bool>("showPlayNowInBackendContextMenu", default: false)
|
||||
|
||||
#if os(macOS)
|
||||
static let playerDetailsPageButtonLabelStyleDefault = ButtonLabelStyle.iconAndText
|
||||
#else
|
||||
static let playerDetailsPageButtonLabelStyleDefault = UIDevice.current.userInterfaceIdiom == .phone ? ButtonLabelStyle.iconOnly : .iconAndText
|
||||
#endif
|
||||
static let playerActionsButtonLabelStyle = Key<ButtonLabelStyle>("playerActionsButtonLabelStyle", default: .iconAndText)
|
||||
|
||||
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
|
||||
|
||||
static let buttonBackwardSeekDuration = Key<String>("buttonBackwardSeekDuration", default: "10")
|
||||
static let buttonForwardSeekDuration = Key<String>("buttonForwardSeekDuration", default: "10")
|
||||
static let gestureBackwardSeekDuration = Key<String>("gestureBackwardSeekDuration", default: "10")
|
||||
static let gestureForwardSeekDuration = Key<String>("gestureForwardSeekDuration", default: "10")
|
||||
static let systemControlsSeekDuration = Key<String>("systemControlsBackwardSeekDuration", default: "10")
|
||||
static let actionButtonShareEnabled = Key<Bool>("actionButtonShareEnabled", default: true)
|
||||
static let actionButtonAddToPlaylistEnabled = Key<Bool>("actionButtonAddToPlaylistEnabled", default: true)
|
||||
static let actionButtonSubscribeEnabled = Key<Bool>("actionButtonSubscribeEnabled", default: false)
|
||||
static let actionButtonSettingsEnabled = Key<Bool>("actionButtonSettingsEnabled", default: true)
|
||||
static let actionButtonHideEnabled = Key<Bool>("actionButtonHideEnabled", default: false)
|
||||
static let actionButtonCloseEnabled = Key<Bool>("actionButtonCloseEnabled", default: true)
|
||||
static let actionButtonFullScreenEnabled = Key<Bool>("actionButtonFullScreenEnabled", default: false)
|
||||
static let actionButtonPipEnabled = Key<Bool>("actionButtonPipEnabled", default: false)
|
||||
static let actionButtonLockOrientationEnabled = Key<Bool>("actionButtonLockOrientationEnabled", default: false)
|
||||
static let actionButtonRestartEnabled = Key<Bool>("actionButtonRestartEnabled", default: false)
|
||||
static let actionButtonAdvanceToNextItemEnabled = Key<Bool>("actionButtonAdvanceToNextItemEnabled", default: false)
|
||||
static let actionButtonMusicModeEnabled = Key<Bool>("actionButtonMusicModeEnabled", default: true)
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLockOrientationEnabled = Key<Bool>("playerControlsLockOrientationEnabled", default: true)
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
static let playerControlsSettingsEnabledDefault = true
|
||||
#else
|
||||
static let playerControlsSettingsEnabledDefault = false
|
||||
#endif
|
||||
static let playerControlsSettingsEnabled = Key<Bool>("playerControlsSettingsEnabled", default: playerControlsSettingsEnabledDefault)
|
||||
static let playerControlsCloseEnabled = Key<Bool>("playerControlsCloseEnabled", default: true)
|
||||
static let playerControlsRestartEnabled = Key<Bool>("playerControlsRestartEnabled", default: false)
|
||||
static let playerControlsAdvanceToNextEnabled = Key<Bool>("playerControlsAdvanceToNextEnabled", default: false)
|
||||
static let playerControlsPlaybackModeEnabled = Key<Bool>("playerControlsPlaybackModeEnabled", default: false)
|
||||
static let playerControlsMusicModeEnabled = Key<Bool>("playerControlsMusicModeEnabled", default: false)
|
||||
|
||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
||||
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
|
||||
static let mpvCachePauseWait = Key<String>("mpvCachePauseWait", default: "3")
|
||||
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
||||
static let mpvDeinterlace = Key<Bool>("mpvDeinterlace", default: false)
|
||||
|
||||
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
|
||||
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
|
||||
|
||||
// MARK: GROUP - Other exportable
|
||||
|
||||
static let lastAccountID = Key<Account.ID?>("lastAccountID")
|
||||
static let lastInstanceID = Key<Instance.ID?>("lastInstanceID")
|
||||
|
||||
static let playerRate = Key<Double>("playerRate", default: 1.0)
|
||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||
|
||||
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
||||
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||
|
||||
static let subscriptionsViewPage = Key<SubscriptionsView.Page>("subscriptionsViewPage", default: .feed)
|
||||
|
||||
static let subscriptionsListingStyle = Key<ListingStyle>("subscriptionsListingStyle", default: .cells)
|
||||
@@ -263,11 +283,22 @@ extension Defaults.Keys {
|
||||
static let searchListingStyle = Key<ListingStyle>("searchListingStyle", default: .cells)
|
||||
static let hideShorts = Key<Bool>("hideShorts", default: false)
|
||||
static let hideWatched = Key<Bool>("hideWatched", default: false)
|
||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||
static let showChapters = Key<Bool>("showChapters", default: true)
|
||||
static let expandChapters = Key<Bool>("expandChapters", default: true)
|
||||
static let showRelated = Key<Bool>("showRelated", default: true)
|
||||
static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: [])
|
||||
|
||||
// MARK: GROUP - Not exportable
|
||||
|
||||
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
|
||||
static let playbackMode = Key<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
|
||||
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
||||
|
||||
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
||||
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
|
||||
|
||||
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
|
||||
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)
|
||||
|
||||
// MARK: LEGACY
|
||||
|
||||
static let homeHistoryItems = Key<Int>("homeHistoryItems", default: 10)
|
||||
}
|
||||
|
||||
enum ResolutionSetting: String, CaseIterable, Defaults.Serializable {
|
||||
@@ -401,6 +432,15 @@ enum ButtonLabelStyle: String, CaseIterable, Defaults.Serializable {
|
||||
var text: Bool {
|
||||
self == .iconAndText
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .iconOnly:
|
||||
return "Icon only"
|
||||
case .iconAndText:
|
||||
return "Icon and text"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ThumbnailsQuality: String, CaseIterable, Defaults.Serializable {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
struct Delay {
|
||||
enum Delay {
|
||||
@discardableResult static func by(_ interval: TimeInterval, block: @escaping () -> Void) -> Timer {
|
||||
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in block() }
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ struct HomeView: View {
|
||||
@Default(.favorites) private var favorites
|
||||
@Default(.widgetsSettings) private var widgetsSettings
|
||||
#endif
|
||||
@Default(.homeHistoryItems) private var homeHistoryItems
|
||||
@Default(.showFavoritesInHome) private var showFavoritesInHome
|
||||
@Default(.showOpenActionsInHome) private var showOpenActionsInHome
|
||||
@Default(.showQueueInHome) private var showQueueInHome
|
||||
|
||||
@@ -68,6 +68,7 @@ struct ContentView: View {
|
||||
SettingsView()
|
||||
}
|
||||
)
|
||||
.modifier(ImportSettingsSheetViewModifier(isPresented: $navigation.presentingSettingsImportSheet, settingsFile: $navigation.settingsImportURL))
|
||||
.background(
|
||||
EmptyView().sheet(isPresented: $navigation.presentingAccounts) {
|
||||
AccountsView()
|
||||
|
||||
@@ -14,6 +14,11 @@ struct OpenURLHandler {
|
||||
var navigationStyle: NavigationStyle
|
||||
|
||||
func handle(_ url: URL) {
|
||||
if url.isFileURL, url.standardizedFileURL.absoluteString.hasSuffix(".\(ImportExportSettingsModel.settingsExtension)") {
|
||||
navigation.presentSettingsImportSheet(url)
|
||||
return
|
||||
}
|
||||
|
||||
if Self.firstHandle {
|
||||
Self.firstHandle = false
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ struct CommentView: View {
|
||||
}
|
||||
|
||||
private var authorAvatar: some View {
|
||||
WebImage(url: URL(string: comment.authorAvatarURL)!, options: [.lowPriority])
|
||||
WebImage(url: URL(string: comment.authorAvatarURL), options: [.lowPriority])
|
||||
.resizable()
|
||||
.placeholder {
|
||||
Rectangle().fill(Color("PlaceholderColor"))
|
||||
|
||||
@@ -153,7 +153,7 @@ struct AccountForm: View {
|
||||
return
|
||||
}
|
||||
|
||||
let account = AccountsModel.add(instance: instance, name: name, username: username, password: password)
|
||||
let account = AccountsModel.add(instance: instance, id: nil, name: name, username: username, password: password)
|
||||
selectedAccount?.wrappedValue = account
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
@@ -5,6 +5,7 @@ struct AdvancedSettings: View {
|
||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||
@Default(.mpvCacheSecs) private var mpvCacheSecs
|
||||
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
|
||||
@Default(.mpvDeinterlace) private var mpvDeinterlace
|
||||
@Default(.mpvEnableLogging) private var mpvEnableLogging
|
||||
@Default(.showCacheStatus) private var showCacheStatus
|
||||
@Default(.feedCacheSize) private var feedCacheSize
|
||||
@@ -87,6 +88,8 @@ struct AdvancedSettings: View {
|
||||
}
|
||||
.multilineTextAlignment(.trailing)
|
||||
|
||||
Toggle("deinterlace", isOn: $mpvDeinterlace)
|
||||
|
||||
if mpvEnableLogging {
|
||||
logButton
|
||||
}
|
||||
|
||||
167
Shared/Settings/ExportSettings.swift
Normal file
167
Shared/Settings/ExportSettings.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ExportSettings: View {
|
||||
@ObservedObject private var model = ImportExportSettingsModel.shared
|
||||
@State private var presentingShareSheet = false
|
||||
@StateObject private var settings = SettingsModel.shared
|
||||
|
||||
private var filesToShare = [ImportExportSettingsModel.exportFile]
|
||||
@ObservedObject private var navigation = NavigationModel.shared
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(macOS)
|
||||
VStack {
|
||||
list
|
||||
|
||||
importExportButtons
|
||||
}
|
||||
#else
|
||||
list
|
||||
#if os(iOS)
|
||||
.listStyle(.insetGrouped)
|
||||
.sheet(
|
||||
isPresented: $presentingShareSheet,
|
||||
onDismiss: { self.model.isExportInProgress = false }
|
||||
) {
|
||||
ShareSheet(activityItems: filesToShare)
|
||||
.id("settings-share-\(filesToShare.count)")
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
.navigationTitle("Export Settings")
|
||||
}
|
||||
|
||||
var list: some View {
|
||||
List {
|
||||
exportView
|
||||
}
|
||||
.onAppear {
|
||||
model.reset()
|
||||
}
|
||||
}
|
||||
|
||||
var importExportButtons: some View {
|
||||
HStack {
|
||||
importButton
|
||||
|
||||
Spacer()
|
||||
|
||||
exportButton
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var importButton: some View {
|
||||
#if os(macOS)
|
||||
Button {
|
||||
navigation.presentingSettingsFileImporter = true
|
||||
} label: {
|
||||
Label("Import", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
struct ExportGroupRow: View {
|
||||
let group: ImportExportSettingsModel.ExportGroup
|
||||
|
||||
@ObservedObject private var model = ImportExportSettingsModel.shared
|
||||
|
||||
var body: some View {
|
||||
Button(action: { model.toggleExportGroupSelection(group) }) {
|
||||
HStack {
|
||||
Text(group.label)
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.opacity(isGroupInSelectedGroups ? 1 : 0)
|
||||
}
|
||||
.animation(nil, value: isGroupInSelectedGroups)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
var isGroupInSelectedGroups: Bool {
|
||||
model.selectedExportGroups.contains(group)
|
||||
}
|
||||
}
|
||||
|
||||
var exportView: some View {
|
||||
Group {
|
||||
Section(header: Text("Settings")) {
|
||||
ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in
|
||||
ExportGroupRow(group: group)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Locations")) {
|
||||
ForEach(ImportExportSettingsModel.ExportGroup.locationsGroups) { group in
|
||||
ExportGroupRow(group: group)
|
||||
.disabled(!model.isGroupEnabled(group))
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Other"), footer: otherGroupsFooter) {
|
||||
ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in
|
||||
ExportGroupRow(group: group)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
Section {
|
||||
exportButton
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(model.isExportInProgress)
|
||||
}
|
||||
|
||||
var exportButton: some View {
|
||||
Button(action: exportSettings) {
|
||||
Label(model.isExportInProgress ? "Export in progress..." : "Export...", systemImage: model.isExportInProgress ? "fireworks" : "square.and.arrow.up")
|
||||
.animation(nil, value: model.isExportInProgress)
|
||||
#if !os(macOS)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
#endif
|
||||
}
|
||||
.disabled(!model.isExportAvailable)
|
||||
}
|
||||
|
||||
@ViewBuilder var otherGroupsFooter: some View {
|
||||
Text("Other data include last used playback preferences and listing options")
|
||||
}
|
||||
|
||||
func exportSettings() {
|
||||
let export = {
|
||||
model.isExportInProgress = true
|
||||
Delay.by(0.3) {
|
||||
model.exportAction()
|
||||
#if !os(macOS)
|
||||
self.presentingShareSheet = true
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if model.isGroupSelected(.accountsUnencryptedPasswords) {
|
||||
settings.presentAlert(Alert(
|
||||
title: Text("Are you sure you want to export unencrypted passwords?"),
|
||||
message: Text("Do not share this file with anyone or you can lose access to your accounts. If you don't select to export passwords you will be asked to provide them during import"),
|
||||
primaryButton: .destructive(Text("Export"), action: export),
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
} else {
|
||||
export()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExportSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
ExportSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
189
Shared/Settings/Import/ImportSettingsAccountRow.swift
Normal file
189
Shared/Settings/Import/ImportSettingsAccountRow.swift
Normal file
@@ -0,0 +1,189 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImportSettingsAccountRow: View {
|
||||
var account: Account
|
||||
var fileModel: ImportSettingsFileModel
|
||||
|
||||
@State private var password = ""
|
||||
|
||||
@State private var isValid = false
|
||||
@State private var isValidated = false
|
||||
@State private var isValidating = false
|
||||
@State private var validationError: String?
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@ObservedObject private var model = ImportSettingsSheetViewModel.shared
|
||||
|
||||
func afterValidation() {
|
||||
if isValid {
|
||||
model.importableAccounts.insert(account.id)
|
||||
model.selectedAccounts.insert(account.id)
|
||||
model.importableAccountsPasswords[account.id] = password
|
||||
} else {
|
||||
model.selectedAccounts.remove(account.id)
|
||||
model.importableAccounts.remove(account.id)
|
||||
model.importableAccountsPasswords.removeValue(forKey: account.id)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: { model.toggleAccount(account, accounts: accounts) }) {
|
||||
let accountExists = AccountsModel.shared.find(account.id) != nil
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(account.username)
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.opacity(isChecked ? 1 : 0)
|
||||
}
|
||||
Text(account.instance?.description ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Group {
|
||||
if let instanceID = account.instanceID {
|
||||
if accountExists {
|
||||
HStack {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
Text("Account already exists")
|
||||
}
|
||||
} else {
|
||||
Group {
|
||||
if InstancesModel.shared.find(instanceID) != nil {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Custom Location already exists")
|
||||
}
|
||||
} else if model.selectedInstances.contains(instanceID) {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Custom Location selected for import")
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("Custom Location not selected for import")
|
||||
}
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 20)
|
||||
|
||||
if account.password.isNil || account.password!.isEmpty {
|
||||
Group {
|
||||
if password.isEmpty {
|
||||
HStack {
|
||||
Image(systemName: "key")
|
||||
Text("Password required to import")
|
||||
}
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
} else {
|
||||
AccountValidationStatus(
|
||||
app: .constant(instance.app),
|
||||
isValid: $isValid,
|
||||
isValidated: $isValidated,
|
||||
isValidating: $isValidating,
|
||||
error: $validationError
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 20)
|
||||
} else {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
|
||||
Text("Password saved in import file")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 2)
|
||||
|
||||
if !accountExists && (account.password.isNil || account.password!.isEmpty) {
|
||||
SecureField("Password", text: $password)
|
||||
.onChange(of: password) { _ in validate() }
|
||||
#if !os(tvOS)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onChange(of: isValid) { _ in afterValidation() }
|
||||
.animation(nil, value: isChecked)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
var isChecked: Bool {
|
||||
model.isSelectedForImport(account)
|
||||
}
|
||||
|
||||
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
|
||||
fileModel.locationsSettingsGroupImporter
|
||||
}
|
||||
|
||||
var accounts: [Account] {
|
||||
fileModel.locationsSettingsGroupImporter?.accounts ?? []
|
||||
}
|
||||
|
||||
private var instance: Instance! {
|
||||
(fileModel.locationsSettingsGroupImporter?.instances ?? []).first { $0.id == account.instanceID }
|
||||
}
|
||||
|
||||
private var validator: AccountValidator {
|
||||
AccountValidator(
|
||||
app: .constant(instance.app),
|
||||
url: instance.apiURLString,
|
||||
account: Account(instanceID: instance.id, urlString: instance.apiURLString, username: account.username, password: password),
|
||||
id: .constant(account.username),
|
||||
isValid: $isValid,
|
||||
isValidated: $isValidated,
|
||||
isValidating: $isValidating,
|
||||
error: $validationError
|
||||
)
|
||||
}
|
||||
|
||||
private func validate() {
|
||||
isValid = false
|
||||
validationDebounce.invalidate()
|
||||
|
||||
guard !account.username.isEmpty, !password.isEmpty else {
|
||||
validator.reset()
|
||||
return
|
||||
}
|
||||
|
||||
isValidating = true
|
||||
|
||||
validationDebounce.debouncing(1) {
|
||||
validator.validateAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportSettingsAccountRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let fileModel = ImportSettingsFileModel(url: URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!)
|
||||
|
||||
return List {
|
||||
ImportSettingsAccountRow(
|
||||
account: .init(name: "arekf", urlString: "https://instance.com", username: "arekf"),
|
||||
fileModel: fileModel
|
||||
)
|
||||
ImportSettingsAccountRow(
|
||||
account: .init(name: "arekf", urlString: "https://instance.com", username: "arekf", password: "a"),
|
||||
fileModel: fileModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct ImportSettingsFileImporterViewModifier: ViewModifier {
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.fileImporter(isPresented: $isPresented, allowedContentTypes: [.json]) { result in
|
||||
do {
|
||||
let selectedFile = try result.get()
|
||||
var urlToOpen: URL?
|
||||
|
||||
if let bookmarkURL = URLBookmarkModel.shared.loadBookmark(selectedFile) {
|
||||
urlToOpen = bookmarkURL
|
||||
}
|
||||
|
||||
if selectedFile.startAccessingSecurityScopedResource() {
|
||||
URLBookmarkModel.shared.saveBookmark(selectedFile)
|
||||
urlToOpen = selectedFile
|
||||
}
|
||||
|
||||
guard let urlToOpen else { return }
|
||||
NavigationModel.shared.presentSettingsImportSheet(urlToOpen, forceSettings: true)
|
||||
} catch {
|
||||
NavigationModel.shared.presentAlert(title: "Could not open Files")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
262
Shared/Settings/Import/ImportSettingsSheetView.swift
Normal file
262
Shared/Settings/Import/ImportSettingsSheetView.swift
Normal file
@@ -0,0 +1,262 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImportSettingsSheetView: View {
|
||||
@Binding var settingsFile: URL?
|
||||
@StateObject private var model = ImportSettingsSheetViewModel.shared
|
||||
@StateObject private var importExportModel = ImportExportSettingsModel.shared
|
||||
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
@State private var presentingCompletedAlert = false
|
||||
|
||||
private let accountsModel = AccountsModel.shared
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(macOS)
|
||||
list
|
||||
.frame(width: 700, height: 800)
|
||||
#else
|
||||
NavigationView {
|
||||
list
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
guard let fileModel else { return }
|
||||
model.reset(fileModel.locationsSettingsGroupImporter)
|
||||
importExportModel.reset(fileModel)
|
||||
}
|
||||
.onChange(of: settingsFile) { _ in
|
||||
importExportModel.reset(fileModel)
|
||||
}
|
||||
}
|
||||
|
||||
var list: some View {
|
||||
List {
|
||||
importGroupView
|
||||
|
||||
importOptions
|
||||
|
||||
metadata
|
||||
}
|
||||
.alert(isPresented: $presentingCompletedAlert) {
|
||||
completedAlert
|
||||
}
|
||||
#if os(iOS)
|
||||
.backport
|
||||
.scrollDismissesKeyboardInteractively()
|
||||
#endif
|
||||
.navigationTitle("Import Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(action: { presentationMode.wrappedValue.dismiss() }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(action: {
|
||||
fileModel?.performImport()
|
||||
presentingCompletedAlert = true
|
||||
ImportExportSettingsModel.shared.reset()
|
||||
}) {
|
||||
Text("Import")
|
||||
}
|
||||
.disabled(!canImport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var completedAlert: Alert {
|
||||
Alert(
|
||||
title: Text("Import Completed"),
|
||||
dismissButton: .default(Text("Close")) {
|
||||
if accountsModel.isEmpty,
|
||||
let account = InstancesModel.shared.all.first?.anonymousAccount
|
||||
{
|
||||
accountsModel.setCurrent(account)
|
||||
}
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var canImport: Bool {
|
||||
return !model.selectedAccounts.isEmpty || !model.selectedInstances.isEmpty || !importExportModel.selectedExportGroups.isEmpty
|
||||
}
|
||||
|
||||
var fileModel: ImportSettingsFileModel? {
|
||||
guard let settingsFile else { return nil }
|
||||
|
||||
return ImportSettingsFileModel(url: settingsFile)
|
||||
}
|
||||
|
||||
var locationsSettingsGroupImporter: LocationsSettingsGroupImporter? {
|
||||
guard let fileModel else { return nil }
|
||||
|
||||
return fileModel.locationsSettingsGroupImporter
|
||||
}
|
||||
|
||||
struct ExportGroupRow: View {
|
||||
let group: ImportExportSettingsModel.ExportGroup
|
||||
|
||||
@ObservedObject private var model = ImportExportSettingsModel.shared
|
||||
|
||||
var body: some View {
|
||||
Button(action: { model.toggleExportGroupSelection(group) }) {
|
||||
HStack {
|
||||
Text(group.label)
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.opacity(isChecked ? 1 : 0)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.foregroundColor(.primary)
|
||||
.animation(nil, value: isChecked)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
var isChecked: Bool {
|
||||
model.selectedExportGroups.contains(group)
|
||||
}
|
||||
}
|
||||
|
||||
var importGroupView: some View {
|
||||
Group {
|
||||
Section(header: Text("Settings")) {
|
||||
ForEach(ImportExportSettingsModel.ExportGroup.settingsGroups) { group in
|
||||
ExportGroupRow(group: group)
|
||||
.disabled(!fileModel!.isGroupIncludedInFile(group))
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Other")) {
|
||||
ForEach(ImportExportSettingsModel.ExportGroup.otherGroups) { group in
|
||||
ExportGroupRow(group: group)
|
||||
.disabled(!fileModel!.isGroupIncludedInFile(group))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var metadata: some View {
|
||||
if let fileModel {
|
||||
Section(header: Text("File information")) {
|
||||
MetadataRow(name: Text("Name"), value: Text(fileModel.filename))
|
||||
|
||||
if let date = fileModel.metadataDate {
|
||||
MetadataRow(name: Text("Date"), value: Text(date))
|
||||
}
|
||||
|
||||
if let build = fileModel.metadataBuild {
|
||||
MetadataRow(name: Text("Build"), value: Text(build))
|
||||
}
|
||||
|
||||
if let platform = fileModel.metadataPlatform {
|
||||
MetadataRow(name: Text("Platform"), value: Text(platform))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MetadataRow: View {
|
||||
let name: Text
|
||||
let value: Text
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
name
|
||||
.layoutPriority(2)
|
||||
|
||||
Spacer()
|
||||
|
||||
value
|
||||
.layoutPriority(1)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var instances: [Instance] {
|
||||
locationsSettingsGroupImporter?.instances ?? []
|
||||
}
|
||||
|
||||
var accounts: [Account] {
|
||||
locationsSettingsGroupImporter?.accounts ?? []
|
||||
}
|
||||
|
||||
struct ImportInstanceRow: View {
|
||||
var instance: Instance
|
||||
var accounts: [Account]
|
||||
|
||||
@ObservedObject private var model = ImportSettingsSheetViewModel.shared
|
||||
|
||||
var body: some View {
|
||||
Button(action: { model.toggleInstance(instance, accounts: accounts) }) {
|
||||
VStack {
|
||||
Group {
|
||||
HStack {
|
||||
Text(instance.description)
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.opacity(isChecked ? 1 : 0)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
if model.isInstanceAlreadyAdded(instance) {
|
||||
HStack {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text("Custom Location already exists")
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.foregroundColor(.primary)
|
||||
.transaction { t in t.animation = nil }
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
var isChecked: Bool {
|
||||
model.isImportable(instance) && model.selectedInstances.contains(instance.id)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var importOptions: some View {
|
||||
if let fileModel {
|
||||
if fileModel.isPublicInstancesSettingsGroupInFile || !instances.isEmpty {
|
||||
Section(header: Text("Locations")) {
|
||||
if fileModel.isPublicInstancesSettingsGroupInFile {
|
||||
ExportGroupRow(group: .locationsSettings)
|
||||
}
|
||||
|
||||
ForEach(instances) { instance in
|
||||
ImportInstanceRow(instance: instance, accounts: accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !accounts.isEmpty {
|
||||
Section(header: Text("Accounts")) {
|
||||
ForEach(accounts) { account in
|
||||
ImportSettingsAccountRow(account: account, fileModel: fileModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportSettingsSheetView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImportSettingsSheetView(settingsFile: .constant(URL(string: "https://gist.githubusercontent.com/arekf/578668969c9fdef1b3828bea864c3956/raw/f794a95a20261bcb1145e656c8dda00bea339e2a/yattee-recents.yatteesettings")!))
|
||||
}
|
||||
}
|
||||
77
Shared/Settings/Import/ImportSettingsSheetViewModel.swift
Normal file
77
Shared/Settings/Import/ImportSettingsSheetViewModel.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class ImportSettingsSheetViewModel: ObservableObject {
|
||||
static let shared = ImportSettingsSheetViewModel()
|
||||
|
||||
@Published var selectedInstances = Set<Instance.ID>()
|
||||
@Published var selectedAccounts = Set<Account.ID>()
|
||||
|
||||
@Published var importableAccounts = Set<Account.ID>()
|
||||
@Published var importableAccountsPasswords = [Account.ID: String]()
|
||||
|
||||
func toggleInstance(_ instance: Instance, accounts: [Account]) {
|
||||
if selectedInstances.contains(instance.id) {
|
||||
selectedInstances.remove(instance.id)
|
||||
} else {
|
||||
guard isImportable(instance) else { return }
|
||||
selectedInstances.insert(instance.id)
|
||||
}
|
||||
|
||||
removeNonImportableFromSelectedAccounts(accounts: accounts)
|
||||
}
|
||||
|
||||
func toggleAccount(_ account: Account, accounts: [Account]) {
|
||||
if selectedAccounts.contains(account.id) {
|
||||
selectedAccounts.remove(account.id)
|
||||
} else {
|
||||
guard isImportable(account.id, accounts: accounts) else { return }
|
||||
selectedAccounts.insert(account.id)
|
||||
}
|
||||
}
|
||||
|
||||
func isSelectedForImport(_ account: Account) -> Bool {
|
||||
importableAccounts.contains(account.id) && selectedAccounts.contains(account.id)
|
||||
}
|
||||
|
||||
func isImportable(_ accountID: Account.ID, accounts: [Account]) -> Bool {
|
||||
guard let account = accounts.first(where: { $0.id == accountID }),
|
||||
let instanceID = account.instanceID,
|
||||
AccountsModel.shared.find(accountID) == nil
|
||||
else { return false }
|
||||
|
||||
return ((account.password != nil && !account.password!.isEmpty) ||
|
||||
importableAccounts.contains(account.id)) && (
|
||||
(InstancesModel.shared.find(instanceID) != nil) ||
|
||||
selectedInstances.contains(instanceID)
|
||||
)
|
||||
}
|
||||
|
||||
func isImportable(_ instance: Instance) -> Bool {
|
||||
!isInstanceAlreadyAdded(instance)
|
||||
}
|
||||
|
||||
func isInstanceAlreadyAdded(_ instance: Instance) -> Bool {
|
||||
InstancesModel.shared.find(instance.id) != nil || InstancesModel.shared.findByURLString(instance.apiURLString) != nil
|
||||
}
|
||||
|
||||
func removeNonImportableFromSelectedAccounts(accounts: [Account]) {
|
||||
selectedAccounts = Set(selectedAccounts.filter { isImportable($0, accounts: accounts) })
|
||||
}
|
||||
|
||||
func reset() {
|
||||
selectedAccounts = []
|
||||
selectedInstances = []
|
||||
importableAccounts = []
|
||||
}
|
||||
|
||||
func reset(_ importer: LocationsSettingsGroupImporter? = nil) {
|
||||
reset()
|
||||
|
||||
guard let importer else { return }
|
||||
|
||||
selectedInstances = Set(importer.instances.filter { isImportable($0) }.map(\.id))
|
||||
importableAccounts = Set(importer.accounts.filter { isImportable($0.id, accounts: importer.accounts) }.map(\.id))
|
||||
selectedAccounts = importableAccounts
|
||||
}
|
||||
}
|
||||
27
Shared/Settings/Import/ImportSettingsSheetViewModifier.swift
Normal file
27
Shared/Settings/Import/ImportSettingsSheetViewModifier.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
|
||||
struct ImportSettingsSheetViewModifier: ViewModifier {
|
||||
@Binding var isPresented: Bool
|
||||
@Binding var settingsFile: URL?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.sheet(isPresented: $isPresented) {
|
||||
ImportSettingsSheetView(settingsFile: $settingsFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportSettingsSheetViewModifier_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Text("")
|
||||
.modifier(
|
||||
ImportSettingsSheetViewModifier(
|
||||
isPresented: .constant(true),
|
||||
settingsFile: .constant(URL(string: "https://gist.githubusercontent.com/arekf/87b4d6702755b01139431dcb809f9fdc/raw/7bb5cdba3ffc0c479f5260430ddc43c4a79a7a72/yattee-177-iPhone.yatteesettings")!)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ struct PlayerControlsSettings: View {
|
||||
@Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration
|
||||
@Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration
|
||||
@Default(.systemControlsSeekDuration) private var systemControlsSeekDuration
|
||||
@Default(.playerActionsButtonLabelStyle) private var playerActionsButtonLabelStyle
|
||||
@Default(.actionButtonShareEnabled) private var actionButtonShareEnabled
|
||||
@Default(.actionButtonSubscribeEnabled) private var actionButtonSubscribeEnabled
|
||||
@Default(.actionButtonCloseEnabled) private var actionButtonCloseEnabled
|
||||
@@ -117,6 +118,15 @@ struct PlayerControlsSettings: View {
|
||||
Section(header: SettingsHeader(text: "Actions Buttons".localized())) {
|
||||
actionButtonToggles
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Action button labels", selection: $playerActionsButtonLabelStyle) {
|
||||
ForEach(ButtonLabelStyle.allCases, id: \.rawValue) { style in
|
||||
Text(style.description).tag(style)
|
||||
}
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
}
|
||||
}
|
||||
|
||||
private var systemControlsCommandsPicker: some View {
|
||||
|
||||
@@ -7,7 +7,7 @@ struct SettingsView: View {
|
||||
|
||||
#if os(macOS)
|
||||
private enum Tabs: Hashable {
|
||||
case browsing, player, controls, quality, history, sponsorBlock, locations, advanced, help
|
||||
case browsing, player, controls, quality, history, sponsorBlock, locations, advanced, importExport, help
|
||||
}
|
||||
|
||||
@State private var selection: Tabs = .browsing
|
||||
@@ -24,13 +24,22 @@ struct SettingsView: View {
|
||||
|
||||
@Default(.instances) private var instances
|
||||
|
||||
@State private var filesToShare = []
|
||||
|
||||
@ObservedObject private var navigation = NavigationModel.shared
|
||||
@ObservedObject private var settingsModel = SettingsModel.shared
|
||||
|
||||
var body: some View {
|
||||
settings
|
||||
.alert(isPresented: $model.presentingAlert) { model.alert }
|
||||
#if os(iOS)
|
||||
.backport
|
||||
.scrollDismissesKeyboardInteractively()
|
||||
#if !os(tvOS)
|
||||
.modifier(ImportSettingsFileImporterViewModifier(isPresented: $navigation.presentingSettingsFileImporter))
|
||||
.modifier(ImportSettingsSheetViewModifier(isPresented: $settingsModel.presentingSettingsImportSheet, settingsFile: $settingsModel.settingsImportURL))
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.backport
|
||||
.scrollDismissesKeyboardInteractively()
|
||||
#endif
|
||||
.alert(isPresented: $model.presentingAlert) { model.alert }
|
||||
}
|
||||
|
||||
var settings: some View {
|
||||
@@ -101,6 +110,14 @@ struct SettingsView: View {
|
||||
}
|
||||
.tag(Tabs.advanced)
|
||||
|
||||
Group {
|
||||
ExportSettings()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Export", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.tag(Tabs.importExport)
|
||||
|
||||
Form {
|
||||
Help()
|
||||
}
|
||||
@@ -110,7 +127,7 @@ struct SettingsView: View {
|
||||
.tag(Tabs.help)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 650, height: windowHeight)
|
||||
.frame(width: 700, height: windowHeight)
|
||||
#else
|
||||
NavigationView {
|
||||
settingsList
|
||||
@@ -206,6 +223,8 @@ struct SettingsView: View {
|
||||
.padding(.horizontal, 20)
|
||||
#endif
|
||||
|
||||
importView
|
||||
|
||||
Section(footer: helpFooter) {
|
||||
NavigationLink {
|
||||
Help()
|
||||
@@ -260,13 +279,35 @@ struct SettingsView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
var importView: some View {
|
||||
Section {
|
||||
Button(action: importSettings) {
|
||||
Label("Import Settings...", systemImage: "square.and.arrow.down")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.buttonStyle(.plain)
|
||||
|
||||
NavigationLink(destination: LazyView(ExportSettings())) {
|
||||
Label("Export Settings", systemImage: "square.and.arrow.up")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func importSettings() {
|
||||
navigation.presentingSettingsFileImporter = true
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private var windowHeight: Double {
|
||||
switch selection {
|
||||
case .browsing:
|
||||
return 800
|
||||
case .player:
|
||||
return 500
|
||||
return 550
|
||||
case .controls:
|
||||
return 920
|
||||
case .quality:
|
||||
@@ -278,7 +319,9 @@ struct SettingsView: View {
|
||||
case .locations:
|
||||
return 600
|
||||
case .advanced:
|
||||
return 380
|
||||
return 500
|
||||
case .importExport:
|
||||
return 580
|
||||
case .help:
|
||||
return 650
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ struct ControlsBar: View {
|
||||
@State private var shareURL: URL?
|
||||
@Binding var expansionState: ExpansionState
|
||||
|
||||
@State var gestureThrottle = Throttle(interval: 0.25) // swiftlint:disable:this swiftui_state_private
|
||||
@State var gestureThrottle = Throttle(interval: 0.25) // swiftlint:disable:this private_swiftui_state
|
||||
|
||||
var presentingControls = true
|
||||
var backgroundEnabled = true
|
||||
|
||||
@@ -21,6 +21,14 @@ struct YatteeApp: App {
|
||||
}
|
||||
|
||||
static var logsDirectory: URL {
|
||||
temporaryDirectory
|
||||
}
|
||||
|
||||
static var settingsExportDirectory: URL {
|
||||
temporaryDirectory
|
||||
}
|
||||
|
||||
private static var temporaryDirectory: URL {
|
||||
URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"Add Account..." = "إضافة حساب…";
|
||||
"Add Location" = "إضافة موقع";
|
||||
"Add Location..." = "إضافة موقع...";
|
||||
"%@ Playlist" = "%@ قائمة تشغيل";
|
||||
"%@ Channel" = "%@ قناة";
|
||||
"%@ subscribers" = "%@ المتابعين";
|
||||
"%@ Playlist" = "قائمة تشغيل %@";
|
||||
"%@ Channel" = "قناة %@";
|
||||
"%@ subscribers" = "مشتركين %@";
|
||||
"Add to %@" = "إضافة إلى %@";
|
||||
"%lld videos" = "%Lld مقاطع الفيديو";
|
||||
"%lld videos" = "مقاطع فيديو %Lld";
|
||||
"Add profile..." = "إضافة ملف تعريف...";
|
||||
"Add Quality Profile" = "إضافة ملف تعريف الجودة";
|
||||
"Add to Playlist" = "إضافة إلى قائمة تشغيل";
|
||||
@@ -23,23 +23,23 @@
|
||||
"Automatic" = "تلقائي";
|
||||
"Autoplaying Next" = "التشغيل التلقائي للتالي";
|
||||
"Are you sure you want to unsubscribe from %@?" = "هل أنت متأكد من رغبتك في إلغاء الإشتراك من ٪@؟";
|
||||
"Badge & Decreased opacity" = "شارة ونقص التعتيم";
|
||||
"Badge & Decreased opacity" = "الشارة و إنخفاض التعتيم";
|
||||
"Browsing" = "التصفح";
|
||||
"Based on system color scheme" = "بناء على نظام ألوان الجهاز";
|
||||
"Battery" = "البطارية";
|
||||
"Blue" = "أزرق";
|
||||
"Buffering stream..." = "جار تخزين بث الفيديو…";
|
||||
"Buffering stream..." = "تخزين بث الفيديو…";
|
||||
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "يمكن إرسال الأخطاء وأفكار الميزات الرائعة إلى أداة تعقب مشكلات فى GitHub. ";
|
||||
"Button" = "زر";
|
||||
"Cancel" = "إلغاء";
|
||||
"Categories to Skip" = "الفئات المطلوب تخطيها";
|
||||
"Category" = "فئة";
|
||||
"Chapters" = "الفصول";
|
||||
"Captions" = "التسميات التوضيحية";
|
||||
"Captions" = "الترجمة";
|
||||
"Cellular" = "خلوي";
|
||||
"Clear History" = "مسح سجل التاريخ";
|
||||
"Clear Search History" = "مسح سجل البحث";
|
||||
"Clear Search History..." = "مسح سجل البحث…";
|
||||
"Clear Search History" = "مسح سجل تاريخ البحث";
|
||||
"Clear Search History..." = "مسح سجل تاريخ البحث…";
|
||||
"Clear All" = "مسح الكل";
|
||||
"Clear All Recents" = "مسح جميع الأخيرة";
|
||||
"Close player when closing video" = "غلق المشغل عند غلق الفيديو";
|
||||
@@ -47,14 +47,14 @@
|
||||
"Close PiP and open player when application enters foreground" = "غلق الفيديو المصغر وفتح المشغل عندما يدخل التطبيق في المقدمة";
|
||||
"Close PiP when player is opened" = "غلق الفيديو المصغر عند فتح المشغل";
|
||||
"Close Video" = "غلق الفيديو";
|
||||
"Close video after playing last in the queue" = "غلق الفيديو عند إنتهاء الفيديو الاخير فى قائمة الانتظار";
|
||||
"Close video after playing last in the queue" = "غلق الفيديو عند إنتهاء الفيديو الأخير فى قائمة الإنتظار";
|
||||
"Comments" = "التعليقات";
|
||||
"Connection failed" = "فشل الاتصال";
|
||||
"Continue" = "الإستمرار";
|
||||
"Connected successfully (%@)" = "تم الاتصال بنجاح (%@)";
|
||||
"Country" = "البلد";
|
||||
"Country Name or Code" = "اسم الدولة أو الرمز";
|
||||
"Copy %@ link" = "نسخ الرابط: %@";
|
||||
"Copy %@ link" = "نسخ رابط %@";
|
||||
"Contributing" = "المساهمة";
|
||||
"Contact" = "التواصل";
|
||||
"Continue from %@" = "الإستمرار من %@";
|
||||
@@ -71,8 +71,8 @@
|
||||
|
||||
/* Video sort order in search */
|
||||
"Date" = "تاريخ";
|
||||
"Decrease rate" = "انخفاض معدل";
|
||||
"Decreased opacity" = "انخفاض التعتيم";
|
||||
"Decrease rate" = "إنخفاض معدل";
|
||||
"Decreased opacity" = "إنخفاض التعتيم";
|
||||
"Enable logging" = "تمكين التسجيل";
|
||||
"Discord Server" = "خادم Discord";
|
||||
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "تجري المناقشات في Discord و Matrix. إنه مكان جيد للأسئلة العامة.";
|
||||
@@ -97,7 +97,7 @@
|
||||
"Help" = "مساعدة";
|
||||
"Hide sidebar" = "إخفاء الشريط الجانبي";
|
||||
"Highest" = "الأعلى";
|
||||
"Highest quality" = "أعلى جودة";
|
||||
"Highest quality" = "جودة أعلى";
|
||||
"History" = "سجل التاريخ";
|
||||
|
||||
/* Video date filter in search */
|
||||
@@ -113,11 +113,11 @@
|
||||
"Info" = "معلومات";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Interaction" = "تفاعل";
|
||||
"Interface" = "واجهه المستخدم";
|
||||
"Interaction" = "التفاعل";
|
||||
"Interface" = "واجهة المستخدم";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Intro" = "مقدمة";
|
||||
"Intro" = "المقدمة";
|
||||
"Issues Tracker" = "تعقب المشاكل";
|
||||
|
||||
/* Selected video has just finished playing */
|
||||
@@ -129,7 +129,7 @@
|
||||
"LIVE" = "مباشر";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Loading streams..." = "جارٍ تحميل بثوث ...";
|
||||
"Loading streams..." = "تحميل بثوث...";
|
||||
"Loading..." = "تحميل...";
|
||||
|
||||
/* Video duration filter in search */
|
||||
@@ -138,8 +138,8 @@
|
||||
"Low quality" = "جودة منخفضة";
|
||||
"Lowest" = "الأدنى";
|
||||
"Mark as watched" = "وضع علامة تمت المشاهدة";
|
||||
"Mark video as watched after playing" = "ضع علامة على الفيديو تمت المشاهدة بعد التشغيل";
|
||||
"Mark watched videos with" = "وضع علامة على مقاطع الفيديو التي تمت المشاهدة باستخدام";
|
||||
"Mark video as watched after playing" = "وضع علامة تمت المشاهدة على الفيديو بعد التشغيل";
|
||||
"Mark watched videos with" = "وضع علامة تمت المشاهدة على مقاطع الفيديو باستخدام";
|
||||
"Matrix Channel" = "قناة Matrix";
|
||||
"Matrix Chat" = "دردشة Matrix";
|
||||
"More info can be found in:" = "يمكن العثور على مزيد من المعلومات في:";
|
||||
@@ -163,11 +163,11 @@
|
||||
"Open Settings" = "فتح الإعدادات";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Opening %@ stream..." = "فتح %@ بث ...";
|
||||
"Opening audio stream..." = "يتم فتح بث صوتي ...";
|
||||
"Opening %@ stream..." = "فتح بث %@ ...";
|
||||
"Opening audio stream..." = "فتح بث صوتي...";
|
||||
"Orientation" = "اتجاه";
|
||||
"Play in PiP" = "تشغيل في الفيديو المصغر";
|
||||
"Play Last" = "تشغيل الاخير";
|
||||
"Play Last" = "تشغيل الأخير";
|
||||
"Play Music" = "تشغل الموسيقى";
|
||||
"Play Next" = "تشغيل التالى";
|
||||
"Play Now" = "تشغيل الآن";
|
||||
@@ -176,16 +176,16 @@
|
||||
"Playlist" = "قائمة تشغيل";
|
||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "سيتم حذف قائمة تشغيل \"%@\".\nلا يمكن التراجع عنه.";
|
||||
"Playlists" = "قوائم تشغيل";
|
||||
"Popular" = "شعبي";
|
||||
"Popular" = "محتوى ذو شعبية";
|
||||
"Preferred Formats" = "التنسيقات المفضلة";
|
||||
"Profiles" = "ملفات تعريف";
|
||||
"Profiles" = "ملفات التعريف";
|
||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "الترويج لمنتج أو خدمة مرتبطة مباشرة بمنشئ المحتوى نفسه. يتضمن هذا عادة البضائع أو الترويج للمنصات التي يتم تحقيق الدخل منها.";
|
||||
"Public Locations" = "المواقع العامة";
|
||||
"Public Manifest" = "البيان العام";
|
||||
"Quality" = "الجودة";
|
||||
"Quality Profile" = "ملف تعريف الجودة";
|
||||
"Queue" = "قائمة الانتظار";
|
||||
"Queue is empty" = "قائمة الانتظار فارغة";
|
||||
"Queue" = "قائمة الإنتظار";
|
||||
"Queue is empty" = "قائمة الإنتظار فارغة";
|
||||
"Rate" = "معدل";
|
||||
|
||||
/* Video sort order in search */
|
||||
@@ -203,19 +203,19 @@
|
||||
"Remove from Favorites" = "إزالة من المفضلة";
|
||||
"Remove from history" = "إزالة من سجل التاريخ";
|
||||
"Remove from Playlist" = "إزالة من قائمة تشغيل";
|
||||
"Remove from the queue" = "إزالة من قائمة الانتظار";
|
||||
"Remove from the queue" = "إزالة من قائمة الإنتظار";
|
||||
"Replies" = "الردود";
|
||||
"Reset" = "إعادة تعيين";
|
||||
"Reset search filters" = "إعادة تعيين عوامل تصفية البحث";
|
||||
"Reset watched status when playing again" = "إعادة تعيين حالة المشاهدة عند التشغيل مرة أخرى";
|
||||
"Resolution" = "دقة";
|
||||
"Restart" = "إعادة تشغيل";
|
||||
"Restart the app to apply the settings above." = "أعد تشغيل التطبيق لتطبيق الإعدادات أعلاه.";
|
||||
"Restart" = "إعادة بدء التشغيل";
|
||||
"Restart the app to apply the settings above." = "إعادة بدء تشغيل التطبيق لتطبيق الإعدادات أعلاه.";
|
||||
"Restore default profiles..." = "استعادة ملفات التعريف الافتراضية ...";
|
||||
"Round corners" = "زوايا مستديرة";
|
||||
"Save" = "حفظ";
|
||||
"Save history of searches, channels and playlists" = "حفظ سجل عمليات البحث والقنوات وقوائم تشغيل";
|
||||
"Search history is empty" = "سجل البحث فارغ";
|
||||
"Save history of searches, channels and playlists" = "حفظ سجل تاريخ عمليات البحث والقنوات وقوائم تشغيل";
|
||||
"Search history is empty" = "سجل تاريخ البحث فارغ";
|
||||
"Search..." = "بحث...";
|
||||
"Sections" = "الأقسام";
|
||||
"Select location closest to you:" = "حدد أقرب موقع إليك:";
|
||||
@@ -224,7 +224,7 @@
|
||||
"Self-promotion" = "الترويج الذاتي";
|
||||
"Settings" = "الإعدادات";
|
||||
"Share %@ link" = "مشاركة رابط %@";
|
||||
"Share %@ link with time" = "مشاركة الرابط %@ مع الوقت";
|
||||
"Share %@ link with time" = "مشاركة رابط %@ مع الوقت";
|
||||
"Share..." = "مشاركة...";
|
||||
|
||||
/* Video duration filter in search */
|
||||
@@ -243,22 +243,22 @@
|
||||
|
||||
/* Player controls layout size */
|
||||
"Small" = "صغير";
|
||||
"Sort" = "صنف";
|
||||
"Sort" = "فرز";
|
||||
"Sort: %@" = "فرز: %@";
|
||||
"Source" = "المصدر";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Sponsor" = "راعي";
|
||||
"Sponsor" = "الراعي";
|
||||
"SponsorBlock" = "حظر الإعلانات";
|
||||
"SponsorBlock API Instance" = "مثيل واجهة برمجة تطبيقات حظر الإعلانات";
|
||||
"Subscribe" = "الإشتراك";
|
||||
|
||||
/* Subscriptions title */
|
||||
"Subscriptions" = "الاشتراكات";
|
||||
"Subscriptions" = "الإشتراكات";
|
||||
"Switch to other public location" = "التبديل إلى موقع عام آخر";
|
||||
"System controls buttons" = "أزرار نظام عناصر التحكم";
|
||||
"This cannot be reverted" = "هذا لا يمكن تغييره";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "لا يمكن التراجع عن هذا. قد تحتاج إلى التبديل بين طرق العرض أو إعادة تشغيل التطبيق للاطلاع على التغييرات.";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "لا يمكن التراجع عن هذا. قد تحتاج إلى التبديل بين طرق العرض أو إعادة بدء تشغيل التطبيق للاطلاع على التغييرات.";
|
||||
"This information will be processed only on your device and used to connect you to the server in the specified country." = "ستتم معالجة هذه المعلومات فقط على جهازك واستخدامها لتوصيلك بالخادم في البلد المحدد.";
|
||||
"Upload date" = "تاريخ الرفع";
|
||||
"URL" = "الرابط";
|
||||
@@ -285,19 +285,19 @@
|
||||
"When partially watched video is played" = "عند تشغيل الفيديو الذي تمت المشاهدة جزئيا";
|
||||
"Wi-Fi" = "واي فاي";
|
||||
"Yattee" = "Yattee";
|
||||
"Yattee %@ (build %@)" = "Yattee%@ (بناء%@)";
|
||||
"Yattee %@ (build %@)" = "Yattee %@ (بناء %@)";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Year" = "سنة";
|
||||
"You can find information about using Yattee in the Wiki pages." = "يمكنك العثور على معلومات حول استخدام Yattee في صفحات الWiki.";
|
||||
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "يمكنك استخدام التحديد التلقائي لملف التعريف استنادا إلى حالة الجهاز الحالية أو تبديله في عناصر التحكم في إعدادات تشغيل الفيديو.";
|
||||
"You have no Playlists" = "ليس لديك قوائم تشغيل";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "ليس لديك قوائم تشغيل\n\nاضغط على \"قائمة تشغيل جديدة\" لإنشاء واحدة";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "ليس لديك قوائم تشغيل\n\nالضغط على \"قائمة تشغيل جديدة\" لإنشاء واحدة";
|
||||
"You need to create an instance and accounts\nto access %@ section" = "تحتاج إلى إنشاء مثيل وحسابات\nللوصول إلى قسم %@";
|
||||
"You need to select an account\nto access %@ section" = "تحتاج إلى تحديد حساب\nللوصول إلى قسم %@";
|
||||
"Unlisted" = "غير مدرج";
|
||||
"Now Playing" = "يعرض الآن";
|
||||
"Playback queue is empty" = "قائمة انتظار التشغيل فارغة";
|
||||
"Playback queue is empty" = "قائمة الإنتظار التشغيل فارغة";
|
||||
"Playing Next" = "تشغيل التالي";
|
||||
"Add Channels, Playlists and Searches to Favorites using" = "إضافة القنوات، قوائم تشغيل، عمليات البحث إلى المفضلة باستخدام";
|
||||
"Make default" = "جعله افتراضي";
|
||||
@@ -305,23 +305,23 @@
|
||||
"Stream & Player" = "بث و تشغيل";
|
||||
"Statistics" = "إحصائيات";
|
||||
"Hardware decoder" = "وحدة فك ترميز الأجهزة";
|
||||
"Rate & Captions" = "التقييم والتسميات التوضيحية";
|
||||
"Rate & Captions" = "معدل سرعة التشغيل و الترجمة";
|
||||
"Dropped frames" = "الإطارات المتساقطة";
|
||||
"Stream FPS" = "عدد الإطارات فى الثانية فى البث";
|
||||
"Stream FPS" = "عدد الإطارات في الثانية في البث";
|
||||
"Any format" = "أي شكل";
|
||||
"%@ formats" = "%@ تنسيقات";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "قائمة تشغيل فارغة\n\nاضغط مع الاستمرار على مقطع الفيديو ثم\n\"إضافة إلى قائمة تشغيل\"";
|
||||
"Press and hold remote button to open captions and quality menus" = "اضغط مع الاستمرار على زر التحكم عن بعد لفتح التسميات التوضيحية وقوائم الجودة";
|
||||
"%@ formats" = "تنسيقات %@";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "قائمة تشغيل فارغة\n\nالضغط مع الإستمرار على مقطع الفيديو ثم\n\"إضافة إلى قائمة تشغيل\"";
|
||||
"Press and hold remote button to open captions and quality menus" = "الضغط مع الإستمرار على زر التحكم عن بعد لفتح الترجمة وقوائم الجودة";
|
||||
"Comments are disabled" = "التعليقات معطلة";
|
||||
"No comments" = "لا توجد تعليقات";
|
||||
"No chapters information available" = "لا توجد معلومات متاحة عن الفصول";
|
||||
"Open logs in Finder" = "فتح السجلات في Finder";
|
||||
"Could not refresh Subscriptions" = "تعذر تحديث الاشتراكات";
|
||||
"Could not refresh Subscriptions" = "تعذر تحديث الإشتراكات";
|
||||
"Could not load streams" = "تعذر تحميل البثوث";
|
||||
"Could not extract channel information" = "تعذر استخراج معلومات القناة";
|
||||
"Could not extract SID from received cookies: %@" = "تعذر استخراج SID من ملفات تعريف الارتباط المستلمة: %@";
|
||||
"Could not update your token." = "تعذر تحديث الرمز المميز الخاص بك.";
|
||||
"Could not refresh Trending" = "تعذر تحديث \"المحتويات الشائعة\"";
|
||||
"Could not refresh Trending" = "تعذر تحديث المحتوى الرائج";
|
||||
"For custom locations you can configure Frontend URL in Locations settings" = "بالنسبة للمواقع المخصصة، يمكنك تكوين عنوان URL للواجهة الأمامية في إعدادات المواقع";
|
||||
"This URL could not be opened" = "تعذر فتح عنوان URL هذا";
|
||||
"Could not open channel" = "تعذر فتح القناة";
|
||||
@@ -349,7 +349,7 @@
|
||||
"Show Inspector" = "إظهار المفتش";
|
||||
"Reload manifest" = "إعادة تحميل البيان";
|
||||
"Open" = "فتح";
|
||||
"Video actions buttons" = "أزرار إجراءات الفيديو";
|
||||
"Video actions buttons" = "أزرار إجراءات مقطع الفيديو";
|
||||
"Open Files" = "فتح الملفات";
|
||||
"Channels" = "القنوات";
|
||||
"Channel" = "القناة";
|
||||
@@ -365,27 +365,27 @@
|
||||
"Remove…" = "إزالة…";
|
||||
"Show sidebar" = "إظهار الشريط الجانبي";
|
||||
"Remove Location" = "إزالة الموقع";
|
||||
" subscribers" = " المتابعين";
|
||||
" subscribers" = " المشتركين";
|
||||
"Accounts" = "الحسابات";
|
||||
"10 seconds forwards/backwards" = "10 ثوان إلى الأمام/للخلف";
|
||||
|
||||
/* Video date filter in search
|
||||
Video duration filter in search */
|
||||
"Any" = "أي";
|
||||
"Are you sure you want to clear search history?" = "هل أنت متأكد من أنك تريد مسح سجل البحث؟";
|
||||
"Are you sure you want to clear search history?" = "هل أنت متأكد من أنك تريد مسح سجل تاريخ البحث؟";
|
||||
"Add to Favorites" = "إضافة إلى المفضلة";
|
||||
"Anonymous" = "مجهول";
|
||||
"Are you sure you want to delete playlist?" = "هل أنت متأكد من أنك تريد حذف قائمة تشغيل؟";
|
||||
"Are you sure you want to clear history of watched videos?" = "هل أنت متأكد من أنك تريد مسح سجل مقاطع الفيديو التي تمت المشاهدة؟";
|
||||
"Clear the queue" = "مسح قائمة الانتظار";
|
||||
"Are you sure you want to clear history of watched videos?" = "هل أنت متأكد من أنك تريد مسح سجل تاريخ مقاطع الفيديو التي تمت المشاهدة؟";
|
||||
"Clear the queue" = "مسح قائمة الإنتظار";
|
||||
"Charging" = "جاري الشحن";
|
||||
"Close" = "غلق";
|
||||
"Close player when starting PiP" = "غلق المشغل عند فتح الفيديو المصغر";
|
||||
"Close player when starting PiP" = "غلق المشغل عند بدء الفيديو المصغر";
|
||||
"High" = "عالي";
|
||||
"Badge color" = "لون الشارة";
|
||||
"Accounts are not supported for the application of this instance" = "الحسابات غير مدعومة للتطبيق الخاص بهذه الحالة";
|
||||
"Backend" = "الواجهة الخلفية";
|
||||
"Badge" = "شارة";
|
||||
"Badge" = "الشارة";
|
||||
"Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
|
||||
"Filter" = " عامل التصفية";
|
||||
@@ -414,7 +414,7 @@
|
||||
"Picture in Picture" = "الفيديو المصغر";
|
||||
"Play All" = "تشغيل الكل";
|
||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "سيتم تحديد التنسيقات بالترتيب كما هو مدرج.\nHLS هو تنسيق قابل للتكيف (لا ينطبق إعداد الدقة).";
|
||||
"Restart/Play next" = "أعد التشغيل / تشغيل التالي";
|
||||
"Restart/Play next" = "إعادة بدء التشغيل / تشغيل التالي";
|
||||
"Rotate to portrait when exiting fullscreen" = "التدوير إلى عمودي عند الخروج من وضع ملء الشاشة";
|
||||
"Save history of played videos" = "حفظ سجل تاريخ مقاطع الفيديو المشغلة";
|
||||
"Search" = "بحث";
|
||||
@@ -430,12 +430,12 @@
|
||||
|
||||
/* Player controls layout size for TV */
|
||||
"TV" = "تلفاز";
|
||||
"unknown" = "مجهول";
|
||||
"unknown" = "غير معروف";
|
||||
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "سيؤدي هذا إلى إزالة جميع ملفات التعريف الخاصة بك وإرجاع قيمها الافتراضية. لا يمكن التراجع عن هذا.";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Today" = "اليوم";
|
||||
"Trending" = "الشائع";
|
||||
"Trending" = "المحتوى الرائج";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "عادةً ما تكون بالقرب من نهاية الفيديو عند ظهور قائمة الأسماء و / أو ظهور بطاقات النهاية.";
|
||||
"Unsubscribe" = "إلغاء الإشتراك";
|
||||
|
||||
@@ -445,17 +445,17 @@
|
||||
"Current Location" = "الموقع الحالي";
|
||||
"You can switch between profiles in playback settings controls." = "يمكنك التبديل بين ملفات التعريف في عناصر التحكم في إعدادات التشغيل.";
|
||||
"Current Playlist" = "قائمة تشغيل الحالية";
|
||||
"Keep last played video in the queue after restart" = "احتفظ بآخر فيديو تم تشغيله في قائمة الانتظار بعد إعادة التشغيل";
|
||||
"Keep last played video in the queue after restart" = "الإحتفاظ بآخر فيديو تم تشغيله في قائمة الإنتظار بعد إعادة بدء التشغيل";
|
||||
"Cached time" = "الوقت المخزن مؤقتا";
|
||||
"It can be changed later in settings. You can use your own locations too." = "يمكن تغييره لاحقا في الإعدادات. يمكنك استخدام المواقع الخاصة بك أيضا.";
|
||||
"Could not open video" = "تعذر فتح الفيديو";
|
||||
"Channel could not be found" = "تعذر العثور على القناة";
|
||||
"Could not refresh Popular" = "تعذر تحديث المحتويات الشعبية";
|
||||
"Could not refresh Popular" = "تعذر تحديث المحتوى ذو الشعبية";
|
||||
"Files" = "الملفات";
|
||||
"Could not create share link" = "تعذر إنشاء رابط مشاركة";
|
||||
"Show Home" = "إظهار الصفحة الرئيسية";
|
||||
"Video Details" = "تفاصيل الفيديو";
|
||||
"Clear Queue before opening" = "مسح قائمة الانتظار قبل الفتح";
|
||||
"Video Details" = "تفاصيل مقطع الفيديو";
|
||||
"Clear Queue before opening" = "مسح قائمة الإنتظار قبل الفتح";
|
||||
"Current: %@\n%@" = "الحالي: %@\n%@";
|
||||
"Thumbnails" = "الصور المصغرة";
|
||||
"Show Open Videos toolbar button" = "أظهار زر شريط الأدوات لمقاطع الفيديو المفتوحة";
|
||||
@@ -488,12 +488,12 @@
|
||||
"Actions buttons" = "أزرار الإجراءات";
|
||||
"Locations Manifest" = "بيان المواقع";
|
||||
"Default Profile" = "الملف الشخصي الإفتراضي";
|
||||
"Playback history is empty" = "سجل التشغيل فارغ";
|
||||
"Copy%@link" = "نسخ الرابط %@";
|
||||
"Share%@link" = "مشاركة الرابط %@";
|
||||
"Playback history is empty" = "سجل تاريخ التشغيل فارغ";
|
||||
"Copy%@link" = "نسخ رابط %@";
|
||||
"Share%@link" = "مشاركة رابط %@";
|
||||
"Instance of current account" = "مثيل الحساب الحالي";
|
||||
"Seek gesture sensitivity" = "حساسية إيماءة التقديم";
|
||||
"Video" = "الفيديو";
|
||||
"Video" = "مقطع الفيديو";
|
||||
"Audio" = "الصوت";
|
||||
"Honor orientation lock" = "قفل توجيه الشرف";
|
||||
"Proxy videos" = "وكيل مقاطع الفيديو";
|
||||
@@ -504,20 +504,20 @@
|
||||
"Sample Rate" = "معدل العينة";
|
||||
"Short videos: visible" = "مقاطع الفيديو القصيرة: مرئية";
|
||||
"Show channel avatars in channels lists" = "إظهار الصور الرمزية للقناة في قوائم القنوات";
|
||||
"Show channel avatars in videos lists" = "أظهر الصور الرمزية للقناة في قوائم مقاطع الفيديو";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = "احتفظ بالقنوات التي تحتوي على مقاطع الفيديو غير المشاهدة على رأس قائمة الاشتراكات";
|
||||
"Show video context menu options to force selected backend" = "أظهر خيارات قائمة سياق الفيديو لفرض الواجهة الخلفية المحددة";
|
||||
"Show channel avatars in videos lists" = "إظهار الصور الرمزية للقناة في قوائم مقاطع الفيديو";
|
||||
"Keep channels with unwatched videos on top of subscriptions list" = " الإحتفاظ بالقنوات التي تحتوي على مقاطع الفيديو غير المشاهدة على رأس قائمة الإشتراكات";
|
||||
"Show video context menu options to force selected backend" = "إظهار خيارات قائمة سياق الفيديو لفرض الواجهة الخلفية المحددة";
|
||||
"Play Now in AVPlayer" = "تشغيل الآن في AVPlayer";
|
||||
"Play Now in MPV" = "تشغيل الآن في MPV";
|
||||
"Enter account credentials to connect..." = "أدخل بيانات اعتماد الحساب للاتصال...";
|
||||
"Seek" = "التقديم";
|
||||
"Show scroll to top button in comments" = "أظهر التمرير إلى الزر العلوي في التعليقات";
|
||||
"Show scroll to top button in comments" = "إظهار التمرير إلى الزر العلوي في التعليقات";
|
||||
"Enter location address to connect..." = "أدخل عنوان الموقع للإتصال...";
|
||||
"File Extension" = "ملحق الملف";
|
||||
"Public account" = "حساب عام";
|
||||
"Your Accounts" = "حساباتك";
|
||||
"Browse without account" = "تصفح بدون حساب";
|
||||
"Mark channel feed as unwatched" = "ضع علامة على تغذية القناة لم تتم المشاهدة";
|
||||
"Mark channel feed as unwatched" = "وضع علامة لم تتم المشاهدة على محتوى القناة";
|
||||
"Play all unwatched" = "تشغيل كل شيء لم تتم المشاهدة";
|
||||
"Player Bar" = "شريط المشغل";
|
||||
"Double tap gesture" = "إيماءة بنقرة المزدوجة";
|
||||
@@ -526,15 +526,15 @@
|
||||
"Clear all" = "مسح الكل";
|
||||
"Maximum width expanded" = "الحد الأقصى للعرض الموسع";
|
||||
"Single tap gesture" = "إيماءة بنقرة الواحدة";
|
||||
"Tap and hold channel thumbnail to open context menu with more actions" = " اضغط مع الاستمرار على الصورة المصغرة للقناة لفتح قائمة السياق مع المزيد من الإجراءات";
|
||||
"Tap and hold channel thumbnail to open context menu with more actions" = " الضغط مع الإستمرار على الصورة المصغرة للقناة لفتح قائمة السياق مع المزيد من الإجراءات";
|
||||
"Right click channel thumbnail to open context menu with more actions" = "نقر زر الماوس الأيمن فوق الصورة المصغرة للقناة لفتح قائمة السياق مع المزيد من الإجراءات";
|
||||
"Seeking" = "التقديم";
|
||||
"Gesture: fowards" = "إيماءة: إلى الأمام";
|
||||
"Show unwatched feed badges" = "أظهر شارات التغذية التي لم تتم المشاهدة";
|
||||
"Show unwatched feed badges" = "إظهار شارات المحتوى التي لم تتم المشاهدة";
|
||||
"Controls button: forwards" = "زر عناصر التحكم: إلى الأمام";
|
||||
"Gesture: backwards" = "إيماءة: للخلف";
|
||||
"Hide player" = "إخفاء المشغل";
|
||||
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لإيماءة النقر المزدوج على الجانب الأيسر / الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة التشغيل.";
|
||||
"Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لإيماءة النقر المزدوج على الجانب الأيسر / الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
||||
"Controls Buttons" = "أزرار عناصر التحكم";
|
||||
"Play next item" = "تشغيل العنصر التالي";
|
||||
"Lock orientation" = "إقفال التدوير";
|
||||
@@ -543,9 +543,9 @@
|
||||
"Total size: %@" = "الحجم الإجمالي: %@";
|
||||
"Actions Buttons" = "أزرار الإجراءات";
|
||||
"Subscribe/Unsubscribe" = "الإشتراك/إلغاء الإشتراك";
|
||||
"Show cache status" = "أظهر حالة ذاكرة التخزين المؤقت";
|
||||
"Show cache status" = "إظهار حالة ذاكرة التخزين المؤقت";
|
||||
"Cache" = "ذاكرة التخزين المؤقت";
|
||||
"Maximum feed items" = "الحد الأقصى لعناصر التغذية";
|
||||
"Maximum feed items" = "الحد الأقصى لعناصر المحتوى";
|
||||
"Open channels with description expanded" = "فتح القنوات مع الوصف موسعاً";
|
||||
"Close video and player on end" = "غلق الفيديو والمشغل عند النهاية";
|
||||
"Use system controls with AVPlayer" = "استخدم نظام عناصر التحكم مع AVPlayer";
|
||||
@@ -556,12 +556,12 @@
|
||||
"System controls" = "نظام عناصر التحكم";
|
||||
"Controls button: backwards" = "زر عناصر التحكم: للخلف";
|
||||
"Are you sure you want to clear cache?" = "هل أنت متأكد من أنك تريد مسح ذاكرة التخزين المؤقت؟";
|
||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لأزرار الأسهم عن بعد (للجيل الثاني من Siri Remote أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة التشغيل.";
|
||||
"Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة الفاصل الزمني للتخطي لأزرار الأسهم عن بعد (للجيل الثاني من Siri Remote أو أحدث). تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
||||
"Opened File" = "ملف مفتوح";
|
||||
"Opening file..." = "فتح الملف...";
|
||||
"Short videos: hidden" = "مقاطع الفيديو القصيرة: مخفية";
|
||||
"Mark channel feed as watched" = "ضع علامة على تغذية القناة تمت المشاهدة";
|
||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة التشغيل.";
|
||||
"Mark channel feed as watched" = "وضع علامة تمت المشاهدة على محتوى القناة";
|
||||
"Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart." = "التحكم في إعدادات إيماءة فترة التخطي للنقر المزدوج على الجانب الأيسر/الأيمن من المشغل. تغيير إعدادات نظام عناصر التحكم يتطلب إعادة بدء التشغيل.";
|
||||
"Available" = "متوفر";
|
||||
"Startup section" = "قسم بدء التشغيل";
|
||||
"Home Settings" = "إعدادات الصفحة الرئيسية";
|
||||
@@ -571,23 +571,23 @@
|
||||
"(watched and shorts hidden)" = "(تمت المشاهدة ومقاطع قصيرة مخفية)";
|
||||
"(shorts hidden)" = "(مقاطع قصيرة مخفية)";
|
||||
"Disable filters" = "تعطيل عوامل التصفية";
|
||||
"(watched hidden)" = "(تم المشاهدة مخفية)";
|
||||
"(watched hidden)" = "(تمت المشاهدة مخفية)";
|
||||
"Limit" = "حد";
|
||||
"Are you sure you want to remove %@ from Favorites?" = "هل أنت متأكد من أنك تريد إزالة %@ من المفضلة؟";
|
||||
"List" = "قائمة";
|
||||
"Cells" = "خلايا";
|
||||
"Toggle size" = "حجم التبديل";
|
||||
"Toggle player" = "تبديل المشغل";
|
||||
"Show Next in Queue" = "أظهر التالي في قائمة الانتظار";
|
||||
"Show Next in Queue" = "إظهار التالي في قائمة الإنتظار";
|
||||
"Show toggle watch status button" = "إظهار تبديل زر حالة الساعة";
|
||||
"Next in Queue" = "التالي في قائمة الإنتظار";
|
||||
"Do nothing" = "لا تفعل شيئا";
|
||||
"Open channel" = "فتح القناة";
|
||||
"Inspector" = "مفتش";
|
||||
"Open video description expanded" = "فتح وصف الفيديو موسعاً";
|
||||
"Feed" = "تغذية";
|
||||
"Mark all as unwatched" = "ضع علامة على الجميع لم تتم المشاهدة";
|
||||
"Mark all as watched" = "ضع علامة على الجميع تمت المشاهدة";
|
||||
"Feed" = "محتوى الإشتراكات";
|
||||
"Mark all as unwatched" = "وضع علامة لم تتم المشاهدة على الجميع";
|
||||
"Mark all as watched" = "وضع علامة تمت المشاهدة على الجميع";
|
||||
"Queue - shuffled" = "قائمة الإنتظار - تم خلطها";
|
||||
"Replay" = "إعادة المشغل";
|
||||
"Lock" = "قفل";
|
||||
@@ -596,7 +596,7 @@
|
||||
"Playback Settings" = "إعدادات التشغيل";
|
||||
"Description" = "وصف";
|
||||
"Autoplay next" = "التشغيل التلقائي التالي";
|
||||
"Stream" = "بث";
|
||||
"Stream" = "البث";
|
||||
"Chapters (if available)" = "الفصول (إن وجدت)";
|
||||
"Open vertical chapters expanded" = "فتح الفصول الرأسية موسعاً";
|
||||
"No preview" = "لا توجد معاينة";
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
" subscribers" = " 人の登録者";
|
||||
"%@ subscribers" = "%@ 人の登録者";
|
||||
"Accounts are not supported for the application of this instance" = "このインスタンスはアカウントに対応していません";
|
||||
"%lld videos" = "本の動画";
|
||||
"%lld videos" = "%lld本の動画";
|
||||
"%@ Channel" = "%@ チャンネル";
|
||||
"%@ Playlist" = "%@ 再生リスト";
|
||||
"Add Location" = "場所を追加";
|
||||
|
||||
@@ -53,10 +53,209 @@
|
||||
"Autoplaying Next" = "Volgende automatisch afspelen";
|
||||
"Cellular" = "Mobiele data";
|
||||
"Add to Playlist" = "Toevoegen aan afspeellijst";
|
||||
"Browsing" = "Zoeken";
|
||||
"Browsing" = "Bladeren";
|
||||
"Captions" = "Ondertiteling";
|
||||
"Advanced" = "Geavanceerd";
|
||||
|
||||
/* Video date filter in search
|
||||
Video duration filter in search */
|
||||
"Any" = "Elk";
|
||||
"If you are reporting a bug, include all relevant details (especially: app version, used device and system version, steps to reproduce)." = "Als je een fout meldt, voeg dan alle relevantie details toe (in het bijzonder: versienummer van de app, gebruikte apparaat en systeemversie, stappen om te reproduceren).";
|
||||
"Opening audio stream..." = "Audio stream aan het openen...";
|
||||
"Part of a video promoting a product or service not directly related to the creator. The creator will receive payment or compensation in the form of money or free products." = "Deel van een video dat een product of dienst promoot dat niet direct van de maker van de video is. De maker ontvangt compensatie in de vorm van betaling of gratis producten.";
|
||||
"Pause when player is closed" = "Pauzeer bij sluiten van de speler";
|
||||
"Remove" = "Verwijder";
|
||||
"Restart" = "Herstart";
|
||||
"Round corners" = "Ronde hoeken";
|
||||
"Restart the app to apply the settings above." = "Herstart de app om deze instellingen toe te passen.";
|
||||
"Save" = "Opslaan";
|
||||
"Close Video" = "Sluit Video";
|
||||
"Close video after playing last in the queue" = "Sluit video na het laatste item in de wachtrij";
|
||||
"Contact" = "Contact";
|
||||
"Copy %@ link" = "Kopieer %@ link";
|
||||
"Copy %@ link with time" = "Kopieer %@ link inclusief tijd";
|
||||
"Custom Locations" = "Aangepaste locaties";
|
||||
"Continue" = "Verder";
|
||||
|
||||
/* Video sort order in search */
|
||||
"Date" = "Datum";
|
||||
"Contributing" = "Bijdragen";
|
||||
"Controls" = "Bediening";
|
||||
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussies vinden plaats op Discord en Matrix. Je kan er terecht voor algemene vragen.";
|
||||
"Enter fullscreen in landscape" = "Open volledig scherm in landschapmodus";
|
||||
"Error when accessing playlist" = "Fout bij openen afspeellijst";
|
||||
"Donations" = "Donaties";
|
||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "Formaten worden geselecteerd in de genoemde volgorde.\nHLS is een adaptief formaat (instellingen voor resolutie worden niet toegepast).";
|
||||
"Backend" = "Backend";
|
||||
"Badge & Decreased opacity" = "Embleem & Verminderde helderheid";
|
||||
"Close PiP and open player when application enters foreground" = "Sluit Beeld in Beeld en open speler wanneer de app op de voorgrond treedt";
|
||||
"Close PiP when player is opened" = "Sluit Beeld in Beeld wanneer de speler wordt geopend";
|
||||
"Close PiP when starting playing other video" = "Sluit Beeld in Beeld wanneer een andere video begint te spelen";
|
||||
"Close player when closing video" = "Sluit speler bij het sluiten van een video";
|
||||
"Close player when starting PiP" = "Sluit speler bij het starten van Beeld in Beeld";
|
||||
"Comments" = "Commentaar";
|
||||
"Connected successfully (%@)" = "Succesvol verbonden (%@)";
|
||||
"Connection failed" = "Verbinding mislukt";
|
||||
"Continue from %@" = "Verder vanaf %@";
|
||||
"Could not load locations manifest" = "Kon locatie manifest niet laden";
|
||||
"Country" = "Land";
|
||||
"Country Name or Code" = "Landnaam of -code";
|
||||
"Create Playlist" = "Creëer afspeellijst";
|
||||
"Current: %@\n%@" = "Huidig: %@\n%@";
|
||||
|
||||
/* Locations settings, custom instance is selected as current */
|
||||
"Custom" = "Aangepast";
|
||||
"Decreased opacity" = "Verlaagde helderheid";
|
||||
"Delete" = "Verwijder";
|
||||
"Disabled" = "Uitgeschakeld";
|
||||
"Don't use public locations" = "Gebruik geen publieke locaties";
|
||||
"Decrease rate" = "Verlaag tempo";
|
||||
"Done" = "Klaar";
|
||||
"Duration" = "Duur";
|
||||
"Edit" = "Bewerken";
|
||||
"Edit Playlist" = "Bewerk Afspeellijst";
|
||||
"Edit Quality Profile" = "Bewerk Kwaliteitsprofiel";
|
||||
"Edit..." = "Bewerk...";
|
||||
"Enable Return YouTube Dislike" = "Schakel Return YouTube Dislike in";
|
||||
"Error" = "Fout";
|
||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Expliciete herinneringen om te liken, abonneren of communiceren met hen op enig ander gratis of betaald platform (bijv. op een video klikken).";
|
||||
"Favorites" = "Favorieten";
|
||||
"Filter" = "Filter";
|
||||
"Filter: active" = "Filter: actief";
|
||||
"Find Other" = "Vind Andere";
|
||||
"Finding something to play..." = "Vind iets om af te spelen...";
|
||||
"For videos which feature music as the primary content." = "Voor videos waarin muziek centraal staat als inhoud.";
|
||||
"Frontend URL" = "Frontend URL";
|
||||
"Fullscreen size" = "Volledig schermformaat";
|
||||
"Gaming" = "Gamen";
|
||||
"Help" = "Hulp";
|
||||
"Hide sidebar" = "Verberg zijbalk";
|
||||
"High" = "Hoog";
|
||||
"Highest" = "Hoogste";
|
||||
"Highest quality" = "Hoogste kwaliteit";
|
||||
"History" = "Geschiedenis";
|
||||
"Honor orientation lock" = "Staanderichtingsslot opvolgen";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Hour" = "Uur";
|
||||
"I am lost" = "Ik ben verdwaald";
|
||||
"I found a bug /" = "Ik heb een fout gevonden /";
|
||||
"I have a feature request" = "Ik heb een functieverzoek";
|
||||
"I like this app!" = "Ik vind deze app leuk!";
|
||||
"I want to ask a question" = "Ik wil een vraag stellen";
|
||||
"If you are interested what's coming in future updates, you can track project Milestones." = "Als je geïnteresseerd bent in toekomstige updates, kan je Milestones van het project volgen.";
|
||||
"Increase rate" = "Verhoog tempo";
|
||||
"Info" = "";
|
||||
"Instance of current account" = "Instantie van huidig account";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Interaction" = "Interactie";
|
||||
"Issues Tracker" = "Problementracker";
|
||||
|
||||
/* Selected video has just finished playing */
|
||||
"Just watched" = "Net bekeken";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Large" = "Groot";
|
||||
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "Grote layout is niet geschikt voor alle apparaten en kan er voor zorgen dat niet alles op het scherm past.";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Loading streams..." = "Streams aan het laden...";
|
||||
"Loading..." = "Laden...";
|
||||
"Locations" = "Locaties";
|
||||
"Lock portrait mode" = "Vergrendel protretmodus";
|
||||
|
||||
/* Video duration filter in search */
|
||||
"Long" = "Lang";
|
||||
"Low" = "Laag";
|
||||
"Low quality" = "Lage kwaliteit";
|
||||
"Lowest" = "Laagste";
|
||||
"Mark as watched" = "Markeer als bekeken";
|
||||
"Mark video as watched after playing" = "Markeer videos als bekeken na afspelen";
|
||||
"Mark watched videos with" = "Markeer videos als bekeken met";
|
||||
"Matrix Channel" = "Matrix Kanaal";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Medium" = "Gemiddeld";
|
||||
"Medium quality" = "Gemiddelde kwaliteit";
|
||||
"Milestones" = "Mijlpalen";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Month" = "Maand";
|
||||
"More info can be found in:" = "Meer informatie beschikbaar in:";
|
||||
"Movies" = "Films";
|
||||
"MPV Documentation" = "MPV Documentatie";
|
||||
"Music" = "Muziek";
|
||||
"Name" = "Naam";
|
||||
"New Playlist" = "Nieuwe Afspeellijst";
|
||||
"Next" = "Volgende";
|
||||
"No description" = "Geen omschrijving";
|
||||
"No Playlists" = "Geen Afspeellijsten";
|
||||
"No results" = "Geen resultaten";
|
||||
"Normal" = "Normaal";
|
||||
"Not available" = "Niet beschikbaar";
|
||||
"Not Playing" = "Niet aan het spelen";
|
||||
"Nothing" = "Niets";
|
||||
|
||||
/* SponsorBlock category name */
|
||||
"Offtopic in Music Videos" = "Ongerelateerd in muziekvideos";
|
||||
"Only when signed in" = "Alleen wanneer ingelogd";
|
||||
"Open \"Playlists\" tab to create new one" = "Open tabblad \"Afspeellijsten\" om een nieuwe te creëeren";
|
||||
"Open Settings" = "Open Instellingen";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Opening %@ stream..." = "Steam %@ aan het openen...";
|
||||
"Orientation" = "Oriëntatie";
|
||||
"Password" = "Wachtwoord";
|
||||
"Pause" = "Pauzeren";
|
||||
"Pause when entering background" = "Pauzeer wanneer op de achtergrond";
|
||||
"Picture in Picture" = "Beeld in Beeld";
|
||||
"Play" = "Speel Af";
|
||||
"Play in PiP" = "Speel af in BiB";
|
||||
"Play All" = "Speel Alle";
|
||||
"Play Last" = "Speel Laatste";
|
||||
"Play Music" = "Speel Muziek";
|
||||
"Play Next" = "Speel Volgende";
|
||||
"Play Now" = "Speel Nu";
|
||||
"Playback" = "Afspelen";
|
||||
"Player" = "Speler";
|
||||
"Playlist" = "Afspeellijst";
|
||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "Afspeellijst \"%@\" wordt verwijderd.\nDit kan niet ongedaan gemaakt worden.";
|
||||
"Playlists" = "Afspeellijsten";
|
||||
"Popular" = "Populair";
|
||||
"Preferred Formats" = "Voorkeursformaten";
|
||||
"Profiles" = "Profielen";
|
||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "Promotie van een product of dienst van de maker van de video zelf. Dit is inclusief promotie van merchandise of betaalde platformen.";
|
||||
"Public Locations" = "Publieke Locaties";
|
||||
"Public Manifest" = "Openbaar Manifest";
|
||||
"Quality" = "Kwaliteit";
|
||||
"Quality Profile" = "Kwaliteitsprofiel";
|
||||
"Queue" = "Wachtrij";
|
||||
"Queue is empty" = "Lege wachtrij";
|
||||
"Rate" = "Tempo";
|
||||
|
||||
/* Video sort order in search */
|
||||
"Rating" = "Beoordeling";
|
||||
"Recents" = "Recent";
|
||||
"Red" = "Rood";
|
||||
"Refresh" = "Ververs";
|
||||
"Regular size" = "Normaal formaat";
|
||||
"Regular Size" = "Normaal Formaat";
|
||||
"Related" = "Gerelateerd";
|
||||
|
||||
/* Video sort order in search */
|
||||
"Relevance" = "Relevantie";
|
||||
"Remove from Favorites" = "Verwijder uit Favorieten";
|
||||
"Remove from history" = "Verwijder uit geschiedenis";
|
||||
"Remove from Playlist" = "Verwijder uit Afspeellijst";
|
||||
"Remove from the queue" = "Verwijder uit de wachtrij";
|
||||
"Replies" = "Reacties";
|
||||
"Reset search filters" = "Reset zoekfilters";
|
||||
"Reset watched status when playing again" = "Reset bekeken-status bij opnieuw afspelen";
|
||||
"Resolution" = "Resolutie";
|
||||
"Restart/Play next" = "Herstart/Speel hierna";
|
||||
"Restore default profiles..." = "Herstel standaard profielen...";
|
||||
"Rotate to portrait when exiting fullscreen" = "Roteer naar portretmodus bij het verlaten van volledig scherm";
|
||||
"Save history of played videos" = "Sla geschiedenis van afgespeelde videos op";
|
||||
"Save history of searches, channels and playlists" = "Sla geschiedenis van zoekopdrachten, kanalen en afspeellijsten op";
|
||||
"Search" = "Zoeken";
|
||||
|
||||
@@ -267,28 +267,28 @@
|
||||
"Save history of searches, channels and playlists" = "Arama, kanal ve çalma listelerinin geçmişini kaydet";
|
||||
"Restore default profiles..." = "Varsayılan profilleri geri yükle...";
|
||||
"Visibility" = "";
|
||||
"Translations" = "";
|
||||
"Translations" = "Çeviriler";
|
||||
"Enter links to open, one per line" = "";
|
||||
"Open Videos" = "";
|
||||
"Playback Mode" = "";
|
||||
|
||||
/* Selected video was played on given date */
|
||||
"Watched %@" = "";
|
||||
"Yattee %@ (build %@)" = "";
|
||||
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "";
|
||||
"Thumbnails" = "";
|
||||
"Watched %@" = "İzlendi %@";
|
||||
"Yattee %@ (build %@)" = "Yattee %@ (versiyon %@)";
|
||||
"That's nice to hear. It is fun to deliver apps other people want to use. You can consider donating to the project or help by contributing to new features development." = "Bunu duymak çok güzel. Başkalarının kullanmak isteyeceği uygulamalar sunmak eğlenceli. Projeye bağış yapmayı düşünebilir veya yeni özelliklerin geliştirilmesine katkıda bulunarak yardımcı olabilirsiniz.";
|
||||
"Thumbnails" = "Küçük Resimler";
|
||||
"Dropped frames" = "";
|
||||
"SponsorBlock API Instance" = "";
|
||||
|
||||
/* Selected video is being played */
|
||||
"Watching now" = "";
|
||||
"Video Details" = "";
|
||||
"Watching now" = "Şu an izlenen";
|
||||
"Video Details" = "Video Bilgileri";
|
||||
"Live Streams" = "";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "";
|
||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "Genellikle videonun sonunda veya sonuna yakın özet açılır ve/veya bitiş arayüzü gösterilir.";
|
||||
"Default Profile" = "";
|
||||
|
||||
/* Player controls layout size for TV */
|
||||
"TV" = "";
|
||||
"TV" = "TV";
|
||||
"Add" = "";
|
||||
"This URL could not be opened" = "";
|
||||
"Hide" = "";
|
||||
@@ -305,38 +305,38 @@
|
||||
"Sign In Required" = "Giriş yapmanız gerekiyor";
|
||||
"Could not create share link" = "";
|
||||
"Locations Manifest" = "";
|
||||
"When partially watched video is played" = "";
|
||||
"When partially watched video is played" = "Video kısmi olarak izlendiyse";
|
||||
"Open Video" = "";
|
||||
"Add Channels, Playlists and Searches to Favorites using" = "";
|
||||
"Always" = "";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Year" = "";
|
||||
"Year" = "Yıl";
|
||||
"Playback queue is empty" = "";
|
||||
"Show Favorites" = "";
|
||||
"Show Favorites" = "Favorileri Göster";
|
||||
"Driver" = "";
|
||||
"Show progress of watching on thumbnails" = "";
|
||||
"Show progress of watching on thumbnails" = "İzlenme durumu görsellerde görünsün";
|
||||
"Left" = "";
|
||||
"URL to Open" = "";
|
||||
"Subscribe" = "";
|
||||
"Yattee" = "";
|
||||
"Show Documents" = "";
|
||||
"Subscribe" = "Üye ol";
|
||||
"Yattee" = "Yattee";
|
||||
"Show Documents" = "Belgeleri Göster";
|
||||
"Press and hold remote button to open captions and quality menus" = "";
|
||||
"No locations available at the moment" = "";
|
||||
"Show account username" = "Hesabın kullanıcı adını göster";
|
||||
"Used to create links from videos, channels and playlists" = "";
|
||||
"Used to create links from videos, channels and playlists" = "Videolardan, kanallardan ve oynatma listelerinden bağlantılar oluşturmak için kullanılır";
|
||||
"Size" = "";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "";
|
||||
"You have no playlists\n\nTap on \"New Playlist\" to create one" = "Çalma listeniz yok\n\nBir çalma listesi oluşturmak için \"Yeni Çalma Listesi\" üzerine dokunun";
|
||||
"Sort: %@" = "Sırala: %@";
|
||||
"Select location closest to you:" = "Size en yakın konumu seçin:";
|
||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "";
|
||||
|
||||
/* Video duration filter in search */
|
||||
"Short" = "Kısa";
|
||||
"Home" = "";
|
||||
"Home" = "Ana Sayfa";
|
||||
"Remove Location" = "";
|
||||
"Edit Favorites…" = "Favorileri düzenle…";
|
||||
"Show Open Videos toolbar button" = "";
|
||||
"Show Open Videos toolbar button" = "Video Aç arayüzünü göster";
|
||||
"Sample Rate" = "";
|
||||
"Private" = "";
|
||||
"Browsing" = "Gezinti";
|
||||
@@ -347,10 +347,10 @@
|
||||
"Current Playlist" = "";
|
||||
"Center" = "";
|
||||
"Address" = "";
|
||||
"Video actions buttons" = "";
|
||||
"Video actions buttons" = "Video eylem butonları";
|
||||
"Keep last played video in the queue after restart" = "";
|
||||
"Remove…" = "";
|
||||
"Trending" = "";
|
||||
"Trending" = "Trendler";
|
||||
"Statistics" = "";
|
||||
"Copy%@link" = "";
|
||||
"Now Playing" = "";
|
||||
@@ -358,111 +358,111 @@
|
||||
"No comments" = "";
|
||||
"Could not open Files" = "";
|
||||
"You need to select an account\nto access %@ section" = "";
|
||||
"Reload manifest" = "";
|
||||
"Reload manifest" = "Yeniden Yükle";
|
||||
"Could not refresh Subscriptions" = "";
|
||||
|
||||
/* Subscriptions title */
|
||||
"Subscriptions" = "";
|
||||
"Upload date" = "";
|
||||
"This information will be processed only on your device and used to connect you to the server in the specified country." = "";
|
||||
"Subscriptions" = "Üyelik";
|
||||
"Upload date" = "Yüklenme tarihi";
|
||||
"This information will be processed only on your device and used to connect you to the server in the specified country." = "Bu bilgiler yalnızca cihazınızda işlenecek ve sizi belirtilen ülkedeki sunucuya bağlamak için kullanılacaktır.";
|
||||
"Shuffle" = "Karıştır";
|
||||
"Buttons labels" = "";
|
||||
"Buttons labels" = "Eylem düğmeleri etiketi";
|
||||
"Share %@ link" = "%@ bağlantısını paylaş";
|
||||
"Could not load streams" = "";
|
||||
"Playback history is empty" = "";
|
||||
"Show icons and text when space permits" = "";
|
||||
"unknown" = "";
|
||||
"unknown" = "Bilinmeyen";
|
||||
"Share..." = "Paylaş...";
|
||||
|
||||
/* Video sort order in search */
|
||||
"Views" = "";
|
||||
"Views" = "İzlenme";
|
||||
"You need to create an instance and accounts\nto access %@ section" = "";
|
||||
"Verified" = "";
|
||||
"Open Files" = "";
|
||||
"Could not refresh Playlists" = "";
|
||||
"Could not refresh Playlists" = "Çalma listesi güncellenemedi";
|
||||
"Actions buttons" = "";
|
||||
"Any format" = "";
|
||||
"Show playback statistics" = "Oynatma istatistiklerini göster";
|
||||
"Pages buttons" = "";
|
||||
"Videos" = "";
|
||||
"Pages buttons" = "Sayfa butonları";
|
||||
"Videos" = "Videolar";
|
||||
"Codec" = "";
|
||||
"Comments are disabled" = "";
|
||||
"Audio" = "";
|
||||
|
||||
|
||||
"Public" = "";
|
||||
"Files" = "";
|
||||
"Show Home" = "";
|
||||
"Open" = "";
|
||||
"Files" = "Dosyalar";
|
||||
"Show Home" = "Ana Sayfayı Göster";
|
||||
"Open" = "Aç";
|
||||
|
||||
/* Loading stream OSD */
|
||||
"Opening %@ stream..." = "%@ akışı açılıyor...";
|
||||
"Clear Queue before opening" = "";
|
||||
"Clear Queue before opening" = "Açmadan önce Bekleme Listesini temizle";
|
||||
"Copy %@ link with time" = "Bağlantıyı %@ zaman ile kopyala";
|
||||
"Show Inspector" = "";
|
||||
"Show Inspector" = "Denetleyiciyi Göster";
|
||||
"Make default" = "";
|
||||
"Are you sure you want to remove %@ location?" = "";
|
||||
"No chapters information available" = "";
|
||||
"Share Logs..." = "";
|
||||
"Enter link to open" = "";
|
||||
"No documents" = "";
|
||||
"Inspector visibility" = "";
|
||||
"No documents" = "Belge yok";
|
||||
"Inspector visibility" = "Denetleyici görünümü";
|
||||
"Could not update your token." = "";
|
||||
"Could not find any links to open in your clipboard" = "";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Week" = "";
|
||||
"Week" = "Hafta";
|
||||
"Sidebar" = "Kenar çubuğu";
|
||||
"Show only icons" = "";
|
||||
"Current: %@\n%@" = "Şuan: %@\n%@";
|
||||
"Show anonymous accounts" = "Anonim hesapları göster";
|
||||
"Could not open playlist" = "";
|
||||
"Could not open playlist" = "Çalma listesi açılamadı";
|
||||
"Round corners" = "";
|
||||
"URL" = "";
|
||||
"URL" = "URL";
|
||||
"Recents" = "";
|
||||
"Show sidebar when space permits" = "Alan olduğu sürece kenar çubuğunu göster";
|
||||
"System controls buttons" = "";
|
||||
"System controls buttons" = "Sistem kontrol butonları";
|
||||
"Could not extract channel information" = "";
|
||||
"Public Locations" = "";
|
||||
"You can find information about using Yattee in the Wiki pages." = "";
|
||||
"You can find information about using Yattee in the Wiki pages." = "Wiki sayfalarında Yattee kullanımı hakkında bilgilere erişebilirsiniz.";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Very Large" = "";
|
||||
"Very Large" = "Çok Büyük";
|
||||
"Continue from %@" = "% devam et";
|
||||
"Copy %@ link" = "Bağlantıyı %@ kopyala";
|
||||
"Seek gesture sensitivity" = "";
|
||||
"Seek gesture sensitivity" = "Kaydırma hassasiyeti";
|
||||
"Show keywords" = "Anahtar kelimeleri göster";
|
||||
"Wiki" = "";
|
||||
"Username" = "";
|
||||
"Could not extract video ID" = "";
|
||||
"Wiki" = "Wiki";
|
||||
"Username" = "Kullanıcı adı";
|
||||
"Could not extract video ID" = "Video ID bilgisi alınamadı";
|
||||
|
||||
/* Player controls layout size */
|
||||
"Smaller" = "Küçült";
|
||||
"Sort" = "Sırala";
|
||||
"This cannot be reverted" = "";
|
||||
"This cannot be reverted" = "Geriye alınamaz";
|
||||
"Public Manifest" = "";
|
||||
"You have no Playlists" = "";
|
||||
"Watched" = "";
|
||||
"You have no Playlists" = "Çalma listeniz bulunmamaktadır";
|
||||
"Watched" = "İzlendi";
|
||||
"Could not open video" = "";
|
||||
"Channel could not be found" = "";
|
||||
"Show video length" = "Video uzunluğunu göster";
|
||||
"Source" = "Kaynak";
|
||||
"Welcome" = "";
|
||||
"Wi-Fi" = "";
|
||||
"Welcome" = "Hoşgeldiniz";
|
||||
"Wi-Fi" = "Wi-Fi";
|
||||
"Could not open channel" = "";
|
||||
"This video could not be opened" = "";
|
||||
"Could not extract playlist ID" = "";
|
||||
"Could not load video" = "";
|
||||
"This video could not be opened" = "Bu video oynatılamadı";
|
||||
"Could not extract playlist ID" = "Çalma listesi ID bilgisi alınamadı";
|
||||
"Could not load video" = "Video yüklenemedi";
|
||||
"Decreased opacity" = "Düşük şeffaflık";
|
||||
"Shuffle All" = "Tümünü karıştır";
|
||||
"Share %@ link with time" = "%@ bağlantısını zaman bilgisiyle birlikte paylaş";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "";
|
||||
"Unsubscribe" = "";
|
||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "Bu geri döndürülemez. Değişiklikleri görmek için görünümler arasında geçiş yapmanız veya uygulamayı yeniden başlatmanız gerekebilir.";
|
||||
"Unsubscribe" = "Abonelikten çık";
|
||||
"Current Location" = "";
|
||||
"Stream & Player" = "";
|
||||
"Hardware decoder" = "Donanımsal çözücü";
|
||||
"Honor orientation lock" = "";
|
||||
"Seek with horizontal swipe on video" = "";
|
||||
"Seek with horizontal swipe on video" = "Video üzerinde yatay kaydırma";
|
||||
"Segments typically found at the start of a video that include an animation, still frame or clip which are also seen in other videos by the same creator." = "";
|
||||
"Switch to public locations" = "";
|
||||
"%@ formats" = "";
|
||||
@@ -480,22 +480,22 @@
|
||||
"System controls show buttons for %@" = "";
|
||||
"Show history" = "Kullanım geçmişini göster";
|
||||
"Could not extract SID from received cookies: %@" = "";
|
||||
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "";
|
||||
"This will remove all your custom profiles and return their default values. This cannot be reverted." = "Bu eylem tüm kişiselleştirilmiş ayarlarınızı kaldıracak ve varsayılan ayarları geri getirecektir. Bu işlem geri döndürülemez.";
|
||||
"Switch to other public location" = "";
|
||||
"SponsorBlock" = "";
|
||||
"Seek gesture speed" = "";
|
||||
"If you want this app to be available in your language, join translation project." = "";
|
||||
"Seek gesture speed" = "Kaydırma hızı";
|
||||
"If you want this app to be available in your language, join translation project." = "Uygulamanın kendi dilinize çevrilmesini istiyorsanız, çeviri projesine katılın.";
|
||||
"Could not refresh Trending" = "";
|
||||
|
||||
/* Video date filter in search */
|
||||
"Today" = "";
|
||||
"Today" = "Bugün";
|
||||
"Shorts" = "";
|
||||
"Channel" = "";
|
||||
"Share files from Finder on a Mac\nor iTunes on Windows" = "";
|
||||
"Share files from Finder on a Mac\nor iTunes on Windows" = "Finder üzerinden Mac ile belge paylaşın\nveya iTunes üzerinden Windows ile";
|
||||
"\"%@\" will be irreversibly removed from this device." = "";
|
||||
"Recent Documents" = "";
|
||||
"Recent History" = "";
|
||||
"Show Open Videos quick actions" = "";
|
||||
"Recent Documents" = "Son kullanılan belgeler";
|
||||
"Recent History" = "Yakın zamanda izlenilenler";
|
||||
"Show Open Videos quick actions" = "Video Aç hızlı eylemlerini göster";
|
||||
"Pages toolbar position" = "";
|
||||
"Video" = "";
|
||||
"Channels" = "";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"originHash" : "9899ef48b3ee49eae175e25421b8330438e40c30a266d96473b299a6ab7c4188",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "activelabel.swift",
|
||||
@@ -59,8 +60,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cxfksword/MPVKit.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "dca1e345a26d09a3d621d7656a94e6427f3f7b83"
|
||||
"revision" : "96825b3dc2b38e5550268156148d47798ce6aa74",
|
||||
"version" : "0.36.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,8 +87,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ashleymills/Reachability.swift",
|
||||
"state" : {
|
||||
"revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2",
|
||||
"version" : "5.1.0"
|
||||
"revision" : "c01127cb51f591045696128effe43c16840d08bf",
|
||||
"version" : "5.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -105,7 +106,7 @@
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "fbe79221b146aa6647dceb5a5c75873a48b69519"
|
||||
"revision" : "a41be90abd89b125cd7588f20b9788108254091a"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -122,8 +123,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImageSwiftUI.git",
|
||||
"state" : {
|
||||
"revision" : "aee64ef39b570c44ccf0f884c440fc6494a23c76",
|
||||
"version" : "2.2.5"
|
||||
"revision" : "261b6cec35686d2dc192b809ab50742b4502a73b",
|
||||
"version" : "2.2.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -131,8 +132,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImageWebPCoder.git",
|
||||
"state" : {
|
||||
"revision" : "db4603921b31a6ce0f8c26d36d6a3fffc2dba481",
|
||||
"version" : "0.14.2"
|
||||
"revision" : "acfb824ca5cd9dbde2c43dc6b5a008c6757dee85",
|
||||
"version" : "0.14.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
<string>public.file-url</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Settings text</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
@@ -68,5 +78,31 @@
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Yattee Settings</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>stream.yattee.app-settings</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>yatteesettings</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>application/json</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Defaults
|
||||
import Logging
|
||||
import UIKit
|
||||
|
||||
struct Orientation {
|
||||
enum Orientation {
|
||||
static var logger = Logger(label: "stream.yattee.orientation")
|
||||
|
||||
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
<string>public.mpeg-4</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Settings</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
@@ -37,5 +49,51 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Yattee Settings</string>
|
||||
<key>UTTypeIcons</key>
|
||||
<dict/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>stream.yattee.app-settings</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>yatteesettings</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Yattee Settings</string>
|
||||
<key>UTTypeIcons</key>
|
||||
<dict/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>stream.yattee.app-settings</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>yatteesettings</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -16,7 +16,7 @@ final class PlayerViewController: NSViewController {
|
||||
return [ratio, 1.0].max()!
|
||||
}
|
||||
|
||||
override func viewDidDisappear() {
|
||||
func viewDidDisappear() {
|
||||
super.viewDidDisappear()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
struct Power {
|
||||
enum Power {
|
||||
static var hasInternalBattery: Bool {
|
||||
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||
|
||||
Reference in New Issue
Block a user