mirror of
https://github.com/yattee/yattee.git
synced 2026-06-15 19:24:20 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d94a50f8c3 | ||
|
|
b7edbe5683 | ||
|
|
42849c1aae | ||
|
|
11f0dff4e2 | ||
|
|
a26044cc04 | ||
|
|
c978ec6b89 | ||
|
|
4a69172bed | ||
|
|
0db1c08b98 | ||
|
|
f9ecfcd3dd | ||
|
|
b9351b502c | ||
|
|
f28fdcec96 | ||
|
|
ba3da4fc03 | ||
|
|
627ee48325 | ||
|
|
f23b010241 | ||
|
|
9a5d377ae0 | ||
|
|
b789e320e0 | ||
|
|
6bdb187d18 | ||
|
|
ca36254661 | ||
|
|
3312e1df82 | ||
|
|
a484aaf889 | ||
|
|
20d0cfc0c7 | ||
|
|
7fe99b09ef | ||
|
|
78f155a3b9 | ||
|
|
6f696c9262 | ||
|
|
b38bd3f444 | ||
|
|
d8e079ac90 | ||
|
|
75812906c1 | ||
|
|
82570b7f34 | ||
|
|
e43eddc8e7 | ||
|
|
c5137a8af8 | ||
|
|
9177abb0ec | ||
|
|
65e86d30ec | ||
|
|
0c4609bcf1 | ||
|
|
36190e62f5 | ||
|
|
e6e69eb757 | ||
|
|
41a33634ee | ||
|
|
aa703f6531 | ||
|
|
db80b6adbb | ||
|
|
6591d503d4 | ||
|
|
1eba731283 | ||
|
|
0913c6d73c | ||
|
|
997de6468d | ||
|
|
1397a2fee6 | ||
|
|
660891f2a5 | ||
|
|
2e27dcd2cf | ||
|
|
5f53e53c7a | ||
|
|
73295e726a | ||
|
|
b0dfd2f9d2 |
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.1'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
cache-version: 1
|
cache-version: 1
|
||||||
- name: Replace signing certificate to Direct with Developer ID
|
- name: Replace signing certificate to Direct with Developer ID
|
||||||
|
|||||||
2
.github/workflows/bump-build.yml
vendored
2
.github/workflows/bump-build.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
git config --local user.name "github-actions[bot]"
|
git config --local user.name "github-actions[bot]"
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.1'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
cache-version: 1
|
cache-version: 1
|
||||||
- uses: maierj/fastlane-action@v3.0.0
|
- uses: maierj/fastlane-action@v3.0.0
|
||||||
|
|||||||
49
.github/workflows/release.yml
vendored
49
.github/workflows/release.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
|||||||
# lane: ['mac beta', 'ios beta', 'tvos beta']
|
# lane: ['mac beta', 'ios beta', 'tvos beta']
|
||||||
lane: ['ios beta', 'tvos beta']
|
lane: ['ios beta', 'tvos beta']
|
||||||
name: Releasing ${{ matrix.lane }} version to TestFlight
|
name: Releasing ${{ matrix.lane }} version to TestFlight
|
||||||
runs-on: macos-latest
|
runs-on: macos-26
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.1'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
cache-version: 1
|
cache-version: 1
|
||||||
- name: Replace signing certificate to AppStore
|
- name: Replace signing certificate to AppStore
|
||||||
@@ -42,8 +42,29 @@ jobs:
|
|||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '26.0.1'
|
xcode-version: '26.0.1'
|
||||||
- name: Clear SPM cache
|
- name: Install iOS/tvOS platform SDKs if missing
|
||||||
run: rm -rf ~/Library/Caches/org.swift.swiftpm/artifacts
|
run: |
|
||||||
|
sudo xcodebuild -downloadPlatform iOS || true
|
||||||
|
sudo xcodebuild -downloadPlatform tvOS || true
|
||||||
|
- name: Resolve SPM dependencies (retry around SPM binary-target race)
|
||||||
|
run: |
|
||||||
|
case "${{ matrix.lane }}" in
|
||||||
|
"ios beta") SCHEME="Yattee (iOS)" ;;
|
||||||
|
"tvos beta") SCHEME="Yattee (tvOS)" ;;
|
||||||
|
*) SCHEME="Yattee (iOS)" ;;
|
||||||
|
esac
|
||||||
|
set +e
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
rm -rf ~/Library/Caches/org.swift.swiftpm ~/Library/org.swift.swiftpm ~/Library/Developer/Xcode/DerivedData
|
||||||
|
echo "::group::Resolve attempt $attempt for $SCHEME"
|
||||||
|
xcodebuild -resolvePackageDependencies -project Yattee.xcodeproj -scheme "$SCHEME"
|
||||||
|
RC=$?
|
||||||
|
echo "::endgroup::"
|
||||||
|
[ $RC -eq 0 ] && exit 0
|
||||||
|
echo "Attempt $attempt failed (rc=$RC), retrying after 15s..."
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
- uses: maierj/fastlane-action@v3.0.0
|
- uses: maierj/fastlane-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
lane: ${{ matrix.lane }}
|
lane: ${{ matrix.lane }}
|
||||||
@@ -54,12 +75,12 @@ jobs:
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
mac_notarized:
|
mac_notarized:
|
||||||
name: Build and notarize macOS app
|
name: Build and notarize macOS app
|
||||||
runs-on: macos-latest
|
runs-on: macos-26
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.1'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
cache-version: 1
|
cache-version: 1
|
||||||
- name: Replace signing certificate to Direct with Developer ID
|
- name: Replace signing certificate to Direct with Developer ID
|
||||||
@@ -69,8 +90,20 @@ jobs:
|
|||||||
- uses: maxim-lobanov/setup-xcode@v1
|
- uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '26.0.1'
|
xcode-version: '26.0.1'
|
||||||
- name: Clear SPM cache
|
- name: Resolve SPM dependencies (retry around SPM binary-target race)
|
||||||
run: rm -rf ~/Library/Caches/org.swift.swiftpm/artifacts
|
run: |
|
||||||
|
set +e
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
rm -rf ~/Library/Caches/org.swift.swiftpm ~/Library/org.swift.swiftpm ~/Library/Developer/Xcode/DerivedData
|
||||||
|
echo "::group::Resolve attempt $attempt for Yattee (macOS)"
|
||||||
|
xcodebuild -resolvePackageDependencies -project Yattee.xcodeproj -scheme "Yattee (macOS)"
|
||||||
|
RC=$?
|
||||||
|
echo "::endgroup::"
|
||||||
|
[ $RC -eq 0 ] && exit 0
|
||||||
|
echo "Attempt $attempt failed (rc=$RC), retrying after 15s..."
|
||||||
|
sleep 15
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
- uses: maierj/fastlane-action@v3.0.0
|
- uses: maierj/fastlane-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
lane: mac build_and_notarize
|
lane: mac build_and_notarize
|
||||||
|
|||||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,3 +1,38 @@
|
|||||||
|
## Build 257 (v1.5.2)
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Fixed Finnish, Indonesian, Korean, Dutch and Swedish translations
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
* Update dependencies
|
||||||
|
|
||||||
|
## Previous builds
|
||||||
|
|
||||||
|
## Build 241
|
||||||
|
|
||||||
|
### iOS Fixes
|
||||||
|
* Fix menu text disappearing in navigation headers and playback settings
|
||||||
|
* Fix fullscreen gesture collision with notification center by adding 60pt safe zone at top
|
||||||
|
* Fix comments scrolling issue - comments at bottom of video details view are now fully accessible
|
||||||
|
* Restrict orientation locking to iPhone only (hide on iPad)
|
||||||
|
|
||||||
|
### tvOS Fixes
|
||||||
|
* Improve controls overlay with single-press menus for quality, stream, captions, and audio track selection
|
||||||
|
* Fix controls overlay button text legibility
|
||||||
|
* Fix captions list always showing as unavailable in MPV
|
||||||
|
|
||||||
|
### API & Backend Fixes
|
||||||
|
* Fix Invidious search API parameters (sort_by→sort, upload_date→date, view_count→views)
|
||||||
|
* Fix Invidious captions URL when companion is enabled
|
||||||
|
* Fix YouTube share links incorrectly including port from Invidious instance
|
||||||
|
|
||||||
|
### UI & Layout
|
||||||
|
* Fix home view empty sections taking excessive vertical space
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
* Add experimental setting to hide videos without duration in Invidious instance settings (can be used to filter shorts)
|
||||||
|
* Add optional AVPlayer support for non-streamable MP4/AVC1 formats in advanced settings with warnings about slow loading
|
||||||
|
|
||||||
## Build 210
|
## Build 210
|
||||||
|
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@@ -1,6 +1,6 @@
|
|||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
gem 'fastlane'
|
gem 'fastlane', '~> 2.232'
|
||||||
|
|
||||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||||
|
|||||||
111
Gemfile.lock
111
Gemfile.lock
@@ -2,13 +2,14 @@ GEM
|
|||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.8)
|
CFPropertyList (3.0.8)
|
||||||
addressable (2.8.7)
|
abbrev (0.1.2)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
addressable (2.9.0)
|
||||||
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1182.0)
|
aws-partitions (1.1240.0)
|
||||||
aws-sdk-core (3.237.0)
|
aws-sdk-core (3.245.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
@@ -16,23 +17,25 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.117.0)
|
aws-sdk-kms (1.123.0)
|
||||||
aws-sdk-core (~> 3, >= 3.234.0)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.203.1)
|
aws-sdk-s3 (1.219.0)
|
||||||
aws-sdk-core (~> 3, >= 3.234.0)
|
aws-sdk-core (~> 3, >= 3.244.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.3.0)
|
base64 (0.2.0)
|
||||||
bigdecimal (3.3.1)
|
benchmark (0.5.0)
|
||||||
|
bigdecimal (4.1.2)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
|
csv (3.3.5)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
@@ -40,7 +43,7 @@ GEM
|
|||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.112.0)
|
excon (0.112.0)
|
||||||
faraday (1.10.4)
|
faraday (1.10.5)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -59,25 +62,29 @@ GEM
|
|||||||
faraday-em_synchrony (1.0.1)
|
faraday-em_synchrony (1.0.1)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.1.1)
|
faraday-multipart (1.2.0)
|
||||||
multipart-post (~> 2.0)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.2)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.4)
|
||||||
faraday_middleware (1.2.1)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.4.0)
|
fastimage (2.4.1)
|
||||||
fastlane (2.228.0)
|
fastlane (2.232.2)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
abbrev (~> 0.1.2)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.197)
|
||||||
babosa (>= 1.0.3, < 2.0.0)
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
bundler (>= 1.12.0, < 3.0.0)
|
base64 (~> 0.2.0)
|
||||||
|
benchmark (>= 0.1.0)
|
||||||
|
bundler (>= 1.17.3, < 5.0.0)
|
||||||
colored (~> 1.2)
|
colored (~> 1.2)
|
||||||
commander (~> 4.6)
|
commander (~> 4.6)
|
||||||
|
csv (~> 3.3)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
excon (>= 0.71.0, < 1.0.0)
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
@@ -89,16 +96,20 @@ GEM
|
|||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-apis-androidpublisher_v3 (~> 0.3)
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
google-cloud-env (>= 1.6.0, <= 2.1.1)
|
||||||
google-cloud-storage (~> 1.31)
|
google-cloud-storage (~> 1.31)
|
||||||
highline (~> 2.0)
|
highline (~> 2.0)
|
||||||
http-cookie (~> 1.0.5)
|
http-cookie (~> 1.0.5)
|
||||||
json (< 3.0.0)
|
json (< 3.0.0)
|
||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
|
logger (>= 1.6, < 2.0)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
mutex_m (~> 0.3.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
|
nkf (~> 0.2.0)
|
||||||
optparse (>= 0.1.1, < 1.0.0)
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
|
ostruct (>= 0.1.0)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
rubyzip (>= 2.0.0, < 3.0.0)
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
security (= 0.1.5)
|
security (= 0.1.5)
|
||||||
@@ -111,41 +122,42 @@ GEM
|
|||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.4.1)
|
xcpretty (~> 0.4.1)
|
||||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-sirp (1.0.0)
|
fastlane-sirp (1.1.0)
|
||||||
sysrandom (~> 1.0)
|
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.54.0)
|
google-apis-androidpublisher_v3 (0.98.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (0.11.3)
|
google-apis-core (0.18.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.3, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
|
mutex_m
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
google-apis-iamcredentials_v1 (0.26.0)
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-playcustomapp_v1 (0.17.0)
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-storage_v1 (0.61.0)
|
||||||
google-apis-storage_v1 (0.31.0)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
|
||||||
google-cloud-core (1.8.0)
|
google-cloud-core (1.8.0)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (2.1.1)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.5.0)
|
google-cloud-errors (1.6.0)
|
||||||
google-cloud-storage (1.47.0)
|
google-cloud-storage (1.59.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-core (>= 0.18, < 2)
|
||||||
google-apis-storage_v1 (~> 0.31.0)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
|
google-apis-storage_v1 (>= 0.42)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.8.1)
|
googleauth (1.11.2)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
@@ -156,27 +168,29 @@ GEM
|
|||||||
httpclient (2.9.0)
|
httpclient (2.9.0)
|
||||||
mutex_m
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.16.0)
|
json (2.19.3)
|
||||||
jwt (2.10.2)
|
jwt (2.10.2)
|
||||||
base64
|
base64
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
multi_json (1.17.0)
|
multi_json (1.20.1)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
nanaimo (0.4.0)
|
nanaimo (0.4.0)
|
||||||
naturally (2.3.0)
|
naturally (2.3.0)
|
||||||
optparse (0.8.0)
|
nkf (0.2.0)
|
||||||
|
optparse (0.8.1)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
|
ostruct (0.6.3)
|
||||||
plist (3.7.2)
|
plist (3.7.2)
|
||||||
public_suffix (6.0.2)
|
public_suffix (7.0.5)
|
||||||
rake (13.3.1)
|
rake (13.4.2)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.4.1)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rouge (3.28.0)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
@@ -190,7 +204,6 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
sysrandom (1.0.5)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
@@ -225,7 +238,7 @@ PLATFORMS
|
|||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
fastlane
|
fastlane (~> 2.232)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.22
|
2.5.22
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
var frontendURL: String?
|
var frontendURL: String?
|
||||||
var proxiesVideos: Bool
|
var proxiesVideos: Bool
|
||||||
var invidiousCompanion: Bool
|
var invidiousCompanion: Bool
|
||||||
|
var hideVideosWithoutDuration: Bool
|
||||||
|
|
||||||
init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false) {
|
init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false, hideVideosWithoutDuration: Bool = false) {
|
||||||
self.app = app
|
self.app = app
|
||||||
self.id = id ?? UUID().uuidString
|
self.id = id ?? UUID().uuidString
|
||||||
self.name = name ?? app.rawValue
|
self.name = name ?? app.rawValue
|
||||||
@@ -20,6 +21,7 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
|
|||||||
self.frontendURL = frontendURL
|
self.frontendURL = frontendURL
|
||||||
self.proxiesVideos = proxiesVideos
|
self.proxiesVideos = proxiesVideos
|
||||||
self.invidiousCompanion = invidiousCompanion
|
self.invidiousCompanion = invidiousCompanion
|
||||||
|
self.hideVideosWithoutDuration = hideVideosWithoutDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiURL: URL! {
|
var apiURL: URL! {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ struct InstancesBridge: Defaults.Bridge {
|
|||||||
"apiURL": value.apiURLString,
|
"apiURL": value.apiURLString,
|
||||||
"frontendURL": value.frontendURL ?? "",
|
"frontendURL": value.frontendURL ?? "",
|
||||||
"proxiesVideos": value.proxiesVideos ? "true" : "false",
|
"proxiesVideos": value.proxiesVideos ? "true" : "false",
|
||||||
"invidiousCompanion": value.invidiousCompanion ? "true" : "false"
|
"invidiousCompanion": value.invidiousCompanion ? "true" : "false",
|
||||||
|
"hideVideosWithoutDuration": value.hideVideosWithoutDuration ? "true" : "false"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,8 @@ struct InstancesBridge: Defaults.Bridge {
|
|||||||
let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"]
|
let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"]
|
||||||
let proxiesVideos = object["proxiesVideos"] == "true"
|
let proxiesVideos = object["proxiesVideos"] == "true"
|
||||||
let invidiousCompanion = object["invidiousCompanion"] == "true"
|
let invidiousCompanion = object["invidiousCompanion"] == "true"
|
||||||
|
let hideVideosWithoutDuration = object["hideVideosWithoutDuration"] == "true"
|
||||||
|
|
||||||
return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion)
|
return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion, hideVideosWithoutDuration: hideVideosWithoutDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ final class InstancesModel: ObservableObject {
|
|||||||
Defaults[.instances][index] = instance
|
Defaults[.instances][index] = instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setHideVideosWithoutDuration(_ instance: Instance, _ hideVideosWithoutDuration: Bool) {
|
||||||
|
guard let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var instance = Defaults[.instances][index]
|
||||||
|
instance.hideVideosWithoutDuration = hideVideosWithoutDuration
|
||||||
|
|
||||||
|
Defaults[.instances][index] = instance
|
||||||
|
}
|
||||||
|
|
||||||
func remove(_ instance: Instance) {
|
func remove(_ instance: Instance) {
|
||||||
let accounts = accounts(instance.id)
|
let accounts = accounts(instance.id)
|
||||||
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {
|
||||||
|
|||||||
@@ -52,11 +52,13 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(self.extractVideo)
|
let videos = content.json.arrayValue.map(self.extractVideo)
|
||||||
|
return self.account.instance.hideVideosWithoutDuration ? videos.filter { $0.length > 0 } : videos
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
content.json.arrayValue.map(self.extractVideo)
|
let videos = content.json.arrayValue.map(self.extractVideo)
|
||||||
|
return self.account.instance.hideVideosWithoutDuration ? videos.filter { $0.length > 0 } : videos
|
||||||
}
|
}
|
||||||
|
|
||||||
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> SearchPage in
|
configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity<JSON>) -> SearchPage in
|
||||||
@@ -70,7 +72,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return ContentItem(playlist: self.extractChannelPlaylist(from: json))
|
return ContentItem(playlist: self.extractChannelPlaylist(from: json))
|
||||||
}
|
}
|
||||||
if type == "video" {
|
if type == "video" {
|
||||||
return ContentItem(video: self.extractVideo(from: json))
|
let video = self.extractVideo(from: json)
|
||||||
|
if self.account.instance.hideVideosWithoutDuration, video.length == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ContentItem(video: video)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -101,7 +107,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity<JSON>) -> [Video] in
|
||||||
if let feedVideos = content.json.dictionaryValue["videos"] {
|
if let feedVideos = content.json.dictionaryValue["videos"] {
|
||||||
return feedVideos.arrayValue.map(self.extractVideo)
|
let videos = feedVideos.arrayValue.map(self.extractVideo)
|
||||||
|
return self.account.instance.hideVideosWithoutDuration ? videos.filter { $0.length > 0 } : videos
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
@@ -402,7 +409,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
func search(_ query: SearchQuery, page: String?) -> Resource {
|
func search(_ query: SearchQuery, page: String?) -> Resource {
|
||||||
var resource = resource(baseURL: account.url, path: basePathAppending("search"))
|
var resource = resource(baseURL: account.url, path: basePathAppending("search"))
|
||||||
.withParam("q", searchQuery(query.query))
|
.withParam("q", searchQuery(query.query))
|
||||||
.withParam("sort_by", query.sortBy.parameter)
|
.withParam("sort", query.sortBy.parameter)
|
||||||
.withParam("type", "all")
|
.withParam("type", "all")
|
||||||
|
|
||||||
if let date = query.date, date != .any {
|
if let date = query.date, date != .any {
|
||||||
@@ -605,6 +612,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
// Some instances are not configured properly and return thumbnail links
|
// Some instances are not configured properly and return thumbnail links
|
||||||
// with an incorrect scheme or a missing port.
|
// with an incorrect scheme or a missing port.
|
||||||
components.scheme = accountUrlComponents.scheme
|
components.scheme = accountUrlComponents.scheme
|
||||||
|
if (components.host ?? "") == "" {
|
||||||
|
components.host = accountUrlComponents.host
|
||||||
|
}
|
||||||
components.port = accountUrlComponents.port
|
components.port = accountUrlComponents.port
|
||||||
|
|
||||||
// If basic HTTP authentication is used,
|
// If basic HTTP authentication is used,
|
||||||
@@ -851,7 +861,14 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
private func extractCaptions(from content: JSON) -> [Captions] {
|
private func extractCaptions(from content: JSON) -> [Captions] {
|
||||||
content["captions"].arrayValue.compactMap { details in
|
content["captions"].arrayValue.compactMap { details in
|
||||||
guard let url = URL(string: details["url"].stringValue, relativeTo: account.url) else { return nil }
|
var urlString = details["url"].stringValue
|
||||||
|
|
||||||
|
// Prefix with /companion if enabled
|
||||||
|
if account.instance.invidiousCompanion {
|
||||||
|
urlString = "/companion" + urlString
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = URL(string: urlString, relativeTo: account.url) else { return nil }
|
||||||
|
|
||||||
return Captions(
|
return Captions(
|
||||||
label: details["label"].stringValue,
|
label: details["label"].stringValue,
|
||||||
@@ -875,7 +892,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
return ContentItem(playlist: extractChannelPlaylist(from: json))
|
return ContentItem(playlist: extractChannelPlaylist(from: json))
|
||||||
}
|
}
|
||||||
if type == "video" {
|
if type == "video" {
|
||||||
return ContentItem(video: extractVideo(from: json))
|
let video = extractVideo(from: json)
|
||||||
|
if account.instance.hideVideosWithoutDuration, video.length == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ContentItem(video: video)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ extension VideosAPI {
|
|||||||
let frontendURL = URL(string: frontendURLString)
|
let frontendURL = URL(string: frontendURLString)
|
||||||
{
|
{
|
||||||
urlComponents = URLComponents(url: frontendURL, resolvingAgainstBaseURL: false)
|
urlComponents = URLComponents(url: frontendURL, resolvingAgainstBaseURL: false)
|
||||||
|
// Ensure port is not included when sharing to external frontends like YouTube
|
||||||
|
if frontendURLString.contains("youtube.com") {
|
||||||
|
urlComponents?.port = nil
|
||||||
|
}
|
||||||
} else if let instanceComponents = account?.instance?.urlComponents {
|
} else if let instanceComponents = account?.instance?.urlComponents {
|
||||||
urlComponents = instanceComponents
|
urlComponents = instanceComponents
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ final class AdvancedSettingsGroupExporter: SettingsGroupExporter {
|
|||||||
[
|
[
|
||||||
"showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu],
|
"showPlayNowInBackendContextMenu": Defaults[.showPlayNowInBackendContextMenu],
|
||||||
"videoLoadingRetryCount": Defaults[.videoLoadingRetryCount],
|
"videoLoadingRetryCount": Defaults[.videoLoadingRetryCount],
|
||||||
|
"avPlayerAllowsNonStreamableFormats": Defaults[.avPlayerAllowsNonStreamableFormats],
|
||||||
"showMPVPlaybackStats": Defaults[.showMPVPlaybackStats],
|
"showMPVPlaybackStats": Defaults[.showMPVPlaybackStats],
|
||||||
"mpvEnableLogging": Defaults[.mpvEnableLogging],
|
"mpvEnableLogging": Defaults[.mpvEnableLogging],
|
||||||
"mpvCacheSecs": Defaults[.mpvCacheSecs],
|
"mpvCacheSecs": Defaults[.mpvCacheSecs],
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ struct AdvancedSettingsGroupImporter {
|
|||||||
Defaults[.videoLoadingRetryCount] = videoLoadingRetryCount
|
Defaults[.videoLoadingRetryCount] = videoLoadingRetryCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let avPlayerAllowsNonStreamableFormats = json["avPlayerAllowsNonStreamableFormats"].bool {
|
||||||
|
Defaults[.avPlayerAllowsNonStreamableFormats] = avPlayerAllowsNonStreamableFormats
|
||||||
|
}
|
||||||
|
|
||||||
if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool {
|
if let showMPVPlaybackStats = json["showMPVPlaybackStats"].bool {
|
||||||
Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats
|
Defaults[.showMPVPlaybackStats] = showMPVPlaybackStats
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import SwiftUI
|
|||||||
final class AVPlayerBackend: PlayerBackend {
|
final class AVPlayerBackend: PlayerBackend {
|
||||||
static let assetKeysToLoad = ["tracks", "playable", "duration"]
|
static let assetKeysToLoad = ["tracks", "playable", "duration"]
|
||||||
|
|
||||||
|
@Default(.avPlayerAllowsNonStreamableFormats) private var allowsNonStreamableFormats
|
||||||
|
|
||||||
private var logger = Logger(label: "avplayer-backend")
|
private var logger = Logger(label: "avplayer-backend")
|
||||||
|
|
||||||
var model: PlayerModel { .shared }
|
var model: PlayerModel { .shared }
|
||||||
@@ -150,7 +152,36 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func canPlay(_ stream: Stream) -> Bool {
|
func canPlay(_ stream: Stream) -> Bool {
|
||||||
stream.kind == .hls || stream.kind == .stream
|
// AVPlayer has a fundamental limitation with MP4/AVC1 progressive downloads:
|
||||||
|
// If the moov atom is at the end of the file (common case), it must download
|
||||||
|
// the entire file before playback can start. MPV doesn't have this limitation.
|
||||||
|
// By default, reject non-HLS MP4/AVC1 streams unless user explicitly enables them.
|
||||||
|
|
||||||
|
// Check if this is a non-streamable format (MP4/AVC1) that isn't HLS
|
||||||
|
let isNonStreamableFormat = stream.kind != .hls && (stream.format == .mp4 || stream.format == .avc1)
|
||||||
|
|
||||||
|
if isNonStreamableFormat && !allowsNonStreamableFormats {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If non-streamable formats are enabled, allow MP4/AVC1 adaptive streams
|
||||||
|
// but limit to 1080p maximum (higher resolutions can't be played properly)
|
||||||
|
if isNonStreamableFormat && allowsNonStreamableFormats {
|
||||||
|
let maxHeight = 1080
|
||||||
|
if let resolution = stream.resolution, resolution.height > maxHeight {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVPlayer works well with HLS and stream formats
|
||||||
|
return stream.kind == .hls || stream.kind == .stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFastLoadingFormat(_ stream: Stream) -> Bool {
|
||||||
|
// HLS and stream formats load quickly
|
||||||
|
// Non-streamable MP4/AVC1 formats may take a long time
|
||||||
|
return stream.kind == .hls || stream.kind == .stream
|
||||||
}
|
}
|
||||||
|
|
||||||
func playStream(
|
func playStream(
|
||||||
@@ -304,12 +335,12 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
preservingTime: Bool = false,
|
preservingTime: Bool = false,
|
||||||
model: PlayerModel
|
model: PlayerModel
|
||||||
) {
|
) {
|
||||||
|
model.logger.info("loading \(type.rawValue) track")
|
||||||
|
|
||||||
asset.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
|
asset.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model.logger.info("loading \(type.rawValue) track")
|
|
||||||
|
|
||||||
let assetTracks = asset.tracks(withMediaType: type)
|
let assetTracks = asset.tracks(withMediaType: type)
|
||||||
|
|
||||||
guard let compositionTrack = self.composition.addMutableTrack(
|
guard let compositionTrack = self.composition.addMutableTrack(
|
||||||
|
|||||||
@@ -133,6 +133,26 @@ extension PlayerModel {
|
|||||||
|
|
||||||
let profile = qualityProfile ?? .defaultProfile
|
let profile = qualityProfile ?? .defaultProfile
|
||||||
|
|
||||||
|
// For AVPlayer, prefer fast-loading formats (HLS/stream) over non-streamable formats
|
||||||
|
// to avoid long loading times when switching backends
|
||||||
|
if activeBackend == .appleAVPlayer, let avBackend = backend as? AVPlayerBackend {
|
||||||
|
// Try to find a fast-loading stream first
|
||||||
|
let fastLoadingStreams = availableStreams.filter { backend.canPlay($0) && avBackend.isFastLoadingFormat($0) }
|
||||||
|
if let fastStream = backend.bestPlayable(
|
||||||
|
fastLoadingStreams.filter { profile.isPreferred($0) },
|
||||||
|
maxResolution: profile.resolution, formatOrder: profile.formats
|
||||||
|
) {
|
||||||
|
return fastStream
|
||||||
|
}
|
||||||
|
// Fallback to any fast-loading stream
|
||||||
|
if let fastStream = backend.bestPlayable(
|
||||||
|
fastLoadingStreams,
|
||||||
|
maxResolution: profile.resolution, formatOrder: profile.formats
|
||||||
|
) {
|
||||||
|
return fastStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// First attempt: Filter by both `canPlay` and `isPreferred`
|
// First attempt: Filter by both `canPlay` and `isPreferred`
|
||||||
if let streamPreferredForProfile = backend.bestPlayable(
|
if let streamPreferredForProfile = backend.bestPlayable(
|
||||||
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
|
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ final class SearchModel: ObservableObject {
|
|||||||
var accounts: AccountsModel { .shared }
|
var accounts: AccountsModel { .shared }
|
||||||
private var resource: Resource!
|
private var resource: Resource!
|
||||||
|
|
||||||
init() {
|
init() {}
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
deinit {}
|
||||||
}
|
|
||||||
|
|
||||||
var isLoading: Bool {
|
var isLoading: Bool {
|
||||||
resource?.isLoading ?? false
|
resource?.isLoading ?? false
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ final class SearchQuery: ObservableObject {
|
|||||||
var parameter: String {
|
var parameter: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .uploadDate:
|
case .uploadDate:
|
||||||
return "upload_date"
|
return "date"
|
||||||
case .viewCount:
|
case .viewCount:
|
||||||
return "view_count"
|
return "views"
|
||||||
default:
|
default:
|
||||||
return rawValue
|
return rawValue
|
||||||
}
|
}
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -3,17 +3,18 @@
|
|||||||
<h1>Yattee</h1>
|
<h1>Yattee</h1>
|
||||||
<p>Privacy oriented video player for iOS, tvOS and macOS<br /></p>
|
<p>Privacy oriented video player for iOS, tvOS and macOS<br /></p>
|
||||||
|
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||||
[](https://github.com/yattee/yattee/issues)
|
[](https://github.com/yattee/yattee/issues)
|
||||||
[](https://github.com/yattee/yattee/pulls)
|
[](https://github.com/yattee/yattee/pulls)
|
||||||
[](https://matrix.to/#/#Yattee:matrix.org)
|
[](https://matrix.to/#/#Yattee:matrix.org)
|
||||||
|
|
||||||
[](https://yattee.stream/discord)
|
[](https://yattee.stream/discord)
|
||||||
|
|
||||||

|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
> **Yattee 2 is in the works!** A new version of the app is being built with a refreshed experience for iOS, tvOS and soon for macOS.
|
||||||
|
> It pairs with the new [Yattee Server](https://github.com/yattee/yattee-server) — a self-hosted backend powered by yt-dlp that supports 1000+ sites.
|
||||||
|
> Join the [TestFlight beta](https://yattee.stream/beta2) to try early builds, and check the new documentation site at [docs.yattee.stream](https://docs.yattee.stream) for guides, roadmap and changelog.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Native user interface built with [SwiftUI](https://developer.apple.com/swiftui/) with customization settings
|
* Native user interface built with [SwiftUI](https://developer.apple.com/swiftui/) with customization settings
|
||||||
* Player queue and history
|
* Player queue and history
|
||||||
@@ -21,30 +22,5 @@
|
|||||||
* Fullscreen, Picture in Picture and background audio playback
|
* Fullscreen, Picture in Picture and background audio playback
|
||||||
* [SponsorBlock](https://sponsor.ajay.app/), configurable categories to skip
|
* [SponsorBlock](https://sponsor.ajay.app/), configurable categories to skip
|
||||||
|
|
||||||
## Documentation
|
|
||||||
* [Installation](https://github.com/yattee/yattee/wiki/Installation-Instructions)
|
|
||||||
* [Building](https://github.com/yattee/yattee/wiki/Building-instructions)
|
|
||||||
* [Features](https://github.com/yattee/yattee/wiki/Features)
|
|
||||||
* [FAQ](https://github.com/yattee/yattee/wiki/FAQ)
|
|
||||||
* [Screenshots Gallery](https://github.com/yattee/yattee/wiki/Screenshots-Gallery)
|
|
||||||
* [Tips](https://github.com/yattee/yattee/wiki/Tips)
|
|
||||||
* [Integrations](https://github.com/yattee/yattee/wiki/Integrations)
|
|
||||||
* [Donations](https://github.com/yattee/yattee/wiki/Donations)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
If you're interestred in contributing, you can browse the [issues](https://github.com/yattee/yattee/issues) list or create a new one to discuss your feature idea. Every contribution is very welcome.
|
|
||||||
|
|
||||||
Use [building instructions](https://github.com/yattee/yattee/wiki/Building-instructions) or
|
|
||||||
join [Discord](https://yattee.stream/discord) or [Matrix Channel](https://matrix.to/#/#yattee:matrix.org) for a chat if you need an advice or want to discuss the project.
|
|
||||||
|
|
||||||
## Translations
|
|
||||||
|
|
||||||
You can help to make this project accessible to everyone by contributing to its localizations.
|
|
||||||
<a href="https://hosted.weblate.org/engage/yattee/">
|
|
||||||
<img src="https://hosted.weblate.org/widgets/yattee/-/localizable-strings/multi-auto.svg" alt="Translation status" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
Localization service and hosting is provided by [Weblate](https://weblate.org/en/).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
Yattee and its components is shared on [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
||||||
|
|||||||
@@ -109,22 +109,7 @@ struct ChannelPlaylistView: View {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private var playlistMenu: some View {
|
private var playlistMenu: some View {
|
||||||
Menu {
|
ZStack {
|
||||||
playButtons
|
|
||||||
|
|
||||||
favoriteButton
|
|
||||||
|
|
||||||
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
|
|
||||||
|
|
||||||
Section {
|
|
||||||
HideWatchedButtons()
|
|
||||||
HideShortsButtons()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
SettingsButtons()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
if let url = store.item?.thumbnailURL ?? playlist.thumbnailURL {
|
if let url = store.item?.thumbnailURL ?? playlist.thumbnailURL {
|
||||||
ThumbnailView(url: url)
|
ThumbnailView(url: url)
|
||||||
@@ -141,8 +126,43 @@ struct ChannelPlaylistView: View {
|
|||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
.transaction { t in t.animation = nil }
|
|
||||||
|
Menu {
|
||||||
|
playButtons
|
||||||
|
|
||||||
|
favoriteButton
|
||||||
|
|
||||||
|
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
HideWatchedButtons()
|
||||||
|
HideShortsButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
SettingsButtons()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
if let url = store.item?.thumbnailURL ?? playlist.thumbnailURL {
|
||||||
|
ThumbnailView(url: url)
|
||||||
|
.frame(width: 60, height: 30)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(playlist.title)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Image(systemName: "chevron.down.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.transaction { t in t.animation = nil }
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -259,26 +259,7 @@ struct ChannelVideosView: View {
|
|||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
var channelMenu: some View {
|
var channelMenu: some View {
|
||||||
Menu {
|
ZStack {
|
||||||
if let channel = presentedChannel {
|
|
||||||
contentTypePicker
|
|
||||||
Section {
|
|
||||||
subscriptionToggleButton
|
|
||||||
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, channel.id, channel.name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if subscriptions.isSubscribing(channel.id) {
|
|
||||||
toggleWatchedButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
|
|
||||||
|
|
||||||
Section {
|
|
||||||
HideWatchedButtons()
|
|
||||||
HideShortsButtons()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
thumbnail
|
thumbnail
|
||||||
|
|
||||||
@@ -311,6 +292,61 @@ struct ChannelVideosView: View {
|
|||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
if let channel = presentedChannel {
|
||||||
|
contentTypePicker
|
||||||
|
Section {
|
||||||
|
subscriptionToggleButton
|
||||||
|
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, channel.id, channel.name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if subscriptions.isSubscribing(channel.id) {
|
||||||
|
toggleWatchedButton
|
||||||
|
}
|
||||||
|
|
||||||
|
ListingStyleButtons(listingStyle: $channelPlaylistListingStyle)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
HideWatchedButtons()
|
||||||
|
HideShortsButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
thumbnail
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(presentedChannel?.name ?? "Channel")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.layoutPriority(1)
|
||||||
|
.frame(minWidth: 160, alignment: .leading)
|
||||||
|
|
||||||
|
Group {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
subscriptionsLabel
|
||||||
|
|
||||||
|
if presentedChannel?.verified ?? false {
|
||||||
|
Image(systemName: "checkmark.seal.fill")
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewsLabel
|
||||||
|
}
|
||||||
|
.frame(minWidth: 160, alignment: .leading)
|
||||||
|
}
|
||||||
|
.font(.caption2.bold())
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: "chevron.down.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ enum Constants {
|
|||||||
static let overlayAnimation = Animation.linear(duration: 0.2)
|
static let overlayAnimation = Animation.linear(duration: 0.2)
|
||||||
static let aspectRatio16x9 = 16.0 / 9.0
|
static let aspectRatio16x9 = 16.0 / 9.0
|
||||||
static let aspectRatio4x3 = 4.0 / 3.0
|
static let aspectRatio4x3 = 4.0 / 3.0
|
||||||
|
static let notificationCenterZoneHeight: Double = 60
|
||||||
|
|
||||||
static var isAppleTV: Bool {
|
static var isAppleTV: Bool {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ extension Defaults.Keys {
|
|||||||
hd1080p60MPVProfile,
|
hd1080p60MPVProfile,
|
||||||
hd1080pMPVProfile,
|
hd1080pMPVProfile,
|
||||||
hd720p60MPVProfile,
|
hd720p60MPVProfile,
|
||||||
hd720pMPVProfile
|
hd720pMPVProfile,
|
||||||
|
hd720pAVPlayerProfile
|
||||||
]
|
]
|
||||||
|
|
||||||
static let batteryCellularProfileDefault = hd720pMPVProfile.id
|
static let batteryCellularProfileDefault = hd720pMPVProfile.id
|
||||||
@@ -208,7 +209,8 @@ extension Defaults.Keys {
|
|||||||
hd1080pMPVProfile,
|
hd1080pMPVProfile,
|
||||||
hd720p60MPVProfile,
|
hd720p60MPVProfile,
|
||||||
hd720pMPVProfile,
|
hd720pMPVProfile,
|
||||||
sd360pMPVProfile
|
sd360pMPVProfile,
|
||||||
|
hd720pAVPlayerProfile
|
||||||
]
|
]
|
||||||
|
|
||||||
static let batteryCellularProfileDefault = sd360pMPVProfile.id
|
static let batteryCellularProfileDefault = sd360pMPVProfile.id
|
||||||
@@ -361,6 +363,7 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
static let showPlayNowInBackendContextMenu = Key<Bool>("showPlayNowInBackendContextMenu", default: false)
|
static let showPlayNowInBackendContextMenu = Key<Bool>("showPlayNowInBackendContextMenu", default: false)
|
||||||
static let videoLoadingRetryCount = Key<Int>("videoLoadingRetryCount", default: 10)
|
static let videoLoadingRetryCount = Key<Int>("videoLoadingRetryCount", default: 10)
|
||||||
|
static let avPlayerAllowsNonStreamableFormats = Key<Bool>("avPlayerAllowsNonStreamableFormats", default: false)
|
||||||
|
|
||||||
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
static let showMPVPlaybackStats = Key<Bool>("showMPVPlaybackStats", default: false)
|
||||||
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ struct FavoriteItemView: View {
|
|||||||
#else
|
#else
|
||||||
.padding(.horizontal, 15)
|
.padding(.horizontal, 15)
|
||||||
#endif
|
#endif
|
||||||
.frame(height: expectedContentHeight)
|
|
||||||
} else {
|
} else {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
// Reserve space immediately to prevent layout shift
|
// Reserve space immediately to prevent layout shift
|
||||||
|
|||||||
@@ -211,19 +211,7 @@ struct HomeView: View {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
var homeMenu: some View {
|
var homeMenu: some View {
|
||||||
Menu {
|
ZStack {
|
||||||
Section {
|
|
||||||
HideWatchedButtons()
|
|
||||||
HideShortsButtons()
|
|
||||||
}
|
|
||||||
Section {
|
|
||||||
Button {
|
|
||||||
navigation.presentingHomeSettings = true
|
|
||||||
} label: {
|
|
||||||
Label("Home Settings", systemImage: "gear")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Text("Home")
|
Text("Home")
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
@@ -233,8 +221,33 @@ struct HomeView: View {
|
|||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = nil }
|
|
||||||
|
Menu {
|
||||||
|
Section {
|
||||||
|
HideWatchedButtons()
|
||||||
|
HideShortsButtons()
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
navigation.presentingHomeSettings = true
|
||||||
|
} label: {
|
||||||
|
Label("Home Settings", systemImage: "gear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Text("Home")
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Image(systemName: "chevron.down.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.transaction { t in t.animation = nil }
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ struct ControlsOverlay: View {
|
|||||||
@ObservedObject private var player = PlayerModel.shared
|
@ObservedObject private var player = PlayerModel.shared
|
||||||
private var model = PlayerControlsModel.shared
|
private var model = PlayerControlsModel.shared
|
||||||
|
|
||||||
@State private var availableCaptions: [Captions] = []
|
|
||||||
@State private var isLoadingCaptions = true
|
|
||||||
@State private var contentSize: CGSize = .zero
|
@State private var contentSize: CGSize = .zero
|
||||||
|
|
||||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||||
@@ -23,7 +21,10 @@ struct ControlsOverlay: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
@State private var presentingButtonHintAlert = false
|
@State private var presentingQualityProfileMenu = false
|
||||||
|
@State private var presentingStreamMenu = false
|
||||||
|
@State private var presentingCaptionsMenu = false
|
||||||
|
@State private var presentingAudioTrackMenu = false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -104,19 +105,9 @@ struct ControlsOverlay: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.padding(.horizontal, 40)
|
.padding(.horizontal, 40)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(tvOS)
|
|
||||||
Text("Press and hold remote button to open captions and quality menus")
|
|
||||||
.frame(maxWidth: 400)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
.frame(maxHeight: overlayHeight)
|
.frame(maxHeight: contentSize.height)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.alert(isPresented: $presentingButtonHintAlert) {
|
|
||||||
Alert(title: Text("Press and hold to open this menu"))
|
|
||||||
}
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
focusedField = .qualityProfile
|
focusedField = .qualityProfile
|
||||||
}
|
}
|
||||||
@@ -127,14 +118,6 @@ struct ControlsOverlay: View {
|
|||||||
player.activeBackend == .mpv ? "Rate & Captions" : "Playback Rate"
|
player.activeBackend == .mpv ? "Rate & Captions" : "Playback Rate"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var overlayHeight: Double {
|
|
||||||
#if os(tvOS)
|
|
||||||
contentSize.height + 80.0
|
|
||||||
#else
|
|
||||||
contentSize.height
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private func controlsHeader(_ text: String) -> some View {
|
private func controlsHeader(_ text: String) -> some View {
|
||||||
Text(text)
|
Text(text)
|
||||||
.font(.system(.caption))
|
.font(.system(.caption))
|
||||||
@@ -279,23 +262,25 @@ struct ControlsOverlay: View {
|
|||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
#else
|
#else
|
||||||
ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) {
|
ControlsOverlayButton(
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .qualityProfile,
|
||||||
|
onSelect: { presentingQualityProfileMenu = true }
|
||||||
|
) {
|
||||||
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.alert("Quality Profile", isPresented: $presentingQualityProfileMenu) {
|
||||||
Button("Automatic") { player.qualityProfileSelection = nil }
|
Button("Automatic") { player.qualityProfileSelection = nil }
|
||||||
|
|
||||||
ForEach(qualityProfiles) { qualityProfile in
|
ForEach(qualityProfiles) { qualityProfile in
|
||||||
Button {
|
Button(qualityProfile.description) {
|
||||||
player.qualityProfileSelection = qualityProfile
|
player.qualityProfileSelection = qualityProfile
|
||||||
} label: {
|
|
||||||
Text(qualityProfile.description)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Cancel", role: .cancel) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -330,7 +315,7 @@ struct ControlsOverlay: View {
|
|||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
#else
|
#else
|
||||||
StreamControl(focusedField: $focusedField)
|
StreamControl(focusedField: $focusedField, presentingStreamMenu: $presentingStreamMenu)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,29 +354,31 @@ struct ControlsOverlay: View {
|
|||||||
.modifier(ControlBackgroundModifier())
|
.modifier(ControlBackgroundModifier())
|
||||||
.mask(RoundedRectangle(cornerRadius: 3))
|
.mask(RoundedRectangle(cornerRadius: 3))
|
||||||
#else
|
#else
|
||||||
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
|
ControlsOverlayButton(
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .captions,
|
||||||
|
onSelect: { presentingCaptionsMenu = true }
|
||||||
|
) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: "text.bubble")
|
Image(systemName: "text.bubble")
|
||||||
if let captions = captionsBinding.wrappedValue,
|
if let captions = captionsBinding.wrappedValue,
|
||||||
let language = LanguageCodes(rawValue: captions.code)
|
let language = LanguageCodes(rawValue: captions.code)
|
||||||
{
|
{
|
||||||
Text("\(language.description.capitalized) (\(language.rawValue))")
|
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
} else {
|
} else {
|
||||||
if captionsBinding.wrappedValue == nil {
|
if player.currentVideo?.captions.isEmpty == false {
|
||||||
Text("Not available")
|
|
||||||
} else {
|
|
||||||
Text("Disabled")
|
Text("Disabled")
|
||||||
.foregroundColor(.accentColor)
|
} else {
|
||||||
|
Text("Not available")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.alert("Captions", isPresented: $presentingCaptionsMenu) {
|
||||||
Button("Disabled") { captionsBinding.wrappedValue = nil }
|
Button("Disabled") { captionsBinding.wrappedValue = nil }
|
||||||
|
|
||||||
ForEach(availableCaptions) { caption in
|
ForEach(player.currentVideo?.captions ?? []) { caption in
|
||||||
Button(caption.description) { captionsBinding.wrappedValue = caption }
|
Button(caption.description) { captionsBinding.wrappedValue = caption }
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
@@ -400,7 +387,7 @@ struct ControlsOverlay: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var captionsPicker: some View {
|
@ViewBuilder private var captionsPicker: some View {
|
||||||
let captions = availableCaptions
|
let captions = player.currentVideo?.captions ?? []
|
||||||
Picker("Captions", selection: captionsBinding) {
|
Picker("Captions", selection: captionsBinding) {
|
||||||
if captions.isEmpty {
|
if captions.isEmpty {
|
||||||
Text("Not available").tag(Captions?.none)
|
Text("Not available").tag(Captions?.none)
|
||||||
@@ -412,31 +399,6 @@ struct ControlsOverlay: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(captions.isEmpty)
|
.disabled(captions.isEmpty)
|
||||||
.onAppear {
|
|
||||||
loadCaptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadCaptions() {
|
|
||||||
isLoadingCaptions = true
|
|
||||||
|
|
||||||
// Fetch captions asynchronously
|
|
||||||
Task {
|
|
||||||
let fetchedCaptions = await fetchCaptions()
|
|
||||||
await MainActor.run {
|
|
||||||
// Update state on the main thread
|
|
||||||
self.availableCaptions = fetchedCaptions
|
|
||||||
self.isLoadingCaptions = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchCaptions() async -> [Captions] {
|
|
||||||
// Access currentVideo from the main actor context
|
|
||||||
await MainActor.run {
|
|
||||||
// Safely access the main actor-isolated currentVideo property
|
|
||||||
player.currentVideo?.captions ?? []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var captionsBinding: Binding<Captions?> {
|
private var captionsBinding: Binding<Captions?> {
|
||||||
@@ -467,11 +429,15 @@ struct ControlsOverlay: View {
|
|||||||
.frame(maxWidth: 240, alignment: .trailing)
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
#else
|
#else
|
||||||
ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) {
|
ControlsOverlayButton(
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .audioTrack,
|
||||||
|
onSelect: { presentingAudioTrackMenu = true }
|
||||||
|
) {
|
||||||
Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
|
Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.alert("Audio Track", isPresented: $presentingAudioTrackMenu) {
|
||||||
ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in
|
ForEach(Array(player.availableAudioTracks.enumerated()), id: \.offset) { index, track in
|
||||||
Button(track.description) { player.selectedAudioTrackIndex = index }
|
Button(track.description) { player.selectedAudioTrackIndex = index }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,16 @@ struct TimelineView: View {
|
|||||||
.gesture(
|
.gesture(
|
||||||
DragGesture(minimumDistance: 5, coordinateSpace: .global)
|
DragGesture(minimumDistance: 5, coordinateSpace: .global)
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
|
#if os(iOS)
|
||||||
|
// In fullscreen, ignore gestures that start in the top notification center area
|
||||||
|
// to allow system notification center gesture to work
|
||||||
|
if player.playingFullScreen {
|
||||||
|
if value.startLocation.y < Constants.notificationCenterZoneHeight {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if !dragging {
|
if !dragging {
|
||||||
controls.removeTimer()
|
controls.removeTimer()
|
||||||
draggedFrom = current
|
draggedFrom = current
|
||||||
|
|||||||
@@ -209,16 +209,22 @@ struct PlaybackSettings: View {
|
|||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.frame(width: 100, alignment: .center)
|
.frame(width: 100, alignment: .center)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
ZStack {
|
||||||
ratePicker
|
|
||||||
} label: {
|
|
||||||
Text(player.rateLabel(player.currentRate))
|
Text(player.rateLabel(player.currentRate))
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.frame(width: 70)
|
.frame(width: 70)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
ratePicker
|
||||||
|
} label: {
|
||||||
|
Text(player.rateLabel(player.currentRate))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(width: 70)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.frame(width: 70, height: 40)
|
.frame(width: 70, height: 40)
|
||||||
#else
|
#else
|
||||||
Text(player.rateLabel(player.currentRate))
|
Text(player.rateLabel(player.currentRate))
|
||||||
@@ -331,12 +337,20 @@ struct PlaybackSettings: View {
|
|||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.frame(width: 300, alignment: .trailing)
|
.frame(width: 300, alignment: .trailing)
|
||||||
#else
|
#else
|
||||||
Menu {
|
ZStack {
|
||||||
playbackModePicker
|
|
||||||
} label: {
|
|
||||||
Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage)
|
Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
playbackModePicker
|
||||||
|
} label: {
|
||||||
|
Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,15 +370,22 @@ struct PlaybackSettings: View {
|
|||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.frame(width: 300, alignment: .trailing)
|
.frame(width: 300, alignment: .trailing)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
ZStack {
|
||||||
qualityProfilePicker
|
|
||||||
} label: {
|
|
||||||
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
.frame(maxWidth: 240, alignment: .trailing)
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
qualityProfilePicker
|
||||||
|
} label: {
|
||||||
|
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
.frame(maxWidth: 240, alignment: .trailing)
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
#else
|
#else
|
||||||
@@ -406,15 +427,22 @@ struct PlaybackSettings: View {
|
|||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.frame(width: 300, alignment: .trailing)
|
.frame(width: 300, alignment: .trailing)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
ZStack {
|
||||||
StreamControl()
|
|
||||||
} label: {
|
|
||||||
Text(player.streamSelection?.resolutionAndFormat ?? "loading...")
|
Text(player.streamSelection?.resolutionAndFormat ?? "loading...")
|
||||||
.frame(width: 140, height: 40, alignment: .trailing)
|
|
||||||
.foregroundColor(player.streamSelection == nil ? .secondary : .accentColor)
|
.foregroundColor(player.streamSelection == nil ? .secondary : .accentColor)
|
||||||
|
.frame(width: 140, height: 40, alignment: .trailing)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
StreamControl()
|
||||||
|
} label: {
|
||||||
|
Text(player.streamSelection?.resolutionAndFormat ?? "loading...")
|
||||||
|
.foregroundColor(player.streamSelection == nil ? .secondary : .accentColor)
|
||||||
|
.frame(width: 140, height: 40, alignment: .trailing)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.frame(width: 140, height: 40, alignment: .trailing)
|
.frame(width: 140, height: 40, alignment: .trailing)
|
||||||
#else
|
#else
|
||||||
StreamControl(focusedField: $focusedField)
|
StreamControl(focusedField: $focusedField)
|
||||||
@@ -429,18 +457,13 @@ struct PlaybackSettings: View {
|
|||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.frame(width: 300, alignment: .trailing)
|
.frame(width: 300, alignment: .trailing)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
ZStack {
|
||||||
if videoCaptions?.isEmpty == false {
|
|
||||||
captionsPicker
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "text.bubble")
|
Image(systemName: "text.bubble")
|
||||||
if let captions = player.captions,
|
if let captions = player.captions,
|
||||||
let language = LanguageCodes(rawValue: captions.code)
|
let language = LanguageCodes(rawValue: captions.code)
|
||||||
{
|
{
|
||||||
Text("\(language.description.capitalized) (\(language.rawValue))")
|
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
} else {
|
} else {
|
||||||
if videoCaptions?.isEmpty == true {
|
if videoCaptions?.isEmpty == true {
|
||||||
Text("Not available")
|
Text("Not available")
|
||||||
@@ -449,13 +472,38 @@ struct PlaybackSettings: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
.frame(alignment: .trailing)
|
.frame(alignment: .trailing)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
.disabled(videoCaptions?.isEmpty == true)
|
|
||||||
|
Menu {
|
||||||
|
if videoCaptions?.isEmpty == false {
|
||||||
|
captionsPicker
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "text.bubble")
|
||||||
|
if let captions = player.captions,
|
||||||
|
let language = LanguageCodes(rawValue: captions.code)
|
||||||
|
{
|
||||||
|
Text("\(language.description.capitalized) (\(language.rawValue))")
|
||||||
|
} else {
|
||||||
|
if videoCaptions?.isEmpty == true {
|
||||||
|
Text("Not available")
|
||||||
|
} else {
|
||||||
|
Text("Disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.frame(alignment: .trailing)
|
||||||
|
.frame(height: 40)
|
||||||
|
.opacity(0)
|
||||||
|
.disabled(videoCaptions?.isEmpty == true)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
#else
|
#else
|
||||||
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
|
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
@@ -500,15 +548,22 @@ struct PlaybackSettings: View {
|
|||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
.frame(width: 300, alignment: .trailing)
|
.frame(width: 300, alignment: .trailing)
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
Menu {
|
ZStack {
|
||||||
audioTrackPicker
|
|
||||||
} label: {
|
|
||||||
Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
|
Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
.frame(maxWidth: 240, alignment: .trailing)
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
audioTrackPicker
|
||||||
|
} label: {
|
||||||
|
Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.transaction { t in t.animation = .none }
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = .none }
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
.frame(maxWidth: 240, alignment: .trailing)
|
.frame(maxWidth: 240, alignment: .trailing)
|
||||||
.frame(height: 40)
|
.frame(height: 40)
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ extension VideoPlayerView {
|
|||||||
state = true
|
state = true
|
||||||
}
|
}
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
|
#if os(iOS)
|
||||||
|
// In fullscreen, ignore gestures that start in the top notification center area
|
||||||
|
// to allow system notification center gesture to work
|
||||||
|
if player.playingFullScreen {
|
||||||
|
if value.startLocation.y < Constants.notificationCenterZoneHeight {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
guard player.presentingPlayer,
|
guard player.presentingPlayer,
|
||||||
!controlsOverlayModel.presenting,
|
!controlsOverlayModel.presenting,
|
||||||
dragGestureState,
|
dragGestureState,
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import SwiftUI
|
|||||||
struct StreamControl: View {
|
struct StreamControl: View {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
var focusedField: FocusState<ControlsOverlay.Field?>.Binding?
|
var focusedField: FocusState<ControlsOverlay.Field?>.Binding?
|
||||||
|
@Binding var presentingStreamMenu: Bool
|
||||||
|
|
||||||
init(focusedField: FocusState<ControlsOverlay.Field?>.Binding?) {
|
init(focusedField: FocusState<ControlsOverlay.Field?>.Binding?, presentingStreamMenu: Binding<Bool>) {
|
||||||
self.focusedField = focusedField
|
self.focusedField = focusedField
|
||||||
|
_presentingStreamMenu = presentingStreamMenu
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -45,16 +47,20 @@ struct StreamControl: View {
|
|||||||
.fixedSize()
|
.fixedSize()
|
||||||
#endif
|
#endif
|
||||||
#else
|
#else
|
||||||
ControlsOverlayButton(focusedField: focusedField!, field: .stream) {
|
ControlsOverlayButton(
|
||||||
|
focusedField: focusedField!,
|
||||||
|
field: .stream,
|
||||||
|
onSelect: { presentingStreamMenu = true }
|
||||||
|
) {
|
||||||
Text(player.streamSelection?.shortQuality ?? "loading")
|
Text(player.streamSelection?.shortQuality ?? "loading")
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
}
|
}
|
||||||
.contextMenu {
|
.alert("Stream Quality", isPresented: $presentingStreamMenu) {
|
||||||
ForEach(streams) { stream in
|
ForEach(streams) { stream in
|
||||||
Button(stream.description) { player.streamSelection = stream }
|
Button(stream.description) { player.streamSelection = stream }
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Close", role: .cancel) {}
|
Button("Cancel", role: .cancel) {}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -79,7 +85,7 @@ struct StreamControl: View {
|
|||||||
struct StreamControl_Previews: PreviewProvider {
|
struct StreamControl_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
StreamControl(focusedField: .none)
|
StreamControl(focusedField: .none, presentingStreamMenu: .constant(false))
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
#else
|
#else
|
||||||
StreamControl()
|
StreamControl()
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ struct VideoDetails: View {
|
|||||||
@Environment(\.navigationStyle) private var navigationStyle
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@@ -397,7 +398,11 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.padding(.bottom, fullScreen ? max(60, safeAreaModel.safeArea.bottom + 20) : player.playerSize.height + safeAreaModel.safeArea.bottom + 20)
|
||||||
|
#else
|
||||||
.padding(.bottom, 60)
|
.padding(.bottom, 60)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@@ -190,40 +190,7 @@ struct PlaylistsView: View {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
var playlistsMenu: some View {
|
var playlistsMenu: some View {
|
||||||
let title = currentPlaylist?.title ?? "Playlists"
|
let title = currentPlaylist?.title ?? "Playlists"
|
||||||
return Menu {
|
return ZStack {
|
||||||
Menu {
|
|
||||||
selectPlaylistButton
|
|
||||||
} label: {
|
|
||||||
Label(title, systemImage: "list.and.film")
|
|
||||||
}
|
|
||||||
Section {
|
|
||||||
if let currentPlaylist {
|
|
||||||
playButtons
|
|
||||||
|
|
||||||
editPlaylistButton
|
|
||||||
|
|
||||||
if let account = accounts.current {
|
|
||||||
FavoriteButton(item: FavoriteItem(section: .playlist(account.id, currentPlaylist.id)))
|
|
||||||
.id(currentPlaylist.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if accounts.signedIn {
|
|
||||||
newPlaylistButton
|
|
||||||
}
|
|
||||||
|
|
||||||
ListingStyleButtons(listingStyle: $playlistListingStyle)
|
|
||||||
|
|
||||||
Section {
|
|
||||||
HideWatchedButtons()
|
|
||||||
HideShortsButtons()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
SettingsButtons()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "list.and.film")
|
Image(systemName: "list.and.film")
|
||||||
@@ -239,9 +206,61 @@ struct PlaylistsView: View {
|
|||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
.transaction { t in t.animation = nil }
|
|
||||||
|
Menu {
|
||||||
|
Menu {
|
||||||
|
selectPlaylistButton
|
||||||
|
} label: {
|
||||||
|
Label(title, systemImage: "list.and.film")
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
if let currentPlaylist {
|
||||||
|
playButtons
|
||||||
|
|
||||||
|
editPlaylistButton
|
||||||
|
|
||||||
|
if let account = accounts.current {
|
||||||
|
FavoriteButton(item: FavoriteItem(section: .playlist(account.id, currentPlaylist.id)))
|
||||||
|
.id(currentPlaylist.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if accounts.signedIn {
|
||||||
|
newPlaylistButton
|
||||||
|
}
|
||||||
|
|
||||||
|
ListingStyleButtons(listingStyle: $playlistListingStyle)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
HideWatchedButtons()
|
||||||
|
HideShortsButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
SettingsButtons()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "list.and.film")
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Image(systemName: "chevron.down.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
.imageScale(.small)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
.disabled(!accounts.signedIn)
|
||||||
}
|
}
|
||||||
.disabled(!accounts.signedIn)
|
.transaction { t in t.animation = nil }
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ struct AdvancedSettings: View {
|
|||||||
@Default(.feedCacheSize) private var feedCacheSize
|
@Default(.feedCacheSize) private var feedCacheSize
|
||||||
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
|
||||||
@Default(.videoLoadingRetryCount) private var videoLoadingRetryCount
|
@Default(.videoLoadingRetryCount) private var videoLoadingRetryCount
|
||||||
|
@Default(.avPlayerAllowsNonStreamableFormats) private var avPlayerAllowsNonStreamableFormats
|
||||||
|
|
||||||
@State private var filesToShare = [MPVClient.logFile]
|
@State private var filesToShare = [MPVClient.logFile]
|
||||||
@State private var presentingShareSheet = false
|
@State private var presentingShareSheet = false
|
||||||
|
|
||||||
|
@ObservedObject private var player = PlayerModel.shared
|
||||||
private var settings = SettingsModel.shared
|
private var settings = SettingsModel.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -73,6 +75,10 @@ struct AdvancedSettings: View {
|
|||||||
videoLoadingRetryCountField
|
videoLoadingRetryCountField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section(header: SettingsHeader(text: "AVPlayer"), footer: avPlayerNonStreamableFormatsFooter) {
|
||||||
|
avPlayerAllowsNonStreamableFormatsToggle
|
||||||
|
}
|
||||||
|
|
||||||
Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) {
|
Section(header: SettingsHeader(text: "MPV"), footer: mpvFooter) {
|
||||||
showMPVPlaybackStatsToggle
|
showMPVPlaybackStatsToggle
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@@ -370,6 +376,22 @@ struct AdvancedSettings: View {
|
|||||||
Text(String(format: "Total size: %@".localized(), BaseCacheModel.shared.totalSizeFormatted))
|
Text(String(format: "Total size: %@".localized(), BaseCacheModel.shared.totalSizeFormatted))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var avPlayerAllowsNonStreamableFormatsToggle: some View {
|
||||||
|
Toggle("Enable non-streamable formats (MP4/AVC1)", isOn: $avPlayerAllowsNonStreamableFormats)
|
||||||
|
.onChange(of: avPlayerAllowsNonStreamableFormats) { _ in
|
||||||
|
// Trigger refresh of available streams when setting changes
|
||||||
|
if let video = player.currentVideo {
|
||||||
|
player.loadAvailableStreams(video)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var avPlayerNonStreamableFormatsFooter: some View {
|
||||||
|
Text("Non-streamable video formats (MP4/AVC1) may take a long time to start playback with AVPlayer. These formats require downloading metadata before playback can begin. Limited to 1080p maximum. For better performance with these formats, use MPV backend instead.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AdvancedSettings_Previews: PreviewProvider {
|
struct AdvancedSettings_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ struct BrowsingSettings: View {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
Toggle("Show Documents", isOn: $showDocuments)
|
Toggle("Show Documents", isOn: $showDocuments)
|
||||||
|
|
||||||
if Constants.isIPad {
|
if Constants.isIPhone {
|
||||||
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
|
||||||
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
.onChange(of: lockPortraitWhenBrowsing) { lock in
|
||||||
if lock {
|
if lock {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ struct InstanceSettings: View {
|
|||||||
@State private var frontendURL = ""
|
@State private var frontendURL = ""
|
||||||
@State private var proxiesVideos = false
|
@State private var proxiesVideos = false
|
||||||
@State private var invidiousCompanion = false
|
@State private var invidiousCompanion = false
|
||||||
|
@State private var hideVideosWithoutDuration = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
@@ -97,6 +98,16 @@ struct InstanceSettings: View {
|
|||||||
.onChange(of: invidiousCompanion) { newValue in
|
.onChange(of: invidiousCompanion) { newValue in
|
||||||
InstancesModel.shared.setInvidiousCompanion(instance, newValue)
|
InstancesModel.shared.setInvidiousCompanion(instance, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section(footer: Text("This can be used to hide shorts".localized())) {
|
||||||
|
hideVideosWithoutDurationToggle
|
||||||
|
.onAppear {
|
||||||
|
hideVideosWithoutDuration = instance.hideVideosWithoutDuration
|
||||||
|
}
|
||||||
|
.onChange(of: hideVideosWithoutDuration) { newValue in
|
||||||
|
InstancesModel.shared.setHideVideosWithoutDuration(instance, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -116,6 +127,10 @@ struct InstanceSettings: View {
|
|||||||
Toggle("Invidious companion", isOn: $invidiousCompanion)
|
Toggle("Invidious companion", isOn: $invidiousCompanion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var hideVideosWithoutDurationToggle: some View {
|
||||||
|
Toggle("Experimental: Hide videos without duration", isOn: $hideVideosWithoutDuration)
|
||||||
|
}
|
||||||
|
|
||||||
private func removeAccount(_ account: Account) {
|
private func removeAccount(_ account: Account) {
|
||||||
AccountsModel.remove(account)
|
AccountsModel.remove(account)
|
||||||
accountsChanged.toggle()
|
accountsChanged.toggle()
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ struct SettingsView: View {
|
|||||||
case .locations:
|
case .locations:
|
||||||
return 600
|
return 600
|
||||||
case .advanced:
|
case .advanced:
|
||||||
return 630
|
return 700
|
||||||
case .importExport:
|
case .importExport:
|
||||||
return 580
|
return 580
|
||||||
case .help:
|
case .help:
|
||||||
|
|||||||
@@ -168,22 +168,7 @@ struct TrendingView: View {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
var trendingMenu: some View {
|
var trendingMenu: some View {
|
||||||
Menu {
|
ZStack {
|
||||||
countryButton
|
|
||||||
|
|
||||||
categoryButton
|
|
||||||
|
|
||||||
ListingStyleButtons(listingStyle: $trendingListingStyle)
|
|
||||||
|
|
||||||
Section {
|
|
||||||
HideWatchedButtons()
|
|
||||||
HideShortsButtons()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
SettingsButtons()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Text("\(country.flag) \(country.name)")
|
Text("\(country.flag) \(country.name)")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
@@ -194,6 +179,35 @@ struct TrendingView: View {
|
|||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
countryButton
|
||||||
|
|
||||||
|
categoryButton
|
||||||
|
|
||||||
|
ListingStyleButtons(listingStyle: $trendingListingStyle)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
HideWatchedButtons()
|
||||||
|
HideShortsButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
SettingsButtons()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Text("\(country.flag) \(country.name)")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Image(systemName: "chevron.down.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 320)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -144,10 +144,15 @@ struct OpenVideosView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
#endif
|
#endif
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
Menu {
|
ZStack {
|
||||||
playbackModePicker
|
|
||||||
} label: {
|
|
||||||
Text(playbackMode.description)
|
Text(playbackMode.description)
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
playbackModePicker
|
||||||
|
} label: {
|
||||||
|
Text(playbackMode.description)
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
playbackModePicker
|
playbackModePicker
|
||||||
|
|||||||
@@ -90,18 +90,7 @@ struct PopularView: View {
|
|||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private var popularMenu: some View {
|
private var popularMenu: some View {
|
||||||
Menu {
|
ZStack {
|
||||||
ListingStyleButtons(listingStyle: $popularListingStyle)
|
|
||||||
|
|
||||||
Section {
|
|
||||||
HideWatchedButtons()
|
|
||||||
HideShortsButtons()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
SettingsButtons()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "chart.bar.fill")
|
Image(systemName: "chart.bar.fill")
|
||||||
@@ -117,8 +106,38 @@ struct PopularView: View {
|
|||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
}
|
}
|
||||||
.transaction { t in t.animation = nil }
|
|
||||||
|
Menu {
|
||||||
|
ListingStyleButtons(listingStyle: $popularListingStyle)
|
||||||
|
|
||||||
|
Section {
|
||||||
|
HideWatchedButtons()
|
||||||
|
HideShortsButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
SettingsButtons()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "chart.bar.fill")
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.imageScale(.small)
|
||||||
|
|
||||||
|
Text("Popular")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: "chevron.down.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.transaction { t in t.animation = nil }
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -33,23 +33,21 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
// Conditional overlay to block taps on underlying views
|
#if os(iOS)
|
||||||
if isOverlayVisible {
|
// Conditional overlay to block taps on underlying views
|
||||||
Color.clear
|
if isOverlayVisible {
|
||||||
.contentShape(Rectangle())
|
Color.clear
|
||||||
#if !os(tvOS)
|
.contentShape(Rectangle())
|
||||||
// This is not available on tvOS < 16 so we leave out.
|
.onTapGesture {
|
||||||
// TODO: remove #if when setting the minimum deployment target to >= 16
|
// Dismiss overlay without triggering other interactions
|
||||||
.onTapGesture {
|
isOverlayVisible = false
|
||||||
// Dismiss overlay without triggering other interactions
|
}
|
||||||
isOverlayVisible = false
|
.ignoresSafeArea() // Ensure overlay covers the entire screen
|
||||||
}
|
.accessibilityLabel("Dismiss context menu")
|
||||||
#endif
|
.accessibilityHint("Tap to close the context")
|
||||||
.ignoresSafeArea() // Ensure overlay covers the entire screen
|
.accessibilityAddTraits(.isButton)
|
||||||
.accessibilityLabel("Dismiss context menu")
|
}
|
||||||
.accessibilityHint("Tap to close the context")
|
#endif
|
||||||
.accessibilityAddTraits(.isButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
if video.videoID != Video.fixtureID {
|
if video.videoID != Video.fixtureID {
|
||||||
contextMenu
|
contextMenu
|
||||||
@@ -156,7 +154,9 @@ struct VideoContextMenuView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
Button("Cancel", role: .cancel) {}
|
if #unavailable(tvOS 18.0) {
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
"%@ Channel" = "%@ Csatorna";
|
"%@ Channel" = "%@ Csatorna";
|
||||||
"%@ Playlist" = "%@ Lejátszási lista";
|
"%@ Playlist" = "%@ Lejátszási lista";
|
||||||
"10 seconds forwards/backwards" = "10 másodperc előre/vissza";
|
"10 seconds forwards/backwards" = "10 másodperc előre/vissza";
|
||||||
"%@ subscribers" = "%@ feliratkozók";
|
"%@ subscribers" = "%@ feliratkozó";
|
||||||
"%lld videos" = "%lld videók";
|
"%lld videos" = "%lld videó";
|
||||||
"No results" = "Nincsenek találatok";
|
"No results" = "Nincsenek találatok";
|
||||||
"No Playlists" = "Nincsenek lejátszási listák";
|
"No Playlists" = "Nincsenek lejátszási listák";
|
||||||
"Mark video as watched after playing" = "Jelölje meg a videót megtekintettként lejátszás után";
|
"Mark video as watched after playing" = "Jelölje meg a videót megtekintettként lejátszás után";
|
||||||
"Mark watched videos with" = "Megtekintett videók megjelölése a következővel";
|
"Mark watched videos with" = "Megtekintett videók megjelölése a következővel";
|
||||||
"Matrix Channel" = "Matrix csatorna";
|
"Matrix Channel" = "Matrix-csatorna";
|
||||||
"Find Other" = "Egyebek keresése";
|
"Find Other" = "Egyebek keresése";
|
||||||
"Hour" = "Óra";
|
"Hour" = "Óra";
|
||||||
"Month" = "Hónap";
|
"Month" = "Hónap";
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
"Honor orientation lock" = "Tájolás zárolása";
|
"Honor orientation lock" = "Tájolás zárolása";
|
||||||
"I am lost" = "Elvesztem";
|
"I am lost" = "Elvesztem";
|
||||||
"I found a bug /" = "Találtam egy hibát /";
|
"I found a bug /" = "Találtam egy hibát /";
|
||||||
"I have a feature request" = "Van egy funkció kérésem";
|
"I have a feature request" = "Van egy funkciójavaslatom";
|
||||||
"I like this app!" = "Tetszik ez az alkalmazás!";
|
"I like this app!" = "Tetszik ez az alkalmazás!";
|
||||||
"I want to ask a question" = "Szeretnék feltenni egy kérdést";
|
"I want to ask a question" = "Szeretnék feltenni egy kérdést";
|
||||||
"If you are interested what's coming in future updates, you can track project Milestones." = "Ha érdekli Önt, hogy mi várható a jövőbeni frissítésekben, akkor nyomon követheti a projekt mérföldköveit.";
|
"If you are interested what's coming in future updates, you can track project Milestones." = "Ha érdekli Önt, hogy mi várható a jövőbeni frissítésekben, akkor nyomon követheti a projekt mérföldköveit.";
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
"Playback" = "Visszajátszás";
|
"Playback" = "Visszajátszás";
|
||||||
"Player" = "Lejátszó";
|
"Player" = "Lejátszó";
|
||||||
"Playlist" = "Lejátszási lista";
|
"Playlist" = "Lejátszási lista";
|
||||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "A(z) „%@” lejátszási lista törlésre kerül.\nEzt nem lehet visszaállítani.";
|
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "A(z) „%@” lejátszási lista törölve lesz.\nEzt nem lehet visszaállítani.";
|
||||||
"Popular" = "Népszerű";
|
"Popular" = "Népszerű";
|
||||||
"Preferred Formats" = "Előnyben részesített formátumok";
|
"Preferred Formats" = "Előnyben részesített formátumok";
|
||||||
"Proxy videos" = "Proxyzott videók";
|
"Proxy videos" = "Proxyzott videók";
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
"Source" = "Forrás";
|
"Source" = "Forrás";
|
||||||
"Sponsor" = "Szponzor";
|
"Sponsor" = "Szponzor";
|
||||||
"SponsorBlock" = "SponsorBlock";
|
"SponsorBlock" = "SponsorBlock";
|
||||||
"SponsorBlock API Instance" = "SponsorBlock API példány";
|
"SponsorBlock API Instance" = "SponsorBlock API-példány";
|
||||||
"Subscribe" = "Feliratkozás";
|
"Subscribe" = "Feliratkozás";
|
||||||
"Subscriptions" = "Feliratkozások";
|
"Subscriptions" = "Feliratkozások";
|
||||||
"Switch to other public location" = "Váltás más nyilvános helyre";
|
"Switch to other public location" = "Váltás más nyilvános helyre";
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
"Stream & Player" = "Közvetítő és lejátszó";
|
"Stream & Player" = "Közvetítő és lejátszó";
|
||||||
"Statistics" = "Statisztika";
|
"Statistics" = "Statisztika";
|
||||||
"Hardware decoder" = "Hardveres dekódoló";
|
"Hardware decoder" = "Hardveres dekódoló";
|
||||||
"Stream FPS" = "FPS folyam";
|
"Stream FPS" = "FPS-folyam";
|
||||||
"Rate & Captions" = "Értékelés és feliratok";
|
"Rate & Captions" = "Értékelés és feliratok";
|
||||||
"Dropped frames" = "Eldobott keretek";
|
"Dropped frames" = "Eldobott keretek";
|
||||||
"Any format" = "Bármilyen formátum";
|
"Any format" = "Bármilyen formátum";
|
||||||
@@ -395,7 +395,7 @@
|
|||||||
"Open Video" = "Videó megnyitása";
|
"Open Video" = "Videó megnyitása";
|
||||||
"Default Profile" = "Alapértelmezett profil";
|
"Default Profile" = "Alapértelmezett profil";
|
||||||
"Share%@link" = "%@ hivatkozás megosztása";
|
"Share%@link" = "%@ hivatkozás megosztása";
|
||||||
"\"%@\" will be irreversibly removed from this device." = "A(z) „%@” visszavonhatatlanul eltávolításra kerül erről az eszközről.";
|
"\"%@\" will be irreversibly removed from this device." = "A(z) „%@” visszavonhatatlanul el lesz távolítva erről az eszközről.";
|
||||||
"Could not delete document" = "A dokumentum törlése nem sikerült";
|
"Could not delete document" = "A dokumentum törlése nem sikerült";
|
||||||
"Are you sure you want to remove %@ location?" = "Biztosan törölni szeretné a(z) %@ helyet?";
|
"Are you sure you want to remove %@ location?" = "Biztosan törölni szeretné a(z) %@ helyet?";
|
||||||
"Live Streams" = "Élő közvetítések";
|
"Live Streams" = "Élő közvetítések";
|
||||||
@@ -471,7 +471,7 @@
|
|||||||
"Disable filters" = "Szűrők kikapcsolása";
|
"Disable filters" = "Szűrők kikapcsolása";
|
||||||
"You need to create an instance and accounts\nto access %@ section" = "Létre kell hoznia egy példányt és fiókokat\na(z) %@ szakasz eléréséhez";
|
"You need to create an instance and accounts\nto access %@ section" = "Létre kell hoznia egy példányt és fiókokat\na(z) %@ szakasz eléréséhez";
|
||||||
"You can switch between profiles in playback settings controls." = "A lejátszási beállítások vezérlőiben válthat a profilok között.";
|
"You can switch between profiles in playback settings controls." = "A lejátszási beállítások vezérlőiben válthat a profilok között.";
|
||||||
"Share files from Finder on a Mac\nor iTunes on Windows" = "Fájlok megosztása a Finderből Macen\nvagy iTunes használatával Windowson";
|
"Share files from Finder on a Mac\nor iTunes on Windows" = "Fájlok megosztása a Finderből Mac számítógépen\nvagy iTunes használatával Windowson";
|
||||||
"Open logs in Finder" = "Naplók megnyitása a Finderben";
|
"Open logs in Finder" = "Naplók megnyitása a Finderben";
|
||||||
"Could not open playlist" = "Nem sikerült megnyitni a lejátszási listát";
|
"Could not open playlist" = "Nem sikerült megnyitni a lejátszási listát";
|
||||||
"Now Playing" = "Jelenleg lejátszás alatt";
|
"Now Playing" = "Jelenleg lejátszás alatt";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"Help" = "Tallalt";
|
"Help" = "Tallalt";
|
||||||
"Movies" = "Isura";
|
"Movies" = "Isura";
|
||||||
"Music" = "Aẓawan";
|
"Music" = "Aẓawan";
|
||||||
"Profiles" = "Imaɣnuyen";
|
"Profiles" = "Imaɣnuten";
|
||||||
"Search" = "Nadi";
|
"Search" = "Nadi";
|
||||||
"Search..." = "Nadi...";
|
"Search..." = "Nadi...";
|
||||||
"Settings" = "Iɣewwaren";
|
"Settings" = "Iɣewwaren";
|
||||||
|
|||||||
10
Shared/kn.lproj/Localizable.strings
Normal file
10
Shared/kn.lproj/Localizable.strings
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
" subscribers" = " ಸಬ್ಸ್ಕ್ರೈಬರ್ಗಳು";
|
||||||
|
"%@ Channel" = "%@ ಚಾನಲ್";
|
||||||
|
"%@ subscribers" = "%@ ಸಬ್ಸ್ಕ್ರೈಬರ್ಗಳು";
|
||||||
|
"Accounts" = "ಖಾತೆ";
|
||||||
|
"Add Account" = "ಖಾತೆಯನ್ನು ಸೇರಿಸಿ";
|
||||||
|
"Add Account..." = "ಖಾತೆಯನ್ನು ಸೇರಿಸಿ...";
|
||||||
|
"Add Location" = "ಸ್ಥಳವನ್ನು ಸೇರಿಸಿ";
|
||||||
|
"Add Location..." = "ಸ್ಥಳವನ್ನು ಸೇರಿಸಿ...";
|
||||||
|
"Add profile..." = "ಪ್ರೊಫೈಲ್ ಸೇರಿಸಿ...";
|
||||||
|
"Add Quality Profile" = "ಕ್ವಾಲಿಟಿ ಪ್ರೊಫೈಲ್ ಸೇರಿಸಿ";
|
||||||
@@ -5,19 +5,19 @@
|
|||||||
"%@ Playlist" = "%@ 播放列表";
|
"%@ Playlist" = "%@ 播放列表";
|
||||||
"%@ subscribers" = "%@ 订阅者";
|
"%@ subscribers" = "%@ 订阅者";
|
||||||
"%lld videos" = "%lld 视频";
|
"%lld videos" = "%lld 视频";
|
||||||
"10 seconds forwards/backwards" = "向前/向后10秒";
|
"10 seconds forwards/backwards" = "快进/快退 10 秒";
|
||||||
"Accounts" = "账号";
|
"Accounts" = "账号";
|
||||||
"Accounts are not supported for the application of this instance" = "此实例的应用程序不支持帐户";
|
"Accounts are not supported for the application of this instance" = "此实例的应用程序不支持账号";
|
||||||
"Add Account" = "新建账户";
|
"Add Account" = "添加账号";
|
||||||
"Add Account..." = "新建账户...";
|
"Add Account..." = "添加账号…";
|
||||||
"Add Location" = "新建地址";
|
"Add Location" = "添加位置";
|
||||||
"Add Location..." = "新建地址...";
|
"Add Location..." = "添加位置…";
|
||||||
"Add profile..." = "新建配置...";
|
"Add profile..." = "添加配置…";
|
||||||
"Add Quality Profile" = "新建质量配置";
|
"Add Quality Profile" = "添加质量配置";
|
||||||
"Add to %@" = "添加到 %@";
|
"Add to %@" = "添加到 %@";
|
||||||
"Add to Favorites" = "添加到喜欢";
|
"Add to Favorites" = "添加到收藏";
|
||||||
"Add to Playlist" = "添加到播放列表";
|
"Add to Playlist" = "添加到播放列表";
|
||||||
"Add to Playlist..." = "添加到播放列表...";
|
"Add to Playlist..." = "添加到播放列表…";
|
||||||
"Advanced" = "高级";
|
"Advanced" = "高级";
|
||||||
|
|
||||||
/* Trending category, section containing all kinds of videos */
|
/* Trending category, section containing all kinds of videos */
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
"Always use AVPlayer for live videos" = "对于直播,总是使用 AVPlayer";
|
"Always use AVPlayer for live videos" = "对于直播,总是使用 AVPlayer";
|
||||||
"Anonymous" = "匿名";
|
"Anonymous" = "匿名";
|
||||||
"Apply to all" = "应用到所有";
|
"Apply to all" = "应用到所有";
|
||||||
"Are you sure you want to clear history of watched videos?" = "你确定要清除视频播放历史记录吗?";
|
"Are you sure you want to clear history of watched videos?" = "是否确定要清除已观看视频的历史记录?";
|
||||||
"Are you sure you want to clear search history?" = "你确定要清除搜索记录吗?";
|
"Are you sure you want to clear search history?" = "是否确定要清除搜索历史记录?";
|
||||||
"Are you sure you want to delete playlist?" = "你确定要删除播放列表吗?";
|
"Are you sure you want to delete playlist?" = "是否确定要删除播放列表?";
|
||||||
"Are you sure you want to restore default quality profiles?" = "你确定要重置默认的质量配置?";
|
"Are you sure you want to restore default quality profiles?" = "是否确定要恢复默认质量配置?";
|
||||||
"Are you sure you want to unsubscribe from %@?" = "你确定要取消订阅 %@?";
|
"Are you sure you want to unsubscribe from %@?" = "是否确定要取消订阅 %@?";
|
||||||
"Automatic" = "自动";
|
"Automatic" = "自动";
|
||||||
"Autoplaying Next" = "自动播放下一个";
|
"Autoplaying Next" = "自动播放下一个";
|
||||||
"Backend" = "后端";
|
"Backend" = "后端";
|
||||||
@@ -38,21 +38,21 @@
|
|||||||
"Battery" = "电池";
|
"Battery" = "电池";
|
||||||
"Blue" = "蓝色";
|
"Blue" = "蓝色";
|
||||||
"Browsing" = "浏览";
|
"Browsing" = "浏览";
|
||||||
"Buffering stream..." = "缓冲中...";
|
"Buffering stream..." = "正在缓冲流…";
|
||||||
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Bug 以及不错的主意可以在 GitHub Issues 界面提出。 ";
|
"Bugs and great feature ideas can be sent to the GitHub issues tracker. " = "Bug 以及不错的主意可以在 GitHub Issues 界面提出。 ";
|
||||||
"Button" = "按钮";
|
"Button" = "按钮";
|
||||||
"Cancel" = "取消";
|
"Cancel" = "取消";
|
||||||
"Captions" = "字幕";
|
"Captions" = "字幕";
|
||||||
"Categories to Skip" = "要调过的类别";
|
"Categories to Skip" = "要跳过的类别";
|
||||||
"Category" = "类别";
|
"Category" = "类别";
|
||||||
"Chapters" = "章节";
|
"Chapters" = "章节";
|
||||||
"Charging" = "充电";
|
"Charging" = "充电";
|
||||||
"Clear" = "清除";
|
"Clear" = "清除";
|
||||||
"Clear All" = "清除所有";
|
"Clear All" = "全部清除";
|
||||||
"Clear All Recents" = "清除最近所有";
|
"Clear All Recents" = "清除最近所有";
|
||||||
"Clear History" = "清除历史记录";
|
"Clear History" = "清除历史记录";
|
||||||
"Clear Search History" = "清除搜索记录";
|
"Clear Search History" = "清除搜索历史记录";
|
||||||
"Clear Search History..." = "清除搜索记录...";
|
"Clear Search History..." = "清除搜索历史记录…";
|
||||||
"Clear the queue" = "清除队列";
|
"Clear the queue" = "清除队列";
|
||||||
"Close" = "关闭";
|
"Close" = "关闭";
|
||||||
"Close PiP and open player when application enters foreground" = "当应用程序进入前台时,关闭 PiP 并打开播放器";
|
"Close PiP and open player when application enters foreground" = "当应用程序进入前台时,关闭 PiP 并打开播放器";
|
||||||
@@ -72,16 +72,16 @@
|
|||||||
"Controls" = "控制";
|
"Controls" = "控制";
|
||||||
"Copy %@ link" = "复制 %@ 的链接";
|
"Copy %@ link" = "复制 %@ 的链接";
|
||||||
"Copy %@ link with time" = "复制 %@ 的链接(包含时间)";
|
"Copy %@ link with time" = "复制 %@ 的链接(包含时间)";
|
||||||
"Could not load locations manifest" = "无法加载地区列表";
|
"Could not load locations manifest" = "无法加载位置清单";
|
||||||
"Country" = "国家和地区";
|
"Country" = "国家/地区";
|
||||||
"Cellular" = "移动网络";
|
"Cellular" = "移动网络";
|
||||||
"Country Name or Code" = "地区名称或代码";
|
"Country Name or Code" = "国家/地区名称或代码";
|
||||||
"Create Playlist" = "创建播放列表";
|
"Create Playlist" = "创建播放列表";
|
||||||
"Current: %@\n%@" = "正在播放:%@\n%@";
|
"Current: %@\n%@" = "正在播放:%@\n%@";
|
||||||
|
|
||||||
/* Locations settings, custom instance is selected as current */
|
/* Locations settings, custom instance is selected as current */
|
||||||
"Custom" = "自定义";
|
"Custom" = "自定义";
|
||||||
"Custom Locations" = "自定义地址";
|
"Custom Locations" = "自定义位置";
|
||||||
|
|
||||||
/* Video sort order in search */
|
/* Video sort order in search */
|
||||||
"Date" = "日期";
|
"Date" = "日期";
|
||||||
@@ -90,33 +90,33 @@
|
|||||||
"Delete" = "删除";
|
"Delete" = "删除";
|
||||||
"Disabled" = "禁用";
|
"Disabled" = "禁用";
|
||||||
"Discord Server" = "Discord 服务器";
|
"Discord Server" = "Discord 服务器";
|
||||||
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "討論在 Discord 及 Matrix 中進行,您可以在裡面詢問一些簡單的問題。";
|
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "讨论在 Discord 和 Matrix 上进行。这里适合提出常规问题。";
|
||||||
"Don't use public locations" = "不要使用公开地址";
|
"Don't use public locations" = "不使用公开位置";
|
||||||
"Donations" = "捐赠";
|
"Donations" = "捐赠";
|
||||||
"Done" = "完成";
|
"Done" = "完成";
|
||||||
"Duration" = "时长";
|
"Duration" = "时长";
|
||||||
"Edit" = "编辑";
|
"Edit" = "编辑";
|
||||||
"Edit Playlist" = "编辑播放列表";
|
"Edit Playlist" = "编辑播放列表";
|
||||||
"Edit Quality Profile" = "编辑质量配置";
|
"Edit Quality Profile" = "编辑质量配置";
|
||||||
"Edit..." = "编辑...";
|
"Edit..." = "编辑…";
|
||||||
"Enable logging" = "启用日志";
|
"Enable logging" = "启用日志";
|
||||||
"Enter fullscreen in landscape" = "在宽屏模式中进入全屏";
|
"Enter fullscreen in landscape" = "在横屏模式下进入全屏";
|
||||||
"Error" = "错误";
|
"Error" = "错误";
|
||||||
"Error when accessing playlist" = "在访问播放列表时出错";
|
"Error when accessing playlist" = "在访问播放列表时出错";
|
||||||
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "明确提醒你在任何付费或免费平台上点赞、订阅或与他们互动(例如点击视频)。";
|
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "明确提醒你在任何付费或免费平台上点赞、订阅或与他们互动(例如点击视频)。";
|
||||||
"Favorites" = "喜欢";
|
"Favorites" = "收藏";
|
||||||
"Filter" = "过滤器";
|
"Filter" = "过滤器";
|
||||||
"Filter: active" = "过滤器:启用";
|
"Filter: active" = "过滤器:启用";
|
||||||
"Find Other" = "寻找其他";
|
"Find Other" = "寻找其他";
|
||||||
"Finding something to play..." = "正在寻找一些视频来播放...";
|
"Finding something to play..." = "正在查找可播放的视频…";
|
||||||
"Enable Return YouTube Dislike" = "启用返回 YouTube 不喜欢";
|
"Enable Return YouTube Dislike" = "启用 Return YouTube Dislike";
|
||||||
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "格式将按列出的顺序选择。\nHLS是一种自适应格式(不应用分辨率设置)。";
|
"Formats will be selected in order as listed.\nHLS is an adaptive format (resolution setting does not apply)." = "格式将按列出的顺序选择。\nHLS是一种自适应格式(不应用分辨率设置)。";
|
||||||
"Frontend URL" = "前端地址";
|
"Frontend URL" = "前端地址";
|
||||||
"Fullscreen size" = "全屏大小";
|
"Fullscreen size" = "全屏大小";
|
||||||
"Gaming" = "游戏";
|
"Gaming" = "游戏";
|
||||||
"Help" = "帮助";
|
"Help" = "帮助";
|
||||||
"Hide sidebar" = "隐藏侧边栏";
|
"Hide sidebar" = "隐藏侧边栏";
|
||||||
"High" = "高度";
|
"High" = "高";
|
||||||
"Highest" = "最高";
|
"Highest" = "最高";
|
||||||
"Highest quality" = "最高质量";
|
"Highest quality" = "最高质量";
|
||||||
"History" = "历史记录";
|
"History" = "历史记录";
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
"If you are interested what's coming in future updates, you can track project Milestones." = "如果您对最近的功能更新感兴趣,您可以跟踪我们的项目里程碑。";
|
"If you are interested what's coming in future updates, you can track project Milestones." = "如果您对最近的功能更新感兴趣,您可以跟踪我们的项目里程碑。";
|
||||||
"Increase rate" = "增长率";
|
"Increase rate" = "增长率";
|
||||||
"Info" = "信息";
|
"Info" = "信息";
|
||||||
"Instance of current account" = "正在使用的帐户实例";
|
"Instance of current account" = "当前账号实例";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
"Interaction" = "交互";
|
"Interaction" = "交互";
|
||||||
@@ -149,11 +149,11 @@
|
|||||||
"Large" = "大";
|
"Large" = "大";
|
||||||
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "大布局并不适合所有设备。使用它可能导致控制按钮在屏幕上并不适合。";
|
"Large layout is not suitable for all devices and using it may cause controls not to fit on the screen." = "大布局并不适合所有设备。使用它可能导致控制按钮在屏幕上并不适合。";
|
||||||
"LIVE" = "直播";
|
"LIVE" = "直播";
|
||||||
"Loading..." = "加载中...";
|
"Loading..." = "正在加载…";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Loading streams…" = "加载流中…";
|
"Loading streams…" = "正在加载流…";
|
||||||
"Locations" = "地址";
|
"Locations" = "位置";
|
||||||
"Lock portrait mode" = "锁定竖屏模式";
|
"Lock portrait mode" = "锁定竖屏模式";
|
||||||
|
|
||||||
/* Video duration filter in search */
|
/* Video duration filter in search */
|
||||||
@@ -185,13 +185,13 @@
|
|||||||
"Normal" = "普通";
|
"Normal" = "普通";
|
||||||
"Not available" = "不可用";
|
"Not available" = "不可用";
|
||||||
"Not Playing" = "没有播放";
|
"Not Playing" = "没有播放";
|
||||||
"Nothing" = "没有东西";
|
"Nothing" = "无";
|
||||||
"Only when signed in" = "仅当登陆时";
|
"Only when signed in" = "仅当登录后";
|
||||||
"Open \"Playlists\" tab to create new one" = "打开“播放列表” 页面创建新的播放列表";
|
"Open \"Playlists\" tab to create new one" = "打开“播放列表” 页面创建新的播放列表";
|
||||||
"Open Settings" = "打开设置";
|
"Open Settings" = "打开设置";
|
||||||
|
|
||||||
/* Loading stream OSD */
|
/* Loading stream OSD */
|
||||||
"Opening %@ stream…" = "正在打开 %@ 的流…";
|
"Opening %@ stream…" = "正在打开 %@ 流…";
|
||||||
"Opening audio stream…" = "正在打开音频流…";
|
"Opening audio stream…" = "正在打开音频流…";
|
||||||
"Orientation" = "方向";
|
"Orientation" = "方向";
|
||||||
|
|
||||||
@@ -209,10 +209,10 @@
|
|||||||
"Play Next" = "播放下一个";
|
"Play Next" = "播放下一个";
|
||||||
"Play Last" = "播放上一个";
|
"Play Last" = "播放上一个";
|
||||||
"Play Now" = "现在播放";
|
"Play Now" = "现在播放";
|
||||||
"Playback" = "回放";
|
"Playback" = "播放";
|
||||||
"Player" = "播放器";
|
"Player" = "播放器";
|
||||||
"Playlist" = "播放列表";
|
"Playlist" = "播放列表";
|
||||||
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "播放列表 “%@” 将被删除。\n此操作不可恢复。";
|
"Playlist \"%@\" will be deleted.\nIt cannot be reverted." = "播放列表“%@”将被删除。\n此操作无法撤销。";
|
||||||
"Playlists" = "播放列表";
|
"Playlists" = "播放列表";
|
||||||
"Popular" = "流行";
|
"Popular" = "流行";
|
||||||
"Preferred Formats" = "首选格式";
|
"Preferred Formats" = "首选格式";
|
||||||
@@ -233,13 +233,13 @@
|
|||||||
"Music" = "音乐";
|
"Music" = "音乐";
|
||||||
"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." = "视频宣传产品或服务的一部分,与创作者没有直接关系。创作者将以金钱或免费产品的形式获得报酬或补偿。";
|
"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." = "视频宣传产品或服务的一部分,与创作者没有直接关系。创作者将以金钱或免费产品的形式获得报酬或补偿。";
|
||||||
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "推广与创作者本身直接相关的产品或服务。这通常包括商品或盈利平台的推广。";
|
"Promoting a product or service that is directly related to the creator themselves. This usually includes merchandise or promotion of monetized platforms." = "推广与创作者本身直接相关的产品或服务。这通常包括商品或盈利平台的推广。";
|
||||||
"Remove from Playlist" = "从播放列表中删除";
|
"Remove from Playlist" = "从播放列表中移除";
|
||||||
"Remove from the queue" = "从队列中删除";
|
"Remove from the queue" = "从队列中移除";
|
||||||
"Restart the app to apply the settings above." = "重启 App 以应用以上设置。";
|
"Restart the app to apply the settings above." = "重启 App 以应用以上设置。";
|
||||||
"Restart/Play next" = "重新播放/播放下一个";
|
"Restart/Play next" = "重新播放/播放下一个";
|
||||||
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "通常在视频结束时或接近视频结尾时,出现 Credits Pop Up 和结束卡片。";
|
"Typically near or at the end of the video when the credits pop up and/or endcards are shown." = "通常在视频结束时或接近视频结尾时,出现 Credits Pop Up 和结束卡片。";
|
||||||
"Public Locations" = "公共地址";
|
"Public Locations" = "公开位置";
|
||||||
"Public Manifest" = "公共清单";
|
"Public Manifest" = "公开清单";
|
||||||
"Quality" = "质量";
|
"Quality" = "质量";
|
||||||
"Quality Profile" = "质量配置";
|
"Quality Profile" = "质量配置";
|
||||||
"Queue" = "队列";
|
"Queue" = "队列";
|
||||||
@@ -257,51 +257,51 @@
|
|||||||
|
|
||||||
/* Video sort order in search */
|
/* Video sort order in search */
|
||||||
"Relevance" = "相关";
|
"Relevance" = "相关";
|
||||||
"Remove" = "删除";
|
"Remove" = "移除";
|
||||||
"Remove from Favorites" = "从收藏中删除";
|
"Remove from Favorites" = "从收藏中移除";
|
||||||
"Remove from history" = "从历史列表中删除";
|
"Remove from history" = "从历史记录中移除";
|
||||||
"Replies" = "回复";
|
"Replies" = "回复";
|
||||||
"Reset" = "重置";
|
"Reset" = "重置";
|
||||||
"Reset search filters" = "重置搜索过滤器";
|
"Reset search filters" = "重置搜索过滤器";
|
||||||
"Reset watched status when playing again" = "重置播放状态当重新播放时";
|
"Reset watched status when playing again" = "重置播放状态当重新播放时";
|
||||||
"Resolution" = "解决方法";
|
"Resolution" = "解决方法";
|
||||||
"Restart" = "重新播放";
|
"Restart" = "重新播放";
|
||||||
"Search history is empty" = "搜索记录为空";
|
"Search history is empty" = "搜索历史记录为空";
|
||||||
"Restore default profiles..." = "重置默认配置文件...";
|
"Restore default profiles..." = "恢复默认配置…";
|
||||||
"Rotate to portrait when exiting fullscreen" = "离开全屏时旋转到竖屏";
|
"Rotate to portrait when exiting fullscreen" = "离开全屏时旋转到竖屏";
|
||||||
"Round corners" = "圆角";
|
"Round corners" = "圆角";
|
||||||
"Save" = "保存";
|
"Save" = "保存";
|
||||||
"Save history of played videos" = "保存视频播放历史记录";
|
"Save history of played videos" = "保存已播放视频的历史记录";
|
||||||
"Save history of searches, channels and playlists" = "保存搜索、频道、播放列表的历史记录";
|
"Save history of searches, channels and playlists" = "保存搜索、频道、播放列表的历史记录";
|
||||||
"Search" = "搜索";
|
"Search" = "搜索";
|
||||||
"Search..." = "搜索...";
|
"Search..." = "搜索…";
|
||||||
"Sections" = "章节";
|
"Sections" = "章节";
|
||||||
"Seek gesture sensitivity" = "手势灵敏度";
|
"Seek gesture sensitivity" = "手势灵敏度";
|
||||||
"Seek gesture speed" = "手势灵敏度速度";
|
"Seek gesture speed" = "手势灵敏度速度";
|
||||||
"Seek with horizontal swipe on video" = "视频水平滑动搜索";
|
"Seek with horizontal swipe on video" = "视频水平滑动搜索";
|
||||||
"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." = "通常在视频开头找到的片段,包括动画、静止帧或剪辑,这些片段也可以由同一创作者在其他视频中看到。";
|
"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." = "通常在视频开头找到的片段,包括动画、静止帧或剪辑,这些片段也可以由同一创作者在其他视频中看到。";
|
||||||
"Select location closest to you:" = "选择离你最近的位置:";
|
"Select location closest to you:" = "选择离您最近的位置:";
|
||||||
|
|
||||||
/* SponsorBlock category name */
|
/* SponsorBlock category name */
|
||||||
"Self-promotion" = "自我推销";
|
"Self-promotion" = "自我推销";
|
||||||
"Settings" = "设置";
|
"Settings" = "设置";
|
||||||
"Share %@ link" = "分享 %@ 的链接";
|
"Share %@ link" = "分享 %@ 的链接";
|
||||||
"Share %@ link with time" = "分享 %@ 的链接(包含时间)";
|
"Share %@ link with time" = "分享 %@ 的链接(包含时间)";
|
||||||
"Share..." = "分享...";
|
"Share..." = "分享…";
|
||||||
|
|
||||||
/* Video duration filter in search */
|
/* Video duration filter in search */
|
||||||
"Short" = "短视频";
|
"Short" = "短视频";
|
||||||
"Show account username" = "显示帐户名称";
|
"Show account username" = "显示账号用户名";
|
||||||
"Show anonymous accounts" = "显示匿名帐户";
|
"Show anonymous accounts" = "显示匿名账号";
|
||||||
"Show channel name" = "显示频道名称";
|
"Show channel name" = "显示频道名称";
|
||||||
"Show history" = "显示历史记录";
|
"Show history" = "显示历史记录";
|
||||||
"Show keywords" = "显示关键词";
|
"Show keywords" = "显示关键词";
|
||||||
"Show playback statistics" = "显示回放统计信息";
|
"Show playback statistics" = "显示播放统计数据";
|
||||||
"Show progress of watching on thumbnails" = "在缩略图上显示观看进度";
|
"Show progress of watching on thumbnails" = "在缩略图上显示观看进度";
|
||||||
"Show sidebar when space permits" = "在空间允许时显示侧边栏";
|
"Show sidebar when space permits" = "在空间允许时显示侧边栏";
|
||||||
"Show video length" = "显示视频长度";
|
"Show video length" = "显示视频长度";
|
||||||
"Shuffle All" = "打乱所有顺序";
|
"Shuffle All" = "全部随机播放";
|
||||||
"Shuffle" = "打乱顺序";
|
"Shuffle" = "随机播放";
|
||||||
"Sidebar" = "侧边栏";
|
"Sidebar" = "侧边栏";
|
||||||
"Sign In Required" = "需要登陆";
|
"Sign In Required" = "需要登陆";
|
||||||
|
|
||||||
@@ -321,15 +321,15 @@
|
|||||||
|
|
||||||
/* Subscriptions title */
|
/* Subscriptions title */
|
||||||
"Subscriptions" = "关注列表";
|
"Subscriptions" = "关注列表";
|
||||||
"Switch to other public location" = "选择其他公共地址";
|
"Switch to other public location" = "切换到其他公开位置";
|
||||||
"Switch to public locations" = "切换到公共地址";
|
"Switch to public locations" = "切换到公开位置";
|
||||||
"System controls buttons" = "系统控制按钮";
|
"System controls buttons" = "系统控制按钮";
|
||||||
"System controls show buttons for %@" = "系统控件显示 %@ 的按钮";
|
"System controls show buttons for %@" = "系统控件显示 %@ 的按钮";
|
||||||
"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." = "很高兴听到您这么说。提供人们想要的应用程序是一件很有趣的事情。您可以考虑为项目捐款,或为新功能开发做出贡献。";
|
"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." = "很高兴听到您这么说。提供人们想要的应用程序是一件很有趣的事情。您可以考虑为项目捐款,或为新功能开发做出贡献。";
|
||||||
"This cannot be reverted" = "此操作不可还原";
|
"This cannot be reverted" = "此操作不可还原";
|
||||||
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "此操作无法恢复。您可能需要在视图之间切换或重新启动 App 以查看更改。";
|
"This cannot be reverted. You might need to switch between views or restart the app to see changes." = "此操作无法恢复。您可能需要在视图之间切换或重新启动 App 以查看更改。";
|
||||||
"This information will be processed only on your device and used to connect you to the server in the specified country." = "此信息将仅在您的设备上处理,并用于将您连接到指定国家/地区的服务器。";
|
"This information will be processed only on your device and used to connect you to the server in the specified country." = "此信息将仅在您的设备上处理,并用于将您连接到指定国家/地区的服务器。";
|
||||||
"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." = "这将移除您所有的自定义配置并恢复其默认值。此操作无法撤销。";
|
||||||
"Thumbnails" = "缩略图";
|
"Thumbnails" = "缩略图";
|
||||||
|
|
||||||
/* Video date filter in search */
|
/* Video date filter in search */
|
||||||
@@ -372,27 +372,27 @@
|
|||||||
"Year" = "年";
|
"Year" = "年";
|
||||||
"You can find information about using Yattee in the Wiki pages." = "您可以在 Wiki 相关页面中找到有关使用 Yattee 的信息。";
|
"You can find information about using Yattee in the Wiki pages." = "您可以在 Wiki 相关页面中找到有关使用 Yattee 的信息。";
|
||||||
"Yattee %@ (build %@)" = "Yattee %@ (内部构建版本 %@)";
|
"Yattee %@ (build %@)" = "Yattee %@ (内部构建版本 %@)";
|
||||||
"You can use automatic profile selection based on current device status or switch it in video playback settings controls." = "您可以使用基于当前设备状态的自动配置文件选择,或在视频播放设置控件中进行切换。";
|
"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" = "你没有播放列表";
|
||||||
"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 create an instance and accounts\nto access %@ section" = "您需要创建实例和账号\n才能访问 %@ 的部分";
|
||||||
"You need to select an account\nto access %@ section" = "你需要选择一个账号\n来访问 %@ 的部分";
|
"You need to select an account\nto access %@ section" = "您需要选择账号\n才能访问 %@ 部分";
|
||||||
|
|
||||||
|
|
||||||
"Public" = "公开";
|
"Public" = "公开";
|
||||||
"Unlisted" = "未列出";
|
"Unlisted" = "未列出";
|
||||||
"Now Playing" = "正在播放";
|
"Now Playing" = "正在播放";
|
||||||
"Current Location" = "当前地址";
|
"Current Location" = "当前位置";
|
||||||
"Private" = "私有";
|
"Private" = "私有";
|
||||||
"Playback queue is empty" = "回放列表空空如也";
|
"Playback queue is empty" = "播放队列为空";
|
||||||
"Playing Next" = "播放下一个";
|
"Playing Next" = "播放下一个";
|
||||||
"You can switch between profiles in playback settings controls." = "您可以在回放设置控件中切换配置文件。";
|
"You can switch between profiles in playback settings controls." = "您可以在播放设置控件中切换配置。";
|
||||||
"Add Channels, Playlists and Searches to Favorites using" = "添加将频道、播放列表和搜索到收藏夹";
|
"Add Channels, Playlists and Searches to Favorites using" = "将频道、播放列表和搜索添加到收藏使用";
|
||||||
"Make default" = "设置默认值";
|
"Make default" = "设置默认值";
|
||||||
"Visibility" = "可见性";
|
"Visibility" = "可见性";
|
||||||
"Current Playlist" = "当前播放列表";
|
"Current Playlist" = "当前播放列表";
|
||||||
"Stream & Player" = "播放流 & 播放器";
|
"Stream & Player" = "播放流 & 播放器";
|
||||||
"Statistics" = "统计";
|
"Statistics" = "统计数据";
|
||||||
"Hardware decoder" = "硬件解码";
|
"Hardware decoder" = "硬件解码";
|
||||||
"Stream FPS" = "流 FPS";
|
"Stream FPS" = "流 FPS";
|
||||||
"Cached time" = "缓存时间";
|
"Cached time" = "缓存时间";
|
||||||
@@ -401,8 +401,8 @@
|
|||||||
"Any format" = "任何格式";
|
"Any format" = "任何格式";
|
||||||
"%@ formats" = "%@ 格式";
|
"%@ formats" = "%@ 格式";
|
||||||
"Keep last played video in the queue after restart" = "重新启动后在队列中保留上次播放的视频";
|
"Keep last played video in the queue after restart" = "重新启动后在队列中保留上次播放的视频";
|
||||||
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "播放列表空空如也\n\n点击并按住视频,然后\n「添加到播放列表」";
|
"Playlist is empty\n\nTap and hold on a video and then \n\"Add to Playlist\"" = "播放列表为空\n\n长按视频,然后\n“添加到播放列表”";
|
||||||
"It can be changed later in settings. You can use your own locations too." = "稍后可以在设置中更改。你也可以使用自己的地址。";
|
"It can be changed later in settings. You can use your own locations too." = "稍后可以在设置中更改。您也可以使用自己的位置。";
|
||||||
"Press and hold remote button to open captions and quality menus" = "按住遥控按钮打开字幕和质量菜单";
|
"Press and hold remote button to open captions and quality menus" = "按住遥控按钮打开字幕和质量菜单";
|
||||||
"Comments are disabled" = "评论区已被关闭";
|
"Comments are disabled" = "评论区已被关闭";
|
||||||
"No comments" = "没有评论";
|
"No comments" = "没有评论";
|
||||||
@@ -410,14 +410,14 @@
|
|||||||
"Share Logs..." = "分享日志…";
|
"Share Logs..." = "分享日志…";
|
||||||
"Open logs in Finder" = "在 Finder 中打开日志";
|
"Open logs in Finder" = "在 Finder 中打开日志";
|
||||||
"Could not refresh Subscriptions" = "无法刷新关注列表";
|
"Could not refresh Subscriptions" = "无法刷新关注列表";
|
||||||
"Could not load streams" = "无法加载视频流";
|
"Could not load streams" = "无法加载流";
|
||||||
"Could not open video" = "无法打开视频";
|
"Could not open video" = "无法打开视频";
|
||||||
"Channel could not be found" = "无法找到频道";
|
"Channel could not be found" = "无法找到频道";
|
||||||
"Could not extract channel information" = "无法获取频道信息";
|
"Could not extract channel information" = "无法获取频道信息";
|
||||||
"Could not extract SID from received cookies: %@" = "无法从收到的 Cookie 中提取 SID:%@";
|
"Could not extract SID from received cookies: %@" = "无法从收到的 Cookie 中提取 SID:%@";
|
||||||
"Could not update your token." = "无法更新你的令牌(Token)。";
|
"Could not update your token." = "无法更新你的令牌(Token)。";
|
||||||
"Could not refresh Trending" = "无法刷新今日趋势";
|
"Could not refresh Trending" = "无法刷新今日趋势";
|
||||||
"For custom locations you can configure Frontend URL in Locations settings" = "对于自定义位置,您可以在设置中配置前端 URL";
|
"For custom locations you can configure Frontend URL in Locations settings" = "对于自定义位置,您可以在位置设置中配置前端 URL";
|
||||||
"This URL could not be opened" = "无法打开此 URL";
|
"This URL could not be opened" = "无法打开此 URL";
|
||||||
"Could not open channel" = "无法打开频道";
|
"Could not open channel" = "无法打开频道";
|
||||||
"Could not refresh Popular" = "无法刷新「流行」";
|
"Could not refresh Popular" = "无法刷新「流行」";
|
||||||
@@ -427,15 +427,15 @@
|
|||||||
"This video could not be opened" = "无法打开这个视频";
|
"This video could not be opened" = "无法打开这个视频";
|
||||||
"Could not extract playlist ID" = "无法获取播放列表 ID";
|
"Could not extract playlist ID" = "无法获取播放列表 ID";
|
||||||
"Could not load video" = "无法加载视频";
|
"Could not load video" = "无法加载视频";
|
||||||
"No locations available at the moment" = "此时没有地址可用";
|
"No locations available at the moment" = "目前没有可用的位置";
|
||||||
"Could not refresh Playlists" = "无法刷新播放列表";
|
"Could not refresh Playlists" = "无法刷新播放列表";
|
||||||
"If you want this app to be available in your language, join translation project." = "如果你想让此 App 在你的语言中可用,或翻译存在错误,请加入翻译项目。";
|
"If you want this app to be available in your language, join translation project." = "如果你想让此 App 在你的语言中可用,或翻译存在错误,请加入翻译项目。";
|
||||||
"Translations" = "翻译";
|
"Translations" = "翻译";
|
||||||
"No documents" = "没有文档";
|
"No documents" = "没有文档";
|
||||||
"Are you sure you want to remove this document?" = "你确定想要删除这个文档吗?";
|
"Are you sure you want to remove this document?" = "是否确定要移除此文档?";
|
||||||
"\"%@\" will be irreversibly removed from this device." = "“%@” 将会从这个设备中删除。此操作不可逆。";
|
"\"%@\" will be irreversibly removed from this device." = "“%@”将从本设备不可逆地移除。";
|
||||||
"Could not delete document" = "无法删除文档";
|
"Could not delete document" = "无法删除文档";
|
||||||
"Are you sure you want to remove %@ location?" = "你确定想要删除 %@ 地址?";
|
"Are you sure you want to remove %@ location?" = "是否确定要移除 %@ 位置?";
|
||||||
"Recent Documents" = "最近的文档";
|
"Recent Documents" = "最近的文档";
|
||||||
"Recent History" = "最近的历史记录";
|
"Recent History" = "最近的历史记录";
|
||||||
"Show Open Videos quick actions" = "显示打开视频快速操作";
|
"Show Open Videos quick actions" = "显示打开视频快速操作";
|
||||||
@@ -448,13 +448,13 @@
|
|||||||
"Enter link to open" = "输入要打开的链接";
|
"Enter link to open" = "输入要打开的链接";
|
||||||
"Show only icons" = "仅显示图标";
|
"Show only icons" = "仅显示图标";
|
||||||
"Video" = "视频";
|
"Video" = "视频";
|
||||||
"Sample Rate" = "码率示例";
|
"Sample Rate" = "采样率";
|
||||||
"Edit Favorites…" = "编辑收藏…";
|
"Edit Favorites…" = "编辑收藏…";
|
||||||
"Show Open Videos toolbar button" = "显示「打开视频」工具栏按钮";
|
"Show Open Videos toolbar button" = "显示「打开视频」工具栏按钮";
|
||||||
"Buttons labels" = "按钮标签";
|
"Buttons labels" = "按钮标签";
|
||||||
"Files" = "文件";
|
"Files" = "文件";
|
||||||
"Show Documents" = "显示文档";
|
"Show Documents" = "显示文档";
|
||||||
"Video Details" = "视频详细描述";
|
"Video Details" = "视频详情";
|
||||||
"Show Inspector" = "显示检查器";
|
"Show Inspector" = "显示检查器";
|
||||||
"Inspector visibility" = "检查器可见性";
|
"Inspector visibility" = "检查器可见性";
|
||||||
"Reload manifest" = "重新加载清单";
|
"Reload manifest" = "重新加载清单";
|
||||||
@@ -466,7 +466,7 @@
|
|||||||
"Paste" = "粘贴";
|
"Paste" = "粘贴";
|
||||||
"Open Videos" = "打开视频";
|
"Open Videos" = "打开视频";
|
||||||
"Enter links to open, one per line" = "输入需要打开的链接,每行一个";
|
"Enter links to open, one per line" = "输入需要打开的链接,每行一个";
|
||||||
"Playback Mode" = "回放模式";
|
"Playback Mode" = "播放模式";
|
||||||
"Add" = "添加";
|
"Add" = "添加";
|
||||||
"Hide" = "隐藏";
|
"Hide" = "隐藏";
|
||||||
"Always" = "总是";
|
"Always" = "总是";
|
||||||
@@ -483,17 +483,17 @@
|
|||||||
"Documents" = "文档";
|
"Documents" = "文档";
|
||||||
"Audio" = "音频";
|
"Audio" = "音频";
|
||||||
"File" = "文件";
|
"File" = "文件";
|
||||||
"Codec" = "编码器(Codec)";
|
"Codec" = "编解码器";
|
||||||
"Size" = "大小";
|
"Size" = "大小";
|
||||||
"FPS" = "FPS";
|
"FPS" = "FPS";
|
||||||
"Could not find any links to open in your clipboard" = "无法在你的剪辑版中找到任何可以打开的链接";
|
"Could not find any links to open in your clipboard" = "无法在你的剪辑版中找到任何可以打开的链接";
|
||||||
"Remove…" = "删除…";
|
"Remove…" = "移除…";
|
||||||
"Playback history is empty" = "回放历史为空";
|
"Playback history is empty" = "播放历史记录为空";
|
||||||
"Address" = "地址";
|
"Address" = "地址";
|
||||||
"Actions buttons" = "动作按钮";
|
"Actions buttons" = "动作按钮";
|
||||||
"Show sidebar" = "显示侧边栏";
|
"Show sidebar" = "显示侧边栏";
|
||||||
"Locations Manifest" = "地址清单";
|
"Locations Manifest" = "位置清单";
|
||||||
"Remove Location" = "删除地址";
|
"Remove Location" = "移除位置";
|
||||||
"Open Video" = "打开视频";
|
"Open Video" = "打开视频";
|
||||||
"Default Profile" = "默认配置";
|
"Default Profile" = "默认配置";
|
||||||
"Copy%@link" = "复制 %@ 的链接";
|
"Copy%@link" = "复制 %@ 的链接";
|
||||||
@@ -515,18 +515,18 @@
|
|||||||
"Show video context menu options to force selected backend" = "显示视频内容目录选项来强制选择的后端";
|
"Show video context menu options to force selected backend" = "显示视频内容目录选项来强制选择的后端";
|
||||||
"Gesture: backwards" = "手势:向后";
|
"Gesture: backwards" = "手势:向后";
|
||||||
"Maximum width expanded" = "最大宽度展开";
|
"Maximum width expanded" = "最大宽度展开";
|
||||||
"Clear all" = "清除所有APP缓存";
|
"Clear all" = "全部清除";
|
||||||
"Total size: %@" = "总大小:%@";
|
"Total size: %@" = "总大小:%@";
|
||||||
"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." = "手势设置控制玩家左右两侧双击手势的跳过间隔。更改系统控制设置需要重新启动。";
|
||||||
"Play next item" = "播放下一个";
|
"Play next item" = "播放下一个";
|
||||||
"Subscribe/Unsubscribe" = "关注/取消关注";
|
"Subscribe/Unsubscribe" = "关注/取消关注";
|
||||||
"Autoplay next" = "自动播放下一个";
|
"Autoplay next" = "自动播放下一个";
|
||||||
"Enter location address to connect..." = "输入链接地址以连接...";
|
"Enter location address to connect..." = "输入要连接的位置地址…";
|
||||||
"Keep channels with unwatched videos on top of subscriptions list" = "保留频道,并且未观看视频放在关注列表最上面";
|
"Keep channels with unwatched videos on top of subscriptions list" = "保留频道,并且未观看视频放在关注列表最上面";
|
||||||
"Play Now in AVPlayer" = "在 AVPlayer 中播放当前项目";
|
"Play Now in AVPlayer" = "在 AVPlayer 中播放当前项目";
|
||||||
"Play Now in MPV" = "在 MPV 中播放当前项目";
|
"Play Now in MPV" = "在 MPV 中播放当前项目";
|
||||||
"Seek" = "探索";
|
"Seek" = "探索";
|
||||||
"Enter account credentials to connect..." = "输入账户凭据来连接...";
|
"Enter account credentials to connect..." = "输入要连接的账号凭据…";
|
||||||
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
|
"Show scroll to top button in comments" = "在评论中显示“滚动到顶部”按钮";
|
||||||
"Opened File" = "打开的文件";
|
"Opened File" = "打开的文件";
|
||||||
"File Extension" = "文件扩展";
|
"File Extension" = "文件扩展";
|
||||||
@@ -551,12 +551,12 @@
|
|||||||
"Show cache status" = "显示缓存状态";
|
"Show cache status" = "显示缓存状态";
|
||||||
"Maximum feed items" = "最大 Feed 项目";
|
"Maximum feed items" = "最大 Feed 项目";
|
||||||
"Open channels with description expanded" = "打开描述展开的频道";
|
"Open channels with description expanded" = "打开描述展开的频道";
|
||||||
"Are you sure you want to clear cache?" = "你确定要清除缓存吗?";
|
"Are you sure you want to clear cache?" = "是否确定要清除缓存?";
|
||||||
"Close video and player on end" = "在播放结束时关闭播放器和视频";
|
"Close video and player on end" = "在播放结束时关闭播放器和视频";
|
||||||
"Use system controls with AVPlayer" = "使用 AVPlayer 与系统控制按钮";
|
"Use system controls with AVPlayer" = "使用 AVPlayer 与系统控制按钮";
|
||||||
"Public account" = "公共账号";
|
"Public account" = "公开账号";
|
||||||
"Your Accounts" = "你的账号";
|
"Your Accounts" = "您的账号";
|
||||||
"Browse without account" = "匿名浏览";
|
"Browse without account" = "无账号浏览";
|
||||||
"Rotate when entering fullscreen on landscape video" = "当观看全景视频,进入全屏时旋转";
|
"Rotate when entering fullscreen on landscape video" = "当观看全景视频,进入全屏时旋转";
|
||||||
"Landscape left" = "全景视频左边";
|
"Landscape left" = "全景视频左边";
|
||||||
"Landscape right" = "全景视频右边";
|
"Landscape right" = "全景视频右边";
|
||||||
@@ -572,7 +572,7 @@
|
|||||||
"(watched hidden)" = "(已观看已隐藏)";
|
"(watched hidden)" = "(已观看已隐藏)";
|
||||||
"(shorts hidden)" = "(短视频已隐藏)";
|
"(shorts hidden)" = "(短视频已隐藏)";
|
||||||
"Limit" = "限制";
|
"Limit" = "限制";
|
||||||
"Are you sure you want to remove %@ from Favorites?" = "你确定要从收藏夹中删除 %@ 吗?";
|
"Are you sure you want to remove %@ from Favorites?" = "是否确定要从收藏中移除 %@?";
|
||||||
"List" = "列表";
|
"List" = "列表";
|
||||||
"Cells" = "Cells";
|
"Cells" = "Cells";
|
||||||
"Toggle size" = "总大小";
|
"Toggle size" = "总大小";
|
||||||
@@ -588,10 +588,44 @@
|
|||||||
"Mark all as unwatched" = "标记所有未看过的";
|
"Mark all as unwatched" = "标记所有未看过的";
|
||||||
"Mark all as watched" = "标记所有看过的";
|
"Mark all as watched" = "标记所有看过的";
|
||||||
"Queue - shuffled" = "队列 - 随机";
|
"Queue - shuffled" = "队列 - 随机";
|
||||||
"Playback Settings" = "回放设置";
|
"Playback Settings" = "播放设置";
|
||||||
"Replay" = "重新播放";
|
"Replay" = "重新播放";
|
||||||
"Fullscreen" = "全屏";
|
"Fullscreen" = "全屏";
|
||||||
"Lock" = "锁定";
|
"Lock" = "锁定";
|
||||||
"Description" = "描述";
|
"Description" = "描述";
|
||||||
"Loop one" = "单个循环";
|
"Loop one" = "单个循环";
|
||||||
"Stream" = "流播放";
|
"Stream" = "流播放";
|
||||||
|
"Accounts passwords (unencrypted)" = "账号密码(未加密)";
|
||||||
|
"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" = "请勿与任何人分享此文件,否则您可能会失去对账号访问权限。如果您未选择导出密码,系统将要求您在导入过程中提供密码";
|
||||||
|
"Account already exists" = "账号已存在";
|
||||||
|
"Add %@" = "添加 %@";
|
||||||
|
"Custom Location already exists" = "自定义位置已存在";
|
||||||
|
"Custom Location selected for import" = "已选择导入的自定义位置";
|
||||||
|
"Custom Location not selected for import" = "未选择导入的自定义位置";
|
||||||
|
"Are you sure you want to export unencrypted passwords?" = "是否确定要导出未加密的密码?";
|
||||||
|
"Other data include last used playback preferences and listing options" = "其他数据包括上次使用的播放偏好设置和列表选项";
|
||||||
|
"Import Settings..." = "导入设置…";
|
||||||
|
"Export..." = "导出…";
|
||||||
|
"Export in progress..." = "导出正在进行中…";
|
||||||
|
"In progress..." = "正在进行中…";
|
||||||
|
"Podcasts" = "播客";
|
||||||
|
"Releases" = "发布";
|
||||||
|
"Other" = "其他";
|
||||||
|
"Export" = "导出";
|
||||||
|
"Build" = "构建";
|
||||||
|
"Platform" = "平台";
|
||||||
|
"Import" = "导入";
|
||||||
|
"No preview" = "无预览";
|
||||||
|
"Description preview" = "描述预览";
|
||||||
|
"Export Settings" = "导出设置";
|
||||||
|
"Other data" = "其他数据";
|
||||||
|
"File information" = "文件信息";
|
||||||
|
"Icon only" = "仅图标";
|
||||||
|
"Chapters (if available)" = "章节(如有)";
|
||||||
|
"Action button labels" = "操作按钮标签";
|
||||||
|
"Icon and text" = "图标和文本";
|
||||||
|
"Password required to import" = "导入需要密码";
|
||||||
|
"Password saved in import file" = "密码已保存在导入文件中";
|
||||||
|
"Show channel avatars in channels lists" = "在频道列表中显示频道头像";
|
||||||
|
"Show channel avatars in videos lists" = "在视频列表中显示频道头像";
|
||||||
|
"Open vertical chapters expanded" = "垂直章节展开";
|
||||||
|
|||||||
@@ -1283,6 +1283,11 @@
|
|||||||
375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsModel.swift; sourceTree = "<group>"; };
|
375AC29D2B66BDD600B680E7 /* ImportExportSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsModel.swift; sourceTree = "<group>"; };
|
||||||
375B537728DF6CBB004C1D19 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
375B537728DF6CBB004C1D19 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
375B537928DF6CC4004C1D19 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
375B537928DF6CC4004C1D19 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
37FFBA77110000000000F001 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
37FFBA77110000000000F002 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
37FFBA77110000000000F003 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
37FFBA77110000000000F004 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
37FFBA77110000000000F005 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
375B8AB228B580D300397B31 /* KeychainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainModel.swift; sourceTree = "<group>"; };
|
375B8AB228B580D300397B31 /* KeychainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainModel.swift; sourceTree = "<group>"; };
|
||||||
375CE60428E4A038009B8EA2 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
375CE60428E4A038009B8EA2 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
375CE60528E4A054009B8EA2 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
375CE60528E4A054009B8EA2 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
@@ -2847,7 +2852,7 @@
|
|||||||
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
|
3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */,
|
||||||
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
37CF8B8228535E4F00B71E37 /* XCRemoteSwiftPackageReference "SDWebImage" */,
|
||||||
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
|
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
|
||||||
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability.swift" */,
|
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */,
|
||||||
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */,
|
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */,
|
||||||
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
375B8AAF28B57F4200397B31 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||||
3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
|
3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
|
||||||
@@ -4154,6 +4159,11 @@
|
|||||||
37367E582B8F63C200436163 /* zh-Hant */,
|
37367E582B8F63C200436163 /* zh-Hant */,
|
||||||
37E21DC52CDE528A008DF47C /* ta */,
|
37E21DC52CDE528A008DF47C /* ta */,
|
||||||
376EC9D82D1DD39800EC4500 /* hu */,
|
376EC9D82D1DD39800EC4500 /* hu */,
|
||||||
|
37FFBA77110000000000F001 /* fi */,
|
||||||
|
37FFBA77110000000000F002 /* id */,
|
||||||
|
37FFBA77110000000000F003 /* ko */,
|
||||||
|
37FFBA77110000000000F004 /* nl */,
|
||||||
|
37FFBA77110000000000F005 /* sv */,
|
||||||
);
|
);
|
||||||
name = Localizable.strings;
|
name = Localizable.strings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -4168,7 +4178,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||||
@@ -4199,7 +4209,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||||
@@ -4230,7 +4240,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
@@ -4250,7 +4260,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
@@ -4414,7 +4424,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
"DEBUG=1",
|
"DEBUG=1",
|
||||||
@@ -4468,7 +4478,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||||
@@ -4522,7 +4532,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -4561,7 +4571,7 @@
|
|||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
@@ -4596,7 +4606,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4619,7 +4629,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4644,7 +4654,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4668,7 +4678,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -4694,7 +4704,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -4734,7 +4744,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
DEVELOPMENT_ASSET_PATHS = "";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -4774,7 +4784,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -4797,7 +4807,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 257;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -5079,7 +5089,7 @@
|
|||||||
minimumVersion = 5.0.2;
|
minimumVersion = 5.0.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability.swift" */ = {
|
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
@@ -5361,7 +5371,7 @@
|
|||||||
};
|
};
|
||||||
37EE6DC428A305AD00BFD632 /* Reachability */ = {
|
37EE6DC428A305AD00BFD632 /* Reachability */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability.swift" */;
|
package = 37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */;
|
||||||
productName = Reachability;
|
productName = Reachability;
|
||||||
};
|
};
|
||||||
37FB2848272207F000A57617 /* SDWebImageWebPCoder */ = {
|
37FB2848272207F000A57617 /* SDWebImageWebPCoder */ = {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
"location" : "https://github.com/mpvkit/MPVKit.git",
|
"location" : "https://github.com/mpvkit/MPVKit.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "360b5002bf607a94f24ec8977db94bd9811d5357"
|
"revision" : "fef0f54bfd7e37e0547e057880b28992540ddbcc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ platform :ios do
|
|||||||
api_key = app_store_connect_api_key(
|
api_key = app_store_connect_api_key(
|
||||||
key_id: DEVELOPER_KEY_ID,
|
key_id: DEVELOPER_KEY_ID,
|
||||||
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
||||||
key_content: DEVELOPER_KEY_CONTENT
|
key_content: DEVELOPER_KEY_CONTENT,
|
||||||
|
is_key_content_base64: true
|
||||||
)
|
)
|
||||||
|
|
||||||
build = get_build_number(xcodeproj: XCODEPROJ)
|
build = get_build_number(xcodeproj: XCODEPROJ)
|
||||||
@@ -131,7 +132,8 @@ platform :tvos do
|
|||||||
api_key = app_store_connect_api_key(
|
api_key = app_store_connect_api_key(
|
||||||
key_id: DEVELOPER_KEY_ID,
|
key_id: DEVELOPER_KEY_ID,
|
||||||
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
||||||
key_content: DEVELOPER_KEY_CONTENT
|
key_content: DEVELOPER_KEY_CONTENT,
|
||||||
|
is_key_content_base64: true
|
||||||
)
|
)
|
||||||
|
|
||||||
build = get_build_number(xcodeproj: XCODEPROJ)
|
build = get_build_number(xcodeproj: XCODEPROJ)
|
||||||
@@ -183,7 +185,8 @@ platform :mac do
|
|||||||
api_key = app_store_connect_api_key(
|
api_key = app_store_connect_api_key(
|
||||||
key_id: DEVELOPER_KEY_ID,
|
key_id: DEVELOPER_KEY_ID,
|
||||||
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
||||||
key_content: DEVELOPER_KEY_CONTENT
|
key_content: DEVELOPER_KEY_CONTENT,
|
||||||
|
is_key_content_base64: true
|
||||||
)
|
)
|
||||||
|
|
||||||
build = get_build_number(xcodeproj: XCODEPROJ)
|
build = get_build_number(xcodeproj: XCODEPROJ)
|
||||||
@@ -234,7 +237,8 @@ platform :mac do
|
|||||||
api_key = app_store_connect_api_key(
|
api_key = app_store_connect_api_key(
|
||||||
key_id: DEVELOPER_KEY_ID,
|
key_id: DEVELOPER_KEY_ID,
|
||||||
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
issuer_id: DEVELOPER_KEY_ISSUER_ID,
|
||||||
key_content: DEVELOPER_KEY_CONTENT
|
key_content: DEVELOPER_KEY_CONTENT,
|
||||||
|
is_key_content_base64: true
|
||||||
)
|
)
|
||||||
|
|
||||||
build = get_build_number(xcodeproj: XCODEPROJ)
|
build = get_build_number(xcodeproj: XCODEPROJ)
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ enum Orientation {
|
|||||||
static var logger = Logger(label: "stream.yattee.orientation")
|
static var logger = Logger(label: "stream.yattee.orientation")
|
||||||
|
|
||||||
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
|
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
|
||||||
|
// Orientation locking is only for iPhone, not iPad
|
||||||
|
guard Constants.isIPhone else {
|
||||||
|
logger.info("skipping orientation lock on iPad")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if let delegate = AppDelegate.instance {
|
if let delegate = AppDelegate.instance {
|
||||||
delegate.orientationLock = orientation
|
delegate.orientationLock = orientation
|
||||||
|
|
||||||
@@ -18,6 +24,12 @@ enum Orientation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
|
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
|
||||||
|
// Orientation locking and rotation is only for iPhone, not iPad
|
||||||
|
guard Constants.isIPhone else {
|
||||||
|
logger.info("skipping orientation lock and rotation on iPad")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
lockOrientation(orientation)
|
lockOrientation(orientation)
|
||||||
|
|
||||||
guard let rotateOrientation else {
|
guard let rotateOrientation else {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct InstancesSettings: View {
|
|||||||
@State private var frontendURL = ""
|
@State private var frontendURL = ""
|
||||||
@State private var proxiesVideos = false
|
@State private var proxiesVideos = false
|
||||||
@State private var invidiousCompanion = false
|
@State private var invidiousCompanion = false
|
||||||
|
@State private var hideVideosWithoutDuration = false
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@ObservedObject private var accounts = AccountsModel.shared
|
@ObservedObject private var accounts = AccountsModel.shared
|
||||||
@@ -109,6 +110,18 @@ struct InstancesSettings: View {
|
|||||||
.onChange(of: invidiousCompanion) { newValue in
|
.onChange(of: invidiousCompanion) { newValue in
|
||||||
InstancesModel.shared.setInvidiousCompanion(selectedInstance, newValue)
|
InstancesModel.shared.setInvidiousCompanion(selectedInstance, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hideVideosWithoutDurationToggle
|
||||||
|
.onAppear {
|
||||||
|
hideVideosWithoutDuration = selectedInstance.hideVideosWithoutDuration
|
||||||
|
}
|
||||||
|
.onChange(of: hideVideosWithoutDuration) { newValue in
|
||||||
|
InstancesModel.shared.setHideVideosWithoutDuration(selectedInstance, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("This can be used to hide shorts")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
|
if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
|
||||||
@@ -201,6 +214,10 @@ struct InstancesSettings: View {
|
|||||||
private var invidiousCompanionToggle: some View {
|
private var invidiousCompanionToggle: some View {
|
||||||
Toggle("Invidious companion", isOn: $invidiousCompanion)
|
Toggle("Invidious companion", isOn: $invidiousCompanion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var hideVideosWithoutDurationToggle: some View {
|
||||||
|
Toggle("Experimental: Hide videos without duration", isOn: $hideVideosWithoutDuration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstancesSettingsView_Previews: PreviewProvider {
|
struct InstancesSettingsView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -4,25 +4,52 @@ struct ControlsOverlayButton<LabelView: View>: View {
|
|||||||
var focusedField: FocusState<ControlsOverlay.Field?>.Binding
|
var focusedField: FocusState<ControlsOverlay.Field?>.Binding
|
||||||
var field: ControlsOverlay.Field
|
var field: ControlsOverlay.Field
|
||||||
let label: LabelView
|
let label: LabelView
|
||||||
|
var onSelect: (() -> Void)?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
focusedField: FocusState<ControlsOverlay.Field?>.Binding,
|
focusedField: FocusState<ControlsOverlay.Field?>.Binding,
|
||||||
field: ControlsOverlay.Field,
|
field: ControlsOverlay.Field,
|
||||||
|
onSelect: (() -> Void)? = nil,
|
||||||
@ViewBuilder label: @escaping () -> LabelView
|
@ViewBuilder label: @escaping () -> LabelView
|
||||||
) {
|
) {
|
||||||
self.focusedField = focusedField
|
self.focusedField = focusedField
|
||||||
self.field = field
|
self.field = field
|
||||||
|
self.onSelect = onSelect
|
||||||
self.label = label()
|
self.label = label()
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
label
|
let isFocused = focusedField.wrappedValue == field
|
||||||
.padding()
|
|
||||||
.frame(width: 400)
|
if let onSelect {
|
||||||
.focusable()
|
Button(action: onSelect) {
|
||||||
|
label
|
||||||
|
.foregroundColor(isFocused ? .black : .white)
|
||||||
|
.padding()
|
||||||
|
.frame(width: 400)
|
||||||
|
}
|
||||||
|
.buttonStyle(TVButtonStyle(isFocused: isFocused))
|
||||||
.focused(focusedField, equals: field)
|
.focused(focusedField, equals: field)
|
||||||
.background(focusedField.wrappedValue == field ? Color.white : Color.secondary)
|
} else {
|
||||||
.foregroundColor(focusedField.wrappedValue == field ? Color.black : Color.white)
|
label
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
.foregroundColor(isFocused ? .black : .white)
|
||||||
|
.padding()
|
||||||
|
.frame(width: 400)
|
||||||
|
.focusable()
|
||||||
|
.focused(focusedField, equals: field)
|
||||||
|
.background(isFocused ? Color.white : Color.gray.opacity(0.5))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TVButtonStyle: ButtonStyle {
|
||||||
|
let isFocused: Bool
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.background(isFocused ? Color.white : Color.gray.opacity(0.5))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user