Compare commits

...

135 Commits

Author SHA1 Message Date
semantic-release-bot 3b88b039e9 chore(release): 1.12.2 [only cd]
## 1.12.2 (2022-04-02)

### Bug Fixes

* Removed videos, fastlane cant upload them d98bf8c
* Update fastlane cef0043
2022-04-02 21:42:33 +00:00
Stefan Schüller 3915cc1bbb ci: Update docker container 2022-04-02 23:35:16 +02:00
Stefan Schüller d98bf8c9a4 fix: Removed videos, fastlane cant upload them 2022-04-02 22:47:25 +02:00
Stefan Schüller cb67a09604 Merge remote-tracking branch 'origin/master' 2022-04-02 22:29:44 +02:00
Stefan Schüller cef0043693 fix: Update fastlane 2022-04-02 22:29:39 +02:00
semantic-release-bot 9839ed928b chore(release): 1.12.1 [only cd]
## 1.12.1 (2022-04-02)

### Bug Fixes

* Too long play store update notes fe187e5
2022-04-02 19:55:37 +00:00
Stefan Schüller fe187e546d fix: Too long play store update notes 2022-04-02 21:50:39 +02:00
semantic-release-bot 7881b56b77 chore(release): 1.12.0 [only cd]
# 1.12.0 (2022-04-02)

### Bug Fixes

* **lang:**  Weblate translation (Albanian) faa840a
* **lang:**  Weblate translation (Albanian) e312176
* **lang:**  Weblate translation (French) a2f5882
* **lang:**  Weblate translation (Portuguese (Brazil)) 527c385
* **lang:**  Weblate translation (Portuguese) 33e6fbb
* **lang:**  Weblate translation (Portuguese) 530b979
* **lang:** Weblate translation (Albanian) ca6c4da
* **lang:** Weblate translation (Arabic) 89f978e
* **lang:** Weblate translation (Bengali) dba93fa
* **lang:** Weblate translation (Chinese (Traditional)) e1be0e8
* **lang:** Weblate translation (English) 90b99c2
* **lang:** Weblate translation (French) 797fe99
* **lang:** Weblate translation (Gaelic) 80e67ee
* **lang:** Weblate translation (German) 8a2782a
* **lang:** Weblate translation (Italian) 5e28585
* **lang:** Weblate translation (Norwegian Bokmål) 7aee682
* **lang:** Weblate translation (Norwegian Bokmål) 1468d9c
* **lang:** Weblate translation (Norwegian Bokmål) 0c51070
* **lang:** Weblate translation (Polish) db39e94
* **lang:** Weblate translation (Polish) 270da31
* **lang:** Weblate translation (Portuguese (Brazil)) 78adff6
* **lang:** Weblate translation (Portuguese) aee7c1e
* **lang:** Weblate translation (Portuguese) abdcb87
* **lang:** Weblate translation (Russian) 5f16f19
* **lang:** Weblate translation (Ukrainian) 0153ff1
* Remove unsupported play store language pt a31cd54
* set required (im)mutable flag when creating pending intent 0a48cb5
* Shortened too long title fr-FR 2401de9

### Features

* **lang:** Added translation using Weblate (Interlingua) 699da52
* weblate merge 21261b0
2022-04-02 19:18:47 +00:00
Stefan Schüller 064ec44d53 Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!59
2022-04-02 19:12:21 +00:00
Stefan Schüller 2401de99a1 fix: Shortened too long title fr-FR 2022-04-02 21:04:04 +02:00
Stefan Schüller a31cd5494f fix: Remove unsupported play store language pt 2022-04-02 21:03:02 +02:00
Stefan Schüller 1d59aed5e7 chore: Update development readme 2022-04-02 20:59:19 +02:00
Stefan Schüller 21261b0d09 feat: weblate merge 2022-04-02 20:56:49 +02:00
Petter Reinholdtsen 7aee68213d
fix(lang): Weblate translation (Norwegian Bokmål)
Currently translated at 56.7% (203 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/nb_NO/
2022-03-24 13:59:09 +01:00
Nikita Epifanov 5f16f194ab
fix(lang): Weblate translation (Russian)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ru/
2022-03-24 13:59:09 +01:00
Petter Reinholdtsen 1468d9c490
fix(lang): Weblate translation (Norwegian Bokmål)
Currently translated at 55.3% (198 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/nb_NO/
2022-03-20 08:54:19 +01:00
Jonathan Soares 527c38584b
fix(lang): Weblate translation (Portuguese (Brazil))
Currently translated at 22.2% (6 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt_BR/
2022-03-17 20:58:10 +01:00
Jonathan Soares 78adff6779
fix(lang): Weblate translation (Portuguese (Brazil))
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pt_BR/
2022-03-17 20:58:09 +01:00
Maxime Leroy a2f5882211
fix(lang): Weblate translation (French)
Currently translated at 100.0% (27 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/fr/
2022-03-12 07:55:55 +01:00
Digiwizkid dba93fa9e8
fix(lang): Weblate translation (Bengali)
Currently translated at 99.7% (357 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/bn/
2022-03-02 08:59:22 +01:00
SC aee7c1e32e
fix(lang): Weblate translation (Portuguese)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pt/
2022-02-25 13:59:04 +01:00
Stefan f1640cfaa9
Merge pull request #284 from dheineman/fix-mutable-intent-flags
fix: set required (im)mutable flag when creating pending intent
2022-02-23 20:44:07 +01:00
Dave Heineman 0a48cb5016
fix: set required (im)mutable flag when creating pending intent
When targeting SDK S+ (version 31 and above) a (im)mutable flags is required when creating a PendingInent.
2022-02-21 15:19:34 +01:00
Besnik Bleta faa840a30f
fix(lang): Weblate translation (Albanian)
Currently translated at 96.2% (26 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/sq/
2022-02-16 14:55:16 +01:00
Besnik Bleta ca6c4da6e6
fix(lang): Weblate translation (Albanian)
Currently translated at 98.8% (354 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/sq/
2022-02-16 14:55:15 +01:00
Besnik Bleta e3121769b5
fix(lang): Weblate translation (Albanian)
Currently translated at 85.1% (23 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/sq/
2022-02-14 23:36:59 +01:00
J. Lavoie 5e28585fea
fix(lang): Weblate translation (Italian)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/it/
2022-02-14 23:36:58 +01:00
J. Lavoie 8a2782a32b
fix(lang): Weblate translation (German)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/de/
2022-02-14 23:36:58 +01:00
Jeff Huang e1be0e8a9f
fix(lang): Weblate translation (Chinese (Traditional))
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/zh_Hant/
2022-02-14 23:36:57 +01:00
J. Lavoie 797fe99831
fix(lang): Weblate translation (French)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fr/
2022-02-14 23:36:57 +01:00
Rex_sa 89f978e3d4
fix(lang): Weblate translation (Arabic)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ar/
2022-02-14 23:36:57 +01:00
Allan Nordhøy 0c51070f37
fix(lang): Weblate translation (Norwegian Bokmål)
Currently translated at 52.5% (188 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/nb_NO/
2022-02-13 20:54:05 +01:00
Allan Nordhøy 90b99c2437
fix(lang): Weblate translation (English)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/en/
2022-02-13 20:54:05 +01:00
SC 33e6fbb827
fix(lang): Weblate translation (Portuguese)
Currently translated at 100.0% (27 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt/
2022-02-11 20:56:11 +01:00
Ihor Hordiichuk 0153ff1a90
fix(lang): Weblate translation (Ukrainian)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/uk/
2022-01-24 11:55:31 +01:00
GunChleoc 80e67eebae
fix(lang): Weblate translation (Gaelic)
Currently translated at 82.4% (295 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/gd/
2022-01-24 11:55:30 +01:00
Sebastian Wilhelm Zarejko db39e9442d
fix(lang): Weblate translation (Polish)
Currently translated at 89.3% (320 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pl/
2022-01-21 19:53:29 +01:00
Software In Interlingua 699da52489
feat(lang): Added translation using Weblate (Interlingua) 2022-01-17 16:52:15 +01:00
gnu-ewm 270da310f7
fix(lang): Weblate translation (Polish)
Currently translated at 88.8% (318 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pl/
2022-01-16 15:54:05 +01:00
semantic-release-bot 6a6405ccdc chore(release): 1.11.0 [only cd]
# 1.11.0 (2022-01-09)

### Features

* Open playlist video in player, string update c23c17d
2022-01-09 13:03:15 +00:00
Stefan Schüller eb3b1eb7ad Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!58
2022-01-09 12:56:23 +00:00
Stefan Schüller 04db4ceb7f
Merge pull request #281 from digiwizkid/develop
feat: Add video to playlist
2022-01-09 13:19:00 +01:00
digiwizkid c23c17d2ef feat: Open playlist video in player, string update 2022-01-08 20:07:30 +05:30
digiwizkid 96ec510f40 Playlist UI WIP 2022-01-08 15:26:48 +05:30
digiwizkid c7893ddd38 Add to playlist feature initial commit 2022-01-08 13:18:41 +05:30
ssantos 530b97979e
fix(lang): Weblate translation (Portuguese)
Currently translated at 77.7% (21 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt/
2022-01-03 19:53:54 +01:00
ssantos abdcb87c3e
fix(lang): Weblate translation (Portuguese)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pt/
2022-01-03 19:53:53 +01:00
Stefan Schüller 4b1efade40 Merge branch 'master' into 'develop'
chore(release): 1.10.0 [only cd]

See merge request sschueller/peertube!57
2022-01-02 15:18:40 +00:00
semantic-release-bot 55a9acccd8 chore(release): 1.10.4 [only cd]
## 1.10.4 (2022-01-02)

### Bug Fixes

* Temp removal of videos in fastlane config 1d25be1
2022-01-02 14:49:20 +00:00
Stefan Schüller d37c788653 Merge branch 'develop' into 'master'
fix: Temp removal of videos in fastlane config

See merge request sschueller/peertube!56
2022-01-02 14:42:43 +00:00
Stefan Schüller 1d25be1bde fix: Temp removal of videos in fastlane config 2022-01-02 15:31:50 +01:00
semantic-release-bot c3aef83ff3 chore(release): 1.10.3 [only cd]
## 1.10.3 (2022-01-02)

### Bug Fixes

* fixed bad video URL 0370a12
2022-01-02 13:18:45 +00:00
Stefan Schüller 85f600b822 Merge branch 'develop' into 'master'
fix: fixed bad video URL

See merge request sschueller/peertube!55
2022-01-02 13:11:07 +00:00
Stefan Schüller 0370a1261d fix: fixed bad video URL 2022-01-02 14:04:35 +01:00
semantic-release-bot 629445e2ae chore(release): 1.10.2 [only cd]
## 1.10.2 (2022-01-02)

### Bug Fixes

* fixed bad video URL 0dd2254
2022-01-02 00:18:47 +00:00
Stefan Schüller 8c9a7ff5c4 Merge branch 'develop' into 'master'
fix: fixed bad video URL

See merge request sschueller/peertube!54
2022-01-02 00:11:54 +00:00
Stefan Schüller 0dd225423b fix: fixed bad video URL 2022-01-02 01:05:08 +01:00
semantic-release-bot 84a67f1e0b chore(release): 1.10.1 [only cd]
## 1.10.1 (2022-01-01)

### Bug Fixes

* fixed broken languages 784c69f
* Removed unsupported language in Google Play store and added test script to ci 18e4949
2022-01-01 23:32:44 +00:00
Stefan Schüller 15f9c59c03 Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!53
2022-01-01 23:25:36 +00:00
Stefan Schüller 784c69fb3b fix: fixed broken languages 2022-01-02 00:20:03 +01:00
Stefan Schüller 18e4949fe4 fix: Removed unsupported language in Google Play store and added test script to ci 2022-01-02 00:12:05 +01:00
semantic-release-bot 8e4ad9c351 chore(release): 1.10.0 [only cd]
# 1.10.0 (2022-01-01)

### Bug Fixes

* **lang:**  Weblate translation (Persian) 9c41d96
* **lang:**  Weblate translation (Turkish) c91da28
* **lang:**  Weblate translation (Ukrainian) c0025bf
* **lang:** Weblate translation (Arabic) 61d0a77
* **lang:** Weblate translation (Catalan) a8b670c
* **lang:** Weblate translation (Chinese (Traditional)) 059a020
* **lang:** Weblate translation (Finnish) 9873349
* **lang:** Weblate translation (French) 8a146a8
* **lang:** Weblate translation (German) d8c48ce
* **lang:** Weblate translation (Italian) 7150d7d
* **lang:** Weblate translation (Persian) 0cd5a39
* **lang:** Weblate translation (Turkish) 42de46c
* **lang:** Weblate translation (Ukrainian) 9eda11e
* Removed unsupported language in Google Play store c8b23e9

### Features

* **lang:** Added translation using Weblate (Catalan) a082878
2022-01-01 21:29:44 +00:00
Stefan Schüller bc7e33da54 Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!52
2022-01-01 21:22:49 +00:00
Stefan Schüller bbd318169a Merge remote-tracking branch 'origin/develop' into develop 2022-01-01 22:17:12 +01:00
Stefan Schüller c8b23e9c1a fix: Removed unsupported language in Google Play store 2022-01-01 22:17:09 +01:00
Stefan Schüller d703438f40 Merge branch 'master' into 'develop'
chore(release): 1.8.4 [only cd]

See merge request sschueller/peertube!51
2022-01-01 20:18:02 +00:00
semantic-release-bot 7c880655ae chore(release): 1.9.0 [only cd]
# 1.9.0 (2022-01-01)

### Features

* New player, updates to newest android SDK, New details view, Removed torrent playback (stopped working correctly), Subscribe / unsubscribe if logged in 5a19390
2022-01-01 20:10:43 +00:00
Stefan Schüller 19f7157b74 Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!50
2022-01-01 20:03:58 +00:00
Stefan Schüller 5a19390e95 feat: New player, updates to newest android SDK, New details view, Removed torrent playback (stopped working correctly), Subscribe / unsubscribe if logged in 2022-01-01 20:03:57 +00:00
Stefan Schüller dd13dfaf3c Merge remote-tracking branch 'weblate/develop' into develop 2022-01-01 20:54:49 +01:00
Stefan Schüller e35d25a292 Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!49
2022-01-01 18:48:39 +00:00
Stefan Schüller 47fe302f94 Merge branch 'player-update' into 'develop'
Player update

See merge request sschueller/peertube!48
2022-01-01 18:43:48 +00:00
Stefan Schüller aefd8df5c6 Player update 2022-01-01 18:43:48 +00:00
semantic-release-bot 8d2946f956 chore(release): 1.8.4 [only cd]
## 1.8.4 (2022-01-01)

### Bug Fixes

* **lang:**  Weblate translation (Chinese (Traditional)) 8280bfb
* **lang:**  Weblate translation (French) 85c8c8e
* **lang:**  Weblate translation (French) 6707cec
* **lang:**  Weblate translation (Indonesian) ada03b3
* **lang:**  Weblate translation (Italian) ed28fe3
* **lang:**  Weblate translation (Norwegian Bokmål) 2742ed6
* **lang:**  Weblate translation (Persian) 7648633
* **lang:**  Weblate translation (Portuguese (Brazil)) 60eca32
* **lang:**  Weblate translation (Portuguese (Portugal)) cc6b424
* **lang:**  Weblate translation (Portuguese) daebed8
* **lang:**  Weblate translation (Portuguese) 4bd0393
* **lang:**  Weblate translation (Russian) 49a579d
* **lang:**  Weblate translation (Sardinian) 792bf89
* **lang:**  Weblate translation (Spanish) cceec2f
* **lang:**  Weblate translation (Turkish) 42a01d4
* **lang:**  Weblate translation (Ukrainian) 68a57c8
* **lang:** Weblate translation (Arabic) 9453b5a
* **lang:** Weblate translation (Arabic) 96246dc
* **lang:** Weblate translation (Bengali) 45ac673
* **lang:** Weblate translation (Chinese (Simplified)) 490d8e3
* **lang:** Weblate translation (Chinese (Traditional)) fc07026
* **lang:** Weblate translation (Finnish) 5fe4353
* **lang:** Weblate translation (French) 5c9c90c
* **lang:** Weblate translation (German) a5b7279
* **lang:** Weblate translation (Indonesian) b98e2ae
* **lang:** Weblate translation (Italian) 76ef906
* **lang:** Weblate translation (Norwegian Bokmål) 08393e8
* **lang:** Weblate translation (Norwegian Bokmål) 672760a
* **lang:** Weblate translation (Persian) 514cd65
* **lang:** Weblate translation (Portuguese (Brazil)) 694c52e
* **lang:** Weblate translation (Portuguese) 60abc3b
* **lang:** Weblate translation (Portuguese) 1995e62
* **lang:** Weblate translation (Russian) 6f4ad5d
* **lang:** Weblate translation (Sardinian) 697044f
* **lang:** Weblate translation (Spanish) 18e1221
* **lang:** Weblate translation (Turkish) 3f51020
* **lang:** Weblate translation (Turkish) 1202dec
* **lang:** Weblate translation (Ukrainian) 3ec9283
2022-01-01 14:31:54 +00:00
Stefan Schüller b29afe52a2 Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!47
2022-01-01 14:09:28 +00:00
Pep Comeres a8b670c248
fix(lang): Weblate translation (Catalan)
Currently translated at 10.8% (39 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ca/
2022-01-01 14:56:17 +01:00
J. Lavoie 98733491e6
fix(lang): Weblate translation (Finnish)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fi/
2022-01-01 14:56:16 +01:00
J. Lavoie 7150d7d752
fix(lang): Weblate translation (Italian)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/it/
2022-01-01 14:56:16 +01:00
J. Lavoie d8c48cee40
fix(lang): Weblate translation (German)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/de/
2022-01-01 14:56:16 +01:00
J. Lavoie 8a146a82f6
fix(lang): Weblate translation (French)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fr/
2022-01-01 14:56:15 +01:00
Stefan Schüller 91b4869934 Merge branch 'player-update' into 'develop'
Player update

See merge request sschueller/peertube!46
2022-01-01 00:53:43 +00:00
Stefan Schüller b83130125d Player update 2022-01-01 00:53:43 +00:00
Pep Comeres a082878cd3
feat(lang): Added translation using Weblate (Catalan) 2021-12-31 10:31:30 +01:00
Danial Behzadi 9c41d96930
fix(lang): Weblate translation (Persian)
Currently translated at 51.8% (14 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/fa/
2021-12-29 17:52:33 +01:00
Ihor Hordiichuk 9eda11ed4f
fix(lang): Weblate translation (Ukrainian)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/uk/
2021-12-29 17:52:33 +01:00
Ihor Hordiichuk c0025bf2ae
fix(lang): Weblate translation (Ukrainian)
Currently translated at 100.0% (27 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/uk/
2021-12-29 17:52:32 +01:00
Oğuz Ersen c91da28639
fix(lang): Weblate translation (Turkish)
Currently translated at 100.0% (27 of 27 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/tr/
2021-12-29 17:52:31 +01:00
Danial Behzadi 0cd5a39e80
fix(lang): Weblate translation (Persian)
Currently translated at 55.8% (200 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fa/
2021-12-29 17:52:30 +01:00
Jeff Huang 059a020636
fix(lang): Weblate translation (Chinese (Traditional))
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/zh_Hant/
2021-12-29 17:52:28 +01:00
Oğuz Ersen 42de46cf58
fix(lang): Weblate translation (Turkish)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/tr/
2021-12-29 17:52:28 +01:00
Rex_sa 61d0a7747f
fix(lang): Weblate translation (Arabic)
Currently translated at 100.0% (358 of 358 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ar/
2021-12-29 17:52:28 +01:00
Stefan Schueller 0d720105ce Merge branch 'develop' of https://hosted.weblate.org/git/peertube/android into develop
 Conflicts:
	fastlane/metadata/android/fa/title.txt
	fastlane/metadata/android/id/title.txt
2021-12-28 11:21:52 +01:00
Stefan Schüller 6d4f6c9979 Merge branch 'master' into 'develop'
chore(release): 1.8.1 [only cd]

See merge request sschueller/peertube!45
2021-12-28 10:18:55 +00:00
semantic-release-bot 34f0046bdd chore(release): 1.8.3 [only cd]
## 1.8.3 (2021-12-27)

### Bug Fixes

* new play store shorter app name requirement 37229a0
2021-12-27 15:24:04 +00:00
Stefan Schüller 0d4fae005d Merge branch 'develop' into 'master'
Release

See merge request sschueller/peertube!44
2021-12-27 15:17:42 +00:00
Stefan Schueller 28198895fb Merge remote-tracking branch 'origin/develop' into develop 2021-12-27 16:12:53 +01:00
Stefan Schueller 37229a0f48 fix: new play store shorter app name requirement 2021-12-27 16:12:47 +01:00
Petter Reinholdtsen 2742ed6064
fix(lang): Weblate translation (Norwegian Bokmål)
Currently translated at 28.5% (6 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/nb_NO/
2021-12-26 23:52:38 +01:00
Petter Reinholdtsen 08393e8275
fix(lang): Weblate translation (Norwegian Bokmål)
Currently translated at 52.5% (187 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/nb_NO/
2021-12-26 23:52:37 +01:00
Andrei Stepanov 49a579dc30
fix(lang): Weblate translation (Russian)
Currently translated at 76.1% (16 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/ru/
2021-12-19 02:52:43 +01:00
Daimar Stein 60eca32501
fix(lang): Weblate translation (Portuguese (Brazil))
Currently translated at 14.2% (3 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt_BR/
2021-12-04 15:52:21 +01:00
Oymate 45ac67333c
fix(lang): Weblate translation (Bengali)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/bn/
2021-12-03 11:50:12 +01:00
Danial Behzadi 7648633684
fix(lang): Weblate translation (Persian)
Currently translated at 42.8% (9 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/fa/
2021-12-02 06:50:33 +01:00
Danial Behzadi 514cd65539
fix(lang): Weblate translation (Persian)
Currently translated at 32.5% (116 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fa/
2021-12-02 06:50:32 +01:00
Dronrs 490d8e30a8
fix(lang): Weblate translation (Chinese (Simplified))
Currently translated at 93.8% (334 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/zh_Hans/
2021-11-27 10:50:10 +01:00
Oğuz Ersen 3f51020174
fix(lang): Weblate translation (Turkish)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/tr/
2021-11-14 07:51:25 +01:00
Mickaël Sibelle 85c8c8e8a8
fix(lang): Weblate translation (French)
Currently translated at 80.9% (17 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/fr/
2021-11-07 17:52:02 +01:00
SC cc6b42474e
fix(lang): Weblate translation (Portuguese (Portugal))
Currently translated at 85.7% (18 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt_PT/
2021-10-24 04:38:51 +02:00
Gabriel Cardoso 694c52e6be
fix(lang): Weblate translation (Portuguese (Brazil))
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pt_BR/
2021-10-24 04:38:50 +02:00
m51d b98e2ae0be
fix(lang): Weblate translation (Indonesian)
Currently translated at 95.5% (340 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/id/
2021-10-21 07:37:03 +02:00
m51d ada03b3cb8
fix(lang): Weblate translation (Indonesian)
Currently translated at 52.3% (11 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/id/
2021-10-21 07:37:02 +02:00
Nikita Epifanov 6f4ad5d639
fix(lang): Weblate translation (Russian)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ru/
2021-10-18 18:02:51 +02:00
SC daebed8bb5
fix(lang): Weblate translation (Portuguese)
Currently translated at 100.0% (21 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt/
2021-10-14 13:36:39 +02:00
SC 60abc3b9fe
fix(lang): Weblate translation (Portuguese)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pt/
2021-10-14 13:36:38 +02:00
SC 4bd03934b3
fix(lang): Weblate translation (Portuguese)
Currently translated at 28.5% (6 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/pt/
2021-10-13 13:23:04 +02:00
Jeff Huang 8280bfb0a4
fix(lang): Weblate translation (Chinese (Traditional))
Currently translated at 95.2% (20 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/zh_Hant/
2021-10-13 09:38:08 +02:00
Ihor Hordiichuk 3ec9283093
fix(lang): Weblate translation (Ukrainian)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/uk/
2021-10-13 09:38:08 +02:00
Ihor Hordiichuk 68a57c8649
fix(lang): Weblate translation (Ukrainian)
Currently translated at 100.0% (21 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/uk/
2021-10-13 09:38:07 +02:00
J. Lavoie ed28fe3789
fix(lang): Weblate translation (Italian)
Currently translated at 71.4% (15 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/it/
2021-10-13 09:38:06 +02:00
Oğuz Ersen 42a01d45bc
fix(lang): Weblate translation (Turkish)
Currently translated at 100.0% (21 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/tr/
2021-10-13 09:38:06 +02:00
J. Lavoie 6707cec7e5
fix(lang): Weblate translation (French)
Currently translated at 71.4% (15 of 21 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/fr/
2021-10-13 09:38:05 +02:00
J. Lavoie 1995e623bd
fix(lang): Weblate translation (Portuguese)
Currently translated at 99.4% (354 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/pt/
2021-10-13 09:38:04 +02:00
J. Lavoie 5fe4353db1
fix(lang): Weblate translation (Finnish)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fi/
2021-10-13 09:38:04 +02:00
J. Lavoie 76ef906e36
fix(lang): Weblate translation (Italian)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/it/
2021-10-13 09:38:04 +02:00
J. Lavoie 18e1221aba
fix(lang): Weblate translation (Spanish)
Currently translated at 83.4% (297 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/es/
2021-10-13 09:38:04 +02:00
J. Lavoie a5b7279bfc
fix(lang): Weblate translation (German)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/de/
2021-10-13 09:38:02 +02:00
Allan Nordhøy 672760acce
fix(lang): Weblate translation (Norwegian Bokmål)
Currently translated at 50.0% (178 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/nb_NO/
2021-10-13 09:38:01 +02:00
Jeff Huang fc07026a0a
fix(lang): Weblate translation (Chinese (Traditional))
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/zh_Hant/
2021-10-13 09:38:01 +02:00
Oğuz Ersen 1202decd38
fix(lang): Weblate translation (Turkish)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/tr/
2021-10-13 09:38:01 +02:00
J. Lavoie 5c9c90cc05
fix(lang): Weblate translation (French)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/fr/
2021-10-13 09:38:01 +02:00
Rex_sa 9453b5a2ef
fix(lang): Weblate translation (Arabic)
Currently translated at 100.0% (356 of 356 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ar/
2021-10-13 09:38:00 +02:00
Manuel González cceec2f6ff
fix(lang): Weblate translation (Spanish)
Currently translated at 100.0% (15 of 15 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/es/
2021-10-11 12:42:05 +02:00
Ivano Peddis 697044fba3
fix(lang): Weblate translation (Sardinian)
Currently translated at 13.5% (48 of 354 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/sc/
2021-10-11 12:42:05 +02:00
Ivano Peddis 792bf89bb0
fix(lang): Weblate translation (Sardinian)
Currently translated at 100.0% (15 of 15 strings)

Translation: PeerTube/PlayStoreMeta
Translate-URL: https://hosted.weblate.org/projects/peertube/playstoremeta/sc/
2021-10-11 12:42:05 +02:00
abidin toumi 96246dc696
fix(lang): Weblate translation (Arabic)
Currently translated at 100.0% (354 of 354 strings)

Translation: PeerTube/Android
Translate-URL: https://hosted.weblate.org/projects/peertube/android/ar/
2021-10-11 12:42:05 +02:00
236 changed files with 5832 additions and 2842 deletions

View File

@ -108,6 +108,13 @@ buildDebug:
script:
- bundle exec fastlane buildDebug
testFastlane:
stage: test
script:
- ./ci-scripts/validate-play-store-lang.sh
tags:
- docker
testDebug:
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
stage: test

View File

@ -1,3 +1,177 @@
## 1.12.2 (2022-04-02)
### Bug Fixes
* Removed videos, fastlane cant upload them d98bf8c
* Update fastlane cef0043
## 1.12.1 (2022-04-02)
### Bug Fixes
* Too long play store update notes fe187e5
# 1.12.0 (2022-04-02)
### Bug Fixes
* **lang:** Weblate translation (Albanian) faa840a
* **lang:** Weblate translation (Albanian) e312176
* **lang:** Weblate translation (French) a2f5882
* **lang:** Weblate translation (Portuguese (Brazil)) 527c385
* **lang:** Weblate translation (Portuguese) 33e6fbb
* **lang:** Weblate translation (Portuguese) 530b979
* **lang:** Weblate translation (Albanian) ca6c4da
* **lang:** Weblate translation (Arabic) 89f978e
* **lang:** Weblate translation (Bengali) dba93fa
* **lang:** Weblate translation (Chinese (Traditional)) e1be0e8
* **lang:** Weblate translation (English) 90b99c2
* **lang:** Weblate translation (French) 797fe99
* **lang:** Weblate translation (Gaelic) 80e67ee
* **lang:** Weblate translation (German) 8a2782a
* **lang:** Weblate translation (Italian) 5e28585
* **lang:** Weblate translation (Norwegian Bokmål) 7aee682
* **lang:** Weblate translation (Norwegian Bokmål) 1468d9c
* **lang:** Weblate translation (Norwegian Bokmål) 0c51070
* **lang:** Weblate translation (Polish) db39e94
* **lang:** Weblate translation (Polish) 270da31
* **lang:** Weblate translation (Portuguese (Brazil)) 78adff6
* **lang:** Weblate translation (Portuguese) aee7c1e
* **lang:** Weblate translation (Portuguese) abdcb87
* **lang:** Weblate translation (Russian) 5f16f19
* **lang:** Weblate translation (Ukrainian) 0153ff1
* Remove unsupported play store language pt a31cd54
* set required (im)mutable flag when creating pending intent 0a48cb5
* Shortened too long title fr-FR 2401de9
### Features
* **lang:** Added translation using Weblate (Interlingua) 699da52
* weblate merge 21261b0
# 1.11.0 (2022-01-09)
### Features
* Open playlist video in player, string update c23c17d
## 1.10.4 (2022-01-02)
### Bug Fixes
* Temp removal of videos in fastlane config 1d25be1
## 1.10.3 (2022-01-02)
### Bug Fixes
* fixed bad video URL 0370a12
## 1.10.2 (2022-01-02)
### Bug Fixes
* fixed bad video URL 0dd2254
## 1.10.1 (2022-01-01)
### Bug Fixes
* fixed broken languages 784c69f
* Removed unsupported language in Google Play store and added test script to ci 18e4949
# 1.10.0 (2022-01-01)
### Bug Fixes
* **lang:** Weblate translation (Persian) 9c41d96
* **lang:** Weblate translation (Turkish) c91da28
* **lang:** Weblate translation (Ukrainian) c0025bf
* **lang:** Weblate translation (Arabic) 61d0a77
* **lang:** Weblate translation (Catalan) a8b670c
* **lang:** Weblate translation (Chinese (Traditional)) 059a020
* **lang:** Weblate translation (Finnish) 9873349
* **lang:** Weblate translation (French) 8a146a8
* **lang:** Weblate translation (German) d8c48ce
* **lang:** Weblate translation (Italian) 7150d7d
* **lang:** Weblate translation (Persian) 0cd5a39
* **lang:** Weblate translation (Turkish) 42de46c
* **lang:** Weblate translation (Ukrainian) 9eda11e
* Removed unsupported language in Google Play store c8b23e9
### Features
* **lang:** Added translation using Weblate (Catalan) a082878
# 1.9.0 (2022-01-01)
### Features
* New player, updates to newest android SDK, New details view, Removed torrent playback (stopped working correctly), Subscribe / unsubscribe if logged in 5a19390
## 1.8.4 (2022-01-01)
### Bug Fixes
* **lang:** Weblate translation (Chinese (Traditional)) 8280bfb
* **lang:** Weblate translation (French) 85c8c8e
* **lang:** Weblate translation (French) 6707cec
* **lang:** Weblate translation (Indonesian) ada03b3
* **lang:** Weblate translation (Italian) ed28fe3
* **lang:** Weblate translation (Norwegian Bokmål) 2742ed6
* **lang:** Weblate translation (Persian) 7648633
* **lang:** Weblate translation (Portuguese (Brazil)) 60eca32
* **lang:** Weblate translation (Portuguese (Portugal)) cc6b424
* **lang:** Weblate translation (Portuguese) daebed8
* **lang:** Weblate translation (Portuguese) 4bd0393
* **lang:** Weblate translation (Russian) 49a579d
* **lang:** Weblate translation (Sardinian) 792bf89
* **lang:** Weblate translation (Spanish) cceec2f
* **lang:** Weblate translation (Turkish) 42a01d4
* **lang:** Weblate translation (Ukrainian) 68a57c8
* **lang:** Weblate translation (Arabic) 9453b5a
* **lang:** Weblate translation (Arabic) 96246dc
* **lang:** Weblate translation (Bengali) 45ac673
* **lang:** Weblate translation (Chinese (Simplified)) 490d8e3
* **lang:** Weblate translation (Chinese (Traditional)) fc07026
* **lang:** Weblate translation (Finnish) 5fe4353
* **lang:** Weblate translation (French) 5c9c90c
* **lang:** Weblate translation (German) a5b7279
* **lang:** Weblate translation (Indonesian) b98e2ae
* **lang:** Weblate translation (Italian) 76ef906
* **lang:** Weblate translation (Norwegian Bokmål) 08393e8
* **lang:** Weblate translation (Norwegian Bokmål) 672760a
* **lang:** Weblate translation (Persian) 514cd65
* **lang:** Weblate translation (Portuguese (Brazil)) 694c52e
* **lang:** Weblate translation (Portuguese) 60abc3b
* **lang:** Weblate translation (Portuguese) 1995e62
* **lang:** Weblate translation (Russian) 6f4ad5d
* **lang:** Weblate translation (Sardinian) 697044f
* **lang:** Weblate translation (Spanish) 18e1221
* **lang:** Weblate translation (Turkish) 3f51020
* **lang:** Weblate translation (Turkish) 1202dec
* **lang:** Weblate translation (Ukrainian) 3ec9283
## 1.8.3 (2021-12-27)
### Bug Fixes
* new play store shorter app name requirement 37229a0
## 1.8.2 (2021-12-27)

View File

@ -4,14 +4,15 @@
2. Locally switch to develop
3. Pull github develop
4. Pull weblate develop
5. Push to develop gitlab and github
5. Push to develop gitlab
6. Merge develop into master and merge
7. Wait for Release Build and release to play store
8. Wait for gitlab -> github sync
9. Run publishGithub
10. Merge master into develop, push to github
## fastlane update
## fastlane update (install ruby2.7 and "gem-2.7 install bundler")
```
bundle update
bundle-2.7 update
```

View File

@ -1,6 +1,5 @@
FROM gradle:7-jdk16
# get link at bottom of https://developer.android.com/studio
ENV ANDROID_SDK_URL https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip
ENV ANDROID_SDK_CHECKSUM 124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e2fcfa7faf
@ -8,7 +7,7 @@ ENV ANDROID_SDK_CHECKSUM 124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e
# higher version casues Warning: Failed to find package
ENV ANDROID_BUILD_TOOLS_VERSION 30.0.2
ENV ANDROID_SDK_ROOT /usr/local/android-sdk-linux
ENV ANDROID_VERSION 30
ENV ANDROID_VERSION 32
# ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
ENV PATH ${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin

View File

@ -8,23 +8,23 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.543.0)
aws-sdk-core (3.125.0)
aws-partitions (1.573.0)
aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.53.0)
aws-sdk-core (~> 3, >= 3.125.0)
aws-sdk-kms (1.55.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.110.0)
aws-sdk-core (~> 3, >= 3.125.0)
aws-sdk-s3 (1.113.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
@ -36,17 +36,18 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
excon (0.89.0)
faraday (1.8.0)
excon (0.92.2)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
multipart-post (>= 1.2, < 3)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
@ -55,14 +56,17 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.199.0)
fastlane (2.205.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -102,9 +106,9 @@ GEM
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.14.0)
google-apis-androidpublisher_v3 (0.17.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.1)
google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@ -113,19 +117,19 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.9.0)
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.6.0)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.10.0)
google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.35.0)
google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@ -133,8 +137,8 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.0)
faraday (>= 0.17.3, < 2.0)
googleauth (1.1.2)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
@ -144,7 +148,7 @@ GEM
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
@ -169,9 +173,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.0)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@ -188,7 +192,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
@ -211,4 +215,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.3.3
2.3.10

View File

@ -1 +1 @@
1.8.2
1.12.2

View File

@ -39,15 +39,15 @@ else {
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 30
compileSdkVersion 32
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "net.schueller.peertube"
minSdkVersion 21
targetSdkVersion 30
versionCode 1068
versionName "1.8.2"
targetSdkVersion 32
versionCode 1080
versionName "1.12.2"
buildConfigField "long", "BUILD_TIME", readPropertyWithDefault('buildTimestamp', System.currentTimeMillis()) + 'L'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -94,10 +94,10 @@ android {
}
def room_version = "2.3.0"
def lifecycleVersion = '2.3.1'
def exoplayer = '2.12.3'
def fragment_version = "1.3.6"
def room_version = "2.4.0"
def lifecycleVersion = '2.4.0'
def exoplayer = '2.16.1'
def fragment_version = "1.4.0"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
@ -105,8 +105,8 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Layouts and design
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
@ -118,7 +118,7 @@ dependencies {
implementation 'com.mikepenz:fontawesome-typeface:5.9.0.2-kotlin@aar'
// http client / REST
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// image downloading and caching library

View File

@ -1,78 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.schueller.peertube">
xmlns:tools="http://schemas.android.com/tools"
package="net.schueller.peertube">
<!-- required to play video in background via notification -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- connect to peertube server -->
<uses-permission android:name="android.permission.INTERNET" /> <!-- required for torrent downloading -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- connect to peertube server -->
<uses-permission android:name="android.permission.INTERNET"/> <!-- required for torrent downloading -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".application.AppApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
android:name=".application.AppApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<!-- Server Address Book -->
<activity
android:name=".activity.ServerAddressBookActivity"
android:label="@string/title_activity_server_address_book"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Video Lists -->
android:name=".activity.ServerAddressBookActivity"
android:label="@string/title_activity_server_address_book"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Video Lists -->
<activity
android:name=".activity.VideoListActivity"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
android:name=".activity.VideoListActivity"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH" />
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity> <!-- Video Player -->
<activity
android:name=".activity.VideoPlayActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:label="@string/title_activity_video_play"
android:launchMode="singleInstance"
android:supportsPictureInPicture="true"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Settings -->
android:name=".activity.VideoPlayActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:label="@string/title_activity_video_play"
android:launchMode="singleInstance"
android:supportsPictureInPicture="true"
android:theme="@style/AppTheme.NoActionBar"/>
<!-- Playlist -->
<activity android:name=".activity.PlaylistActivity"
android:label="Playlist"
android:theme="@style/AppTheme.NoActionBar"/>
<!-- Settings -->
<activity
android:name=".activity.SettingsActivity"
android:label="@string/title_activity_settings"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Server Selection -->
android:name=".activity.SettingsActivity"
android:label="@string/title_activity_settings"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Server Selection -->
<activity
android:name=".activity.SearchServerActivity"
android:label="@string/title_activity_select_server"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Me -->
android:name=".activity.SearchServerActivity"
android:label="@string/title_activity_select_server"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Me -->
<activity
android:name=".activity.MeActivity"
android:label="@string/title_activity_me"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Account -->
android:name=".activity.MeActivity"
android:label="@string/title_activity_me"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Account -->
<activity
android:name=".activity.AccountActivity"
android:label="@string/title_activity_account"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Content provider for search suggestions -->
android:name=".activity.AccountActivity"
android:label="@string/title_activity_account"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Content provider for search suggestions -->
<provider
android:name=".provider.SearchSuggestionsProvider"
android:authorities="net.schueller.peertube.provider.SearchSuggestionsProvider"
android:enabled="true"
android:exported="false" />
android:name=".provider.SearchSuggestionsProvider"
android:authorities="net.schueller.peertube.provider.SearchSuggestionsProvider"
android:enabled="true"
android:exported="false"/>
<service android:name=".service.VideoPlayerService" />
<service android:name=".service.VideoPlayerService"/>
<receiver android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>

View File

@ -175,11 +175,11 @@ public class AccountActivity extends CommonActivity {
if (response.isSuccessful()) {
Account account = response.body();
String owner = MetaDataHelper.getOwnerString(account.getName(),
account.getHost(),
AccountActivity.this
String owner = MetaDataHelper.getOwnerString(account,
AccountActivity.this, true
);
// set view data
TextView ownerStringView = findViewById(R.id.account_owner_string);
ownerStringView.setText(owner);

View File

@ -27,7 +27,9 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import com.squareup.picasso.Picasso;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
@ -36,18 +38,12 @@ import net.schueller.peertube.model.Me;
import net.schueller.peertube.network.GetUserService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.network.Session;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import com.squareup.picasso.Picasso;
import java.util.Objects;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import java.util.Objects;
import static net.schueller.peertube.application.AppApplication.getContext;
public class MeActivity extends CommonActivity {
@ -85,11 +81,16 @@ public class MeActivity extends CommonActivity {
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
LinearLayout account = findViewById(R.id.a_me_account_line);
LinearLayout playlist = findViewById(R.id.a_me_playlist);
LinearLayout settings = findViewById(R.id.a_me_settings);
LinearLayout help = findViewById(R.id.a_me_helpnfeedback);
TextView logout = findViewById(R.id.a_me_logout);
playlist.setOnClickListener(view -> {
Intent playlistActivity = new Intent(getContext(), PlaylistActivity.class);
startActivity(playlistActivity);
});
settings.setOnClickListener(view -> {
Intent settingsActivity = new Intent(getContext(), SettingsActivity.class);
@ -124,7 +125,7 @@ public class MeActivity extends CommonActivity {
call.enqueue(new Callback<Me>() {
LinearLayout account = findViewById(R.id.a_me_account_line);
final LinearLayout account = findViewById(R.id.a_me_account_line);
@Override
public void onResponse(@NonNull Call<Me> call, @NonNull Response<Me> response) {
@ -162,7 +163,7 @@ public class MeActivity extends CommonActivity {
@Override
public void onFailure(@NonNull Call<Me> call, @NonNull Throwable t) {
ErrorHelper.showToastFromCommunicationError( MeActivity.this, t );
ErrorHelper.showToastFromCommunicationError(MeActivity.this, t);
account.setVisibility(View.GONE);
}
});

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.adapter.MultiViewRecyclerViewHolder
import net.schueller.peertube.adapter.PlaylistAdapter
import net.schueller.peertube.database.Video
import net.schueller.peertube.database.VideoViewModel
import net.schueller.peertube.databinding.ActivityPlaylistBinding
class PlaylistActivity : CommonActivity() {
private val TAG = "PlaylistAct"
private val mVideoViewModel: VideoViewModel by viewModels()
private lateinit var mBinding: ActivityPlaylistBinding
override fun onSupportNavigateUp(): Boolean {
finish() // close this activity as oppose to navigating up
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityPlaylistBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(mBinding.toolBarServerAddressBook)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
}
showServers()
}
private fun onVideoClick(video: Video) {
val intent = Intent(this, VideoPlayActivity::class.java)
intent.putExtra(MultiViewRecyclerViewHolder.EXTRA_VIDEOID, video.videoUUID)
startActivity(intent)
}
private fun showServers() {
val adapter = PlaylistAdapter(mutableListOf(), { onVideoClick(it) }).also {
mBinding.serverListRecyclerview.adapter = it
}
// Delete items on swipe
val helper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
AlertDialog.Builder(this@PlaylistActivity)
.setTitle(getString(R.string.remove_video))
.setMessage(getString(R.string.remove_video_warning_message))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val position = viewHolder.bindingAdapterPosition
val video = adapter.getVideoAtPosition(position)
// Delete the video
mVideoViewModel.delete(video)
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
})
helper.attachToRecyclerView(mBinding.serverListRecyclerview)
// Update the cached copy of the words in the adapter.
mVideoViewModel.allVideos.observe(this, { videos: List<Video> ->
adapter.setVideos(videos)
})
}
companion object
}

View File

@ -41,7 +41,7 @@ import java.util.*
class ServerAddressBookActivity : CommonActivity() {
private val TAG = "ServerAddressBookActivity"
private val TAG = "ServerAddBookAct"
private val mServerViewModel: ServerViewModel by viewModels()
private var addServerFragment: AddServerFragment? = null
@ -133,15 +133,15 @@ class ServerAddressBookActivity : CommonActivity() {
AlertDialog.Builder(this@ServerAddressBookActivity)
.setTitle(getString(R.string.server_book_del_alert_title))
.setMessage(getString(R.string.server_book_del_alert_msg))
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
val position = viewHolder.adapterPosition
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val position = viewHolder.bindingAdapterPosition
val server = adapter.getServerAtPosition(position)
// Toast.makeText(ServerAddressBookActivity.this, "Deleting " +
// server.getServerName(), Toast.LENGTH_LONG).show();
// Delete the server
mServerViewModel.delete(server)
}
.setNegativeButton(android.R.string.no) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.adapterPosition) }
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}

View File

@ -19,6 +19,7 @@ package net.schueller.peertube.activity
import android.Manifest.permission
import android.R.drawable
import android.R.string
import android.app.Activity
import android.app.AlertDialog.Builder
import android.app.SearchManager
import android.content.Context
@ -33,6 +34,7 @@ import android.view.MenuItem
import android.view.MenuItem.OnActionExpandListener
import android.view.View
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnSuggestionListener
import androidx.appcompat.widget.Toolbar
@ -116,7 +118,7 @@ class VideoListActivity : CommonActivity() {
Builder(this@VideoListActivity)
.setTitle(getString(R.string.clear_search_history))
.setMessage(getString(R.string.clear_search_history_prompt))
.setPositiveButton(string.yes) { _, _ ->
.setPositiveButton(string.ok) { _, _ ->
val suggestions = SearchRecentSuggestions(
applicationContext,
SearchSuggestionsProvider.AUTHORITY,
@ -124,7 +126,7 @@ class VideoListActivity : CommonActivity() {
)
suggestions.clearHistory()
}
.setNegativeButton(string.no, null)
.setNegativeButton(string.cancel, null)
.setIcon(drawable.ic_dialog_alert)
.show()
true
@ -160,8 +162,7 @@ class VideoListActivity : CommonActivity() {
position
) as Cursor
return cursor.getString(
cursor
.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
)
}
@ -178,15 +179,26 @@ class VideoListActivity : CommonActivity() {
stopService(Intent(this, VideoPlayerService::class.java))
}
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == SWITCH_INSTANCE) {
if (resultCode == RESULT_OK) {
loadVideos(currentStart, count, sort, filter)
}
// public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// super.onActivityResult(requestCode, resultCode, data)
// if (requestCode == SWITCH_INSTANCE) {
// if (resultCode == RESULT_OK) {
// loadVideos(currentStart, count, sort, filter)
// }
// }
// }
private var resultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
loadVideos(currentStart, count, sort, filter)
}
}
private fun openActivityForResult(intent: Intent) {
resultLauncher.launch(intent)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
@ -213,7 +225,7 @@ class VideoListActivity : CommonActivity() {
}
id.action_server_address_book -> {
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
openActivityForResult(addressBookActivityIntent)
return false
}
else -> {
@ -461,7 +473,7 @@ class VideoListActivity : CommonActivity() {
// new IconicsDrawable(this, FontAwesome.Icon.faw_user_circle));
// Click Listener
navigation.setOnNavigationItemSelectedListener { menuItem: MenuItem ->
navigation.setOnItemSelectedListener { menuItem: MenuItem ->
when (menuItem.itemId) {
id.navigation_overview -> {
// TODO
@ -470,7 +482,7 @@ class VideoListActivity : CommonActivity() {
loadOverview(currentPage)
overViewActive = true
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_trending -> {
//Log.v(TAG, "navigation_trending");
@ -482,7 +494,7 @@ class VideoListActivity : CommonActivity() {
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_recent -> {
if (!isLoading) {
@ -493,7 +505,7 @@ class VideoListActivity : CommonActivity() {
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_local -> {
//Log.v(TAG, "navigation_trending");
@ -505,15 +517,15 @@ class VideoListActivity : CommonActivity() {
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
id.navigation_subscriptions -> //Log.v(TAG, "navigation_subscriptions");
if (!Session.getInstance().isLoggedIn) {
// Intent intent = new Intent(this, LoginActivity.class);
// this.startActivity(intent);
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
return@setOnNavigationItemSelectedListener false
openActivityForResult(addressBookActivityIntent)
return@setOnItemSelectedListener false
} else {
if (!isLoading) {
overViewActive = false
@ -523,7 +535,7 @@ class VideoListActivity : CommonActivity() {
subscriptions = true
loadVideos(currentStart, count, sort, filter)
}
return@setOnNavigationItemSelectedListener true
return@setOnItemSelectedListener true
}
}
false
@ -574,6 +586,5 @@ class VideoListActivity : CommonActivity() {
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
const val SWITCH_INSTANCE = 2
}
}

View File

@ -1,502 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity;
import android.annotation.SuppressLint;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.PictureInPictureParams;
import android.app.RemoteAction;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.util.Rational;
import android.util.TypedValue;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import net.schueller.peertube.R;
import net.schueller.peertube.fragment.VideoMetaDataFragment;
import net.schueller.peertube.fragment.VideoPlayerFragment;
import net.schueller.peertube.service.VideoPlayerService;
import java.util.ArrayList;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PAUSE;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PLAY;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
public class VideoPlayActivity extends AppCompatActivity {
private static final String TAG = "VideoPlayActivity";
static boolean floatMode = false;
private static final int REQUEST_CODE = 101;
private BroadcastReceiver receiver;
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
@SuppressLint("NewApi")
public void makePipControls() {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
ArrayList<RemoteAction> actions = new ArrayList<>();
Intent actionIntent = new Intent(getString(R.string.app_background_audio));
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
@SuppressLint({"NewApi", "LocalSuppress"}) Icon icon = Icon.createWithResource(getApplicationContext(), android.R.drawable.stat_sys_speakerphone);
@SuppressLint({"NewApi", "LocalSuppress"}) RemoteAction remoteAction = new RemoteAction(icon, "close pip", "from pip window custom command", pendingIntent);
actions.add(remoteAction);
actionIntent = new Intent(ACTION_STOP);
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop);
remoteAction = new RemoteAction(icon, "play", "stop the media", pendingIntent);
actions.add(remoteAction);
assert videoPlayerFragment != null;
if (videoPlayerFragment.isPaused()) {
Log.e(TAG, "setting actions with play button");
actionIntent = new Intent(ACTION_PLAY);
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_play);
remoteAction = new RemoteAction(icon, "play", "play the media", pendingIntent);
} else {
Log.e(TAG, "setting actions with pause button");
actionIntent = new Intent(ACTION_PAUSE);
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause);
remoteAction = new RemoteAction(icon, "pause", "pause the media", pendingIntent);
}
actions.add(remoteAction);
//add custom actions to pip window
PictureInPictureParams params =
new PictureInPictureParams.Builder()
.setActions(actions)
.build();
setPictureInPictureParams(params);
}
public void changedToPipMode() {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.showControls(false);
//create custom actions
makePipControls();
//setup receiver to handle customer actions
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STOP);
filter.addAction(ACTION_PAUSE);
filter.addAction(ACTION_PLAY);
filter.addAction((getString(R.string.app_background_audio)));
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
assert action != null;
if (action.equals(ACTION_PAUSE)) {
videoPlayerFragment.pauseVideo();
makePipControls();
}
if (action.equals(ACTION_PLAY)) {
videoPlayerFragment.unPauseVideo();
makePipControls();
}
if (action.equals(getString(R.string.app_background_audio))) {
unregisterReceiver(receiver);
finish();
}
if (action.equals(ACTION_STOP)) {
unregisterReceiver(receiver);
finishAndRemoveTask();
}
}
};
registerReceiver(receiver, filter);
Log.v(TAG, "switched to pip ");
floatMode = true;
videoPlayerFragment.showControls(false);
}
public void changedToNormalMode() {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.showControls(true);
if (receiver != null) {
unregisterReceiver(receiver);
}
Log.v(TAG, "switched to normal");
floatMode = false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set theme
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
setTheme(getResources().getIdentifier(
sharedPref.getString(
getString(R.string.pref_theme_key),
getString(R.string.app_default_theme)
),
"style",
getPackageName())
);
setContentView(R.layout.activity_video_play);
// get video ID
Intent intent = getIntent();
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
String playingVideo = videoPlayerFragment.getVideoUuid();
Log.v(TAG, "oncreate click: " + videoUuid + " is trying to replace: " + playingVideo);
if (TextUtils.isEmpty(playingVideo)) {
Log.v(TAG, "oncreate no video currently playing");
videoPlayerFragment.start(videoUuid);
} else if (!playingVideo.equals(videoUuid)) {
Log.v(TAG, "oncreate different video playing currently");
videoPlayerFragment.stopVideo();
videoPlayerFragment.start(videoUuid);
} else {
Log.v(TAG, "oncreate same video playing currently");
}
// if we are in landscape set the video to fullscreen
int orientation = this.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true);
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
Log.v(TAG, "new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.getVideoUuid());
String playingVideo = videoPlayerFragment.getVideoUuid();
if (TextUtils.isEmpty(playingVideo)) {
Log.v(TAG, "new intent no video currently playing");
videoPlayerFragment.start(videoUuid);
} else if (!playingVideo.equals(videoUuid)) {
Log.v(TAG, "new intent different video playing currently");
videoPlayerFragment.stopVideo();
videoPlayerFragment.start(videoUuid);
} else {
Log.v(TAG, "new intent same video playing currently");
}
// if we are in landscape set the video to fullscreen
int orientation = this.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true);
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
Log.v(TAG, "onConfigurationChanged()...");
super.onConfigurationChanged(newConfig);
// Checking the orientation changes of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true);
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
setOrientation(false);
}
}
private void setOrientation(Boolean isLandscape) {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
VideoMetaDataFragment videoMetaFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
assert videoPlayerFragment != null;
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) videoPlayerFragment.requireView().getLayoutParams();
params.width = FrameLayout.LayoutParams.MATCH_PARENT;
params.height = isLandscape ? FrameLayout.LayoutParams.MATCH_PARENT : (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 250, getResources().getDisplayMetrics());
videoPlayerFragment.requireView().setLayoutParams(params);
if (videoMetaFragment != null) {
FragmentTransaction transaction = fragmentManager.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
if (isLandscape) {
transaction.hide(videoMetaFragment);
} else {
transaction.show(videoMetaFragment);
}
transaction.commit();
}
videoPlayerFragment.setIsFullscreen(isLandscape);
if ( isLandscape ) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@Override
protected void onDestroy() {
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.destroyVideo();
super.onDestroy();
Log.v(TAG, "onDestroy...");
}
@Override
protected void onPause() {
super.onPause();
Log.v(TAG, "onPause()...");
}
@Override
protected void onResume() {
super.onResume();
Log.v(TAG, "onResume()...");
}
@Override
protected void onStop() {
super.onStop();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
videoPlayerFragment.stopVideo();
Log.v(TAG, "onStop()...");
}
@Override
protected void onStart() {
super.onStart();
Log.v(TAG, "onStart()...");
}
@SuppressLint("NewApi")
@Override
public void onUserLeaveHint() {
Log.v(TAG, "onUserLeaveHint()...");
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
assert videoPlayerFragment != null;
assert backgroundBehavior != null;
if ( videoMetaDataFragment.isLeaveAppExpected() )
{
super.onUserLeaveHint();
return;
}
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
Log.v(TAG, "stop the video");
videoPlayerFragment.pauseVideo();
stopService(new Intent(this, VideoPlayerService.class));
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
Log.v(TAG, "play the Audio");
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
Log.v(TAG, "play in floating video");
//canEnterPIPMode makes sure API level is high enough
if (canEnterPipMode(this)) {
Log.v(TAG, "enabling pip");
enterPipMode();
} else {
Log.v(TAG, "unable to use pip");
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback");
super.onBackPressed();
}
}
// @RequiresApi(api = Build.VERSION_CODES.O)
@SuppressLint("NewApi")
public void onBackPressed() {
Log.v(TAG, "onBackPressed()...");
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
assert videoPlayerFragment != null;
// copying Youtube behavior to have back button exit full screen.
if (videoPlayerFragment.getIsFullscreen()) {
Log.v(TAG, "exiting full screen");
videoPlayerFragment.fullScreenToggle();
return;
}
// pause video if pref is enabled
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
videoPlayerFragment.pauseVideo();
}
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
assert backgroundBehavior != null;
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
Log.v(TAG, "stop the video");
videoPlayerFragment.pauseVideo();
stopService(new Intent(this, VideoPlayerService.class));
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
Log.v(TAG, "play the Audio");
super.onBackPressed();
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
Log.v(TAG, "play in floating video");
//canEnterPIPMode makes sure API level is high enough
if (canEnterPipMode(this)) {
Log.v(TAG, "enabling pip");
enterPipMode();
//fixes problem where back press doesn't bring up video list after returning from PIP mode
Intent intentSettings = new Intent(this, VideoListActivity.class);
this.startActivity(intentSettings);
} else {
Log.v(TAG, "Unable to enter PIP mode");
super.onBackPressed();
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback");
super.onBackPressed();
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
public void enterPipMode() {
final FragmentManager fragmentManager = getSupportFragmentManager();
final VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById( R.id.video_player_fragment );
if ( videoPlayerFragment.getVideoAspectRatio() == 0 ) {
Log.i( TAG, "impossible to switch to pip" );
} else {
Rational rational = new Rational( (int) ( videoPlayerFragment.getVideoAspectRatio() * 100 ), 100 );
PictureInPictureParams mParams =
new PictureInPictureParams.Builder()
.setAspectRatio( rational )
// .setSourceRectHint(new Rect(0,500,400,600))
.build();
enterPictureInPictureMode( mParams );
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
FragmentManager fragmentManager = getSupportFragmentManager();
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
if (videoPlayerFragment != null) {
if (isInPictureInPictureMode) {
changedToPipMode();
Log.v(TAG, "switched to pip ");
videoPlayerFragment.useController(false);
} else {
changedToNormalMode();
Log.v(TAG, "switched to normal");
videoPlayerFragment.useController(true);
}
} else {
Log.e(TAG, "videoPlayerFragment is NULL");
}
}
}

View File

@ -0,0 +1,462 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity
import androidx.appcompat.app.AppCompatActivity
import android.annotation.SuppressLint
import net.schueller.peertube.fragment.VideoPlayerFragment
import net.schueller.peertube.R
import android.app.RemoteAction
import android.app.PendingIntent
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import android.app.PictureInPictureParams
import android.content.*
import android.content.res.Configuration
import android.graphics.drawable.Icon
import android.os.Bundle
import android.text.TextUtils
import net.schueller.peertube.fragment.VideoMetaDataFragment
import android.widget.RelativeLayout
import android.widget.FrameLayout
import android.util.TypedValue
import android.view.WindowManager
import net.schueller.peertube.service.VideoPlayerService
import net.schueller.peertube.helper.VideoHelper
import androidx.annotation.RequiresApi
import android.os.Build
import android.util.Log
import android.util.Rational
import androidx.fragment.app.Fragment
import net.schueller.peertube.fragment.VideoDescriptionFragment
import java.util.ArrayList
class VideoPlayActivity : CommonActivity() {
private var receiver: BroadcastReceiver? = null
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
@SuppressLint("NewApi")
fun makePipControls() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val actions = ArrayList<RemoteAction>()
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
var actionIntent = Intent(getString(R.string.app_background_audio))
var pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags)
@SuppressLint("NewApi", "LocalSuppress") var icon = Icon.createWithResource(
applicationContext, android.R.drawable.stat_sys_speakerphone
)
@SuppressLint("NewApi", "LocalSuppress") var remoteAction =
RemoteAction(icon!!, "close pip", "from pip window custom command", pendingIntent!!)
actions.add(remoteAction)
actionIntent = Intent(PlayerNotificationManager.ACTION_STOP)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop
)
remoteAction = RemoteAction(icon, "play", "stop the media", pendingIntent)
actions.add(remoteAction)
assert(videoPlayerFragment != null)
if (videoPlayerFragment!!.isPaused) {
Log.e(TAG, "setting actions with play button")
actionIntent = Intent(PlayerNotificationManager.ACTION_PLAY)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play
)
remoteAction = RemoteAction(icon, "play", "play the media", pendingIntent)
} else {
Log.e(TAG, "setting actions with pause button")
actionIntent = Intent(PlayerNotificationManager.ACTION_PAUSE)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, flags)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause
)
remoteAction = RemoteAction(icon, "pause", "pause the media", pendingIntent)
}
actions.add(remoteAction)
//add custom actions to pip window
val params = PictureInPictureParams.Builder()
.setActions(actions)
.build()
setPictureInPictureParams(params)
}
private fun changedToPipMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.showControls(false)
//create custom actions
makePipControls()
//setup receiver to handle customer actions
val filter = IntentFilter()
filter.addAction(PlayerNotificationManager.ACTION_STOP)
filter.addAction(PlayerNotificationManager.ACTION_PAUSE)
filter.addAction(PlayerNotificationManager.ACTION_PLAY)
filter.addAction(getString(R.string.app_background_audio))
receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action!!
if (action == PlayerNotificationManager.ACTION_PAUSE) {
videoPlayerFragment.pauseVideo()
makePipControls()
}
if (action == PlayerNotificationManager.ACTION_PLAY) {
videoPlayerFragment.unPauseVideo()
makePipControls()
}
if (action == getString(R.string.app_background_audio)) {
unregisterReceiver(receiver)
finish()
}
if (action == PlayerNotificationManager.ACTION_STOP) {
unregisterReceiver(receiver)
finishAndRemoveTask()
}
}
}
registerReceiver(receiver, filter)
Log.v(TAG, "switched to pip ")
floatMode = true
videoPlayerFragment.showControls(false)
}
private fun changedToNormalMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.showControls(true)
if (receiver != null) {
unregisterReceiver(receiver)
}
Log.v(TAG, "switched to normal")
floatMode = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set theme
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
setTheme(
resources.getIdentifier(
sharedPref.getString(
getString(R.string.pref_theme_key),
getString(R.string.app_default_theme)
),
"style",
packageName
)
)
setContentView(R.layout.activity_video_play)
// get video ID
val intent = intent
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
val playingVideo = videoPlayerFragment.videoUuid
Log.v(TAG, "oncreate click: $videoUuid is trying to replace: $playingVideo")
when {
TextUtils.isEmpty(playingVideo) -> {
Log.v(TAG, "oncreate no video currently playing")
videoPlayerFragment.start(videoUuid)
}
playingVideo != videoUuid -> {
Log.v(TAG, "oncreate different video playing currently")
videoPlayerFragment.stopVideo()
videoPlayerFragment.start(videoUuid)
}
else -> {
Log.v(TAG, "oncreate same video playing currently")
}
}
// if we are in landscape set the video to fullscreen
val orientation = this.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
Log.v(
TAG,
"new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.videoUuid
)
val playingVideo = videoPlayerFragment.videoUuid
when {
TextUtils.isEmpty(playingVideo) -> {
Log.v(TAG, "new intent no video currently playing")
videoPlayerFragment.start(videoUuid)
}
playingVideo != videoUuid -> {
Log.v(TAG, "new intent different video playing currently")
videoPlayerFragment.stopVideo()
videoPlayerFragment.start(videoUuid)
}
else -> {
Log.v(TAG, "new intent same video playing currently")
}
}
// if we are in landscape set the video to fullscreen
val orientation = this.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
Log.v(TAG, "onConfigurationChanged()...")
super.onConfigurationChanged(newConfig)
// Checking the orientation changes of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
setOrientation(false)
}
}
private fun setOrientation(isLandscape: Boolean) {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val videoMetaFragment =
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
assert(videoPlayerFragment != null)
val params = videoPlayerFragment!!.requireView().layoutParams as RelativeLayout.LayoutParams
params.width = FrameLayout.LayoutParams.MATCH_PARENT
params.height =
if (isLandscape) FrameLayout.LayoutParams.MATCH_PARENT else TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
250f,
resources.displayMetrics
)
.toInt()
videoPlayerFragment.requireView().layoutParams = params
if (videoMetaFragment != null) {
val transaction = fragmentManager.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
if (isLandscape) {
transaction.hide(videoMetaFragment)
} else {
transaction.show(videoMetaFragment)
}
transaction.commit()
}
videoPlayerFragment.setIsFullscreen(isLandscape)
if (isLandscape) {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
}
override fun onDestroy() {
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.destroyVideo()
super.onDestroy()
Log.v(TAG, "onDestroy...")
}
override fun onPause() {
super.onPause()
Log.v(TAG, "onPause()...")
}
override fun onResume() {
super.onResume()
Log.v(TAG, "onResume()...")
}
override fun onStop() {
super.onStop()
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.stopVideo()
// TODO: doesn't remove fragment??
val fragment: Fragment? = supportFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
if (fragment != null) {
Log.v(TAG, "remove VideoDescriptionFragment")
supportFragmentManager.beginTransaction().remove(fragment).commit()
}
Log.v(TAG, "onStop()...")
}
override fun onStart() {
super.onStart()
Log.v(TAG, "onStart()...")
}
@SuppressLint("NewApi")
public override fun onUserLeaveHint() {
Log.v(TAG, "onUserLeaveHint()...")
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val videoMetaDataFragment =
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
val backgroundBehavior = sharedPref.getString(
getString(R.string.pref_background_behavior_key),
getString(R.string.pref_background_stop_key)
)
assert(videoPlayerFragment != null)
assert(backgroundBehavior != null)
if (videoMetaDataFragment!!.isLeaveAppExpected) {
super.onUserLeaveHint()
return
}
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
Log.v(TAG, "stop the video")
videoPlayerFragment!!.pauseVideo()
stopService(Intent(this, VideoPlayerService::class.java))
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
Log.v(TAG, "play the Audio")
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
Log.v(TAG, "play in floating video")
//canEnterPIPMode makes sure API level is high enough
if (VideoHelper.canEnterPipMode(this)) {
Log.v(TAG, "enabling pip")
enterPipMode()
} else {
Log.v(TAG, "unable to use pip")
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback")
super.onBackPressed()
}
}
// @RequiresApi(api = Build.VERSION_CODES.O)
@SuppressLint("NewApi")
override fun onBackPressed() {
Log.v(TAG, "onBackPressed()...")
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
// copying Youtube behavior to have back button exit full screen.
if (videoPlayerFragment.getIsFullscreen()) {
Log.v(TAG, "exiting full screen")
videoPlayerFragment.fullScreenToggle()
return
}
// pause video if pref is enabled
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
videoPlayerFragment.pauseVideo()
}
val backgroundBehavior = sharedPref.getString(
getString(R.string.pref_background_behavior_key),
getString(R.string.pref_background_stop_key)
)!!
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
Log.v(TAG, "stop the video")
videoPlayerFragment.pauseVideo()
stopService(Intent(this, VideoPlayerService::class.java))
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
Log.v(TAG, "play the Audio")
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
Log.v(TAG, "play in floating video")
//canEnterPIPMode makes sure API level is high enough
if (VideoHelper.canEnterPipMode(this)) {
Log.v(TAG, "enabling pip")
enterPipMode()
//fixes problem where back press doesn't bring up video list after returning from PIP mode
val intentSettings = Intent(this, VideoListActivity::class.java)
this.startActivity(intentSettings)
} else {
Log.v(TAG, "Unable to enter PIP mode")
super.onBackPressed()
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback")
super.onBackPressed()
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
fun enterPipMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
if (videoPlayerFragment!!.videoAspectRatio == 0.toFloat()) {
Log.i(TAG, "impossible to switch to pip")
} else {
val rational = Rational((videoPlayerFragment.videoAspectRatio * 100).toInt(), 100)
val mParams = PictureInPictureParams.Builder()
.setAspectRatio(rational) // .setSourceRectHint(new Rect(0,500,400,600))
.build()
enterPictureInPictureMode(mParams)
}
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
if (videoPlayerFragment != null) {
if (isInPictureInPictureMode) {
changedToPipMode()
Log.v(TAG, "switched to pip ")
videoPlayerFragment.useController(false)
} else {
changedToNormalMode()
Log.v(TAG, "switched to normal")
videoPlayerFragment.useController(true)
}
} else {
Log.e(TAG, "videoPlayerFragment is NULL")
}
}
companion object {
private const val TAG = "VideoPlayActivity"
var floatMode = false
private const val REQUEST_CODE = 101
}
}

View File

@ -94,15 +94,15 @@ public class ChannelAdapter extends RecyclerView.Adapter<ChannelAdapter.AccountV
holder.videoMeta.setText(
MetaDataHelper.getMetaString(videoList.get(position).getCreatedAt(),
videoList.get(position).getViews(),
context
context,
false
)
);
// set owner
holder.videoOwner.setText(
MetaDataHelper.getOwnerString(videoList.get(position).getAccount().getName(),
videoList.get(position).getAccount().getHost(),
context
MetaDataHelper.getOwnerString(videoList.get(position).getAccount(),
context, true
)
);

View File

@ -4,19 +4,14 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
import net.schueller.peertube.databinding.ItemChannelTitleBinding
import net.schueller.peertube.databinding.ItemTagTitleBinding
import net.schueller.peertube.databinding.RowVideoListBinding
import net.schueller.peertube.model.Category
import net.schueller.peertube.model.Channel
import net.schueller.peertube.model.TagVideo
import net.schueller.peertube.model.Video
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import net.schueller.peertube.model.ui.VideoMetaViewItem
import java.util.ArrayList
class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
class MultiViewRecycleViewAdapter(private val videoMetaDataFragment: VideoMetaDataFragment? = null) : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
private var items = ArrayList<OverviewRecycleViewItem>()
set(value) {
@ -34,6 +29,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
notifyDataSetChanged()
}
fun setVideoMeta(videoMetaViewItem: VideoMetaViewItem) {
items.add(videoMetaViewItem)
notifyDataSetChanged()
}
fun setCategoryTitle(category: Category) {
items.add(category)
notifyDataSetChanged()
@ -49,6 +49,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
notifyDataSetChanged()
}
fun setVideoComment(commentThread: CommentThread) {
items.add(commentThread)
notifyDataSetChanged()
}
fun clearData() {
items.clear()
notifyDataSetChanged()
@ -83,6 +88,21 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
false
)
)
R.layout.item_video_meta -> MultiViewRecyclerViewHolder.VideoMetaViewHolder(
ItemVideoMetaBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
videoMetaDataFragment
)
R.layout.item_video_comments_overview -> MultiViewRecyclerViewHolder.VideoCommentsViewHolder(
ItemVideoCommentsOverviewBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException("Invalid ViewType Provided")
}
}
@ -93,6 +113,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
is MultiViewRecyclerViewHolder.CategoryViewHolder -> holder.bind(items[position] as Category)
is MultiViewRecyclerViewHolder.ChannelViewHolder -> holder.bind(items[position] as Channel)
is MultiViewRecyclerViewHolder.TagViewHolder -> holder.bind(items[position] as TagVideo)
is MultiViewRecyclerViewHolder.VideoMetaViewHolder -> holder.bind(items[position] as VideoMetaViewItem)
is MultiViewRecyclerViewHolder.VideoCommentsViewHolder -> holder.bind(items[position] as CommentThread)
}
}
@ -104,6 +126,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
is Channel -> R.layout.item_channel_title
is Category -> R.layout.item_category_title
is TagVideo -> R.layout.item_tag_title
is VideoMetaViewItem -> R.layout.item_video_meta
is CommentThread -> R.layout.item_video_comments_overview
else -> { return 0}
}
}

View File

@ -16,48 +16,320 @@
*/
package net.schueller.peertube.adapter
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.View.GONE
import android.widget.Toast
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.gson.JsonObject
import com.mikepenz.iconics.Iconics.Builder
import com.squareup.picasso.Picasso
import net.schueller.peertube.R
import net.schueller.peertube.R.color
import net.schueller.peertube.R.string
import net.schueller.peertube.R.*
import net.schueller.peertube.activity.AccountActivity
import net.schueller.peertube.activity.VideoListActivity
import net.schueller.peertube.activity.VideoListActivity.Companion
import net.schueller.peertube.activity.VideoPlayActivity
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
import net.schueller.peertube.databinding.ItemChannelTitleBinding
import net.schueller.peertube.databinding.RowVideoListBinding
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.MetaDataHelper.getCreatorAvatar
import net.schueller.peertube.helper.MetaDataHelper.getCreatorString
import net.schueller.peertube.helper.MetaDataHelper.getDuration
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import net.schueller.peertube.model.Avatar
import net.schueller.peertube.model.Category
import net.schueller.peertube.model.Channel
import net.schueller.peertube.model.Video
import com.mikepenz.iconics.Iconics.Builder
import net.schueller.peertube.R.id
import net.schueller.peertube.R.menu
import net.schueller.peertube.databinding.ItemTagTitleBinding
import net.schueller.peertube.helper.MetaDataHelper.isChannel
import net.schueller.peertube.intents.Intents
import net.schueller.peertube.model.TagVideo
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetUserService
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.network.Session
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding): MultiViewRecyclerViewHolder(binding) {
var videoRating: Rating? = null
var isLeaveAppExpected = false
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(category: Category) {
binding.textViewTitle.text = category.label
}
}
class ChannelViewHolder(private val binding: ItemChannelTitleBinding): MultiViewRecyclerViewHolder(binding) {
class VideoCommentsViewHolder(private val binding: ItemVideoCommentsOverviewBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(commentThread: CommentThread) {
binding.videoCommentsTotalCount.text = commentThread.total.toString()
if (commentThread.comments.isNotEmpty()) {
val highlightedComment: Comment = commentThread.comments[0]
// owner / creator Avatar
val avatar = highlightedComment.account.avatar
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(binding.videoHighlightedAvatar.context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.videoHighlightedAvatar)
}
binding.videoHighlightedComment.text = highlightedComment.text
}
}
}
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?) : MultiViewRecyclerViewHolder(binding) {
fun bind(videoMetaViewItem: VideoMetaViewItem) {
val video = videoMetaViewItem.video
if (video != null && videoMetaDataFragment != null) {
val context = binding.avatar.context
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
val userService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetUserService::class.java
)
// Title
binding.videoName.text = video.name
binding.videoOpenDescription.setOnClickListener {
videoMetaDataFragment.showDescriptionFragment(video)
}
// Thumbs up
binding.videoThumbsUpWrapper.setOnClickListener {
rateVideo(true, video, context, binding)
}
// Thumbs Down
binding.videoThumbsDownWrapper.setOnClickListener {
rateVideo(false, video, context, binding)
}
// Add to playlist
binding.videoAddToPlaylistWrapper.setOnClickListener {
videoMetaDataFragment.saveToPlaylist(video)
Toast.makeText(context, context.getString(string.saved_to_playlist), Toast.LENGTH_SHORT).show()
}
binding.videoBlockWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
binding.videoFlagWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
// video rating
videoRating = Rating()
videoRating!!.rating = RATING_NONE // default
updateVideoRating(video, binding)
// Retrieve which rating the user gave to this video
if (Session.getInstance().isLoggedIn) {
val call = videoDataService.getVideoRating(video.id)
call.enqueue(object : Callback<Rating?> {
override fun onResponse(call: Call<Rating?>, response: Response<Rating?>) {
videoRating = response.body()
updateVideoRating(video, binding)
}
override fun onFailure(call: Call<Rating?>, t: Throwable) {
// Do nothing.
}
})
}
// Share
binding.videoShare.setOnClickListener {
isLeaveAppExpected = true
Intents.Share(context, video)
}
// hide download if not supported by video
if (video.downloadEnabled) {
binding.videoDownloadWrapper.setOnClickListener {
Intents.Download(context, video)
}
} else {
binding.videoDownloadWrapper.visibility = GONE
}
// created at / views
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context,
true
)
// owner / creator
val displayNameAndHost = getOwnerString(video.account, context)
if (isChannel(video)) {
binding.videoBy.text = context.resources.getString(string.video_by_line, displayNameAndHost)
} else {
binding.videoBy.visibility = GONE
}
binding.videoOwner.text = getCreatorString(video, context)
// owner / creator Avatar
val avatar = getCreatorAvatar(video, context)
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// videoOwnerSubscribers
binding.videoOwnerSubscribers.text = context.resources.getQuantityString(R.plurals.video_channel_subscribers, video.channel.followersCount, video.channel.followersCount)
// video owner click
binding.videoCreatorInfo.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// avatar click
binding.avatar.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(Companion.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// get subscription status
var isSubscribed = false
if (Session.getInstance().isLoggedIn) {
val subChannel = video.channel.name + "@" + video.channel.host
val call = userService.subscriptionsExist(subChannel)
call.enqueue(object : Callback<JsonObject> {
override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
if (response.isSuccessful) {
// {"video.channel.name + "@" + video.channel.host":true}
if (response.body()?.get(video.channel.name + "@" + video.channel.host)!!.asBoolean) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
isSubscribed = true
} else {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
}
}
}
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
// Do nothing.
}
})
}
// TODO: update subscriber count
binding.videoOwnerSubscribeButton.setOnClickListener {
if (Session.getInstance().isLoggedIn) {
if (!isSubscribed) {
val payload = video.channel.name + "@" + video.channel.host
val body = "{\"uri\":\"$payload\"}".toRequestBody("application/json".toMediaType())
val call = userService.subscribe(body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
isSubscribed = true
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
} else {
AlertDialog.Builder(context)
.setTitle(context.getString(string.video_sub_del_alert_title))
.setMessage(context.getString(string.video_sub_del_alert_msg))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// Yes
val payload = video.channel.name + "@" + video.channel.host
val call = userService.unsubscribe(payload)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
isSubscribed = false
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
// No
}
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
class ChannelViewHolder(private val binding: ItemChannelTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(channel: Channel) {
val context = binding.avatar.context
@ -68,22 +340,22 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.placeholder(R.drawable.test_image)
.into(binding.avatar)
.load(baseUrl + avatarPath)
.placeholder(R.drawable.test_image)
.into(binding.avatar)
}
binding.textViewTitle.text = channel.displayName
}
}
class TagViewHolder(private val binding: ItemTagTitleBinding): MultiViewRecyclerViewHolder(binding) {
class TagViewHolder(private val binding: ItemTagTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(tag: TagVideo) {
binding.textViewTitle.text = tag.tag
}
}
class VideoViewHolder(private val binding: RowVideoListBinding): MultiViewRecyclerViewHolder(binding) {
class VideoViewHolder(private val binding: RowVideoListBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(video: Video) {
@ -92,18 +364,18 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
// Temp Loading Image
Picasso.get()
.load(baseUrl + video.previewPath)
.placeholder(R.drawable.test_image)
.error(R.drawable.test_image)
.into(binding.thumb)
.load(baseUrl + video.previewPath)
.placeholder(R.drawable.test_image)
.error(R.drawable.test_image)
.into(binding.thumb)
// Avatar
val avatar: Avatar? = video.account.avatar
val avatar = getCreatorAvatar(video, context)
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// set Name
binding.slRowName.text = video.name
@ -119,18 +391,14 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
// set age and view count
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context
video.createdAt,
video.views,
context
)
// set owner
val displayNameAndHost = getOwnerString(
video.account.name,
video.account.host,
context
)
binding.videoOwner.text = displayNameAndHost
val displayNameAndHost = getOwnerString(video.account, context, true)
binding.videoOwner.text = getCreatorString(video, context, true)
// video owner click
binding.videoOwner.setOnClickListener {
@ -159,8 +427,8 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
binding.moreButton.setOnClickListener { v: View? ->
val popup = PopupMenu(
context,
v!!
context,
v!!
)
popup.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
@ -178,4 +446,117 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
}
fun updateVideoRating(video: Video?, binding: ItemVideoMetaBinding) {
when (videoRating!!.rating) {
RATING_NONE -> {
Log.v("MWCVH", "RATING_NONE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_LIKE -> {
Log.v("MWCVH", "RATING_LIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up_filled)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_DISLIKE -> {
Log.v("MWCVH", "RATING_DISLIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down_filled)
}
}
// Update the texts
binding.videoThumbsUpTotal.text = video?.likes.toString()
binding.videoThumbsDownTotal.text = video?.dislikes.toString()
}
/**
* TODO: move this out and get update when rating changes
*/
fun rateVideo(like: Boolean, video: Video, context: Context, binding: ItemVideoMetaBinding) {
if (Session.getInstance().isLoggedIn) {
val ratePayload: String = when (videoRating!!.rating) {
RATING_LIKE -> if (like) RATING_NONE else RATING_DISLIKE
RATING_DISLIKE -> if (like) RATING_LIKE else RATING_NONE
RATING_NONE -> if (like) RATING_LIKE else RATING_DISLIKE
else -> if (like) RATING_LIKE else RATING_DISLIKE
}
val body = "{\"rating\":\"$ratePayload\"}".toRequestBody("application/json".toMediaType())
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL, APIUrlHelper.useInsecureConnection(
context
)
).create(
GetVideoDataService::class.java
)
val call = videoDataService.rateVideo(video.id, body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
// if 20x, update likes/dislikes
if (response.isSuccessful) {
val previousRating = videoRating!!.rating
// Update the likes/dislikes count of the video, if needed.
// This is only a visual trick, as the actual like/dislike count has
// already been modified on the PeerTube instance.
if (previousRating != ratePayload) {
when (previousRating) {
RATING_NONE -> if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
} else {
video.dislikes = video.dislikes + 1
}
RATING_LIKE -> {
video.likes = video.likes - 1
if (ratePayload == RATING_DISLIKE) {
video.dislikes = video.dislikes + 1
}
}
RATING_DISLIKE -> {
video.dislikes = video.dislikes - 1
if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
}
}
}
}
videoRating!!.rating = ratePayload
updateVideoRating(video, binding)
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Toast.makeText(
context,
context.getString(string.video_rating_failed),
Toast.LENGTH_SHORT
).show()
}
})
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
companion object {
private const val RATING_NONE = "none"
private const val RATING_LIKE = "like"
private const val RATING_DISLIKE = "dislike"
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.database.Video
import net.schueller.peertube.databinding.RowPlaylistBinding
class PlaylistAdapter(private val mVideos: MutableList<Video>, private val onClick: (Video) -> Unit) : RecyclerView.Adapter<PlaylistAdapter.VideoViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder {
val binding = RowPlaylistBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return VideoViewHolder(binding)
}
override fun onBindViewHolder(holder: VideoViewHolder, position: Int) {
holder.bind(mVideos[position])
}
fun setVideos(videos: List<Video>) {
mVideos.clear()
mVideos.addAll(videos)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return mVideos.size
}
inner class VideoViewHolder(private val binding: RowPlaylistBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(video: Video) {
binding.videoName.text = video.videoName
binding.videoDescription.text = video.videoDescription
binding.root.setOnClickListener { onClick(video) }
}
}
fun getVideoAtPosition(position: Int): Video {
return mVideos[position]
}
}

View File

@ -19,7 +19,9 @@ package net.schueller.peertube.database;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {Server.class}, version = 1)
@Database(entities = {Server.class, Video.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract ServerDao serverDao();
public abstract VideoDao videoDao();
}

View File

@ -20,7 +20,7 @@ import android.os.Parcelable
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.Entity
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "server_table")

View File

@ -17,7 +17,6 @@
package net.schueller.peertube.database
import android.app.Application
import android.os.AsyncTask
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@ -0,0 +1,25 @@
package net.schueller.peertube.database
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@Entity(tableName = "watch_later")
data class Video(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
@ColumnInfo(name = "video_uuid")
var videoUUID: String,
@ColumnInfo(name = "video_name")
var videoName: String,
@ColumnInfo(name = "video_description")
var videoDescription: String?
) : Parcelable

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface VideoDao {
@Insert
suspend fun insert(video: Video)
@Update
suspend fun update(video: Video)
@Query("DELETE FROM watch_later")
suspend fun deleteAll()
@Delete
suspend fun delete(video: Video)
@get:Query("SELECT * from watch_later ORDER BY video_name DESC")
val allVideos: LiveData<List<Video>>
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import android.app.Application
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class VideoRepository(application: Application) {
private val mVideoDao: VideoDao
val allVideos: LiveData<List<Video>>
get() = mVideoDao.allVideos
init {
val db = VideoRoomDatabase.getDatabase(application)
mVideoDao = db.videoDao()
}
suspend fun update(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.update(video)
}
suspend fun insert(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.insert(video)
}
suspend fun delete(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.delete(video)
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {Video.class}, version = 1, exportSchema = false)
public abstract class VideoRoomDatabase extends RoomDatabase {
public abstract VideoDao videoDao();
private static volatile VideoRoomDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
public static VideoRoomDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (VideoRoomDatabase.class) {
if (INSTANCE == null) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
VideoRoomDatabase.class, "playlist_database")
.build();
}
}
}
}
return INSTANCE;
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class VideoViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: VideoRepository = VideoRepository(application)
val allVideos: LiveData<List<Video>> = mRepository.allVideos
fun insert(video: Video) {
viewModelScope.launch {
mRepository.insert(video)
}
}
fun update(video: Video) {
viewModelScope.launch {
mRepository.update(video)
}
}
fun delete(video: Video) {
viewModelScope.launch {
mRepository.delete(video)
}
}
}

View File

@ -19,12 +19,13 @@ package net.schueller.peertube.fragment
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import net.schueller.peertube.R
@ -52,7 +53,7 @@ class AddServerFragment : Fragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
mBinding = FragmentAddServerBinding.inflate(inflater, container, false)
return mBinding.root
}
@ -115,7 +116,7 @@ class AddServerFragment : Fragment() {
mBinding.pickServerUrl.setOnClickListener {
val intentServer = Intent(activity, SearchServerActivity::class.java)
this.startActivityForResult(intentServer, PICK_SERVER)
openActivityForResult(intentServer)
}
}
@ -132,35 +133,24 @@ class AddServerFragment : Fragment() {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != PICK_SERVER) {
return
}
if (resultCode != Activity.RESULT_OK) {
return
}
val serverUrlTest = data?.getStringExtra("serverUrl")
//Log.d(TAG, "serverUrl " + serverUrlTest);
mBinding.serverUrl.setText(serverUrlTest)
mBinding.serverLabel.apply {
if (text.toString().isBlank()) {
setText(data?.getStringExtra("serverName"))
private var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
val serverUrlTest = intent?.getStringExtra("serverUrl")
mBinding.serverUrl.setText(serverUrlTest)
mBinding.serverLabel.apply {
if (text.toString().isBlank()) {
setText(intent?.getStringExtra("serverName"))
}
}
}
}
private fun openActivityForResult(intent: Intent) {
resultLauncher.launch(intent)
}
companion object {
private const val TAG = "AddServerFragment"
private const val PICK_SERVER = 1
private const val SERVER_ARG = "server"
fun newInstance(server: Server) = AddServerFragment().apply {

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ImageButton
import net.schueller.peertube.R
import android.widget.TextView
import androidx.fragment.app.Fragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.Description
import net.schueller.peertube.model.Video
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class VideoDescriptionFragment : Fragment () {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(
R.layout.fragment_video_description, container,
false
)
val video = video
if (video != null) {
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
// description, get extended if available
val videoDescription = view.findViewById<TextView>(R.id.description)
val shortDescription = video.description
if (shortDescription != null && shortDescription.length > 237) {
val call = videoDataService.getVideoFullDescription(video.uuid);
call.enqueue(object : Callback<Description?> {
override fun onResponse(call: Call<Description?>, response: Response<Description?>) {
val videoFullDescription: Description? = response.body();
videoDescription.text = videoFullDescription?.description
}
override fun onFailure(call: Call<Description?>, t: Throwable) {
Log.wtf(TAG, t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(activity, t)
}
})
}
videoDescription.text = shortDescription;
val closeButton = view.findViewById<ImageButton>(R.id.video_description_close_button)
closeButton.setOnClickListener {
videoMetaDataFragment!!.hideDescriptionFragment()
}
// video privacy
val videoPrivacy = view.findViewById<TextView>(R.id.video_privacy);
videoPrivacy.text = video!!.privacy.label;
// video category
val videoCategory = view.findViewById<TextView>(R.id.video_category);
videoCategory.text = video!!.category.label;
// video privacy
val videoLicense = view.findViewById<TextView>(R.id.video_license);
videoLicense.text = video!!.licence.label;
// video language
val videoLanguage = view.findViewById<TextView>(R.id.video_language);
videoLanguage.text = video!!.language.label;
// video privacy
val videoTags = view.findViewById<TextView>(R.id.video_tags);
videoTags.text = android.text.TextUtils.join(", ", video!!.tags);
}
return view
}
companion object {
private var video: Video? = null
private var videoMetaDataFragment: VideoMetaDataFragment? = null
const val TAG = "VideoDescr"
fun newInstance(mVideo: Video?, mVideoMetaDataFragment: VideoMetaDataFragment): VideoDescriptionFragment {
video = mVideo
videoMetaDataFragment = mVideoMetaDataFragment
return VideoDescriptionFragment()
}
}
}

View File

@ -1,408 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.mikepenz.iconics.Iconics;
import com.squareup.picasso.Picasso;
import java.util.Objects;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.intents.Intents;
import net.schueller.peertube.model.Account;
import net.schueller.peertube.model.Avatar;
import net.schueller.peertube.model.Description;
import net.schueller.peertube.model.Rating;
import net.schueller.peertube.model.Video;
import net.schueller.peertube.network.GetVideoDataService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.network.Session;
import net.schueller.peertube.service.VideoPlayerService;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class VideoMetaDataFragment extends Fragment {
private static final String TAG = "VideoMetaDataFragment";
private static final String RATING_NONE = "none";
private static final String RATING_LIKE = "like";
private static final String RATING_DISLIKE = "dislike";
private Rating videoRating;
private ColorStateList defaultTextColor;
private boolean leaveAppExpected = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_video_meta, container, false);
}
@Override
public void onPause()
{
leaveAppExpected = false;
super.onPause();
}
public boolean isLeaveAppExpected()
{
return leaveAppExpected;
}
public void updateVideoMeta(Video video, VideoPlayerService mService) {
Context context = getContext();
Activity activity = getActivity();
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
// Thumbs up
Button thumbsUpButton = activity.findViewById(R.id.video_thumbs_up);
defaultTextColor = thumbsUpButton.getTextColors();
thumbsUpButton.setText(R.string.video_thumbs_up_icon);
new Iconics.Builder().on(thumbsUpButton).build();
thumbsUpButton.setOnClickListener(v -> {
rateVideo(true, video);
});
// Thumbs Down
Button thumbsDownButton = activity.findViewById(R.id.video_thumbs_down);
thumbsDownButton.setText(R.string.video_thumbs_down_icon);
new Iconics.Builder().on(thumbsDownButton).build();
thumbsDownButton.setOnClickListener(v -> {
rateVideo(false, video);
});
// video rating
videoRating = new Rating();
videoRating.setRating(RATING_NONE); // default
updateVideoRating(video);
// Retrieve which rating the user gave to this video
if (Session.getInstance().isLoggedIn()) {
Call<Rating> call = videoDataService.getVideoRating(video.getId());
call.enqueue(new Callback<Rating>() {
@Override
public void onResponse(Call<Rating> call, Response<Rating> response) {
videoRating = response.body();
updateVideoRating(video);
}
@Override
public void onFailure(Call<Rating> call, Throwable t) {
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
// Do nothing.
}
});
}
// Share
Button videoShareButton = activity.findViewById(R.id.video_share);
videoShareButton.setText(R.string.video_share_icon);
new Iconics.Builder().on(videoShareButton).build();
videoShareButton.setOnClickListener(v ->
{
leaveAppExpected = true;
Intents.Share( context, video );
} );
// Download
Button videoDownloadButton = activity.findViewById(R.id.video_download);
videoDownloadButton.setText(R.string.video_download_icon);
new Iconics.Builder().on(videoDownloadButton).build();
videoDownloadButton.setOnClickListener(v -> {
// get permission to store file
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
leaveAppExpected = true;
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
Intents.Download(context, video);
} else {
Toast.makeText(context, getString(R.string.video_download_permission_error), Toast.LENGTH_LONG).show();
}
} else {
Intents.Download(context, video);
}
});
Account account = video.getAccount();
// owner / creator Avatar
Avatar avatar = account.getAvatar();
if (avatar != null) {
ImageView avatarView = activity.findViewById(R.id.avatar);
String baseUrl = APIUrlHelper.getUrl(context);
String avatarPath = avatar.getPath();
Picasso.get()
.load(baseUrl + avatarPath)
.into(avatarView);
}
// title / name
TextView videoName = activity.findViewById(R.id.sl_row_name);
videoName.setText(video.getName());
// created at / views
TextView videoMeta = activity.findViewById(R.id.videoMeta);
videoMeta.setText(
MetaDataHelper.getMetaString(
video.getCreatedAt(),
video.getViews(),
context
)
);
// owner / creator
TextView videoOwner = activity.findViewById(R.id.videoOwner);
videoOwner.setText(
MetaDataHelper.getOwnerString(video.getAccount().getName(),
video.getAccount().getHost(),
context
)
);
// description
TextView videoDescription = activity.findViewById(R.id.description);
String shortDescription = video.getDescription();
if (shortDescription != null && Objects.requireNonNull(shortDescription).length() > 237) {
shortDescription += "\n" + getString(R.string.video_description_read_more);
videoDescription.setOnClickListener(v -> {
Call<Description> call = videoDataService.getVideoFullDescription(video.getUuid());
call.enqueue(new Callback<Description>() {
@Override
public void onResponse(Call<Description> call, Response<Description> response) {
if (response.isSuccessful() && response.body() != null) {
new Description();
Description videoFullDescription;
videoFullDescription = response.body();
videoDescription.setText(videoFullDescription.getDescription());
}
}
@Override
public void onFailure(Call<Description> call, Throwable t) {
Toast.makeText(getContext(), getString(R.string.video_get_full_description_failed), Toast.LENGTH_SHORT).show();
}
});
});
}
videoDescription.setText(shortDescription);
// video privacy
TextView videoPrivacy = activity.findViewById(R.id.video_privacy);
videoPrivacy.setText(video.getPrivacy().getLabel());
// video category
TextView videoCategory = activity.findViewById(R.id.video_category);
videoCategory.setText(video.getCategory().getLabel());
// video privacy
TextView videoLicense = activity.findViewById(R.id.video_license);
videoLicense.setText(video.getLicence().getLabel());
// video language
TextView videoLanguage = activity.findViewById(R.id.video_language);
videoLanguage.setText(video.getLanguage().getLabel());
// video privacy
TextView videoTags = activity.findViewById(R.id.video_tags);
videoTags.setText(android.text.TextUtils.join(", ", video.getTags()));
// more button
TextView moreButton = activity.findViewById(R.id.moreButton);
moreButton.setText(R.string.video_more_icon);
new Iconics.Builder().on(moreButton).build();
moreButton.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(context, v);
popup.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.video_more_report:
Log.v(TAG, "Report");
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
return true;
case R.id.video_more_blacklist:
Log.v(TAG, "Blacklist");
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
return true;
default:
return false;
}
});
popup.inflate(R.menu.menu_video_more);
popup.show();
});
// video player options
TextView videoOptions = activity.findViewById(R.id.exo_more);
videoOptions.setText(R.string.video_more_icon);
new Iconics.Builder().on(videoOptions).build();
videoOptions.setOnClickListener(v -> {
VideoOptionsFragment videoOptionsFragment =
VideoOptionsFragment.newInstance(mService, video.getFiles());
videoOptionsFragment.show(getActivity().getSupportFragmentManager(),
VideoOptionsFragment.TAG);
});
}
void updateVideoRating(Video video) {
Button thumbsUpButton = getActivity().findViewById(R.id.video_thumbs_up);
Button thumbsDownButton = getActivity().findViewById(R.id.video_thumbs_down);
TypedValue typedValue = new TypedValue();
TypedArray a = getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorPrimary});
int accentColor = a.getColor(0, 0);
// Change the color of the thumbs
switch (videoRating.getRating()) {
case RATING_NONE:
thumbsUpButton.setTextColor(defaultTextColor);
thumbsDownButton.setTextColor(defaultTextColor);
break;
case RATING_LIKE:
thumbsUpButton.setTextColor(accentColor);
thumbsDownButton.setTextColor(defaultTextColor);
break;
case RATING_DISLIKE:
thumbsUpButton.setTextColor(defaultTextColor);
thumbsDownButton.setTextColor(accentColor);
break;
}
// Update the texts
TextView thumbsDownTotal = getActivity().findViewById(R.id.video_thumbs_down_total);
TextView thumbsUpTotal = getActivity().findViewById(R.id.video_thumbs_up_total);
thumbsUpTotal.setText(String.valueOf(video.getLikes()));
thumbsDownTotal.setText(String.valueOf(video.getDislikes()));
a.recycle();
}
void rateVideo(Boolean like, Video video) {
if (Session.getInstance().isLoggedIn()) {
final String ratePayload;
switch (videoRating.getRating()) {
case RATING_LIKE:
ratePayload = like ? RATING_NONE : RATING_DISLIKE;
break;
case RATING_DISLIKE:
ratePayload = like ? RATING_LIKE : RATING_NONE;
break;
case RATING_NONE:
default:
ratePayload = like ? RATING_LIKE : RATING_DISLIKE;
break;
}
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"), "{\"rating\":\"" + ratePayload + "\"}");
String apiBaseURL = APIUrlHelper.getUrlWithVersion(getContext());
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(getContext())).create(GetVideoDataService.class);
Call<ResponseBody> call = videoDataService.rateVideo(video.getId(), body);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//Log.v(TAG, response.toString());
// if 20x, update likes/dislikes
if (response.isSuccessful()) {
String previousRating = videoRating.getRating();
// Update the likes/dislikes count of the video, if needed.
// This is only a visual trick, as the actual like/dislike count has
// already been modified on the PeerTube instance.
if (!previousRating.equals(ratePayload)) {
switch (previousRating) {
case RATING_NONE:
if (ratePayload.equals(RATING_LIKE)) {
video.setLikes(video.getLikes() + 1);
} else {
video.setDislikes(video.getDislikes() + 1);
}
break;
case RATING_LIKE:
video.setLikes(video.getLikes() - 1);
if (ratePayload.equals(RATING_DISLIKE)) {
video.setDislikes(video.getDislikes() + 1);
}
break;
case RATING_DISLIKE:
video.setDislikes(video.getDislikes() - 1);
if (ratePayload.equals(RATING_LIKE)) {
video.setLikes(video.getLikes() + 1);
}
break;
}
}
videoRating.setRating(ratePayload);
updateVideoRating(video);
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Toast.makeText(getContext(), getString(R.string.video_rating_failed), Toast.LENGTH_SHORT).show();
}
});
} else {
Toast.makeText(getContext(), getString(R.string.video_login_required_for_service), Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -0,0 +1,230 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.R
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
import net.schueller.peertube.database.VideoViewModel
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.CommentThread
import net.schueller.peertube.model.Rating
import net.schueller.peertube.model.Video
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.service.VideoPlayerService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class VideoMetaDataFragment : Fragment() {
private var videoRating: Rating? = null
private var defaultTextColor: ColorStateList? = null
private var recyclerView: RecyclerView? = null
private var mMultiViewAdapter: MultiViewRecycleViewAdapter? = null
private lateinit var videoDescriptionFragment: VideoDescriptionFragment
private val mVideoViewModel: VideoViewModel by activityViewModels()
var isLeaveAppExpected = false
private set
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_video_meta, container, false)
}
override fun onPause() {
isLeaveAppExpected = false
super.onPause()
}
fun showDescriptionFragment(video: Video) {
// show full description fragment
videoDescriptionFragment = VideoDescriptionFragment.newInstance(video, this)
childFragmentManager.beginTransaction()
.add(R.id.video_meta_data_fragment, videoDescriptionFragment, VideoDescriptionFragment.TAG).commit()
}
fun hideDescriptionFragment() {
val fragment: Fragment? = childFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
if (fragment != null) {
childFragmentManager.beginTransaction().remove(fragment).commit()
}
}
fun updateVideoMeta(video: Video, mService: VideoPlayerService?) {
// Remove description if it is open as we are loading a new video
hideDescriptionFragment()
val context = context
val activity: Activity? = activity
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
// related videos
recyclerView = activity!!.findViewById(R.id.relatedVideosView)
val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this@VideoMetaDataFragment.context)
recyclerView?.layoutManager = layoutManager
mMultiViewAdapter = MultiViewRecycleViewAdapter(this)
recyclerView?.adapter = mMultiViewAdapter
val videoMetaViewItem = VideoMetaViewItem()
videoMetaViewItem.video = video
mMultiViewAdapter?.setVideoMeta(videoMetaViewItem)
loadVideos()
// loadComments(video.id)
// mMultiViewAdapter?.setVideoComment()
// videoOwnerSubscribeButton
// description
// video player options
val videoOptions = activity.findViewById<TextView>(R.id.exo_more)
videoOptions.setText(R.string.video_more_icon)
Iconics.Builder().on(videoOptions).build()
videoOptions.setOnClickListener {
val videoOptionsFragment = VideoOptionsFragment.newInstance(mService, video.files)
videoOptionsFragment.show(
getActivity()!!.supportFragmentManager,
VideoOptionsFragment.TAG
)
}
}
private fun loadComments(videoId: Int) {
val context = context
val start = 0
val count = 1
val sort = "-createdAt"
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call: Call<CommentThread> = service.getCommentThreads(videoId, start, count, sort)
call.enqueue(object : Callback<CommentThread?> {
override fun onResponse(call: Call<CommentThread?>, response: Response<CommentThread?>) {
if (response.body() != null) {
val commentThread = response.body()
if (commentThread != null) {
mMultiViewAdapter!!.setVideoComment(commentThread)
}
}
}
override fun onFailure(call: Call<CommentThread?>, t: Throwable) {
Log.wtf("err", t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
}
})
}
private fun loadVideos() {
val context = context
val start = 0
val count = 6
val sort = "-createdAt"
val filter: String? = null
val sharedPref = context?.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
var nsfw = "false"
var languages: Set<String>? = emptySet()
if (sharedPref != null) {
nsfw = if (sharedPref.getBoolean(getString(R.string.pref_show_nsfw_key), false)) "both" else "false"
languages = sharedPref.getStringSet(getString(R.string.pref_video_language_key), null)
}
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call: Call<VideoList> = service.getVideosData(start, count, sort, nsfw, filter, languages)
/*Log the URL called*/Log.d("URL Called", call.request().url.toString() + "")
// Toast.makeText(VideoListActivity.this, "URL Called: " + call.request().url(), Toast.LENGTH_SHORT).show();
call.enqueue(object : Callback<VideoList?> {
override fun onResponse(call: Call<VideoList?>, response: Response<VideoList?>) {
if (response.body() != null) {
val videoList = response.body()
if (videoList != null) {
mMultiViewAdapter!!.setVideoListData(videoList)
}
}
}
override fun onFailure(call: Call<VideoList?>, t: Throwable) {
Log.wtf("err", t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
}
})
}
fun saveToPlaylist(video: Video) {
val playlistVideo: net.schueller.peertube.database.Video = net.schueller.peertube.database.Video(videoUUID = video.uuid, videoName = video.name, videoDescription = video.description)
mVideoViewModel.insert(playlistVideo)
}
companion object {
const val TAG = "VMDF"
}
}

View File

@ -1,516 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.github.se_bastiaan.torrentstream.StreamStatus;
import com.github.se_bastiaan.torrentstream.Torrent;
import com.github.se_bastiaan.torrentstream.TorrentOptions;
import com.github.se_bastiaan.torrentstream.TorrentStream;
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.mikepenz.iconics.Iconics;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.model.File;
import net.schueller.peertube.model.Video;
import net.schueller.peertube.network.GetVideoDataService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.service.VideoPlayerService;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
public class VideoPlayerFragment extends Fragment implements VideoRendererEventListener {
private String mVideoUuid;
private ProgressBar progressBar;
private PlayerView simpleExoPlayerView;
private Intent videoPlayerIntent;
private Boolean mBound = false;
private Boolean isFullscreen = false;
private VideoPlayerService mService;
private TorrentStream torrentStream;
private LinearLayout torrentStatus;
private float aspectRatio;
private static final String TAG = "VideoPlayerFragment";
private GestureDetector mDetector;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "onServiceConnected");
VideoPlayerService.LocalBinder binder = (VideoPlayerService.LocalBinder) service;
mService = binder.getService();
// 2. Create the player
simpleExoPlayerView.setPlayer(mService.player);
mBound = true;
loadVideo();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG, "onServiceDisconnected");
simpleExoPlayerView.setPlayer(null);
mBound = false;
}
};
private AspectRatioFrameLayout.AspectRatioListener aspectRatioListerner = new AspectRatioFrameLayout.AspectRatioListener()
{
@Override
public void onAspectRatioUpdated( float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch )
{
aspectRatio = targetAspectRatio;
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_video_player, container, false);
}
public void start(String videoUuid) {
// start service
Context context = getContext();
Activity activity = getActivity();
mVideoUuid = videoUuid;
assert activity != null;
progressBar = activity.findViewById(R.id.torrent_progress);
progressBar.setMax(100);
assert context != null;
simpleExoPlayerView = new PlayerView(context);
simpleExoPlayerView = activity.findViewById(R.id.video_view);
simpleExoPlayerView.setControllerShowTimeoutMs(1000);
simpleExoPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
mDetector = new GestureDetector(context, new MyGestureListener());
simpleExoPlayerView.setOnTouchListener(touchListener);
simpleExoPlayerView.setAspectRatioListener( aspectRatioListerner );
torrentStatus = activity.findViewById(R.id.exo_torrent_status);
// Full screen Icon
TextView fullscreenText = activity.findViewById(R.id.exo_fullscreen);
FrameLayout fullscreenButton = activity.findViewById(R.id.exo_fullscreen_button);
fullscreenText.setText(R.string.video_expand_icon);
new Iconics.Builder().on(fullscreenText).build();
fullscreenButton.setOnClickListener(view -> {
Log.d(TAG, "Fullscreen");
fullScreenToggle();
});
if (!mBound) {
videoPlayerIntent = new Intent(context, VideoPlayerService.class);
activity.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE);
}
}
private void loadVideo() {
Context context = getContext();
// get video details from api
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
Call<Video> call = service.getVideoData(mVideoUuid);
call.enqueue(new Callback<Video>() {
@Override
public void onResponse(@NonNull Call<Video> call, @NonNull Response<Video> response) {
Video video = response.body();
mService.setCurrentVideo(video);
if (video == null) {
Toast.makeText(context, "Unable to retrieve video information, try again later.", Toast.LENGTH_SHORT).show();
return;
}
playVideo(video);
}
@Override
public void onFailure(@NonNull Call<Video> call, @NonNull Throwable t) {
Log.wtf(TAG, t.fillInStackTrace());
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
}
});
}
public void useController(boolean value) {
if (mBound) {
simpleExoPlayerView.setUseController(value);
}
}
private void playVideo(Video video) {
Context context = getContext();
// video Meta fragment
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment)
requireActivity().getSupportFragmentManager().findFragmentById(R.id.video_meta_data_fragment);
assert videoMetaDataFragment != null;
videoMetaDataFragment.updateVideoMeta(video, mService);
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
if (sharedPref.getBoolean(getString(R.string.pref_torrent_player_key), false)) {
torrentStatus.setVisibility(View.VISIBLE);
String stream = video.getFiles().get(0).getTorrentUrl();
Log.v(TAG, "getTorrentUrl : " + video.getFiles().get(0).getTorrentUrl());
torrentStream = setupTorrentStream();
torrentStream.startStream(stream);
} else {
Integer videoQuality = sharedPref.getInt(getString(R.string.pref_quality_key), 999999);
String urlToPlay = null;
boolean isHLS = false;
// try HLS stream first
// get video qualities
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
if (video.getStreamingPlaylists().size() > 0) {
urlToPlay = video.getStreamingPlaylists().get( 0 ).getPlaylistUrl();
isHLS = true;
} else {
if (video.getFiles().size() > 0) {
urlToPlay = video.getFiles().get( 0 ).getFileUrl(); // default, take first found, usually highest res
for ( File file : video.getFiles() ) {
// Set quality if it matches
if ( file.getResolution().getId().equals( videoQuality ) ) {
urlToPlay = file.getFileUrl();
}
}
}
}
if (!urlToPlay.isEmpty()) {
mService.setCurrentStreamUrl( urlToPlay, isHLS);
torrentStatus.setVisibility(View.GONE);
startPlayer();
} else {
stopVideo();
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show();
}
}
Log.v(TAG, "end of load Video");
}
private void startPlayer() {
Util.startForegroundService(requireContext(), videoPlayerIntent);
}
public void destroyVideo() {
simpleExoPlayerView.setPlayer(null);
if (torrentStream != null) {
torrentStream.stopStream();
}
}
public void pauseVideo() {
if (mBound) {
mService.player.setPlayWhenReady(false);
}
}
public void pauseToggle() {
if (mBound) {
mService.player.setPlayWhenReady(!mService.player.getPlayWhenReady());
}
}
public void unPauseVideo() {
if (mBound) {
mService.player.setPlayWhenReady(true);
}
}
public float getVideoAspectRatio() { return aspectRatio; }
public boolean isPaused() {
return !mService.player.getPlayWhenReady();
}
public void showControls(boolean value) {
simpleExoPlayerView.setUseController(value);
}
public void stopVideo() {
if (mBound) {
requireContext().unbindService(mConnection);
mBound = false;
}
}
public void setIsFullscreen(Boolean fullscreen) {
isFullscreen = fullscreen;
TextView fullscreenButton = requireActivity().findViewById(R.id.exo_fullscreen);
if (fullscreen) {
fullscreenButton.setText(R.string.video_compress_icon);
} else {
fullscreenButton.setText(R.string.video_expand_icon);
}
new Iconics.Builder().on(fullscreenButton).build();
}
public Boolean getIsFullscreen() {
return isFullscreen;
}
public void fullScreenToggle() {
if (!isFullscreen) {
setIsFullscreen(true);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setIsFullscreen(false);
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
/**
* Torrent Playback
*
* @return torrent stream
*/
private TorrentStream setupTorrentStream() {
TorrentOptions torrentOptions = new TorrentOptions.Builder()
.saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
.removeFilesAfterStop(true)
.build();
TorrentStream torrentStream = TorrentStream.init(torrentOptions);
torrentStream.addListener(new TorrentListener() {
@Override
public void onStreamReady(Torrent torrent) {
String videopath = Uri.fromFile(torrent.getVideoFile()).toString();
Log.d(TAG, "Ready! torrentStream videopath:" + videopath);
mService.setCurrentStreamUrl(videopath, false);
startPlayer();
}
@Override
public void onStreamProgress(Torrent torrent, StreamStatus streamStatus) {
if (streamStatus.bufferProgress <= 100 && progressBar.getProgress() < 100 && progressBar.getProgress() != streamStatus.bufferProgress) {
//Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
progressBar.setProgress(streamStatus.bufferProgress);
}
}
@Override
public void onStreamStopped() {
Log.d(TAG, "Stopped");
}
@Override
public void onStreamPrepared(Torrent torrent) {
Log.d(TAG, "Prepared");
}
@Override
public void onStreamStarted(Torrent torrent) {
Log.d(TAG, "Started");
}
@Override
public void onStreamError(Torrent torrent, Exception e) {
Log.d(TAG, "Error: " + e.getMessage());
}
});
return torrentStream;
}
@Override
public void onVideoEnabled(DecoderCounters counters) {
Log.v(TAG, "onVideoEnabled()...");
}
@Override
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {
}
@Override
public void onVideoInputFormatChanged(Format format) {
}
@Override
public void onDroppedFrames(int count, long elapsedMs) {
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
}
@Override
public void onRenderedFirstFrame(Surface surface) {
}
@Override
public void onVideoDisabled(DecoderCounters counters) {
Log.v(TAG, "onVideoDisabled()...");
}
View.OnTouchListener touchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return mDetector.onTouchEvent(event);
}
};
public String getVideoUuid() {
return mVideoUuid;
}
class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
/*
@Override
public boolean onDown(MotionEvent event) {
Log.d("TAG","onDown: ");
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("TAG", "onSingleTapConfirmed: ");
pauseToggle();
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("TAG", "onLongPress: ");
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("TAG", "onDoubleTap: ");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("TAG", "onScroll: ");
return true;
}
*/
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public boolean onFling(MotionEvent event1, MotionEvent event2,
float velocityX, float velocityY) {
Log.d(TAG, event1.toString());
Log.d(TAG, event2.toString());
Log.d(TAG, String.valueOf(velocityX));
Log.d(TAG, String.valueOf(velocityY));
//arbitrarily velocity speeds that seem to work to differentiate events.
if (velocityY > 4000) {
Log.d(TAG, "we have a drag down event");
if (canEnterPipMode(getContext())) {
requireActivity().enterPictureInPictureMode();
}
}
if ((velocityX > 2000) && (Math.abs(velocityY) < 2000)) {
Log.d(TAG, "swipe right " + velocityY);
}
if ((velocityX < 2000) && (Math.abs(velocityY) < 2000)) {
Log.d(TAG, "swipe left " + velocityY);
}
return true;
}
}
}

View File

@ -0,0 +1,496 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.fragment
import com.google.android.exoplayer2.video.VideoRendererEventListener
import com.google.android.exoplayer2.ui.PlayerView
import android.content.Intent
import net.schueller.peertube.service.VideoPlayerService
import com.github.se_bastiaan.torrentstream.TorrentStream
import android.widget.LinearLayout
import android.view.GestureDetector
import android.content.ServiceConnection
import android.content.ComponentName
import android.os.IBinder
import net.schueller.peertube.service.VideoPlayerService.LocalBinder
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.AspectRatioListener
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import net.schueller.peertube.R
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import android.widget.TextView
import android.widget.FrameLayout
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import android.widget.Toast
import net.schueller.peertube.helper.ErrorHelper
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Build
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener
import com.github.se_bastiaan.torrentstream.Torrent
import com.github.se_bastiaan.torrentstream.StreamStatus
import com.google.android.exoplayer2.decoder.DecoderCounters
import android.view.View.OnTouchListener
import android.view.MotionEvent
import android.view.GestureDetector.SimpleOnGestureListener
import androidx.annotation.RequiresApi
import android.os.Build.VERSION_CODES
import android.os.Environment
import android.util.Log
import android.view.View
import android.widget.ProgressBar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.github.se_bastiaan.torrentstream.TorrentOptions.Builder
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.util.Util
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.R.layout
import net.schueller.peertube.R.string
import net.schueller.peertube.helper.VideoHelper
import net.schueller.peertube.model.Video
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.Exception
import kotlin.math.abs
class VideoPlayerFragment : Fragment(), VideoRendererEventListener {
var videoUuid: String? = null
private set
// private var progressBar: ProgressBar? = null
private var exoPlayer: PlayerView? = null
private var videoPlayerIntent: Intent? = null
private var mBound = false
private var isFullscreen = false
private var mService: VideoPlayerService? = null
private var torrentStream: TorrentStream? = null
// private var torrentStatus: LinearLayout? = null
var videoAspectRatio = 0f
private set
private var mDetector: GestureDetector? = null
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d(TAG, "onServiceConnected")
val binder = service as LocalBinder
mService = binder.service
// 2. Create the player
exoPlayer!!.player = mService!!.player
mBound = true
loadVideo()
}
override fun onServiceDisconnected(componentName: ComponentName) {
Log.d(TAG, "onServiceDisconnected")
exoPlayer!!.player = null
mBound = false
}
}
private val aspectRatioListener: AspectRatioListener = AspectRatioListener {
targetAspectRatio, _, _ -> videoAspectRatio = targetAspectRatio
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(layout.fragment_video_player, container, false)
}
fun start(videoUuid: String?) {
// start service
val context = context
val activity: Activity? = activity
this.videoUuid = videoUuid
assert(activity != null)
// progressBar = activity?.findViewById(R.id.torrent_progress)
// progressBar?.max = 100
assert(context != null)
exoPlayer = PlayerView(context!!)
exoPlayer = activity?.findViewById(R.id.video_view)
exoPlayer?.controllerShowTimeoutMs = 1000
exoPlayer?.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
mDetector = GestureDetector(context, MyGestureListener())
exoPlayer?.setOnTouchListener(touchListener)
exoPlayer?.setAspectRatioListener(aspectRatioListener)
// torrentStatus = activity?.findViewById(R.id.exo_torrent_status)
// Full screen Icon
val fullscreenText = activity?.findViewById<TextView>(R.id.exo_fullscreen)
val fullscreenButton = activity?.findViewById<FrameLayout>(R.id.exo_fullscreen_button)
fullscreenText?.setText(string.video_expand_icon)
if (fullscreenText != null) {
Iconics.Builder().on(fullscreenText).build()
fullscreenButton?.setOnClickListener {
Log.d(TAG, "Fullscreen")
fullScreenToggle()
}
}
if (!mBound) {
videoPlayerIntent = Intent(context, VideoPlayerService::class.java)
activity?.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE)
}
}
private fun loadVideo() {
val context = context
// get video details from api
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call = service.getVideoData(videoUuid)
call.enqueue(object : Callback<Video?> {
override fun onResponse(call: Call<Video?>, response: Response<Video?>) {
val video = response.body()
mService!!.setCurrentVideo(video)
if (video == null) {
Toast.makeText(
context,
"Unable to retrieve video information, try again later.",
Toast.LENGTH_SHORT
).show()
return
}
playVideo(video)
}
override fun onFailure(call: Call<Video?>, t: Throwable) {
Log.wtf(TAG, t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(activity, t)
}
})
}
fun useController(value: Boolean) {
if (mBound) {
exoPlayer!!.useController = value
}
}
private fun playVideo(video: Video) {
val context = context
// video Meta fragment
val videoMetaDataFragment =
(requireActivity().supportFragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?)!!
videoMetaDataFragment.updateVideoMeta(video, mService)
val sharedPref = context?.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
var prefTorrentPlayer = false
var videoQuality = 999999
if (sharedPref != null) {
prefTorrentPlayer = sharedPref.getBoolean(getString(string.pref_torrent_player_key), false)
videoQuality = sharedPref.getInt(getString(string.pref_quality_key), 999999)
}
// if (prefTorrentPlayer) {
// torrentStatus!!.visibility = View.VISIBLE
// val stream = video.files[0].torrentUrl
// Log.v(TAG, "getTorrentUrl : " + video.files[0].torrentUrl)
// torrentStream = setupTorrentStream()
// torrentStream!!.startStream(stream)
// } else {
var urlToPlay: String? = null
var isHLS = false
// try HLS stream first
// get video qualities
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
if (video.streamingPlaylists.size > 0) {
urlToPlay = video.streamingPlaylists[0].playlistUrl
isHLS = true
} else {
if (video.files.size > 0) {
urlToPlay = video.files[0].fileUrl // default, take first found, usually highest res
for (file in video.files) {
// Set quality if it matches
if (file.resolution.id == videoQuality) {
urlToPlay = file.fileUrl
}
}
}
}
if (urlToPlay!!.isNotEmpty()) {
mService!!.setCurrentStreamUrl(urlToPlay, isHLS)
// torrentStatus!!.visibility = View.GONE
startPlayer()
} else {
stopVideo()
Toast.makeText(context, string.api_error, Toast.LENGTH_LONG).show()
}
// }
Log.v(TAG, "end of load Video")
}
private fun startPlayer() {
Util.startForegroundService(requireContext(), videoPlayerIntent!!)
}
fun destroyVideo() {
exoPlayer!!.player = null
if (torrentStream != null) {
torrentStream!!.stopStream()
}
}
fun pauseVideo() {
if (mBound) {
mService!!.player!!.playWhenReady = false
}
}
// fun pauseToggle() {
// if (mBound) {
// mService!!.player!!.playWhenReady = !mService!!.player!!.playWhenReady
// }
// }
fun unPauseVideo() {
if (mBound) {
mService!!.player!!.playWhenReady = true
}
}
val isPaused: Boolean
get() = !mService!!.player!!.playWhenReady
fun showControls(value: Boolean) {
exoPlayer!!.useController = value
}
fun stopVideo() {
if (mBound) {
requireContext().unbindService(mConnection)
mBound = false
}
}
/**
* triggered rotation and button press
*/
fun setIsFullscreen(fullscreen: Boolean) {
isFullscreen = fullscreen
val fullscreenButton = requireActivity().findViewById<TextView>(R.id.exo_fullscreen)
if (fullscreen) {
hideSystemBars()
fullscreenButton.setText(string.video_compress_icon)
} else {
restoreSystemBars()
fullscreenButton.setText(string.video_expand_icon)
}
Iconics.Builder().on(fullscreenButton).build()
}
private fun hideSystemBars()
{
val view = this.view
if (view != null) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(view) ?: return
// Configure the behavior of the hidden system bars
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
}
private fun restoreSystemBars()
{
val view = this.view
if (view != null) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(view) ?: return
// Show both the status bar and the navigation bar
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
fun getIsFullscreen(): Boolean {
return isFullscreen
}
/**
* Triggered by button press
*/
fun fullScreenToggle() {
if (!isFullscreen) {
setIsFullscreen(true)
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} else {
setIsFullscreen(false)
// we want to force portrait if fullscreen is switched of as we do not have a min. landscape view
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
//
// /**
// * Torrent Playback
// *
// * @return torrent stream
// */
// private fun setupTorrentStream(): TorrentStream {
// val torrentOptions = Builder()
// .saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
// .removeFilesAfterStop(true)
// .build()
// val torrentStream = TorrentStream.init(torrentOptions)
// torrentStream.addListener(object : TorrentListener {
// override fun onStreamReady(torrent: Torrent) {
// val videoPath = Uri.fromFile(torrent.videoFile).toString()
// Log.d(TAG, "Ready! torrentStream videoPath:$videoPath")
// mService!!.setCurrentStreamUrl(videoPath, false)
// startPlayer()
// }
//
// override fun onStreamProgress(torrent: Torrent, streamStatus: StreamStatus) {
// if (streamStatus.bufferProgress <= 100 && progressBar!!.progress < 100 && progressBar!!.progress != streamStatus.bufferProgress) {
// //Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
// progressBar!!.progress = streamStatus.bufferProgress
// }
// }
//
// override fun onStreamStopped() {
// Log.d(TAG, "Stopped")
// }
//
// override fun onStreamPrepared(torrent: Torrent) {
// Log.d(TAG, "Prepared")
// }
//
// override fun onStreamStarted(torrent: Torrent) {
// Log.d(TAG, "Started")
// }
//
// override fun onStreamError(torrent: Torrent, e: Exception) {
// Log.d(TAG, "Error: " + e.message)
// }
// })
// return torrentStream
// }
override fun onVideoEnabled(counters: DecoderCounters) {
Log.v(TAG, "onVideoEnabled()...")
}
override fun onVideoDecoderInitialized(
decoderName: String,
initializedTimestampMs: Long,
initializationDurationMs: Long
) {
}
override fun onVideoInputFormatChanged(format: Format) {}
override fun onDroppedFrames(count: Int, elapsedMs: Long) {}
override fun onVideoDisabled(counters: DecoderCounters) {
Log.v(TAG, "onVideoDisabled()...")
}
// touch event on video player
private var touchListener = OnTouchListener { _, event ->
//v.performClick() // causes flicker but should be implemented for accessibility
mDetector!!.onTouchEvent(event)
}
internal inner class MyGestureListener : SimpleOnGestureListener() {
/*
@Override
public boolean onDown(MotionEvent event) {
Log.d("TAG","onDown: ");
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("TAG", "onSingleTapConfirmed: ");
pauseToggle();
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("TAG", "onLongPress: ");
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("TAG", "onDoubleTap: ");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("TAG", "onScroll: ");
return true;
}
*/
@RequiresApi(api = VERSION_CODES.N)
override fun onFling(
event1: MotionEvent, event2: MotionEvent,
velocityX: Float, velocityY: Float
): Boolean {
Log.d(TAG, event1.toString())
Log.d(TAG, event2.toString())
Log.d(TAG, velocityX.toString())
Log.d(TAG, velocityY.toString())
//arbitrarily velocity speeds that seem to work to differentiate events.
if (velocityY > 4000) {
Log.d(TAG, "we have a drag down event")
if (VideoHelper.canEnterPipMode(context)) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
val pipParams = PictureInPictureParams.Builder()
requireActivity().enterPictureInPictureMode(pipParams.build())
}
}
}
if (velocityX > 2000 && abs(velocityY) < 2000) {
Log.d(TAG, "swipe right $velocityY")
}
if (velocityX < 2000 && abs(velocityY) < 2000) {
Log.d(TAG, "swipe left $velocityY")
}
return true
}
}
companion object {
private const val TAG = "VideoPlayerFragment"
}
}

View File

@ -18,32 +18,92 @@ package net.schueller.peertube.helper
import android.content.Context
import android.text.format.DateUtils
import net.schueller.peertube.R
import net.schueller.peertube.R.string
import net.schueller.peertube.model.Account
import net.schueller.peertube.model.Avatar
import net.schueller.peertube.model.Video
import org.ocpsoft.prettytime.PrettyTime
import java.util.Date
import java.util.Locale
import kotlin.math.absoluteValue
object MetaDataHelper {
@JvmStatic
fun getMetaString(getCreatedAt: Date, viewCount: Int, context: Context): String {
fun getMetaString(getCreatedAt: Date, viewCount: Int, context: Context, reversed: Boolean = false): String {
// Compatible with SDK 21+
val currentLanguage = Locale.getDefault().displayLanguage
val p = PrettyTime(currentLanguage)
val relativeTime = p.format(Date(getCreatedAt.time))
return relativeTime +
context.resources.getString(string.meta_data_seperator) +
viewCount + context.resources.getString(string.meta_data_views)
return if (reversed) {
viewCount.toString() +
context.resources.getString(string.meta_data_views) +
context.resources.getString(string.meta_data_seperator) +
relativeTime
} else {
relativeTime +
context.resources.getString(string.meta_data_seperator) +
viewCount + context.resources.getString(string.meta_data_views)
}
}
fun getTagsString(video: Video): String {
return if (video.tags.isNotEmpty()) {
" #" + video.tags.joinToString(" #", "", "", 3, "")
} else {
" "
}
}
@JvmStatic
fun getOwnerString(accountName: String, serverHost: String, context: Context): String {
return accountName +
context.resources.getString(string.meta_data_owner_seperator) +
serverHost
fun getCreatorString(video: Video, context: Context, fqdn: Boolean = false): String {
return if (isChannel(video)) {
if (!fqdn) {
video.channel.displayName
} else {
getConcatFqdnString(video.channel.name, video.channel.host, context)
}
} else {
getOwnerString(video.account, context, fqdn)
}
}
@JvmStatic
fun getOwnerString(account: Account, context: Context, fqdn: Boolean = true): String {
return if (!fqdn) {
account.name
} else {
getConcatFqdnString(account.name, account.host, context)
}
}
private fun getConcatFqdnString(user: String, host: String, context: Context): String {
return context.resources.getString(string.video_owner_fqdn_line, user, host)
}
@JvmStatic
fun getCreatorAvatar(video: Video, context: Context): Avatar? {
return if (isChannel(video)) {
if (video.channel.avatar == null) {
video.account.avatar
} else {
video.channel.avatar
}
} else {
video.account.avatar
}
}
@JvmStatic
fun isChannel(video: Video): Boolean {
// c285b523-d688-43c5-a9ad-f745ff09bbd1
return !video.channel.name.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}".toRegex())
}
@JvmStatic
fun getDuration(duration: Long?): String {
return DateUtils.formatElapsedTime(duration!!)

View File

@ -1,93 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.intents;
import android.Manifest;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import android.widget.Toast;
import com.github.se_bastiaan.torrentstream.TorrentOptions;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.model.Video;
import androidx.core.app.ActivityCompat;
public class Intents {
private static final String TAG = "Intents";
/**
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
public static void Share(Context context, Video video) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_SUBJECT, video.getName());
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.getUuid()) );
intent.setType("text/plain");
context.startActivity(intent);
}
/**
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
public static void Download(Context context, Video video) {
if (video.getFiles().size() > 0)
{
String url = video.getFiles().get( 0 ).getFileDownloadUrl();
// make sure it is a valid filename
String destFilename = video.getName().replaceAll( "[^a-zA-Z0-9]", "_" ) + "." + MimeTypeMap.getFileExtensionFromUrl( URLUtil.guessFileName( url, null, null ) );
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
DownloadManager.Request request = new DownloadManager.Request( Uri.parse( url ) );
request.setDescription( video.getDescription() );
request.setTitle( video.getName() );
request.allowScanningByMediaScanner();
request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, destFilename );
// get download service and enqueue file
DownloadManager manager = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
manager.enqueue( request );
} else {
Toast.makeText( context, R.string.api_error, Toast.LENGTH_LONG ).show();
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.intents
import android.Manifest
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import net.schueller.peertube.helper.APIUrlHelper
import android.webkit.MimeTypeMap
import android.os.Environment
import android.webkit.URLUtil
import android.widget.Toast
import androidx.core.app.ActivityCompat
import net.schueller.peertube.R
import net.schueller.peertube.model.Video
import android.content.ContextWrapper
import android.app.Activity
object Intents {
private const val TAG = "Intents"
/**
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
@JvmStatic
fun Share(context: Context, video: Video) {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_SUBJECT, video.name)
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.uuid))
intent.type = "text/plain"
context.startActivity(intent)
}
/**
* @param context context
* @param video video
*/
// TODO, offer which version to download
fun Download(context: Context, video: Video) {
// deal withe permissions here
// get permission to store file
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
val activity = getActivity(context)
if (activity != null) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
0
)
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
startDownload(video, context)
} else {
Toast.makeText(
context,
context.getString(R.string.video_download_permission_error),
Toast.LENGTH_LONG
).show()
}
} else {
startDownload(video, context)
}
}
private fun startDownload(video: Video, context: Context)
{
if (video.files.size > 0) {
val url = video.files[0].fileDownloadUrl
// make sure it is a valid filename
val destFilename = video.name.replace(
"[^a-zA-Z0-9]".toRegex(),
"_"
) + "." + MimeTypeMap.getFileExtensionFromUrl(
URLUtil.guessFileName(url, null, null)
)
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
val request = DownloadManager.Request(Uri.parse(url))
request.setDescription(video.description)
request.setTitle(video.name)
request.allowScanningByMediaScanner()
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, destFilename)
// get download service and enqueue file
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
manager.enqueue(request)
} else {
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show()
}
}
private fun getActivity(context: Context?): Activity? {
if (context == null) {
return null
} else if (context is ContextWrapper) {
return if (context is Activity) {
context
} else {
getActivity(context.baseContext)
}
}
return null
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import java.util.*
class Comment(
val id: Int,
val url: String,
val text: String,
val threadId: Int,
val inReplyToCommentId: Int? = null,
val videoId: Int,
val createdAt: Date,
val updatedAt: Date,
val deletedAt: Date? = null,
val isDeleted: Boolean,
val totalRepliesFromVideoAuthor: Int,
val totalReplies: Int,
val account: Account
)

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import com.google.gson.annotations.SerializedName
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import java.util.ArrayList
class CommentThread(
val total: Int,
val totalNotDeletedComments: Int,
@SerializedName("data")
val comments: ArrayList<Comment>
): OverviewRecycleViewItem()

View File

@ -35,7 +35,7 @@ class Video(
var licence: Licence,
var language: Language,
var nsfw: Boolean,
var description: String,
var description: String? = null,
var local: Boolean,
var live: Boolean,
var duration: Int,
@ -66,7 +66,7 @@ class Video(
companion object {
@JvmStatic
fun getMediaDescription(context: Context?, video: Video): MediaDescriptionCompat {
fun getMediaDescription(video: Video): MediaDescriptionCompat {
// String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);

View File

@ -0,0 +1,7 @@
package net.schueller.peertube.model.ui
import net.schueller.peertube.model.Video
class VideoMetaViewItem: OverviewRecycleViewItem() {
var video: Video? = null
}

View File

@ -16,15 +16,23 @@
*/
package net.schueller.peertube.network;
import com.google.gson.JsonObject;
import net.schueller.peertube.model.Account;
import net.schueller.peertube.model.Channel;
import net.schueller.peertube.model.ChannelList;
import net.schueller.peertube.model.Me;
import net.schueller.peertube.model.VideoList;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
@ -52,5 +60,18 @@ public interface GetUserService {
@Path(value = "displayName", encoded = true) String displayName
);
@GET("users/me/subscriptions/exist")
Call<JsonObject> subscriptionsExist(
@Query("uris") String videoChannelNameAndHost
);
@POST("users/me/subscriptions")
Call<ResponseBody> subscribe(
@Body RequestBody params
);
@DELETE("users/me/subscriptions/{videoChannelNameAndHost}")
Call<ResponseBody> unsubscribe(
@Path(value = "videoChannelNameAndHost", encoded = true) String videoChannelNameAndHost
);
}

View File

@ -16,6 +16,7 @@
*/
package net.schueller.peertube.network;
import net.schueller.peertube.model.CommentThread;
import net.schueller.peertube.model.Description;
import net.schueller.peertube.model.Overview;
import net.schueller.peertube.model.Rating;
@ -91,4 +92,12 @@ public interface GetVideoDataService {
@Query("page") int page
);
// https://troll.tv/api/v1/videos/{id}/comment-threads?start=0&count=10&sort=-createdAt
@GET("videos/{id}/comment-threads")
Call<CommentThread> getCommentThreads(
@Path(value = "id", encoded = true) Integer id,
@Query("start") int start,
@Query("count") int count,
@Query("sort") String sort
);
}

View File

@ -1,358 +0,0 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.service;
import static android.media.session.PlaybackState.ACTION_PAUSE;
import static android.media.session.PlaybackState.ACTION_PLAY;
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
import static net.schueller.peertube.activity.VideoListActivity.EXTRA_VIDEOID;
import static net.schueller.peertube.network.UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import android.webkit.URLUtil;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Util;
import net.schueller.peertube.R;
import net.schueller.peertube.activity.VideoPlayActivity;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.model.Video;
import okhttp3.OkHttpClient;
public class VideoPlayerService extends Service {
private static final String TAG = "VideoPlayerService";
private static final String MEDIA_SESSION_TAG = "peertube_player";
private final IBinder mBinder = new LocalBinder();
private static final String PLAYBACK_CHANNEL_ID = "playback_channel";
private static final Integer PLAYBACK_NOTIFICATION_ID = 1;
public SimpleExoPlayer player;
private Video currentVideo;
private String currentStreamUrl;
private boolean currentStreamUrlIsHLS;
private PlayerNotificationManager playerNotificationManager;
private IntentFilter becomeNoisyIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private BecomingNoisyReceiver myNoisyAudioStreamReceiver = new BecomingNoisyReceiver();
@Override
public void onCreate() {
Log.v(TAG, "onCreate...");
super.onCreate();
player = new SimpleExoPlayer.Builder(getApplicationContext())
.setTrackSelector(new DefaultTrackSelector(getApplicationContext()))
.build();
// Stop player if audio device changes, e.g. headphones unplugged
player.addListener(new Player.EventListener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
Log.v(TAG, "ACTION_PLAY: " + playbackState);
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter);
}
if (playbackState
== ACTION_PLAY) { // this means that play is available, hence the audio is paused or stopped
Log.v(TAG, "ACTION_PAUSE: " + playbackState);
safeUnregisterReceiver();
}
}
});
}
public class LocalBinder extends Binder {
public VideoPlayerService getService() {
// Return this instance of VideoPlayerService so clients can call public methods
return VideoPlayerService.this;
}
}
@Override
public void onDestroy() {
Log.v(TAG, "onDestroy...");
if (playerNotificationManager != null) {
playerNotificationManager.setPlayer(null);
}
//Was seeing an error when exiting the program about not unregistering the receiver.
safeUnregisterReceiver();
if (player != null) {
player.release();
player = null;
}
super.onDestroy();
}
private void safeUnregisterReceiver()
{
try {
unregisterReceiver(myNoisyAudioStreamReceiver);
} catch (Exception e) {
Log.e("VideoPlayerService", "attempted to unregister a nonregistered service");
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Context context = this;
Log.v(TAG, "onStartCommand...");
if (!URLUtil.isValidUrl(currentStreamUrl)) {
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show();
return START_NOT_STICKY;
} else {
playVideo();
return START_STICKY;
}
}
public void setCurrentVideo(Video video) {
Log.v(TAG, "setCurrentVideo...");
currentVideo = video;
}
public void setCurrentStreamUrl(String streamUrl, boolean isHLS) {
Log.v(TAG, "setCurrentStreamUrl..." + streamUrl);
currentStreamUrlIsHLS = isHLS;
currentStreamUrl = streamUrl;
}
//Playback speed control
public void setPlayBackSpeed(float speed) {
Log.v(TAG, "setPlayBackSpeed...");
player.setPlaybackParameters(new PlaybackParameters(speed));
}
/**
* Returns the current playback speed of the player.
*
* @return the current playback speed of the player.
*/
public float getPlayBackSpeed() {
return player.getPlaybackParameters().speed;
}
public void playVideo() {
Context context = this;
// We need a valid URL
Log.v(TAG, "playVideo...");
// Produces DataSource instances through which media data is loaded.
// DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(),
// Util.getUserAgent(getApplicationContext(), "PeerTube"), null);
OkHttpClient.Builder okhttpClientBuilder;
if (!APIUrlHelper.useInsecureConnection(this)) {
okhttpClientBuilder = new OkHttpClient.Builder();
} else {
okhttpClientBuilder = getUnsafeOkHttpClientBuilder();
}
// Create a data source factory.
DataSource.Factory dataSourceFactory = new OkHttpDataSourceFactory(okhttpClientBuilder.build(), Util.getUserAgent(getApplicationContext(), "PeerTube"));
// Create a progressive media source pointing to a stream uri.
MediaSource mediaSource;
if (currentStreamUrlIsHLS) {
mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
} else {
mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
}
// Set the media source to be played.
player.setMediaSource(mediaSource);
// Prepare the player.
player.prepare();
// Auto play
player.setPlayWhenReady(true);
//set playback speed to global default
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
float speed = Float.parseFloat(sharedPref.getString(getString(R.string.pref_video_speed_key), "1.0"));
this.setPlayBackSpeed(speed);
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
context, PLAYBACK_CHANNEL_ID, R.string.playback_channel_name,
PLAYBACK_NOTIFICATION_ID,
new PlayerNotificationManager.MediaDescriptionAdapter() {
@Override
public String getCurrentContentTitle(Player player) {
return currentVideo.getName();
}
@Nullable
@Override
public PendingIntent createCurrentContentIntent(Player player) {
Intent intent = new Intent(context, VideoPlayActivity.class);
intent.putExtra(EXTRA_VIDEOID, currentVideo.getUuid());
return PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
public String getCurrentContentText(Player player) {
return MetaDataHelper.getMetaString(
currentVideo.getCreatedAt(),
currentVideo.getViews(),
getBaseContext()
);
}
@Nullable
@Override
public Bitmap getCurrentLargeIcon(Player player,
PlayerNotificationManager.BitmapCallback callback) {
return null;
}
}
);
playerNotificationManager.setSmallIcon(R.drawable.ic_logo_bw);
// don't show skip buttons in notification
playerNotificationManager.setUseNavigationActions(false);
playerNotificationManager.setUseStopAction(true);
playerNotificationManager.setNotificationListener(
new PlayerNotificationManager.NotificationListener() {
@Override
public void onNotificationStarted(int notificationId, Notification notification) {
startForeground(notificationId, notification);
}
@Override
public void onNotificationCancelled(int notificationId) {
Log.v(TAG, "onNotificationCancelled...");
stopForeground(true);
Intent killFloat = new Intent(ACTION_STOP);
sendBroadcast(killFloat);
/*
Intent killFloat = new Intent(BROADCAST_ACTION);
Intent killFloatingWindow = new Intent(getApplicationContext(),VideoPlayActivity.class);
killFloatingWindow.putExtra("killFloat",true);
startActivity(killFloatingWindow);
// TODO: only kill the notification if we no longer have a bound activity
stopForeground(true);
*/
}
}
);
playerNotificationManager.setPlayer(player);
// external Media control, Android Wear / Google Home etc.
MediaSessionCompat mediaSession = new MediaSessionCompat(context, MEDIA_SESSION_TAG);
mediaSession.setActive(true);
playerNotificationManager.setMediaSessionToken(mediaSession.getSessionToken());
MediaSessionConnector mediaSessionConnector = new MediaSessionConnector(mediaSession);
mediaSessionConnector.setQueueNavigator(new TimelineQueueNavigator(mediaSession) {
@Override
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
return Video.getMediaDescription(context, currentVideo);
}
});
mediaSessionConnector.setPlayer(player);
// Audio Focus
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE)
.build();
player.setAudioAttributes(audioAttributes, true);
}
// pause playback on audio output change
private class BecomingNoisyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
player.setPlayWhenReady(false);
}
}
}
}

View File

@ -0,0 +1,318 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.service
import android.annotation.SuppressLint
import android.app.Notification
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.model.Video.Companion.getMediaDescription
import android.os.IBinder
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import android.content.IntentFilter
import android.media.AudioManager
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import android.media.session.PlaybackState
import android.content.Intent
import android.widget.Toast
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.UnsafeOkHttpClient
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager.MediaDescriptionAdapter
import android.app.PendingIntent
import android.app.Service
import net.schueller.peertube.activity.VideoPlayActivity
import net.schueller.peertube.activity.VideoListActivity
import android.graphics.Bitmap
import android.support.v4.media.session.MediaSessionCompat
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import android.support.v4.media.MediaDescriptionCompat
import android.content.BroadcastReceiver
import android.content.Context
import android.net.Uri
import android.os.Binder
import android.os.Build
import android.util.Log
import android.webkit.URLUtil
import androidx.core.app.NotificationCompat
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager.NotificationListener
import net.schueller.peertube.R.drawable
import net.schueller.peertube.R.string
import net.schueller.peertube.model.Video
import java.lang.Exception
class VideoPlayerService : Service() {
private val mBinder: IBinder = LocalBinder()
@JvmField
var player: ExoPlayer? = null
private var currentVideo: Video? = null
private var currentStreamUrl: String? = null
private var currentStreamUrlIsHLS = false
private var playerNotificationManager: PlayerNotificationManager? = null
private val becomeNoisyIntentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
private val myNoisyAudioStreamReceiver = BecomingNoisyReceiver()
override fun onCreate() {
Log.v(TAG, "onCreate...")
super.onCreate()
player = ExoPlayer.Builder(applicationContext)
.setTrackSelector(DefaultTrackSelector(applicationContext))
.build()
// Stop player if audio device changes, e.g. headphones unplugged
player!!.addListener(object : Player.Listener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if (playbackState.toLong() == PlaybackState.ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
Log.v(TAG, "ACTION_PLAY: $playbackState")
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter)
}
if (playbackState
.toLong() == PlaybackState.ACTION_PLAY
) { // this means that play is available, hence the audio is paused or stopped
Log.v(TAG, "ACTION_PAUSE: $playbackState")
safeUnregisterReceiver()
}
}
})
}
inner class LocalBinder : Binder() {
// Return this instance of VideoPlayerService so clients can call public methods
val service: VideoPlayerService
get() =// Return this instance of VideoPlayerService so clients can call public methods
this@VideoPlayerService
}
override fun onDestroy() {
Log.v(TAG, "onDestroy...")
if (playerNotificationManager != null) {
playerNotificationManager!!.setPlayer(null)
}
//Was seeing an error when exiting the program about not unregistering the receiver.
safeUnregisterReceiver()
if (player != null) {
player!!.release()
player = null
}
super.onDestroy()
}
private fun safeUnregisterReceiver() {
try {
unregisterReceiver(myNoisyAudioStreamReceiver)
} catch (e: Exception) {
Log.e("VideoPlayerService", "attempted to unregister a non-registered service")
}
}
override fun onBind(intent: Intent): IBinder {
return mBinder
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val context: Context = this
Log.v(TAG, "onStartCommand...")
return if (!URLUtil.isValidUrl(currentStreamUrl)) {
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show()
START_NOT_STICKY
} else {
playVideo()
START_STICKY
}
}
fun setCurrentVideo(video: Video?) {
Log.v(TAG, "setCurrentVideo...")
currentVideo = video
}
fun setCurrentStreamUrl(streamUrl: String, isHLS: Boolean) {
Log.v(TAG, "setCurrentStreamUrl...$streamUrl")
currentStreamUrlIsHLS = isHLS
currentStreamUrl = streamUrl
}
/**
* Returns the current playback speed of the player.
*
* @return the current playback speed of the player.
*///Playback speed control
var playBackSpeed: Float
get() = player!!.playbackParameters.speed
set(speed) {
Log.v(TAG, "setPlayBackSpeed...")
player!!.playbackParameters = PlaybackParameters(speed)
}
private fun playVideo() {
val context: Context = this
// We need a valid URL
Log.v(TAG, "playVideo...")
// Produces DataSource instances through which media data is loaded.
val okhttpClientBuilder: okhttp3.OkHttpClient.Builder = if (!APIUrlHelper.useInsecureConnection(this)) {
okhttp3.OkHttpClient.Builder()
} else {
UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder()
}
// Create a data source factory.
val dataSourceFactory: OkHttpDataSource.Factory = OkHttpDataSource.Factory(
okhttpClientBuilder.build()
)
// Create a progressive media source pointing to a stream uri.
val mediaSource: MediaSource = if (currentStreamUrlIsHLS) {
HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
} else {
ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
}
// Set the media source to be played.
player!!.setMediaSource(mediaSource)
// Prepare the player.
player!!.prepare()
// Auto play
player!!.playWhenReady = true
//set playback speed to global default
val sharedPref = getSharedPreferences(
packageName + "_preferences",
Context.MODE_PRIVATE
)
val speed = sharedPref.getString(getString(string.pref_video_speed_key), "1.0")!!.toFloat()
playBackSpeed = speed
playerNotificationManager = PlayerNotificationManager.Builder(
this,
PLAYBACK_NOTIFICATION_ID,
PLAYBACK_CHANNEL_ID,
).setMediaDescriptionAdapter(
object : MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): CharSequence {
return currentVideo!!.name
}
@SuppressLint("UnspecifiedImmutableFlag")
override fun createCurrentContentIntent(player: Player): PendingIntent? {
val intent = Intent(context, VideoPlayActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_VIDEOID, currentVideo!!.uuid)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
else
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
override fun getCurrentContentText(player: Player): CharSequence {
return getMetaString(
currentVideo!!.createdAt,
currentVideo!!.views,
baseContext
)
}
override fun getCurrentSubText(player: Player): CharSequence { return ""}
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
return null
}
}
).setNotificationListener(
object : NotificationListener {
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
super.onNotificationPosted(notificationId, notification, ongoing)
if (ongoing) // allow notification to be dismissed if player is stopped
startForeground(notificationId, notification)
else
stopForeground(false)
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
super.onNotificationCancelled(notificationId, dismissedByUser)
stopSelf()
stopForeground(true)
}
}
).setChannelNameResourceId(string.playback_channel_name)
.setChannelDescriptionResourceId(string.playback_channel_description)
.build()
playerNotificationManager!!.setPriority(NotificationCompat.PRIORITY_DEFAULT)
playerNotificationManager!!.setSmallIcon(drawable.ic_logo_bw)
// don't show skip buttons in notification
playerNotificationManager!!.setUseNextAction(false)
playerNotificationManager!!.setUsePreviousAction(false)
playerNotificationManager!!.setPlayer(player)
// external Media control, Android Wear / Google Home etc.
val mediaSession = MediaSessionCompat(context, MEDIA_SESSION_TAG)
mediaSession.isActive = true
playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken)
val mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
return getMediaDescription(currentVideo!!)
}
})
mediaSessionConnector.setPlayer(player)
// Audio Focus
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE)
.build()
player!!.setAudioAttributes(audioAttributes, true)
}
// pause playback on audio output change
private inner class BecomingNoisyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
player!!.playWhenReady = false
}
}
}
companion object {
private const val TAG = "VideoPlayerService"
private const val MEDIA_SESSION_TAG = "peertube_player"
private const val PLAYBACK_CHANNEL_ID = "playback_channel"
private const val PLAYBACK_NOTIFICATION_ID = 1
}
}

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,9l6,6l6,-6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,6L6,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,6L18,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21,15v4a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M7,10l5,5l5,-5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,15L12,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M13,19l9,-7l-9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M2,19l9,-7l-9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,15s1,-1 4,-1 5,2 8,2 4,-1 4,-1V3s-1,1 -4,1 -5,-2 -8,-2 -4,1 -4,1z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M4,22L4,15"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:pathData="M6,4h4v16h-4z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M14,4h4v16h-4z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M5,3l14,9l-14,9l0,-18z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:viewportHeight="426.7"
android:viewportWidth="426.7" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff" android:pathData="M0,64h256v42.7H0zM0,149.3h256V192H0zM0,234.7h170.7v42.7H0z"/>
<path android:fillColor="#ffffff" android:pathData="M341.3,234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector android:height="52dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M11,19l-9,-7l9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M22,19l-9,-7l9,-7l0,14z"
android:strokeColor="#ffffff" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,41 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,5m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M18,19m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M8.59,13.51L15.42,17.49"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M15.41,6.51L8.59,10.49"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M4.93,4.93L19.07,19.07"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,15v4a3,3 0,0 0,3 3l4,-9L17,2L5.72,2a2,2 0,0 0,-2 1.7l-1.38,9a2,2 0,0 0,2 2.3zM17,2h2.67A2.31,2.31 0,0 1,22 4v7a2.31,2.31 0,0 1,-2.33 2L17,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,15v4a3,3 0,0 0,3 3l4,-9L17,2L5.72,2a2,2 0,0 0,-2 1.7l-1.38,9a2,2 0,0 0,2 2.3zM17,2h2.67A2.31,2.31 0,0 1,22 4v7a2.31,2.31 0,0 1,-2.33 2L17,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#ffffff"
android:strokeColor="#555555"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14,9V5a3,3 0,0 0,-3 -3l-4,9v11h11.28a2,2 0,0 0,2 -1.7l1.38,-9a2,2 0,0 0,-2 -2.3zM7,22H4a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2h3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14,9V5a3,3 0,0 0,-3 -3l-4,9v11h11.28a2,2 0,0 0,2 -1.7l1.38,-9a2,2 0,0 0,-2 -2.3zM7,22H4a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2h3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#ffffff"
android:strokeColor="#555555"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,175 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".activity.MeActivity"
android:orientation="vertical">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MeActivity"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar_me"
android:id="@+id/appbar_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp" />
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"/>
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:id="@+id/a_me_account_line"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/a_me_avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="0dp"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"/>
<LinearLayout
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:id="@+id/a_me_account_line"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/a_me_username"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
android:orientation="horizontal">
<TextView
android:id="@+id/a_me_email"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/a_me_avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="0dp"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"/>
<TextView
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_logout_button"
android:id="@+id/a_me_logout"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/a_me_username"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
<TextView
android:id="@+id/a_me_email"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
<TextView
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_logout_button"
android:id="@+id/a_me_logout"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/darker_gray"/>
<LinearLayout
android:id="@+id/a_me_playlist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_settings_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/playlist"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
<LinearLayout
android:id="@+id/a_me_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_settings_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_activity_settings"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
<LinearLayout
android:id="@+id/a_me_helpnfeedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_help_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_help_and_feedback_button"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/darker_gray" />
</ScrollView>
<LinearLayout
android:id="@+id/a_me_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<!-- <LinearLayout-->
<!-- android:layout_marginBottom="0dp"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="vertical">-->
<TextView
android:drawableStart="@drawable/ic_baseline_settings_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_activity_settings"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
</LinearLayout>
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_username"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<LinearLayout
android:id="@+id/a_me_helpnfeedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_help_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_help_and_feedback_button"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- <LinearLayout-->
<!-- android:layout_marginBottom="0dp"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="vertical">-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_username"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_email"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<!-- </LinearLayout>-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_email"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<!-- </LinearLayout>-->
</LinearLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.ServerAddressBookActivity"
android:id="@+id/server_book">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_server_address_book"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar_server_address_book"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways"
android:elevation="4dp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/server_list_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:listitem="@layout/row_playlist"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -6,36 +6,31 @@
android:keepScreenOn="true"
tools:context="net.schueller.peertube.activity.VideoPlayActivity">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="net.schueller.peertube.fragment.VideoPlayerFragment"
<fragment
android:id="@+id/video_player_fragment"
android:name="net.schueller.peertube.fragment.VideoPlayerFragment"
android:layout_width="match_parent"
android:layout_height="250dp" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/video_player_fragment"
android:layout_marginTop="250dp"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/video_player_fragment"
android:orientation="vertical">
<ScrollView
android:id="@+id/login_form"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:name="net.schueller.peertube.fragment.VideoMetaDataFragment"
android:id="@+id/video_meta_data_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</RelativeLayout>
<fragment
android:id="@+id/video_meta_data_fragment"
android:name="net.schueller.peertube.fragment.VideoMetaDataFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?android:colorBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:gravity="start"
android:text="@string/video_meta_title_description"
android:textSize="24sp" />
<ImageButton
android:id="@+id/video_description_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:width="24dp"
android:height="24dp"
android:background="@android:color/transparent"
android:gravity="end"
android:src="@drawable/ic_close"
app:tint="?attr/colorPrimary" />
</RelativeLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:autoLink="web"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/description"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_privacy"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_privacy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_category"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_license"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_language"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_tags"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</ScrollView>
</LinearLayout>

View File

@ -1,364 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp">
android:id="@+id/videoMetaFragment"
android:layout_height="wrap_content">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:contentDescription="@string/video_row_account_avatar"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp" />
<TextView
android:id="@+id/sl_row_name"
<!-- Related Videos -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/relatedVideosView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="24dp"
android:layout_toEndOf="@+id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
android:layout_height="match_parent">
<TextView
android:id="@+id/videoMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/sl_row_name"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="12dp"
android:layout_toEndOf="@+id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<TextView
android:id="@+id/videoOwner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/videoMeta"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:layout_toEndOf="@id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<TextView
android:id="@+id/moreButton"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_marginStart="-16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="0dp"
android:layout_toEndOf="@+id/sl_row_name"
android:background="@null"
android:contentDescription="@string/descr_overflow_button"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<LinearLayout
android:id="@+id/video_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/videoOwner"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_thumbs_up"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_thumbs_up_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_thumbs_down"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_thumbs_down_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_share"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_share_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_share"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="65dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/video_download"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:selectableItemBackground"
android:gravity="center" />
<TextView
android:id="@+id/video_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_download"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_actions"
android:layout_alignParentStart="true"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:autoLink="web"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/description"
android:layout_marginStart="18dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_privacy"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_privacy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_category"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_license"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_license"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_language"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
tools:text="@tools:sample/lorem/random"
android:lines="2"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_tags"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<Space
android:layout_width="12dp"
android:layout_height="1dp" />
<TextView
android:id="@+id/video_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|start"
tools:text="@tools:sample/lorem/random"
android:lines="2"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</LinearLayout>
</LinearLayout>
</androidx.recyclerview.widget.RecyclerView>
</RelativeLayout>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:padding="6dp"
android:id="@+id/video_title_block"
>
<RelativeLayout
android:id="@+id/video_comments_title_wrapper"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/video_comments_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_marginStart="0dp"
android:layout_marginTop="6dp"
android:text="@string/video_comments_title"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/video_comments_total_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="24dp"
android:layout_toEndOf="@+id/video_comments_title"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@android:color/transparent"
android:clickable="false"
android:contentDescription="@string/video_meta_show_description"
android:src="@drawable/ic_chevron_down"
app:tint="?attr/colorPrimary" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/video_highlighted_comment_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/video_comments_title_wrapper">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/video_highlighted_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/video_highlighted_comment"
android:layout_toEndOf="@+id/video_highlighted_avatar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:textSize="12sp"
android:layout_marginTop="0dp"
android:layout_marginEnd="6dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</RelativeLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,412 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<!-- Video Title Block -->
<RelativeLayout
android:background="?android:selectableItemBackground"
android:id="@+id/video_title_block"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="6dp"
android:paddingTop="6dp">
<RelativeLayout
android:id="@+id/video_open_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp">
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="0dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="24dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@android:color/transparent"
android:clickable="false"
android:contentDescription="@string/video_meta_show_description"
android:src="@drawable/ic_chevron_down"
app:tint="?attr/colorPrimary" />
</RelativeLayout>
<TextView
android:id="@+id/videoMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_open_description"
android:layout_alignParentEnd="true"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="6dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</RelativeLayout>
<!-- video actions -->
<HorizontalScrollView
android:id="@+id/video_actions_block"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/video_title_block"
android:paddingBottom="6dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/video_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="18dp"
android:paddingEnd="18dp"
android:paddingStart="18dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_thumbs_up_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_thumbs_up"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_thumbs_up"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_thumbs_up_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_thumbs_down_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_thumbs_down"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_thumbs_down"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_thumbs_down_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_share_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_share"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_share_2"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_share_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_share"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_download_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_download"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_download"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_download_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_meta_button_download"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_add_to_playlist_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_add_to_playlist"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_playlist_add"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/video_add_to_playlist_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_add_to_playlist"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_block_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_block"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_slash"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/vvideo_block_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_block"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="70dp"
android:id="@+id/video_flag_wrapper"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageButton
android:id="@+id/video_flag"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
android:background="?android:selectableItemBackground"
android:gravity="center"
android:clickable="false"
android:src="@drawable/ic_flag"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/vvideo_flag_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/video_flag"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>
<TextView
android:layout_below="@+id/video_actions_block"
android:id="@+id/video_action_block_line"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="?android:colorEdgeEffect"
android:height="1dp"
android:gravity="center_horizontal"/>
<RelativeLayout
android:id="@+id/video_account_block"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/video_action_block_line"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingEnd="6dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:contentDescription="@string/video_row_account_avatar"
android:paddingStart="6dp"
android:paddingEnd="6dp" />
<LinearLayout
android:id="@+id/video_creator_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="6dp"
android:layout_toEndOf="@+id/avatar"
android:orientation="vertical">
<TextView
android:id="@+id/videoOwner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/videoBy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<TextView
android:id="@+id/videoOwnerSubscribers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
</LinearLayout>
<TextView
android:id="@+id/videoOwnerSubscribeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:gravity="end"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button" />
<!-- <TextView-->
<!-- android:id="@+id/moreButton"-->
<!-- android:layout_width="45dp"-->
<!-- android:layout_height="45dp"-->
<!-- android:layout_marginStart="-16dp"-->
<!-- android:layout_marginTop="16dp"-->
<!-- android:layout_marginEnd="0dp"-->
<!-- android:layout_toEndOf="@+id/sl_row_name"-->
<!-- android:background="@null"-->
<!-- android:contentDescription="@string/descr_overflow_button"-->
<!-- android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />-->
</RelativeLayout>
<TextView
android:layout_below="@+id/video_account_block"
android:id="@+id/video_account_block_line"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="?android:colorEdgeEffect"
android:height="1dp"
android:gravity="center_horizontal"/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
card_view:cardCornerRadius="0dp"
card_view:cardElevation="0dp"
card_view:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="12dp">
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:layout_constraintTop_toTopOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
tools:text="@tools:sample/lorem"
/>
<TextView
android:id="@+id/video_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
card_view:layout_constraintTop_toBottomOf="@id/video_name"
card_view:layout_constraintStart_toStartOf="parent"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead"
tools:text="@tools:sample/lorem"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -8,7 +8,7 @@
android:background="#CC000000"
android:layoutDirection="ltr"
android:orientation="vertical"
tools:targetApi="28">
tools:targetApi="32">
<FrameLayout
android:id="@+id/exo_more_button"
@ -18,14 +18,14 @@
<TextView
android:id="@+id/exo_more"
android:layout_width="18dp"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:paddingTop="12dp"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:textColor="#FFBEBEBE"
android:textSize="12sp" />
android:textSize="18sp" />
</FrameLayout>
@ -43,27 +43,63 @@
android:gravity="center"
android:orientation="horizontal"
android:paddingTop="8dp">
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind" />
android:layout_width="72sp"
android:layout_height="52sp"
android:layout_gravity="start"
android:background="@android:color/transparent"
android:contentDescription="@string/exo_controls_rewind_description"
android:scaleType="center"
android:src="@drawable/ic_rewind" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton" />
<ImageButton
android:id="@id/exo_play"
style="@style/ExoMediaButton.Play" />
android:contentDescription="@string/exo_controls_play_description"
android:src="@drawable/ic_play"
android:layout_height="52sp"
android:layout_width="72sp"
android:scaleType="center"
android:background="@android:color/transparent"
/>
<ImageButton
android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause" />
android:contentDescription="@string/exo_controls_pause_description"
android:src="@drawable/ic_pause"
android:layout_height="52sp"
android:layout_width="72sp"
android:scaleType="center"
android:background="@android:color/transparent"
/>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward" />
android:layout_width="72sp"
android:layout_height="52sp"
android:layout_gravity="end"
android:background="@android:color/transparent"
android:contentDescription="@string/exo_controls_fastforward_description"
android:scaleType="center"
android:src="@drawable/ic_fast_forward" />
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
@ -86,9 +122,29 @@
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:layout_gravity="center"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:textColor="#FFBEBEBE"
android:paddingStart="12dp"
android:paddingEnd="2dp"
android:textColor="#FFFFFF"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#BABABA"
android:textSize="14sp"
android:includeFontPadding="false"
android:text="@string/player_time_seperator" />
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:includeFontPadding="false"
android:paddingStart="2dp"
android:paddingEnd="6dp"
android:textColor="#BABABA"
android:textSize="14sp" />
<View
@ -96,18 +152,6 @@
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:layout_gravity="center"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:textColor="#FFBEBEBE"
android:textSize="14sp" />
<FrameLayout
android:id="@+id/exo_fullscreen_button"
@ -117,13 +161,13 @@
<TextView
android:id="@+id/exo_fullscreen"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:textColor="#FFBEBEBE"
android:textSize="12sp" />
android:textSize="18sp" />
</FrameLayout>
@ -146,20 +190,20 @@
</LinearLayout>
<LinearLayout
android:visibility="gone"
android:id="@+id/exo_torrent_status"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- <LinearLayout-->
<!-- android:visibility="gone"-->
<!-- android:id="@+id/exo_torrent_status"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content">-->
<ProgressBar
android:id="@+id/torrent_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="false"
android:max="100" />
<!-- <ProgressBar-->
<!-- android:id="@+id/torrent_progress"-->
<!-- style="?android:attr/progressBarStyleHorizontal"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:indeterminate="false"-->
<!-- android:max="100" />-->
</LinearLayout>
<!-- </LinearLayout>-->
</LinearLayout>

View File

@ -345,7 +345,7 @@
<string name="pref_title_back_pause">ايقاف عند الضغط على زر العودة</string>
<string name="pref_title_buildtime">تاريخ ووقت البناء</string>
<string name="network_error">خطأ في الوصول للشبكة، تحقق من اتصالك</string>
<string name="server_selection_filter_hint">فلترة القائمة</string>
<string name="server_selection_filter_hint">تنقيح القائمة</string>
<string name="pref_background_behavior">إعدادات التشغيل في الخلفية</string>
<string name="pref_background_stop">إيقاف تشغيل الكل</string>
<string name="pref_background_audio">تابع تشغيل الصوت في الخلفية</string>
@ -361,4 +361,9 @@
<string name="server_book_add_save_button">حفظ</string>
<string name="pref_title_accept_insecure">تعطيل التحقق من شهادة SSL</string>
<string name="pref_description_accept_insecure">تجاهل الاتصالات غير الآمنة. استخدم هذا فقط إذا كنت تعرف الخادم الذي تتصل به. يتطلب إعادة تشغيل التطبيق.</string>
<string name="pref_title_video_speed">سرعة التشغيل الافتراضية</string>
<string name="pref_description_video_speed">حدد سرعة تشغيل الفيديو العامة</string>
<string name="action_bar_title_address_book">دفتر العناوين</string>
<string name="video_get_full_description_failed">تعذر جلب وصف الفيديو الكامل</string>
<string name="video_description_read_more">اقرأ المزيد</string>
</resources>

View File

@ -276,7 +276,7 @@
<string name="video_login_required_for_service">এই সেবা ব্যবহারের জন্য লগ ইন করো</string>
<string name="video_meta_button_share">শেয়ার</string>
<string name="video_meta_button_download">ডাউনলোড</string>
<string name="video_meta_button_privacy">Privacy</string>
<string name="video_meta_button_privacy"></string>
<string name="video_meta_button_category">বিভাগ</string>
<string name="video_meta_button_license">অনুমতিপত্র</string>
<string name="video_meta_button_language">ভাষা</string>
@ -295,7 +295,7 @@
<string name="account_about_description">বিস্তারিত:</string>
<string name="account_about_joined">যুক্ত হয়েছে:</string>
<string name="api_error">কিছু সমস্যা হয়েছে, অনুগ্রহ করে পরে চেষ্টা করো!</string>
<string name="action_set_url">সার্ভার পছন্দ করো</string>
<string name="action_set_url">সার্ভার পছন্দ ক</string>
<string name="server_selection_signup_allowed_yes">হ্যা</string>
<string name="server_selection_signup_allowed_no">না</string>
<string name="server_selection_peertube_server_url">পিয়ারটিউব সার্ভার URL</string>
@ -333,7 +333,7 @@
<string name="authentication_login_success">লগইন হয়েছে</string>
<string name="bn_rBD">বাংলা (বাংলাদেশ)</string>
<string name="clear_search_history_prompt">আপনি কি পুরোপুরি সার্চ ইতিহাস মুছে ফেলতে চান\?</string>
<string name="clear_search_history">সার্চ ইতিহাস মুছে ফেল</string>
<string name="clear_search_history">সার্চ ইতিহাস মুছে ফেলুন</string>
<string name="pref_background_behavior_summary">চালু ভিডিও কি করবে যখন পেছনে যাবে</string>
<string name="settings_permissions_error_float">অ্যান্ড্রয়েড সেটিং এ পিকচার ইন পিকচার পারমিশন বন্ধ আছে এই আয়াপ এ</string>
<string name="settings_api_error_float">অ্যান্ড্রয়েড ভার্সন ভাসমান ভিডিও সাপোর্ট করে</string>
@ -355,11 +355,16 @@
<string name="server_selection_filter_hint">ফিল্টার তালিকা</string>
<string name="pref_insecure_confirm_message">তুমি থোরিয়াম এর সকল এসএসএল প্রত্যয়ন বৈধতা নিষ্ক্রিয় করতে যাচ্ছ। এটি নিষ্ক্রিয় করা খুবই বিপজ্জনক হতে পারে যদি পিয়ারটিউব সার্ভারটি তোমার নিয়ন্ত্রণে না থাকে, কারণ একটি ম্যান-ইন-দ্য-মিডল আক্রমণ তোমার অজান্তেই অন্য সার্ভারে ট্রাফিক পরিচালনা করতে পারে। একজন আক্রমণকারী পাসওয়ার্ড এবং অন্যান্য ব্যক্তিগত তথ্য রেকর্ড করতে পারে।</string>
<string name="pref_description_accept_insecure">অনিরাপদ সংযোগ উপেক্ষা করো। তুমি যে সার্ভারের সাথে সংযুক্ত হচ্ছো তা জানলে তবেই এটি ব্যবহার করো। অ্যাপ পুনর্সূচনা প্রয়োজন।</string>
<string name="pref_title_accept_insecure">এসএসএল সার্টিফিকেট চেক নিষ্ক্রিয় করো</string>
<string name="pref_title_accept_insecure">এসএসএল সার্টিফিকেট চেক নিষ্ক্রিয় ক</string>
<string name="pref_insecure_confirm_yes">হ্যাঁ</string>
<string name="pref_insecure_confirm_no">না</string>
<string name="pref_insecure_confirm_title">সতর্কতা!</string>
<string name="settings_activity_advanced_category_title">উন্নত</string>
<string name="server_book_add_save_button">সংরক্ষন</string>
<string name="video_list_live_marker">লাইভ</string>
<string name="action_bar_title_address_book">ঠিকানা বই</string>
<string name="pref_title_video_speed">সহজাত গতি</string>
<string name="pref_description_video_speed">সর্বজনীন গতি নির্ধারণ করো</string>
<string name="video_description_read_more">আরও পড়ুন</string>
<string name="video_get_full_description_failed">ভিডিওর বিবরণ খুঁজে পাওয়া যায় নি</string>
</resources>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="title_activity_login">Accés</string>
<string name="action_bar_title_search">Cerca</string>
<string name="action_sign_in">Accés</string>
<string name="action_sign_in_short">Accés</string>
<string name="bottom_nav_title_discover">Inici</string>
<string name="bottom_nav_title_trending">Tendències</string>
<string name="bottom_nav_title_recent">Recent</string>
<string name="bottom_nav_title_local">Local</string>
<string name="bottom_nav_title_subscriptions">Subscripcions</string>
<string name="bottom_nav_title_account">Compte</string>
<string name="meta_data_views">" Vistes"</string>
<string name="video_row_video_thumbnail">Miniatura de vídeo</string>
<string name="video_row_account_avatar">Avatar del compte</string>
<string name="title_activity_url_video_play">UrlVideoPlayActivity</string>
<string name="search_hint">Cerca a PerrTube</string>
<string name="title_activity_search">Cerca</string>
<string name="descr_overflow_button">Més</string>
<string name="menu_share">Comparteix</string>
<string name="invalid_url">URL no vàlida</string>
<string name="pref_title_dark_mode">Mode fosc</string>
<string name="pref_title_app_theme">Tema de l\'aplicació</string>
<string name="pref_description_app_theme">Reiniciar perquè s\'activi el tema.</string>
<string name="pref_title_torrent_player">Reproductor de vídeo Torrent</string>
<string name="pref_title_license">Llicència</string>
<string name="prompt_email">Email / Nom d\'usuari</string>
<string name="title_activity_settings">Configuració</string>
<string name="prompt_server">Servidor</string>
<string name="prompt_password">Contrasenya</string>
<string name="no_data_available">Sense resultats</string>
<string name="error_invalid_password">La contrasenya és massa curta</string>
<string name="error_field_required">Aquest camp és obligatori</string>
<string name="action_bar_title_settings">Configuració</string>
<string name="action_bar_title_logout">Tanca</string>
<string name="action_bar_title_account">Compte</string>
<string name="error_invalid_email">Aquest email no és vàlid</string>
<string name="error_incorrect_password">La contrasenya és incorrecta</string>
<string name="permission_rationale">Dona permís al contacte per completar l\'email.</string>
<string name="pref_description_dark_mode">Reiniciar perquè s\'apliqui el mode obscur</string>
<string name="pref_description_torrent_player">Reproducció de vídeo per flux de torrent. Requereix permisos d\'emmagatzematge. (Alfa, no estable!)</string>
</resources>

View File

@ -356,4 +356,9 @@
<string name="pref_title_accept_insecure">SSL-Zertifikatsprüfung deaktivieren</string>
<string name="pref_description_accept_insecure">Unsichere Verbindungen ignorieren. Verwenden Sie dies nur, wenn Sie den Server kennen, mit dem Sie sich verbinden. Erfordert einen Neustart der Anwendung.</string>
<string name="video_list_live_marker">LIVE</string>
<string name="pref_title_video_speed">Standard-Wiedergabegeschwindigkeit</string>
<string name="pref_description_video_speed">Wählen Sie die globale Videowiedergabegeschwindigkeit</string>
<string name="action_bar_title_address_book">Adressbuch</string>
<string name="video_get_full_description_failed">Die vollständige Videobeschreibung konnte nicht abgerufen werden</string>
<string name="video_description_read_more">Mehr lesen</string>
</resources>

View File

@ -118,13 +118,13 @@
<string name="me_logout_button">Cerrar sesión</string>
<string name="server_book_valid_url_is_required">Una URL válida es requerida</string>
<string name="server_book_label_is_required">La etiqueta de servidor es requerida</string>
<string name="rsl">Ruso (Lenguaje de señas)</string>
<string name="rsl">Lengua de señas rusa</string>
<string name="ru">Ruso</string>
<string name="ro">Romano</string>
<string name="ro">Rumano</string>
<string name="pt">Portugués</string>
<string name="pl">Polaco</string>
<string name="no">Noruego</string>
<string name="pks">Pakistaní (Lenguaje de señas)</string>
<string name="pks">Lengua de señas de Pakistán</string>
<string name="fsl">Francés (Lenguaje de señas)</string>
<string name="fr">Francés</string>
<string name="fi">Finlandés</string>
@ -193,4 +193,111 @@
<string name="pref_description_back_pause">Pausa la reproducción de fondo al presionar atrás durante la reproducción de vídeo.</string>
<string name="video_list_live_marker">EN VIVO</string>
<string name="co">Corso</string>
<string name="nv">Navajo</string>
<string name="sl">Esloveno</string>
<string name="fj">Fiyiano</string>
<string name="cv">Chuvasio</string>
<string name="cr">Cree</string>
<string name="nl">Holandés</string>
<string name="dz">Dzongkha</string>
<string name="et">Estonio</string>
<string name="ee">Ewe</string>
<string name="hi">Hindi</string>
<string name="ho">Hiri Motu</string>
<string name="hu">Húngaro</string>
<string name="is">Islandés</string>
<string name="ga">Irlandés</string>
<string name="it">Italiano</string>
<string name="kr">Kanuri</string>
<string name="mg">Malgache</string>
<string name="ml">Malayalam</string>
<string name="mt">Maltés</string>
<string name="gv">Manés</string>
<string name="mi">Maori</string>
<string name="pa">Panyabí</string>
<string name="fa">Persa</string>
<string name="rm">Romanche</string>
<string name="sr">Serbio</string>
<string name="ta">Tamil</string>
<string name="te">Telugu</string>
<string name="bo">Tibetano</string>
<string name="ti">Tigriña</string>
<string name="ts">Tsonga</string>
<string name="tn">Tswana</string>
<string name="tr">Turco</string>
<string name="tk">Turcomano</string>
<string name="tw">Twi</string>
<string name="ug">Uigur</string>
<string name="mn">Mongol</string>
<string name="lg">Ganda</string>
<string name="xh">Xhosa</string>
<string name="th">Tailandés</string>
<string name="lb">Luxemburgués</string>
<string name="jv">Javanés</string>
<string name="kw">Córnico</string>
<string name="dv">Dhivehi</string>
<string name="fo">Feroés</string>
<string name="ff">Fula</string>
<string name="gl">Gallego</string>
<string name="ka">Georgiano</string>
<string name="de">Alemán</string>
<string name="gsg">Lengua de señas alemana</string>
<string name="gn">Guaraní</string>
<string name="gu">Guyaratí</string>
<string name="ht">Haitiano</string>
<string name="ha">Hausa</string>
<string name="he">Hebreo</string>
<string name="hz">Herero</string>
<string name="ig">Igbo</string>
<string name="id">Indonesio</string>
<string name="iu">Inuktitut</string>
<string name="ik">Inupiaq</string>
<string name="ja">Japonés</string>
<string name="jsl">Lengua de señas japonesa</string>
<string name="kl">Kalaallisut</string>
<string name="kn">Kannada</string>
<string name="ks">Cachemiro</string>
<string name="tlh">Klingon</string>
<string name="ko">Coreano</string>
<string name="ku">Kurdo</string>
<string name="lo">Lao</string>
<string name="lv">Letón</string>
<string name="kk">Kazajo</string>
<string name="km">Jémer</string>
<string name="ki">Kikuyu</string>
<string name="mk">Macedonio</string>
<string name="nb">Noruego bokmål</string>
<string name="nn">Noruego nynorsk</string>
<string name="oc">Occitano</string>
<string name="oj">Ojibwa</string>
<string name="ps">Pastún</string>
<string name="qu">Quechua</string>
<string name="sdl">Lenguaje de señas de Arabia Saudita</string>
<string name="sk">Eslovaco</string>
<string name="so">Somalí</string>
<string name="sfs">Lengua de señas sudafricana</string>
<string name="es">Español</string>
<string name="sv">Sueco</string>
<string name="swl">Lengua de señas sueca</string>
<string name="tl">Tagalo</string>
<string name="ty">Tahitiano</string>
<string name="tg">Tayiko</string>
<string name="to">Tonga (Islas Tonga)</string>
<string name="uk">Ucraniano</string>
<string name="ur">Urdu</string>
<string name="uz">Uzbeko</string>
<string name="ve">Venda</string>
<string name="vi">Vietnamita</string>
<string name="wa">Valón</string>
<string name="cy">Galés</string>
<string name="fy">Frisón occidental</string>
<string name="wo">Wolof</string>
<string name="yi">Yidis</string>
<string name="yo">Yoruba</string>
<string name="zu">Zulú</string>
<string name="pref_title_video_speed">Velocidad de reproducción por defecto</string>
<string name="pref_description_video_speed">Seleccione la velocidad global de reproducción de vídeo</string>
<string name="ms">Malayo (macrolengua)</string>
<string name="gd">Gaélico escocés</string>
<string name="sh">Serbo-croata</string>
</resources>

View File

@ -20,25 +20,25 @@
<string name="bottom_nav_title_local">محلی</string>
<string name="bottom_nav_title_subscriptions">اشتراک‌ها</string>
<string name="bottom_nav_title_account">حساب</string>
<string name="meta_data_views">" بازدیدها"</string>
<string name="video_row_video_thumbnail">تصویر ویدئو</string>
<string name="video_row_account_avatar">آواتار حساب</string>
<string name="search_hint">جستجو در پیرتیوب</string>
<string name="title_activity_search">جستجو</string>
<string name="meta_data_views">" نمایش"</string>
<string name="video_row_video_thumbnail">بندانگشتی ویدیو</string>
<string name="video_row_account_avatar">چهرک حساب</string>
<string name="search_hint">جست‌وجوی پیرتیوب</string>
<string name="title_activity_search">جست‌وجو</string>
<string name="no_data_available">بدون نتیجه</string>
<string name="descr_overflow_button">بیشتر</string>
<string name="descr_overflow_button">بیشتر</string>
<string name="menu_share">هم‌رسانی</string>
<string name="invalid_url">نشانی نامعتبر</string>
<string name="invalid_url">نشانی نامعتبر.</string>
<string name="pref_title_dark_mode">حالت تاریک</string>
<string name="pref_description_dark_mode">برای اعمال حالت تاریک، برنامه را از اول راه‌اندازی کنید.</string>
<string name="pref_title_app_theme">سبک برنامه</string>
<string name="pref_description_app_theme">برای اعمال سبک، برنامه را از اول راه‌اندازی کنید</string>
<string name="pref_title_torrent_player">پخش‌کننده ویدئوی تورنت</string>
<string name="pref_title_app_theme">زمینهٔ کاره</string>
<string name="pref_description_app_theme">برای تأثیر گذاشتن زمینه، کاره را دوباره آغاز کنید.</string>
<string name="pref_title_torrent_player">پخش‌کنندهٔ ویدیوی تورنت</string>
<string name="pref_title_license">پروانه</string>
<string name="pref_title_version">نسخه</string>
<string name="pref_title_show_nsfw">محتوا NSFW</string>
<string name="pref_title_version">نگارش</string>
<string name="pref_title_show_nsfw">محتوای NSFW</string>
<string name="pref_description_show_nsfw">نمایش محتوای NSFW</string>
<string name="pref_language">صافی زبان</string>
<string name="pref_language">پالایهٔ زبان</string>
<string name="pref_title_peertube_server">کارساز پیرتیوب</string>
<string name="pref_title_background_play">پخش در پس‌زمینه</string>
<string name="menu_video_more_report">گزارش</string>
@ -89,7 +89,7 @@
<string name="fr">فرانسوی</string>
<string name="fi">فنلاندی</string>
<string name="en">انگلیسی</string>
<string name="as"/>
<string name="as">آسامی</string>
<string name="da">دانمارکی</string>
<string name="zh">چینی</string>
<string name="bg">بلغاری</string>
@ -98,23 +98,109 @@
<string name="az">آذربایجانی</string>
<string name="hy">ارمنی</string>
<string name="ar">عربی</string>
<string name="pref_description_background_play">در صورت فعال بودن، پخش ویدئو در پس‌زمینه ادامه می‌یابد.</string>
<string name="pref_description_language">به جای نشان دادن همه ویدئه تحت همه زبان‌ها، یک زبان برای ویدئو انتخاب کنید.</string>
<string name="pref_description_background_play">اگر به کار افتاده باشد، پخش ویدیو را در پس‌زمینه ادامه می‌دهد.</string>
<string name="pref_description_language">به جای نمایش تمامی ویدیوها به همهٔ زبان‌ها، زبانی برای ویدیو برگزینید.</string>
<string name="pref_description_license">
\n<b>پروانه عمومی همگانی آفرو نسخه ۳ AGPLv3</b>
\n
\nمجوزهای این پروانه که قوی‌ترین پروانه کپی‌لفت است مشروط به دردسترس قرار دادن کامل کد منبع کارهای تحت پروانه و نسخه‌های تغییریافته‌شان است که شامل کارهای بزرگ‌تری که تحت همین پروانه × از این کار استفاده می‌کنند می‌شود. تذکر پروانه و کپی‌رایت باید محفوظ بماند. مشارکت‌کنندگان باید واگذاری حقوق پتنت را اعلام کنند. وقتی نسخه تغییر یافته برای ارائه خدمت روی شبکه استفاده شود، کد منبع نسخه تغییر یافته بایستی به صورت کامل دردسترس قرار بگیرد.</string>
<string name="pref_description_torrent_player">پخش ویدئو از طریق جریان تورنت. این ویژگی، نیازمند مجور دسترسی به فضای ذخیره‌سازی است. (آلفا، ناپایدار!)</string>
<string name="pref_description_torrent_player">پخش ویدیو با جریان تورنت. این ویژگی، نیازمند اجازهٔ ذخیره‌سازی است. (آلفا، ناپایدار!)</string>
<string name="bottom_nav_title_discover">نوار ناوبری پایین</string>
<string name="settings_api_error_float">نسخه اندروید از ویدئوی شناور پشتیبانی نمی کند</string>
<string name="pref_background_behavior">پیکربندی پخش در پس زمینه</string>
<string name="pref_background_float">پخش ویدیو را در پنجره شناور ادامه دهید</string>
<string name="pref_background_stop">تمام پخش را متوقف کنید</string>
<string name="pref_background_audio">به عنوان جریان صوتی پس زمینه ادامه دهید</string>
<string name="pref_description_language_app">انتخاب زبان برای رابط برنامه برای اعمال تغییرات ، برنامه را مجدداً راه اندازی کنید.</string>
<string name="settings_api_error_float">نگارش اندروید از ویدیوی شناور پشتیبانی نمی‌کند</string>
<string name="pref_background_behavior">پیکربندی پخش پس‌زمینه</string>
<string name="pref_background_float">ادامهٔ پخش ویدیو در پنجرهٔ شناور</string>
<string name="pref_background_stop">توقّف تمامی پخش</string>
<string name="pref_background_audio">ادامه به شکل جریان صوتی پس‌زمینه</string>
<string name="pref_description_language_app">گزینش زبان رابط برنامه.برای تأثیر گذاشتن تغییرات، کاره را دوباره آغاز کنید.</string>
<string name="pref_language_app">زبان برنامه</string>
<string name="pref_description_back_pause">هنگام فشار دادن به عقب در حین پخش ویدئو ، پخش پس زمینه را متوقف کنید.</string>
<string name="pref_title_back_pause">دکمه پشت مکث</string>
<string name="title_activity_url_video_play">فعالیت پخش ویدئو Url</string>
<string name="permission_rationale">برای تکمیل ایمیل مجوز تماس بگیرید.</string>
<string name="pref_description_back_pause">مکث پخش پس‌زمینه هنگام فشردن بازگشت حین پخش ویدیو.</string>
<string name="pref_title_back_pause">مکث با دکمهٔ بازگشت</string>
<string name="title_activity_url_video_play">UrlVideoPlayActivity</string>
<string name="permission_rationale">اعطای اجازهٔ آشنا برای تکمیل رایانامه.</string>
<string name="pref_title_video_speed">سرعت پخش پیش‌گزیده</string>
<string name="pref_description_video_speed">گزینش سرعت پخش ویدیوی عمومی</string>
<string name="network_error">خطای دسترسی به شبکه. لطفاً اتّصالتان را بررسی کنید</string>
<string name="login_current_server_hint">کارساز کنونی</string>
<string name="server_book_add_save_button">ذخیره</string>
<string name="authentication_token_refresh_failed">نتوانست ژتون را تازه کند</string>
<string name="clear_search_history_prompt">می خواهید تاریخچهٔ جست‌وجو را برای همیشه حذف کنید؟</string>
<string name="server_selection_video_totals" formatted="false">ویدیوها: %s* ویدیوهای محلّی: %s</string>
<string name="pref_description_accept_insecure">چشم‌پوشی از اتّصال‌های ناامن. فقط اگر کارسازی که به آن وصل می‌شوید را می‌شناسید، از این گزینه استفاده کنید. نیاز به آغاز دوبارهٔ کاره.</string>
<string name="video_description_read_more">خواندن بیش‌تر</string>
<string name="authentication_login_success">وارد شده</string>
<string name="pref_title_accept_insecure">از کار انداختن بررسی گواهینامهٔ SSL</string>
<string name="authentication_token_refresh_success">ژتون تازه شد</string>
<string name="menu_video_options_quality_automated">خودکار</string>
<string name="server_selection_nsfw_instance">نمونهٔ ن‌م‌ب‌ک</string>
<string name="settings_activity_about_category_title">درباره</string>
<string name="title_activity_select_server">کارساز جست‌وجو</string>
<string name="video_get_full_description_failed">گرفتن شرح کامل ویدیو شکست خورد</string>
<string name="server_book_del_alert_title">برداشتن کارساز</string>
<string name="video_list_live_marker">زنده</string>
<string name="server_book_label_is_required">نیازمند برچسب کارساز</string>
<string name="pref_insecure_confirm_yes">بله</string>
<string name="pref_insecure_confirm_message">داید تمامی تأییدیه‌های گواهینامهٔ SSL را در توریوم از کار می‌اندازید. از کار انداختن این گزینه هنگامی که کارساز تحت کنترلتان ینست، می‌تواند بسیار خطرناک باشد. چرا که یک حملهٔ مرد میانی می‌تواند شدآمد را به بدون اطَلاعتان به کارسازی دیگر هدایت کند. حمله‌کننده می‌تواند گذرواژه‌ها و دیگر داده‌های شخصیتان را ضبط کند.</string>
<string name="settings_activity_advanced_category_title">پیش‌رفته</string>
<string name="pref_insecure_confirm_title">هشدار!</string>
<string name="pref_title_buildtime">زمان ساخت</string>
<string name="settings_activity_look_and_feel_category_title">ظاهر</string>
<string name="settings_activity_video_playback_category_title">پخش ویدیو</string>
<string name="settings_activity_video_list_category_title">فهرست ویدیو</string>
<string name="server_book_del_alert_msg">مطمئنید که می‌خواهید این کارساز را از دفتر نشانی بردارید؟</string>
<string name="video_speed_125">۱٫۲۵×</string>
<string name="video_speed_075">۰٫۷۵×</string>
<string name="title_activity_server_address_book">دفتر نشانی</string>
<string name="server_book_no_servers_found">دفترچهٔ کارساز خالیست</string>
<string name="authentication_login_failed">ورود شکست خورد!</string>
<string name="action_bar_title_address_book">دفتر نشانی</string>
<string name="video_meta_button_privacy">محرمانگی</string>
<string name="action_bar_title_server_selection">گزینش کارساز</string>
<string name="video_speed_20">۲×</string>
<string name="video_meta_button_download">بارگیری</string>
<string name="video_meta_button_share">هم‌رسانی</string>
<string name="video_login_required_for_service">برای استفاده از این خدمت باید وارد شوید</string>
<string name="server_selection_filter_hint">سیاههٔ پالایه</string>
<string name="purple">ارغوانی</string>
<string name="server_selection_set_server">کارساز تنظیم شده به: %s</string>
<string name="server_selection_signup_allowed_no">نه</string>
<string name="video_speed_15">۱٫۵×</string>
<string name="video_speed_05">۰٫۵×</string>
<string name="amber">کهربایی</string>
<string name="teal">سبز دودی</string>
<string name="indigo">لاجوردی</string>
<string name="deeppurple">ارغوانی تیره</string>
<string name="pink">صورتی</string>
<string name="red">قرمز</string>
<string name="pref_background_behavior_summary">چگونگی واکنش پخش ویدیو هنگام رفتن به پس‌زمینه</string>
<string name="clear_search_history">پاک‌سازی تاریخچهٔ جست‌وجو</string>
<string name="af">آفریقایی</string>
<string name="title_activity_me">حساب</string>
<string name="server_book_list_has_login">دارای ورود</string>
<string name="server_book_add_add_button">افزودن</string>
<string name="server_book_add_password">گذرواژه</string>
<string name="server_book_add_username">نام کاربری</string>
<string name="server_book_add_pick_server_button">جست‌وجو</string>
<string name="server_book_add_server_url">نشانی کارساز</string>
<string name="server_book_add_label">برچسب</string>
<string name="me_help_and_feedback_button">یاری و بازخورد</string>
<string name="me_logout_button">خروج</string>
<string name="server_book_valid_url_is_required">نیازمند نشانی اینترنتی معتبر</string>
<string name="api_error">چیزی اشتباه پیش رفت. لطفاً بعداً دوباره تلاش کنید!</string>
<string name="account_about_joined">پیوسته:</string>
<string name="account_about_description">شرح:</string>
<string name="account_about_subscribers">مشترکان:</string>
<string name="account_about_account">حساب:</string>
<string name="account_bottom_menu_about">درباره</string>
<string name="account_bottom_menu_channels">کانال‌ها</string>
<string name="account_bottom_menu_videos">ویدیوها</string>
<string name="menu_video_options_quality" formatted="true">کیفیت (%1$s)</string>
<string name="menu_video_options_playback_speed" formatted="true">سرعت پخش (%1$s)</string>
<string name="video_meta_button_tags">برچسب‌ها</string>
<string name="video_meta_button_language">زبان</string>
<string name="video_meta_button_license">پروانه</string>
<string name="video_meta_button_category">دسته</string>
<string name="video_rating_failed">رتبه‌بندی شکست خورد</string>
<string name="video_download_permission_error">نمی‌توان بدون دسترسی نوشتن، ویدیو بارگیری کرد</string>
<string name="menu_video_more_blacklist">فهرست سیاه</string>
<string name="pref_insecure_confirm_no">نه</string>
</resources>

View File

@ -356,4 +356,9 @@
<string name="server_book_add_pick_server_button">Hae</string>
<string name="server_book_add_label">Leima</string>
<string name="video_list_live_marker">SUORA</string>
<string name="pref_title_video_speed">Oletusarvoinen toistonopeus</string>
<string name="pref_description_video_speed">Valitse yleinen videon toistonopeus</string>
<string name="action_bar_title_address_book">Osoitekirja</string>
<string name="video_get_full_description_failed">Koko videon kuvauksen saaminen epäonnistui</string>
<string name="video_description_read_more">Lue lisää</string>
</resources>

View File

@ -171,7 +171,7 @@
<string name="de">allemand</string>
<string name="gsg">langue des signes allemande</string>
<string name="gn">guarani</string>
<string name="gu">goudjarâtî</string>
<string name="gu">goudjarati</string>
<string name="ht">haïtien</string>
<string name="ha">haoussa</string>
<string name="he">hébreu</string>
@ -361,4 +361,9 @@
<string name="pref_title_accept_insecure">Désactiver la vérification des certificats SSL</string>
<string name="pref_description_accept_insecure">Ignorer les connexions non sécuritaires. Utiliser ceci seulement si vous connaissez le serveur sur lequel vous vous connectez. Requiert un redémarrage de l\'application.</string>
<string name="video_list_live_marker">DIRECT</string>
<string name="pref_title_video_speed">Vitesse de lecture par défaut</string>
<string name="pref_description_video_speed">Sélectionnez la vitesse globale de lecture vidéo</string>
<string name="action_bar_title_address_book">Carnet d\'adresses</string>
<string name="video_get_full_description_failed">Impossible de récupérer la description complète de la vidéo</string>
<string name="video_description_read_more">Lire la suite</string>
</resources>

View File

@ -297,4 +297,5 @@
<string name="api_error">Chaidh rudeigin ceàrr, feuch ris a-rithist an ceann greis!</string>
<string name="action_bar_title_server_selection">Tagh frithealaiche</string>
<string name="login_current_server_hint">Am frithealaiche làithreach</string>
<string name="bottom_nav_title_discover">Foir-shealladh</string>
</resources>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -131,7 +131,7 @@
<string name="video_speed_075">0.75x</string>
<string name="title_activity_server_address_book">Buku Alamat</string>
<string name="login_current_server_hint">Server Saat Ini</string>
<string name="server_book_list_has_login">Memiliki Login</string>
<string name="server_book_list_has_login">Telah Login</string>
<string name="server_book_add_save_button">Simpan</string>
<string name="server_book_add_add_button">Tambah</string>
<string name="server_book_add_password">Kata sandi</string>
@ -265,7 +265,7 @@
<string name="os">Ossetia</string>
<string name="om">Oromo</string>
<string name="or">Oriya (bahasa makro)</string>
<string name="teal">Teal</string>
<string name="teal">Sian Hijau</string>
<string name="sfs">Bahasa Isyarat Afrika Selatan</string>
<string name="so">Somalia</string>
<string name="sl">Slovenia</string>
@ -281,5 +281,66 @@
<string name="sc">Sardinia</string>
<string name="sg">Sango</string>
<string name="sm">Samoa</string>
<string name="bluegray">Bluegray</string>
<string name="bluegray">Abu-abu Biru</string>
<string name="ka">Georgia</string>
<string name="de">Jerman</string>
<string name="ik">Iñupiat (Alaska)</string>
<string name="ja">Jepang</string>
<string name="jv">Jawa (Indonesia)</string>
<string name="km">Kamboja</string>
<string name="lb">Luksembur</string>
<string name="mk">Macedonia</string>
<string name="mg">Madagaskar</string>
<string name="co">Korsika</string>
<string name="dz">Bhutan</string>
<string name="ff">Senegambia</string>
<string name="lg">Ganda</string>
<string name="gu">Gujarat (India)</string>
<string name="ht">Haiti</string>
<string name="ha">Hausa (Afro-Asia)</string>
<string name="he">Ibrani</string>
<string name="hz">Herero - Afrika Selatan</string>
<string name="hi">Hindi (India Utara)</string>
<string name="iu">Inuit (Kanada)</string>
<string name="ga">Irlandia</string>
<string name="it">Italia</string>
<string name="ms">Melayu</string>
<string name="mn">Mongolia</string>
<string name="el">Yunani Modern</string>
<string name="pref_description_license">
\n<b>Lisensi Publik Umum GNU Affero v3.0</b>
\n
\nIzin dari lisensi copyleft ini dikondisikan untuk menyediakan kode sumber yang lengkap dari karya berlisensi dan modifikasi, yang mencakup karya yang lebih besar menggunakan karya berlisensi, di bawah lisensi yang sama. Hak cipta dan pemberitahuan lisensi harus dipertahankan. Kontributor memberikan hibah hak paten secara tegas. Ketika versi modifikasi digunakan untuk menyediakan layanan melalui jaringan, kode sumber lengkap dari versi modifikasi harus tersedia.</string>
<string name="pref_background_audio">Lanjutkan stream suara di latar belakang</string>
<string name="gn">Guarani (Paraguai)</string>
<string name="pref_insecure_confirm_message">Anda akan menonaktifkan semua validasi Sertifikasi SSL di Thorium. Menonaktifkannya bisa sangat berbahaya jika server peertube tidak berada di bawah kendali Anda, karena serangan man-in-the-middle dapat mengarahkan lalu lintas ke server lain tanpa sepengetahuan Anda. Seorang penyerang dapat merekam kata sandi dan data pribadi lainnya.</string>
<string name="ki">Kikuyu (Kenya)</string>
<string name="rw">Rwanda</string>
<string name="tlh">StarTrek</string>
<string name="lv">Latvia</string>
<string name="ko">Korea</string>
<string name="lt">Lithuania</string>
<string name="kv">Komi (Rusia)</string>
<string name="kg">Kongo</string>
<string name="avk">Kotava</string>
<string name="am">Ethiopia</string>
<string name="av">Kaukasian</string>
<string name="ho">Papua Nugini</string>
<string name="is">Islandia</string>
<string name="ig">Igbo (Nigeria)</string>
<string name="id">Indonesia</string>
<string name="jsl">Bahasa Isyarat Jepang</string>
<string name="kl">Greenland</string>
<string name="kn">Kannada (barat daya India)</string>
<string name="kr">Kanuri (Nigeria)</string>
<string name="ks">Kasmir (India)</string>
<string name="kk">Kazakhstan</string>
<string name="pref_title_video_speed">Kecepatan putar standar</string>
<string name="pref_description_video_speed">Pilih secara umum kecepatan putar standar video</string>
<string name="pref_description_accept_insecure">Acuhkan koneksi yang tidak aman. Gunakan ini jika Anda tahu peladan yang anda tuju. Muat Ulang Aplikasi.</string>
<string name="dv">Maldiva</string>
<string name="gl">Galisia (barat laut Spanyol)</string>
<string name="hu">Hungaria</string>
<string name="ky">Kirgistan</string>
<string name="server_selection_nsfw_instance">Saluran NSFW</string>
</resources>

View File

@ -356,4 +356,9 @@
<string name="settings_activity_advanced_category_title">Avanzato</string>
<string name="server_book_add_save_button">Salva</string>
<string name="pref_description_accept_insecure">Ignora le connessioni insicure. Usalo solo se conosci il server a cui ti stai connettendo. Richiede il riavvio dell\'applicazione.</string>
<string name="action_bar_title_address_book">Rubrica</string>
<string name="pref_title_video_speed">Velocità di riproduzione predefinita</string>
<string name="pref_description_video_speed">Seleziona la velocità globale di riproduzione video</string>
<string name="video_get_full_description_failed">Impossibile recuperare la descrizione completa del video</string>
<string name="video_description_read_more">Leggi di più</string>
</resources>

View File

@ -60,13 +60,13 @@
<string name="account_about_subscribers">Abonnenter:</string>
<string name="account_about_description">Beskrivelse:</string>
<string name="account_about_joined">Tok del:</string>
<string name="api_error">Noe gikk galt, prøv igjen senere.</string>
<string name="api_error">Noe gikk galt, prøv igjen senere!</string>
<string name="permission_rationale">Innvilg kontakttilgang for fullføring av e-postadresser.</string>
<string name="bottom_nav_title_trending">Populært</string>
<string name="meta_data_views">" visninger"</string>
<string name="meta_data_views">" Visninger"</string>
<string name="video_row_video_thumbnail">Video-miniatyrbilde</string>
<string name="pref_title_show_nsfw">VOKSENT innhold</string>
<string name="pref_description_show_nsfw">Vis VOKSENT innhold</string>
<string name="pref_title_show_nsfw">innhold upassende på jobb</string>
<string name="pref_description_show_nsfw">Vis innhold som er upassende på jobb</string>
<string name="pref_language">Språkfilter</string>
<string name="pref_description_language">Velg videospråk, istedenfor å vise alle videoer på alle språk.</string>
<string name="title_activity_url_video_play">UrlVideoPlayActivity</string>
@ -87,7 +87,7 @@
<string name="pref_title_dark_mode">Mørkt modus</string>
<string name="pref_description_dark_mode">Start programmet for å ikle mørk drakt.</string>
<string name="pref_title_app_theme">Programdrakt</string>
<string name="pref_description_app_theme">Start programmet på ny for å ikle drakt.</string>
<string name="pref_description_app_theme">Start programmet på nytt for å ikle drakt.</string>
<string name="indigo">Indigo</string>
<string name="cyan">Turkis</string>
<string name="lime">Lime</string>
@ -95,7 +95,7 @@
<string name="video_speed_05">0,5×</string>
<string name="video_speed_15">1,5×</string>
<string name="video_speed_20">2×</string>
<string name="pref_description_background_play">Hvis påskrudd vil videoer spilles videre i bakgrunnen.</string>
<string name="pref_description_background_play">Video spiller videre i bakgrunnen om dette aktiveres.</string>
<string name="video_login_required_for_service">Du må logge inn for å bruke denne tjenesten</string>
<string name="ko">Koreansk</string>
<string name="action_set_url">Velg tjener</string>
@ -103,7 +103,7 @@
<string name="server_selection_signup_allowed_yes">Ja</string>
<string name="server_selection_signup_allowed_no">Nei</string>
<string name="server_selection_set_server">Tjener satt til: %s</string>
<string name="server_selection_select_a_server">Velg en tjener fra listen, eller skriv den inn direkte</string>
<string name="server_selection_select_a_server">Velg en tjener fra listen under, eller skriv inn tjenernavn direkte.</string>
<string name="server_selection_peertube_server_url">PeerTube tjenernettadresse</string>
<string name="action_bar_title_server_selection">Velg tjener</string>
<string name="da">Dansk</string>
@ -132,14 +132,14 @@
<string name="pref_background_behavior">Oppsett av bakgrunnsavspilling</string>
<string name="pref_background_float">Fortsett video|avspilling i flytende vindu</string>
<string name="pref_description_back_pause">Opphold i bakgrunnsavspilling når \"Tilbake\" trykkes under videoavspilling.</string>
<string name="pref_title_back_pause">Sett på pause med \"Tilbake\"-knapp</string>
<string name="pref_title_back_pause">Pause når tilbake-knappen brukes</string>
<string name="pref_background_stop">Stopp all avspilling</string>
<string name="pref_background_audio">Fortsett som lydstrøm i bakgrunnen</string>
<string name="pref_description_language_app">Velg språk for programgrensesnittet. Start programmet på ny for å utføre endringene.</string>
<string name="pref_language_app">Programspråk</string>
<string name="server_book_add_server_url">Tjener-nettadresse</string>
<string name="server_book_add_pick_server_button">Søk</string>
<string name="server_book_add_username">Username</string>
<string name="server_book_add_username">Brukernavn</string>
<string name="server_book_add_add_button">Legg til</string>
<string name="server_book_add_password">Passord</string>
<string name="title_activity_server_address_book">Adressebok</string>
@ -164,7 +164,7 @@
<string name="me_logout_button">Logg ut</string>
<string name="server_book_valid_url_is_required">Gyldig nettadresse kreves</string>
<string name="server_book_label_is_required">Tjeneretikett kreves</string>
<string name="authentication_login_failed">Kunne ikke logge inn</string>
<string name="authentication_login_failed">Kunne ikke logge inn!</string>
<string name="authentication_login_success">Innlogget</string>
<string name="hello_blank_fragment">Hei blanke fragment</string>
<string name="network_error">Tilknytningsfeil, sjekk tilkoblingen din</string>
@ -185,14 +185,25 @@
<string name="gu">gujarati</string>
<string name="bn">bengalsk</string>
<string name="hy">armensk</string>
<string name="server_selection_nsfw_instance">Arbeidsutrygg insans</string>
<string name="server_selection_nsfw_instance">jobb-upassende instans</string>
<string name="pref_insecure_confirm_yes">Ja</string>
<string name="pref_insecure_confirm_no">Nei</string>
<string name="pref_insecure_confirm_title">Advarsel!</string>
<string name="settings_activity_advanced_category_title">Avansert</string>
<string name="server_book_add_save_button">Lagre</string>
<string name="pref_insecure_confirm_message">Du er i ferd med å skru av all SSL-sertifisering i Thorium. Å skru av dette kan være veldig farlig hvis Peertube ikke er under din kontroll, fordi mellommanns-angrep kan sende trafikk til en annen tjener uten at du vet det. En angriper kan da se passordene når de blir brukt, og annen personlig data.</string>
<string name="pref_insecure_confirm_message">Du er i ferd med å skru av all kontroll av SSL-sertifikater i Thorium. Å skru av dette kan være veldig farlig hvis Peertube-tjeneren ikke er under din kontroll, fordi mellommanns-angrep kan styre trafikk til en annen tjener uten at du vet det. En angriper kan da registrere passord, og annen personlig data.</string>
<string name="video_list_live_marker">Sanntid</string>
<string name="pref_title_accept_insecure">Skru av SSL-sertifikatssjekk</string>
<string name="pref_description_accept_insecure">Ignorer usikre tilkoblinger. Kun bruk dette hvis du vet hvilken tjener du kobler til. Krever programomstart.</string>
<string name="pref_description_accept_insecure">Ignorer usikre tilkoblinger. Bruk kun dette hvis du vet hvilken tjener du kobler til. Krever programomstart.</string>
<string name="es">Spansk</string>
<string name="id">Indonesisk</string>
<string name="action_bar_title_address_book">Adressebok</string>
<string name="pref_title_video_speed">Forvalgt avspillingshastighet</string>
<string name="pref_description_video_speed">Velg videoavspillingshastighet for hele systemet</string>
<string name="ab">Abkhasisk</string>
<string name="video_description_read_more">Les mer</string>
<string name="video_get_full_description_failed">Kunne ikke hente komplett videobeskrivelse</string>
<string name="so">Somalisk</string>
<string name="sk">Slovakisk</string>
<string name="sfs">Sørafrikansk tegnspråk</string>
</resources>

View File

@ -318,4 +318,9 @@
<string name="pref_background_behavior">Ustawienia odtwarzania w tle</string>
<string name="pref_background_float">Kontynuuj odtwarzanie w ruchomym oknie</string>
<string name="pref_language_app">Język Aplikacji</string>
<string name="pref_title_video_speed">Domyślna prędkość odtwarzania</string>
<string name="pref_description_video_speed">Wybierz globalną szybkość odtwarzania wideo</string>
<string name="pref_title_back_pause">Wstrzymanie po naciśnięciu przycisku wstecz</string>
<string name="settings_api_error_float">Android nie wspiera odtwarzania w ruchomym oknie</string>
<string name="pref_description_language_app">Wybierz język interfejsu aplikacji. Wymaga restartu, by zmiany nabrały efektu.</string>
</resources>

View File

@ -356,4 +356,9 @@
<string name="pref_title_accept_insecure">Desativar check do certificado SSL</string>
<string name="pref_description_accept_insecure">Ignorar conexões não seguras. Use isto apenas se você conhece o servidor ao qual está se conectando. Requer o reinício do aplicativo.</string>
<string name="video_list_live_marker">AO VIVO</string>
<string name="pref_title_video_speed">Velocidade de reprodução padrão</string>
<string name="pref_description_video_speed">Selecione a velocidade global de reprodução de vídeo</string>
<string name="action_bar_title_address_book">Lista de endereços</string>
<string name="video_get_full_description_failed">Não foi possível obter a descrição completa do vídeo</string>
<string name="video_description_read_more">Leia mais</string>
</resources>

View File

@ -356,4 +356,9 @@
<string name="server_book_add_save_button">Guardar</string>
<string name="pref_title_accept_insecure">Desativar a verificação do certificado SSL</string>
<string name="pref_description_accept_insecure">Ignorar conexões inseguras. Use isto apenas se souber a qual servidor está a conectar-se. Requer reinicialização da aplicação.</string>
<string name="pref_title_video_speed">Velocidade de reprodução predefinida</string>
<string name="pref_description_video_speed">Selecione a velocidade de reprodução de vídeo global</string>
<string name="action_bar_title_address_book">Lista de contactos</string>
<string name="video_get_full_description_failed">Não foi possível obter a descrição completa do vídeo</string>
<string name="video_description_read_more">Leia mais</string>
</resources>

View File

@ -362,4 +362,9 @@
<string name="pref_insecure_confirm_message">"Вы собираетесь отключить валидацию всех SSL сертификатов в Thorium. Это может быть очень опасно если peertube сервер вами не контролируется, потому что \"атака посредника\" может направить трафик на другой сервер. Злоумышленник может записывать пароли и другие личные данные."</string>
<string name="server_book_add_save_button">Сохранить</string>
<string name="video_list_live_marker">В ЭФИРЕ</string>
<string name="action_bar_title_address_book">Адресная книга</string>
<string name="pref_title_video_speed">Скорость воспроизведения по умолчанию</string>
<string name="pref_description_video_speed">Выберите глобальную скорость воспроизведения видео</string>
<string name="video_description_read_more">Подробнее</string>
<string name="video_get_full_description_failed">Не удалось получить полное описание видео</string>
</resources>

View File

@ -1,2 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="title_activity_login">Intra</string>
<string name="prompt_password">Crae</string>
<string name="action_bar_title_search">Chirca</string>
<string name="action_bar_title_account">Contu</string>
<string name="bottom_nav_title_account">Contu</string>
<string name="action_bar_title_settings">impostatziones</string>
<string name="action_sign_in">Intra</string>
<string name="action_sign_in_short">Intra</string>
<string name="bottom_nav_title_recent">Reghente</string>
<string name="bottom_nav_title_local">Locale</string>
<string name="descr_overflow_button">Àteru</string>
<string name="menu_share">Cumpartzi</string>
<string name="pref_title_dark_mode">Modalidade iscura</string>
<string name="pref_title_app_theme">Tema de s\'aplicatzione</string>
<string name="pref_title_torrent_player">Riprodusidore de vìdeu Torrent</string>
<string name="pref_title_version">Versione</string>
<string name="pref_title_show_nsfw">Cuntenutu pro adultos</string>
<string name="pref_description_show_nsfw">Ammustra cuntenutu pro adultos</string>
<string name="pref_title_peertube_server">Serbidore PeerTube</string>
<string name="invalid_url">URL non bàlidu.</string>
<string name="pref_title_background_play">Riprodutzione in isfundu</string>
<string name="account_about_account">Contu:</string>
<string name="account_about_subscribers">Sutiscritos:</string>
<string name="server_book_add_username">Nòmine usuàriu</string>
<string name="server_book_add_password">Crae</string>
<string name="server_book_add_add_button">Annanghe</string>
<string name="settings_activity_about_category_title">A pitzu de</string>
<string name="pref_insecure_confirm_title">Atentzione!</string>
<string name="title_activity_search">Chirca</string>
<string name="title_activity_settings">Impostatziones</string>
<string name="search_hint">Chirca PeerTube</string>
<string name="prompt_server">Serbidore</string>
<string name="no_data_available">Perunu resurtadu</string>
<string name="pref_language">Filtru de limba</string>
<string name="pref_title_license">Litzèntzia</string>
<string name="pref_insecure_confirm_yes">Eja</string>
<string name="server_book_add_label">Eticheta</string>
<string name="account_about_description">Descritzione:</string>
<string name="account_bottom_menu_about">A pitzu de</string>
<string name="server_book_add_pick_server_button">Chirca</string>
<string name="server_book_add_save_button">Sarva</string>
<string name="video_speed_075">0.75x</string>
<string name="video_speed_125">1.25x</string>
<string name="title_activity_settings2">ImpostatzionesAtividade2</string>
<string name="title_activity_me">Contu</string>
<string name="pref_insecure_confirm_no">Nono</string>
<string name="video_list_live_marker">IN DIRETA</string>
</resources>

View File

@ -350,4 +350,11 @@
<string name="prompt_server">Shërbyes</string>
<string name="title_activity_login">Hyni</string>
<string name="title_activity_settings">Rregullime</string>
<string name="action_bar_title_address_book">Libër Adresash</string>
<string name="video_get_full_description_failed">Su pru dot përshkrimi i plotë i videos</string>
<string name="video_description_read_more">Lexoni Më Tepër</string>
<string name="pref_description_video_speed">Përzgjidhni Shpejtësi globale për Luajtje Videosh</string>
<string name="pref_title_video_speed">Shpejtësi Parazgjedhje Luajtjeje</string>
<string name="rm">Romansh</string>
<string name="pref_title_buildtime">Kohë Montimi</string>
</resources>

View File

@ -89,7 +89,7 @@
<string name="da">Danimarkaca</string>
<string name="dsl">Danimarka İşaret Dili</string>
<string name="dv">Maldivce</string>
<string name="nl">Flemenkçe</string>
<string name="nl">Felemenkçe</string>
<string name="dz">Dzongka</string>
<string name="en">İngilizce</string>
<string name="eo">Esperanto</string>
@ -371,4 +371,9 @@
<string name="pref_description_accept_insecure">Güvenli olmayan bağlantıları yok sayın. Bunu yalnızca bağlandığınız sunucuyu biliyorsanız kullanın. Uygulamanın yeniden başlatılmasını gerektirir.</string>
<string name="server_book_add_save_button">Kaydet</string>
<string name="video_list_live_marker">CANLI</string>
<string name="pref_title_video_speed">Öntanımlı Oynatma Hızı</string>
<string name="pref_description_video_speed">Genel Video Oynatma Hızını Seçin</string>
<string name="action_bar_title_address_book">Adres Defteri</string>
<string name="video_get_full_description_failed">Tam video açıklaması alınamadı</string>
<string name="video_description_read_more">Daha Fazla Oku</string>
</resources>

View File

@ -12,7 +12,7 @@
<string name="bottom_nav_title_discover">Огляд</string>
<string name="menu_video_options_quality_automated">Автоматизовано</string>
<string name="server_selection_video_totals" formatted="false">Відео: %s, Локальні відео: %s</string>
<string name="server_selection_nsfw_instance">Екземпляр NSFW</string>
<string name="server_selection_nsfw_instance">Сервер NSFW</string>
<string name="settings_activity_video_playback_category_title">Відтворення відео</string>
<string name="settings_activity_video_list_category_title">Список відео</string>
<string name="title_activity_settings2">SettingsActivity2</string>
@ -39,16 +39,16 @@
<string name="pref_background_behavior_summary">Поведінка відтворюваного відео після переходу в фоновий режим</string>
<string name="settings_permissions_error_float">Дозвіл на зображення в зображенні вимкнено для цього застосунку в Налаштуваннях Android</string>
<string name="settings_api_error_float">Версія Android не підтримує плаваюче відео</string>
<string name="pref_background_behavior">Налаштування відтворення у тлі</string>
<string name="pref_background_behavior">Налаштування відтворення у фоні</string>
<string name="pref_background_float">Продовжити відтворення відео у плаваючому вікні</string>
<string name="pref_background_stop">Зупинити все відтворення</string>
<string name="pref_background_audio">Продовжити фоновим аудіопотоком</string>
<string name="pref_description_language_app">Виберіть мову інтерфейсу застосунку. Перезапустіть застосунок, щоб зміни набули чинності.</string>
<string name="pref_language_app">Мова застосунку</string>
<string name="pref_description_back_pause">Зупиняти відтворення в тлі натисканням кнопки назад під час відтворення відео.</string>
<string name="pref_description_back_pause">Зупиняти відтворення у фоні натисканням кнопки назад під час відтворення відео.</string>
<string name="pref_title_back_pause">Зупиняти кнопкою назад</string>
<string name="pref_description_background_play">Якщо ввімкнено, продовжує відтворювати відео у фоновому режимі.</string>
<string name="pref_title_background_play">Відтворення в тлі</string>
<string name="pref_title_background_play">Відтворення у фоні</string>
<string name="pref_title_peertube_server">Сервер PeerTube</string>
<string name="pref_description_language">Виберіть мову відео, замість перегляду всіх відео всіма мовами.</string>
<string name="pref_language">Фільтр мов</string>
@ -115,7 +115,7 @@
<string name="pink">Рожевий</string>
<string name="red">Червоний</string>
<string name="zu">зулуська</string>
<string name="za">чуанг</string>
<string name="za">чжуанська</string>
<string name="yo">йоруба</string>
<string name="yi">ідиш</string>
<string name="xh">коса</string>
@ -312,7 +312,7 @@
<string name="pref_title_torrent_player">Торрент-програвач відео</string>
<string name="pref_description_app_theme">Перезапустіть застосунок, щоб темний режим набув чинності.</string>
<string name="pref_title_app_theme">Тема застосунку</string>
<string name="pref_description_dark_mode">Перезапустіть програму, щоб темний режим набув чинності.</string>
<string name="pref_description_dark_mode">Перезапустіть застосунок, щоб темний режим набув чинності.</string>
<string name="pref_title_dark_mode">Темний режим</string>
<string name="invalid_url">Недійсна URL-адреса.</string>
<string name="menu_share">Поділитися</string>
@ -325,9 +325,9 @@
<string name="video_row_video_thumbnail">Ескіз відео</string>
<string name="meta_data_views">" переглядів"</string>
<string name="bottom_nav_title_subscriptions">Підписки</string>
<string name="bottom_nav_title_local">Локальне</string>
<string name="bottom_nav_title_recent">Нещодавнє</string>
<string name="bottom_nav_title_trending">Популярне</string>
<string name="bottom_nav_title_local">Локальні</string>
<string name="bottom_nav_title_recent">Нещодавні</string>
<string name="bottom_nav_title_trending">Популярні</string>
<string name="action_bar_title_search">Шукати</string>
<string name="permission_rationale">Надайте доступ до контактів для заповнення електронної пошти.</string>
<string name="prompt_password">Пароль</string>
@ -343,12 +343,12 @@
<string name="title_activity_me">Обліковий запис</string>
<string name="action_bar_title_account">Обліковий запис</string>
<string name="action_bar_title_logout">Вийти</string>
<string name="action_bar_title_settings">Параметри</string>
<string name="action_bar_title_settings">Налаштування</string>
<string name="prompt_server">Сервер</string>
<string name="title_activity_login">Увійти</string>
<string name="title_activity_settings">Параметри</string>
<string name="title_activity_settings">Налаштування</string>
<string name="pref_title_accept_insecure">Вимкнути перевірку SSL сертифіката</string>
<string name="pref_description_accept_insecure">Ігнорувати незахищені з\'єднання. Використовуйте це лише якщо знаєте сервер до якого підключаєтесь. Перезапустіть застосунок, щоб зміни набули чинності.</string>
<string name="pref_description_accept_insecure">Ігнорувати незахищені з\'єднання. Використовуйте лише якщо знаєте сервер до якого підʼєднуєтесь. Перезапустіть застосунок, щоб зміни набули чинності.</string>
<string name="pref_insecure_confirm_yes">Так</string>
<string name="pref_insecure_confirm_no">Ні</string>
<string name="pref_insecure_confirm_title">Увага!</string>
@ -356,4 +356,9 @@
<string name="settings_activity_advanced_category_title">Додатково</string>
<string name="server_book_add_save_button">Зберегти</string>
<string name="video_list_live_marker">НАЖИВО</string>
<string name="pref_title_video_speed">Типова швидкість відтворення</string>
<string name="pref_description_video_speed">Вибрати загальну швидкість відтворення відео</string>
<string name="action_bar_title_address_book">Адресна книга</string>
<string name="video_get_full_description_failed">Не вдалося отримати повний опис відео</string>
<string name="video_description_read_more">Детальніше</string>
</resources>

View File

@ -127,7 +127,7 @@
<string name="server_selection_select_a_server">从下面的列表选择一个服务器或者手动输入。</string>
<string name="server_selection_peertube_server_url">PeerTube 服务器 URL</string>
<string name="action_bar_title_server_selection">选择服务器</string>
<string name="login_current_server_hint">现在的服务器</string>
<string name="login_current_server_hint">当前的服务器</string>
<string name="video_speed_075">0.75倍速</string>
<string name="video_speed_125">1.25倍速</string>
<string name="pt">葡萄牙语</string>
@ -308,4 +308,33 @@
<string name="ro">罗马尼亚语</string>
<string name="qu">克丘亚语</string>
<string name="ps">普什图语</string>
<string name="me_help_and_feedback_button">帮助与反馈</string>
<string name="authentication_login_failed">登录失败!</string>
<string name="server_selection_filter_hint">过滤列表</string>
<string name="pref_title_video_speed">默认播放速度</string>
<string name="settings_activity_about_category_title">关于</string>
<string name="server_book_del_alert_msg">您确定您想从地址薄中移除该服务器吗?</string>
<string name="pref_description_video_speed">选择全局视频播放速度</string>
<string name="server_book_add_label">标签</string>
<string name="server_book_add_server_url">服务器 URL</string>
<string name="server_book_add_pick_server_button">搜索</string>
<string name="server_book_add_username">用户名</string>
<string name="server_book_add_password">密码</string>
<string name="server_book_add_add_button">添加</string>
<string name="server_book_add_save_button">保存</string>
<string name="server_book_list_has_login">已登录</string>
<string name="title_activity_server_address_book">地址薄</string>
<string name="server_book_label_is_required">服务器标签必填</string>
<string name="network_error">网络访问错误,请检查您的网络连接</string>
<string name="authentication_login_success">已登录</string>
<string name="me_logout_button">退出登录</string>
<string name="server_book_del_alert_title">移除服务器</string>
<string name="title_activity_select_server">搜索服务器</string>
<string name="title_activity_me">账号</string>
<string name="settings_activity_video_playback_category_title">视频播放</string>
<string name="server_book_valid_url_is_required">有效的 URL 必填</string>
<string name="settings_activity_video_list_category_title">视频列表</string>
<string name="pref_insecure_confirm_yes"></string>
<string name="pref_insecure_confirm_no"></string>
<string name="video_list_live_marker">直播</string>
</resources>

View File

@ -356,4 +356,9 @@
<string name="pref_description_accept_insecure">忽略不安全的連線。僅在您了解您要連線的伺服器時才使用此選項。需要重新啟動應用程式。</string>
<string name="server_book_add_save_button">儲存</string>
<string name="video_list_live_marker">直播</string>
<string name="pref_title_video_speed">預設播放速度</string>
<string name="pref_description_video_speed">選取全域影片播放速度</string>
<string name="action_bar_title_address_book">通訊錄</string>
<string name="video_get_full_description_failed">無法擷取完整影片描述</string>
<string name="video_description_read_more">閱讀更多資訊</string>
</resources>

View File

@ -58,12 +58,12 @@
<string name="video_download_icon" translatable="false">{faw-download}</string>
<string name="video_save_icon" translatable="false">{faw-save}</string>
<string name="meta_data_owner_seperator" translatable="false">\@</string>
<string name="meta_data_seperator" translatable="false">\u0020-\u0020</string>
<string name="title_activity_video_play" translatable="false">VideoPlayActivity</string>
<string name="playback_channel_name" translatable="false">PeerTube</string>
<string name="playback_channel_description" translatable="false">playback_channel</string>
<string name="peertube_instance_search_default_description" translatable="false">PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.</string>

View File

@ -364,6 +364,28 @@
<string name="pref_insecure_confirm_yes">Yes</string>
<string name="pref_insecure_confirm_message">You are about the disable all SSL Certification validation in Thorium. Disabling this can be very dangerous if the peertube server is not under your control, because a man-in-the-middle attack could direct traffic to another server without your knowledge. An attacker could record passwords and other personal data.</string>
<string name="video_list_live_marker">LIVE</string>
<string name="video_get_full_description_failed">Getting full video description failed</string>
<string name="video_get_full_description_failed">Could not fetch the full video description</string>
<string name="video_description_read_more">Read More</string>
<string name="player_time_seperator">/</string>
<string name="video_meta_show_description">Show Description</string>
<string name="video_meta_title_description">Description</string>
<string name="video_add_to_playlist">Save</string>
<string name="video_block">Block</string>
<string name="video_flag">Flag</string>
<string name="video_feature_not_yet_implemented">This feature has not yet been implemented. Coming soon!</string>
<string name="subscribe">Subscribe</string>
<string name="unsubscribe">Unsubscribe</string>
<string name="video_comments_title">Comments</string>
<plurals name="video_channel_subscribers">
<item quantity="one">%1$d subscriber</item>
<item quantity="other">%1$d subscribers</item>
</plurals>
<string name="video_by_line">By %1$s</string>
<string name="video_owner_fqdn_line">%1$s@%2$s</string>
<string name="video_sub_del_alert_title">Unsubscribe</string>
<string name="video_sub_del_alert_msg">Are you sure you would like to unsubscribe?</string>
<string name="saved_to_playlist">Saved to playlist</string>
<string name="remove_video">Remove Video</string>
<string name="remove_video_warning_message">Are you sure you want to remove this video from playlist?</string>
<string name="playlist">Playlist</string>
</resources>

View File

@ -76,12 +76,12 @@
app:title="@string/pref_background_behavior"
app:iconSpaceReserved="false"/>
<SwitchPreference
app:defaultValue="false"
app:key="@string/pref_torrent_player_key"
app:summary="@string/pref_description_torrent_player"
app:title="@string/pref_title_torrent_player"
app:iconSpaceReserved="false"/>
<!-- <SwitchPreference-->
<!-- app:defaultValue="false"-->
<!-- app:key="@string/pref_torrent_player_key"-->
<!-- app:summary="@string/pref_description_torrent_player"-->
<!-- app:title="@string/pref_title_torrent_player"-->
<!-- app:iconSpaceReserved="false"/>-->
</PreferenceCategory>

View File

@ -2,11 +2,11 @@
buildscript {
ext.kotlin_version = '1.5.31'
ext.kotlin_version = '1.6.10'
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
@ -21,7 +21,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven {
url 'https://oss.sonatype.org/content/repositories/snapshots'

View File

@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Reset
Color_Off='\033[0m' # Text Reset
# Regular Colors
Black='\033[0;30m' # Black
Red='\033[0;31m' # Red
Green='\033[0;32m' # Green
Yellow='\033[0;33m' # Yellow
Blue='\033[0;34m' # Blue
Purple='\033[0;35m' # Purple
Cyan='\033[0;36m' # Cyan
White='\033[0;37m' # White
exitcode=0
supportedlangs=(af ar am hy-AM az-AZ eu-ES be bn-BD bg my-MM ca zh-HK zh-CN zh-TW hr cs-CZ da-DK nl-NL en-AU en-CA en-IN en-SG en-GB en-US et fil fi-FI fr-FR fr-CA gl-ES ka-GE de-DE el-GR iw-IL hi-IN hu-HU is-IS id it-IT ja-JP kn-IN km-KH ko-KR ky-KG lo-LA lv lt mk-MK ms ml-IN mr-IN mn-MN ne-NP no-NO fa pl-PL pt-BR pt-PT ro rm ru-RU sr si-LK sk sl es-419 es-ES es-US sq sw sv-SE ta-IN te-IN th tr-TR uk vi zu)
readarray -t dirs < <(find fastlane/metadata/android -mindepth 1 -maxdepth 1 -type d -printf '%P\n')
echo -e "${Green}Checking for Valid fastlane files...${Color_Off}"
for target in "${supportedlangs[@]}"; do
for i in "${!dirs[@]}"; do
if [[ ${dirs[i]} = $target ]]; then
unset 'dirs[i]'
fi
done
done
if [[ ${#dirs[@]} -gt 0 ]]; then
exitcode=1
echo -e "${Red}Invalid Lang Play Store Listing found: ${#dirs[@]}${Color_Off}"
echo -e "${Red}Invalid Lang codes:${dirs[@]}${Color_Off}"
else
echo -e "${Green}All found lang codes are valid${Color_Off}"
fi
# check we have required files
requiredfiles=(title.txt full_description.txt short_description.txt)
for d in fastlane/metadata/android/* ; do
[ -L "${d%/}" ] && continue
for rfile in "${requiredfiles[@]}"; do
if test ! -f "$d/$rfile"; then
echo -e "${Red}$d/$rfile missing.${Color_Off}"
exitcode=1
fi
done
# check title is under 30 characters
if test -f "$d/title.txt"; then
if [[ $(wc -m < "$d/title.txt") -gt 30 ]]; then
echo -e "${Red}$d/title.txt title too long.${Color_Off}"
exitcode=1
fi
fi
# if test -f "$d/video.txt"; then
# fcontents=$(cat "$d/video.txt")
# if [[ "$fcontents" != "https://www.youtube.com/watch?v=lVJs26gE2Ek" ]]; then
# exitcode=1
# echo -e "${Blue}$fcontents${Color_Off} --> $d/video.txt - not correct video URL"
# fi
# fi
done
exit $exitcode

View File

@ -1 +1 @@
الثوريوم عميل غير رسمي لشركة P
Thorium a PeerTube client

View File

@ -1 +0,0 @@
https://www.youtube.com/watch?v=lVJs26gE2Ek

View File

@ -1 +1 @@
থোরিয়াম একটি অনানুষ্ঠানিক পিয়ারটিউব ক্লায়েন্ট
Thorium a PeerTube client

View File

@ -1 +0,0 @@
https://www.youtube.com/watch?v=lVJs26gE2Ek

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