Compare commits
184 Commits
v1.1.3+for
...
v1.1.4+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef207f885b | ||
|
|
c4eee28335 | ||
|
|
6fe466779e | ||
|
|
e71db1b883 | ||
|
|
b6efafe99d | ||
|
|
688d466f8e | ||
|
|
3c5797932e | ||
|
|
48a5e262ce | ||
|
|
63b0365208 | ||
|
|
972c05d60b | ||
|
|
acb5778e0b | ||
|
|
7694c50358 | ||
|
|
7ffa368d10 | ||
|
|
93d57d847e | ||
|
|
83a4f5eec2 | ||
|
|
836c2dba8d | ||
|
|
13ae8d2ebe | ||
|
|
98dafb4e49 | ||
|
|
85b8bae42e | ||
|
|
64416ef9ee | ||
|
|
6274ab3384 | ||
|
|
4ea5d94dc6 | ||
|
|
db4dd436b7 | ||
|
|
6906ac6c8f | ||
|
|
bc0b56a90e | ||
|
|
349d95b8ee | ||
|
|
8c8eb395de | ||
|
|
50c844aa25 | ||
|
|
85b232330b | ||
|
|
b4c305d094 | ||
|
|
be2ee3a029 | ||
|
|
596799bf2f | ||
|
|
10a405ef13 | ||
|
|
118c5d4b44 | ||
|
|
a4cb05080a | ||
|
|
699ececc42 | ||
|
|
4f8f698911 | ||
|
|
30083837a5 | ||
|
|
ea7aa6c52f | ||
|
|
244f2ed911 | ||
|
|
99a71c67e4 | ||
|
|
b3c0550a86 | ||
|
|
39a0c6d08a | ||
|
|
0ee494bcfc | ||
|
|
4233c743e2 | ||
|
|
eea00b0d53 | ||
|
|
7cef4d282c | ||
|
|
df6e085c97 | ||
|
|
70cf48355e | ||
|
|
da06c798d7 | ||
|
|
9055c094e8 | ||
|
|
5ab3ae3d59 | ||
|
|
92ebb2f339 | ||
|
|
d8c034dfa3 | ||
|
|
247516849e | ||
|
|
aad9996b6f | ||
|
|
76c1ad07a3 | ||
|
|
49599f48bb | ||
|
|
158d128316 | ||
|
|
00897e7388 | ||
|
|
1867f294c8 | ||
|
|
eaa189d6b0 | ||
|
|
87d11dcb0a | ||
|
|
ba99394890 | ||
|
|
6b8236021e | ||
|
|
68b12a579c | ||
|
|
3f0ae887b8 | ||
|
|
187f9a2246 | ||
|
|
e8566bc24b | ||
|
|
e375fc7d4d | ||
|
|
89157efce8 | ||
|
|
5e95291016 | ||
|
|
cbd6e668dc | ||
|
|
898e62490b | ||
|
|
326ab4edf1 | ||
|
|
f25a965478 | ||
|
|
f3f3bea7b3 | ||
|
|
583b0788c6 | ||
|
|
ca29cee586 | ||
|
|
ec905448b0 | ||
|
|
e8fa82d0de | ||
|
|
e381de812c | ||
|
|
1a9752d53b | ||
|
|
09cc5c5fd2 | ||
|
|
4e8d510d38 | ||
|
|
7f7f6bae80 | ||
|
|
358ff12fba | ||
|
|
8ff3ecb4d4 | ||
|
|
47df35f0fd | ||
|
|
e226851e03 | ||
|
|
7228907682 | ||
|
|
1dbabed716 | ||
|
|
4224cd037d | ||
|
|
9aa6d3f531 | ||
|
|
c9823ae9d0 | ||
|
|
1fa8a9e858 | ||
|
|
212e8893b9 | ||
|
|
367057421b | ||
|
|
c821480842 | ||
|
|
3ef7c11e3b | ||
|
|
1112756bc2 | ||
|
|
878ed43135 | ||
|
|
01b746d30a | ||
|
|
a900351729 | ||
|
|
4114a6e5d8 | ||
|
|
04dd232e38 | ||
|
|
bc26dfe856 | ||
|
|
0322f845af | ||
|
|
7daa2d63e6 | ||
|
|
3a8a41f568 | ||
|
|
65cd7d076b | ||
|
|
a1f71091fe | ||
|
|
131834af86 | ||
|
|
c0789cbdb9 | ||
|
|
7d12d866ab | ||
|
|
7c2589c35b | ||
|
|
fd2031ccf5 | ||
|
|
6a165ec9f4 | ||
|
|
fd8dad487a | ||
|
|
e9443b841a | ||
|
|
89ec6acac9 | ||
|
|
9b389b346f | ||
|
|
4a522bffc0 | ||
|
|
9b62f8b7a2 | ||
|
|
96342d67e3 | ||
|
|
95fc2bef9e | ||
|
|
4f36fbe3b9 | ||
|
|
a501d8b995 | ||
|
|
74691650b3 | ||
|
|
9325781cbc | ||
|
|
b383dd6419 | ||
|
|
fe98ecf0cc | ||
|
|
01970ab69b | ||
|
|
3aa252f681 | ||
|
|
18633291e6 | ||
|
|
b79619b6e4 | ||
|
|
dbb03ee688 | ||
|
|
00894b41d2 | ||
|
|
8868ace90d | ||
|
|
a7586eeba8 | ||
|
|
19b89c606a | ||
|
|
b6c1e7d11e | ||
|
|
ca90c89b2a | ||
|
|
170a758f5f | ||
|
|
43dcd6e7f4 | ||
|
|
063542b2f5 | ||
|
|
70e4ae4fb4 | ||
|
|
8f1a4c60df | ||
|
|
07212dba96 | ||
|
|
fb95caadbe | ||
|
|
51e86b686d | ||
|
|
6ec335087d | ||
|
|
96040e15fd | ||
|
|
12a5670441 | ||
|
|
b756fe2cdb | ||
|
|
3732a4c844 | ||
|
|
5bd59bd999 | ||
|
|
90ba5551d2 | ||
|
|
484b6891fd | ||
|
|
0435d5f0c7 | ||
|
|
864c8de8de | ||
|
|
8bcea5bfb8 | ||
|
|
beded04579 | ||
|
|
5dcd41170c | ||
|
|
8aeda56fc8 | ||
|
|
3bb921a859 | ||
|
|
38edbde645 | ||
|
|
f531a90b41 | ||
|
|
ff52c37868 | ||
|
|
8fb2b454dd | ||
|
|
265b2ad32c | ||
|
|
ba3219d9fc | ||
|
|
bcac7401ee | ||
|
|
b44e3424e3 | ||
|
|
c15f6519c0 | ||
|
|
2809b27be0 | ||
|
|
1b6e096bf9 | ||
|
|
85db37f6a7 | ||
|
|
0cee490466 | ||
|
|
a148c92da2 | ||
|
|
6272797834 | ||
|
|
c048134ef2 | ||
|
|
6f57cd9ffe | ||
|
|
76815f8194 |
34
README.md
34
README.md
@@ -1,34 +1,38 @@
|
||||

|
||||
|
||||
# Mastodon for Android Fork
|
||||
# Mastodos
|
||||
|
||||
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly won’t ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer.
|
||||
|
||||
[](https://github.com/sk22/mastodon-android-fork/releases/latest/download/mastodos.apk)
|
||||
|
||||
## Changes
|
||||
|
||||
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
|
||||
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
|
||||
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/8))
|
||||
### Features
|
||||
|
||||
* [Add “Unlisted” as a post visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
|
||||
* [Add “Federation” tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/8))
|
||||
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/tree/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
|
||||
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodon-android-fork/tree/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
|
||||
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/tree/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
|
||||
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/tree/feature/always-preserve-cw) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/tree/feature/back-returns-home) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||
* [Implement a bookmark button and list](https://github.com/sk22/mastodon-android-fork/tree/feature/bookmarks) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||
|
||||
### Behavior
|
||||
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/tree/feature/back-returns-home) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/tree/feature/always-preserve-cw) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/tree/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
|
||||
* [Implement deleting and re-drafting](https://github.com/sk22/mastodon-android-fork/tree/feature/delete-redraft) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/21))
|
||||
|
||||
## Fork-specific changes
|
||||
### Branding
|
||||
|
||||
* Custom app name
|
||||
* App name “Mastodos”
|
||||
* Pink primary color
|
||||
* Custom icon: Modulate upstream icon using ImageMagick
|
||||
|
||||
```bash
|
||||
mogrify -modulate 90,100,140 mastodon/src/main/res/mipmap-*/ic_launcher*.png
|
||||
```
|
||||
|
||||
* Custom primary color: Hue of all `primary` colors in `colors.xml` is rotated, on basis of upstream Mastodon's [old branding](https://github.com/mastodon/mastodon-android/commit/74f03026cfcfcfd23237c38ff47d2b2a98a6f92a#diff-59134ec2a1cf3761f80b0ecccbbf8b9e433d9780d2f5c5d6ac3ac8cc254e808f)
|
||||
by `109.8°` (equivalent of `161%`, done by hand using
|
||||
[PineTools](https://pinetools.com/shift-hue-color))
|
||||
|
||||
## Building
|
||||
|
||||
As this app is using Java 17 features, you need JDK 17 or newer to build it. Other than that, everything is pretty standard. You can either import the project into Android Studio and build it from there, or run the following command in the project directory:
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jednné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, kteří mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom vysílají na Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v bezreklamní a chronologické časové linii. Vyjádřete se pomocí vlastních emojí, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a reblogujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se svými sledujícími, jen s lidmi, které zmiňujete, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a hlášení nástrojů pomáhá předcházet zneužití.
|
||||
|
||||
More features:
|
||||
Další funkce:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo zcela černém režimu
|
||||
• Ankety: Požádejte sledující o jejich názor a spojte se s jejich hlasováním
|
||||
• Průzkum: Trendové hashtagy a účty jsou pryč na jedno klepnutí
|
||||
• Upozornění: Dostávejte upozornění na nové sledování, odpovědi a reblogy
|
||||
• Sdílení: Odesílání přímo do Mastodonu z libovolného seznamu sdílení v jakékoliv aplikaci
|
||||
• Roztomilost: Naším maskotem je roztomilý slon, kterého čas od času uvidíte
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a my máme v plánu to udržet.
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
Decentralizovaná sociální síť
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Website ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal was dich interessiert, auf Mastodon kannst du interessierte Leute treffen, die darüber schreiben!
|
||||
|
||||
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bilderns, GIFs, Videos und Audio in 500-Zeichen-Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen, um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten, und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bildern, GIFs, Videos und Audio in 500-Zeichen-Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen, um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||
|
||||
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
|
||||
|
||||
@@ -8,9 +8,9 @@ Weitere Funktionen:
|
||||
|
||||
• Dunkler Modus: Beiträge im hellen, dunklen oder schwarzen Modus lesen
|
||||
• Umfragen: Frage deine Follower nach ihrer Meinung und zähle die Stimmen
|
||||
• Entdecken: Trending Hashtags und Accounts sind nur einen Fingertipp entfernt
|
||||
• Entdecken: trendende Hashtags und Profile sind nur einen Fingertipp entfernt
|
||||
• Benachrichtigungen: Erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
|
||||
• Teilen: Veröffentliche auf Mastodon aus jeder beliebigen anderen App
|
||||
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant, und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
||||
|
||||
Mastodon ist eine eingetragene gemeinnützige Organisation, und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen es so zu erhalten.
|
||||
Mastodon ist eine eingetragene gemeinnützige Organisation, und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen, das auch so beizubehalten.
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon là mạng xã hội liên hợp lớn nhất trên internet. Thay vì một trang web duy nhất, nó là một mạng lưới hàng triệu người dùng trong các máy chủ độc lập, tất cả đều có thể tương tác với nhau một cách liền mạch. Bất kể bạn thích gì, bạn đều có thể gặp gỡ những người đăng tút về nó trên Mastodon!
|
||||
|
||||
Tham gia một máy chủ và tạo trang hồ sơ của bạn. Tìm, theo dõi những người thú vị và đọc tút của họ theo trình tự thời gian, không có quảng cáo. Thể hiện bản thân bằng emoji, hình ảnh, GIF, video và âm thanh trong tút tối đa 500 ký tự. Trả lời tút và đăng lại tút từ bất kỳ ai để chia sẻ những điều tuyệt vời. Tìm những người dùng mới để theo dõi và các hashtag xu hướng để mở rộng mạng lưới của bạn.
|
||||
Tham gia một máy chủ và tạo trang hồ sơ của bạn. Tìm, theo dõi những người thú vị và đọc tút của họ theo trình tự thời gian, không có quảng cáo. Thể hiện bản thân bằng emoji, hình ảnh, GIF, video và âm thanh trong tút tối đa 500 ký tự. Trả lời tút và đăng lại tút từ bất kỳ ai để chia sẻ những điều tuyệt vời. Tìm những người dùng mới để theo dõi và các hashtag nổi bật để mở rộng mạng lưới của bạn.
|
||||
|
||||
Mastodon được xây dựng tập trung vào sự riêng tư và an toàn. Quyết định xem tút của bạn được chia sẻ với những người theo dõi, chỉ những người bạn nhắc đến hay cả thế giới. Nội dung ẩn cho phép bạn ẩn các tút chứa nội dung nhạy cảm hoặc chơi chữ cho đến khi bạn sẵn sàng tương tác với chúng. Mỗi máy chủ có các nguyên tắc riêng và kiểm duyệt viên riêng để giữ an toàn cho các thành viên, song song với các công cụ chặn và báo cáo mạnh mẽ giúp ngăn chặn hành vi bậy.
|
||||
|
||||
@@ -8,7 +8,7 @@ Tính năng khác:
|
||||
|
||||
• Chế độ Tối: Đọc tút ở chế độ sáng, tối hoặc đen
|
||||
• Bình chọn: Hỏi cộng đồng về ý kiến của họ và đếm lượt bình chọn
|
||||
• Khám phá: Xem hashtag xu hướng và tài khoản chỉ bằng một nhấn
|
||||
• Khám phá: Xem hashtag nổi bật và tài khoản chỉ bằng một nhấn
|
||||
• Thông báo: Nhận thông báo về người theo dõi, lượt trả lời và đăng lại mới
|
||||
• Chia sẻ: Đăng trực tiếp lên Mastodon từ bất kỳ ứng dụng nào
|
||||
• Đáng yêu: Linh vật của chúng tôi là một chú voi ma mút và bạn sẽ thấy anh ấy thỉnh thoảng xuất hiện
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon 是網際網路上最大的去中心化社交網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能在 Mastodon 上遇到充滿熱情的人們討論該話題。
|
||||
|
||||
加入社群並建立您的個人檔案。 尋找並追蹤迷人的夥伴,並在無廣告、按時間順序排列的時間軸上閱讀他們的貼文。 在 500 個字元的貼文中使用自訂表情符號、GIF、視訊與音訊來表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要追蹤的新帳號與熱門主題標籤來拓展您的網路。
|
||||
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並在無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 在 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊來表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤來拓展您的網路。
|
||||
|
||||
Mastodon 以隱私與安全為要。 決定您的貼文要與您的追蹤者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的貼文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
|
||||
Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
|
||||
|
||||
更多功能:
|
||||
|
||||
• 深色模式:以淺色、深色或純黑色模式閱讀貼文
|
||||
• 投票:詢問追蹤的意見並計票
|
||||
• 投票:詢問跟隨者們的意見並計票
|
||||
• 探索:僅需輕點一下,即可看到熱門主題標籤與帳號
|
||||
• 通知:取得關於新追蹤、回覆與轉發的通知
|
||||
• 分享:從任何應用程式中的分享表中直接發表貼文到 Mastodon 中
|
||||
• 通知:取得關於新跟隨者們、回覆與轉發的通知
|
||||
• 分享:從任何應用程式中的分享表中直接發表嘟文到 Mastodon 中
|
||||
• 可愛:我們的吉祥物是一隻可愛的大象,您會不時看到牠出現
|
||||
|
||||
Mastodon 是一家註冊的非營利組織,您的捐款會直接支援開發工作。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。
|
||||
@@ -4,13 +4,13 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
compileSdk 33
|
||||
defaultConfig {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 31
|
||||
versionCode 20
|
||||
versionName '1.1.3+fork.20'
|
||||
targetSdk 33
|
||||
versionCode 28
|
||||
versionName "1.1.4+fork.28"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ android {
|
||||
initWith release
|
||||
versionNameSuffix "-beta"
|
||||
}
|
||||
githubRelease{
|
||||
initWith release
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -46,6 +49,16 @@ android {
|
||||
appcenterPublicBeta{
|
||||
setRoot "src/appcenter"
|
||||
}
|
||||
githubRelease{
|
||||
setRoot "src/github"
|
||||
}
|
||||
debug {
|
||||
setRoot "src/github"
|
||||
}
|
||||
}
|
||||
lintOptions{
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
mastodon/src/github/AndroidManifest.xml
Normal file
21
mastodon/src/github/AndroidManifest.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.joinmastodon.android">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
|
||||
<application>
|
||||
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>-->
|
||||
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- </receiver>-->
|
||||
<provider
|
||||
android:authorities="${applicationId}.self_update_provider"
|
||||
android:name=".updater.SelfUpdateContentProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,356 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Keep
|
||||
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
private static final long CHECK_PERIOD=6*3600*1000L;
|
||||
private static final String TAG="GithubSelfUpdater";
|
||||
|
||||
private UpdateState state=UpdateState.NO_UPDATE;
|
||||
private UpdateInfo info;
|
||||
private long downloadID;
|
||||
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
|
||||
MastodonApp.context.unregisterReceiver(this);
|
||||
setState(UpdateState.DOWNLOADED);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public GithubSelfUpdaterImpl(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
|
||||
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
|
||||
info=new UpdateInfo();
|
||||
info.version=prefs.getString("version", null);
|
||||
info.size=prefs.getLong("apkSize", 0);
|
||||
downloadID=prefs.getLong("downloadID", 0);
|
||||
if(downloadID==0 || !getUpdateApkFile().exists()){
|
||||
state=UpdateState.UPDATE_AVAILABLE;
|
||||
}else{
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
|
||||
if(state==UpdateState.DOWNLOADING){
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
}
|
||||
}
|
||||
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
|
||||
// We are in a new version, running for the first time after update. Gotta clean things up.
|
||||
long id=getPrefs().getLong("downloadID", 0);
|
||||
if(id!=0){
|
||||
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
|
||||
}
|
||||
getUpdateApkFile().delete();
|
||||
getPrefs().edit()
|
||||
.remove("apkSize")
|
||||
.remove("version")
|
||||
.remove("apkURL")
|
||||
.remove("checkedByBuild")
|
||||
.remove("downloadID")
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeCheckForUpdates(){
|
||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
|
||||
if(timeSinceLastCheck>=CHECK_PERIOD){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkForUpdates() {
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
|
||||
private void actuallyCheckForUpdates(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://api.github.com/repos/sk22/mastodon-android-fork/releases/latest")
|
||||
.build();
|
||||
Call call=MastodonAPIController.getHttpClient().newCall(req);
|
||||
try(Response resp=call.execute()){
|
||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||
return;
|
||||
}
|
||||
int newMajor=Integer.parseInt(matcher.group(1)),
|
||||
newMinor=Integer.parseInt(matcher.group(2)),
|
||||
newRevision=Integer.parseInt(matcher.group(3)),
|
||||
newForkNumber=Integer.parseInt(matcher.group(4));
|
||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||
return;
|
||||
}
|
||||
int curMajor=Integer.parseInt(matcher.group(1)),
|
||||
curMinor=Integer.parseInt(matcher.group(2)),
|
||||
curRevision=Integer.parseInt(matcher.group(3)),
|
||||
curForkNumber=Integer.parseInt(matcher.group(4));
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber || BuildConfig.DEBUG){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
JsonObject asset=el.getAsJsonObject();
|
||||
if("application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
|
||||
long size=asset.get("size").getAsLong();
|
||||
String url=asset.get("browser_download_url").getAsString();
|
||||
|
||||
UpdateInfo info=new UpdateInfo();
|
||||
info.size=size;
|
||||
info.version=version;
|
||||
this.info=info;
|
||||
|
||||
getPrefs().edit()
|
||||
.putLong("apkSize", size)
|
||||
.putString("version", version)
|
||||
.putString("apkURL", url)
|
||||
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
|
||||
.remove("downloadID")
|
||||
.apply();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "actuallyCheckForUpdates", x);
|
||||
}finally{
|
||||
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(UpdateState state){
|
||||
this.state=state;
|
||||
E.post(new SelfUpdateStateChangedEvent(state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateState getState(){
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateInfo getUpdateInfo(){
|
||||
return info;
|
||||
}
|
||||
|
||||
public File getUpdateApkFile(){
|
||||
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadUpdate(){
|
||||
if(state==UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
downloadID=dm.enqueue(
|
||||
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
||||
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
||||
);
|
||||
getPrefs().edit().putLong("downloadID", downloadID).apply();
|
||||
setState(UpdateState.DOWNLOADING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void installUpdate(Activity activity){
|
||||
if(state!=UpdateState.DOWNLOADED)
|
||||
throw new IllegalStateException();
|
||||
Uri uri;
|
||||
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}else{
|
||||
uri=Uri.fromFile(getUpdateApkFile());
|
||||
}
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||
activity.startActivity(intent);
|
||||
|
||||
// TODO figure out how to restart the app when updating via this new API
|
||||
/*
|
||||
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
|
||||
try{
|
||||
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
|
||||
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
|
||||
@Override
|
||||
public void onCreated(int i){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBadgingChanged(int i){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveChanged(int i, boolean b){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(int id, float progress){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(int id, boolean success){
|
||||
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
});
|
||||
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
|
||||
PackageInstaller.Session session=installer.openSession(sid);
|
||||
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
|
||||
byte[] buffer=new byte[16384];
|
||||
int read;
|
||||
while((read=in.read(buffer))>0){
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
session.commit(intent.getIntentSender());
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "installUpdate", x);
|
||||
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDownloadProgress(){
|
||||
if(state!=UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
|
||||
if(cursor.moveToFirst()){
|
||||
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
|
||||
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
|
||||
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
|
||||
return total>0 ? (float)loaded/total : 0f;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDownload(){
|
||||
if(state!=UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
dm.remove(downloadID);
|
||||
downloadID=0;
|
||||
getPrefs().edit().remove("downloadID").apply();
|
||||
setState(UpdateState.UPDATE_AVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleIntentFromInstaller(Intent intent, Activity activity){
|
||||
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
activity.startActivity(confirmIntent);
|
||||
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
|
||||
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
|
||||
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}else if(status!=PackageInstaller.STATUS_SUCCESS){
|
||||
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
|
||||
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent){
|
||||
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
|
||||
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
|
||||
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
|
||||
Intent restartIntent=new Intent(context, MainActivity.class)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setPackage(context.getPackageName());
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
|
||||
context.startActivity(restartIntent);
|
||||
}else{
|
||||
// Bypass activity starting restrictions by starting it from a notification
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
|
||||
nm.createNotificationChannel(chan);
|
||||
Notification n=new Notification.Builder(context, "selfUpdateRestart")
|
||||
.setContentTitle(context.getString(R.string.update_installed))
|
||||
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
|
||||
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.build();
|
||||
nm.notify(1, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class SelfUpdateContentProvider extends ContentProvider{
|
||||
@Override
|
||||
public boolean onCreate(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri){
|
||||
if(isCorrectUri(uri))
|
||||
return "application/vnd.android.package-archive";
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||
if(isCorrectUri(uri)){
|
||||
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
private boolean isCorrectUri(Uri uri){
|
||||
return "/update.apk".equals(uri.getPath());
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Application;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -17,6 +21,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@@ -59,6 +64,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else{
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +75,8 @@ public class MainActivity extends FragmentStackActivity{
|
||||
try{
|
||||
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
|
||||
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
|
||||
}else if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +105,9 @@ public class MainActivity extends FragmentStackActivity{
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
|
||||
}*/
|
||||
}
|
||||
|
||||
private void showFragmentForNotification(Notification notification, String accountID){
|
||||
@@ -131,4 +142,10 @@ public class MainActivity extends FragmentStackActivity{
|
||||
compose.setArguments(composeArgs);
|
||||
showFragment(compose);
|
||||
}
|
||||
|
||||
private void maybeRequestNotificationsPermission(){
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
@@ -184,7 +184,7 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ public class MastodonAPIController{
|
||||
if(call.isCanceled())
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed: "+e);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
req.onError(e.getLocalizedMessage(), 0);
|
||||
req.onError(e.getLocalizedMessage(), 0, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -133,7 +133,7 @@ public class MastodonAPIController{
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code());
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class MastodonAPIController{
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code());
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class MastodonAPIController{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||
if(error.has("details")){
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code());
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||
JsonObject errorDetails=error.getAsJsonObject("details");
|
||||
for(String key:errorDetails.keySet()){
|
||||
@@ -172,12 +172,12 @@ public class MastodonAPIController{
|
||||
err.detailedErrors=details;
|
||||
req.onError(err);
|
||||
}else{
|
||||
req.onError(error.get("error").getAsString(), response.code());
|
||||
req.onError(error.get("error").getAsString(), response.code(), null);
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
req.onError(response.code()+" "+response.message(), response.code());
|
||||
req.onError(response.code()+" "+response.message(), response.code(), x);
|
||||
}catch(Exception x){
|
||||
req.onError("Error parsing an API error", response.code());
|
||||
req.onError("Error parsing an API error", response.code(), x);
|
||||
}
|
||||
}
|
||||
}catch(Exception x){
|
||||
@@ -189,7 +189,7 @@ public class MastodonAPIController{
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage(), 0);
|
||||
req.onError(x.getLocalizedMessage(), 0, x);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -197,4 +197,8 @@ public class MastodonAPIController{
|
||||
public static void runInBackground(Runnable action){
|
||||
thread.postRunnable(action, 0);
|
||||
}
|
||||
|
||||
public static OkHttpClient getHttpClient(){
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
account.getApiController().submitRequest(this);
|
||||
}catch(Exception x){
|
||||
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
|
||||
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1));
|
||||
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1, x));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -194,8 +194,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
invokeErrorCallback(err);
|
||||
}
|
||||
|
||||
void onError(String msg, int httpStatus){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
|
||||
void onError(String msg, int httpStatus, Throwable exception){
|
||||
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
|
||||
}
|
||||
|
||||
void onSuccess(T resp){
|
||||
|
||||
@@ -7,8 +7,8 @@ import java.util.Map;
|
||||
public class MastodonDetailedErrorResponse extends MastodonErrorResponse{
|
||||
public Map<String, List<FieldError>> detailedErrors;
|
||||
|
||||
public MastodonDetailedErrorResponse(String error, int httpStatus){
|
||||
super(error, httpStatus);
|
||||
public MastodonDetailedErrorResponse(String error, int httpStatus, Throwable exception){
|
||||
super(error, httpStatus, exception);
|
||||
}
|
||||
|
||||
public static class FieldError{
|
||||
|
||||
@@ -12,10 +12,12 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
public class MastodonErrorResponse extends ErrorResponse{
|
||||
public final String error;
|
||||
public final int httpStatus;
|
||||
public final Throwable underlyingException;
|
||||
|
||||
public MastodonErrorResponse(String error, int httpStatus){
|
||||
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
|
||||
this.error=error;
|
||||
this.httpStatus=httpStatus;
|
||||
this.underlyingException=exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("exclude_replies", "true");
|
||||
addQueryParameter("exclude_reblogs", "true");
|
||||
}
|
||||
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
INCLUDE_REPLIES,
|
||||
PINNED,
|
||||
MEDIA,
|
||||
NO_REBLOGS
|
||||
NO_REBLOGS,
|
||||
OWN_POSTS_AND_REPLIES
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
|
||||
public class GetPreferences extends MastodonAPIRequest<Preferences> {
|
||||
public GetPreferences(){
|
||||
super(HttpMethod.GET, "/preferences", Preferences.class);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String clientName="Mastodon for Android Fork";
|
||||
public String clientName="Mastodos";
|
||||
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
||||
public String scopes=AccountSessionManager.SCOPE;
|
||||
public String website="https://github.com/sk22/mastodon-android-fork";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class EditStatus extends MastodonAPIRequest<Status>{
|
||||
public EditStatus(CreateStatus.Request req, String id){
|
||||
super(HttpMethod.PUT, "/statuses/"+id, Status.class);
|
||||
setRequestBody(req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetAttachmentByID extends MastodonAPIRequest<Attachment>{
|
||||
public GetAttachmentByID(String id){
|
||||
super(HttpMethod.GET, "/media/"+id, Attachment.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
|
||||
if(httpResponse.code()==206)
|
||||
respObj.url="";
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
||||
public GetStatusEditHistory(String id){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException{
|
||||
int i=0;
|
||||
for(Status s:respObj){
|
||||
s.uri="";
|
||||
s.id="fakeID"+i;
|
||||
s.visibility=StatusPrivacy.PUBLIC;
|
||||
s.mentions=Collections.emptyList();
|
||||
s.tags=Collections.emptyList();
|
||||
i++;
|
||||
}
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
|
||||
public GetStatusSourceText(String id){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
|
||||
}
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public static class Response extends BaseModel{
|
||||
public String id;
|
||||
public String text;
|
||||
public String spoilerText;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.io.IOException;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||
private Uri uri;
|
||||
@@ -40,6 +41,18 @@ public class UploadAttachment extends MastodonAPIRequest<Attachment>{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
|
||||
if(respObj.url==null)
|
||||
respObj.url="";
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder builder=new MultipartBody.Builder()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
public class SelfUpdateStateChangedEvent{
|
||||
public final GithubSelfUpdater.UpdateState state;
|
||||
|
||||
public SelfUpdateStateChangedEvent(GithubSelfUpdater.UpdateState state){
|
||||
this.state=state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusUpdatedEvent{
|
||||
public Status status;
|
||||
|
||||
public StatusUpdatedEvent(Status status){
|
||||
this.status=status;
|
||||
}
|
||||
}
|
||||
@@ -457,7 +457,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
status.spoilerRevealed=true;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
@@ -579,6 +579,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return true;
|
||||
}
|
||||
|
||||
public ArrayList<StatusDisplayItem> getDisplayItems(){
|
||||
return displayItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
|
||||
|
||||
@@ -4,13 +4,18 @@ import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.RenderEffect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.icu.text.BreakIterator;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -52,19 +57,27 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.ProgressListener;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
|
||||
@@ -76,6 +89,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
|
||||
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ComposeEditText;
|
||||
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
|
||||
@@ -84,6 +98,9 @@ import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
|
||||
import org.parceler.Parcel;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -105,6 +122,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private static final int MEDIA_RESULT=717;
|
||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
private static final int MAX_ATTACHMENTS=4;
|
||||
private static final String TAG="ComposeFragment";
|
||||
|
||||
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@@ -152,8 +170,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
||||
|
||||
private ArrayList<DraftMediaAttachment> queuedAttachments=new ArrayList<>(), failedAttachments=new ArrayList<>(), attachments=new ArrayList<>(), allAttachments=new ArrayList<>();
|
||||
private DraftMediaAttachment uploadingAttachment;
|
||||
private ArrayList<DraftMediaAttachment> attachments=new ArrayList<>();
|
||||
|
||||
private List<EmojiCategory> customEmojis;
|
||||
private CustomEmojiPopupKeyboard emojiKeyboard;
|
||||
@@ -168,13 +185,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private ImageView sendError;
|
||||
private View sendingOverlay;
|
||||
private WindowManager wm;
|
||||
private StatusPrivacy statusVisibility=StatusPrivacy.UNLISTED;
|
||||
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
|
||||
private ComposeAutocompleteSpan currentAutocompleteSpan;
|
||||
private FrameLayout mainEditTextWrap;
|
||||
private ComposeAutocompleteViewController autocompleteViewController;
|
||||
private Instance instance;
|
||||
private boolean attachmentsErrorShowing;
|
||||
|
||||
private Status editingStatus;
|
||||
private boolean pollChanged;
|
||||
private boolean creatingView;
|
||||
private boolean ignoreSelectionChanges=false;
|
||||
private Runnable updateUploadEtaRunnable;
|
||||
|
||||
public static DraftMediaAttachment redraftAttachment(Attachment att) {
|
||||
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||
draft.serverAttachment=att;
|
||||
@@ -194,6 +217,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
instanceDomain=session.domain;
|
||||
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
|
||||
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
|
||||
if(getArguments().containsKey("editStatus")){
|
||||
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
|
||||
}
|
||||
if(instance==null){
|
||||
Nav.finish(this);
|
||||
return;
|
||||
@@ -209,25 +235,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
else
|
||||
charLimit=500;
|
||||
|
||||
if(getArguments().containsKey("replyTo")){
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||
statusVisibility=replyTo.visibility;
|
||||
}
|
||||
|
||||
if(getArguments().containsKey("visibility")){
|
||||
statusVisibility=(StatusPrivacy) getArguments().getSerializable("visibility");
|
||||
}
|
||||
|
||||
if(savedInstanceState!=null){
|
||||
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
loadDefaultStatusVisibility(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(uploadingAttachment!=null && uploadingAttachment.uploadRequest!=null)
|
||||
uploadingAttachment.uploadRequest.cancel();
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.isUploadingOrProcessing())
|
||||
att.cancelUpload();
|
||||
}
|
||||
if(updateUploadEtaRunnable!=null){
|
||||
UiUtils.removeCallbacks(updateUploadEtaRunnable);
|
||||
updateUploadEtaRunnable=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -239,6 +260,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
creatingView=true;
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain);
|
||||
emojiKeyboard.setListener(this::onCustomEmojiClick);
|
||||
|
||||
@@ -317,6 +339,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
updatePollOptionHints();
|
||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
|
||||
}else if(savedInstanceState==null && editingStatus!=null && editingStatus.poll!=null){
|
||||
pollBtn.setSelected(true);
|
||||
mediaBtn.setEnabled(false);
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
for(Poll.Option eopt:editingStatus.poll.options){
|
||||
DraftPollOption opt=createDraftPollOption();
|
||||
opt.edit.setText(eopt.title);
|
||||
}
|
||||
updatePollOptionHints();
|
||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
|
||||
}else{
|
||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=getResources().getQuantityString(R.plurals.x_days, 1, 1)));
|
||||
}
|
||||
@@ -329,6 +361,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerBtn.setSelected(true);
|
||||
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
|
||||
hasSpoiler=true;
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText));
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
|
||||
ArrayList<Parcelable> serializedAttachments=(savedInstanceState!=null ? savedInstanceState : getArguments())
|
||||
@@ -340,12 +377,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
attachments.add(att);
|
||||
}
|
||||
attachmentsView.setVisibility(View.VISIBLE);
|
||||
}else if(!allAttachments.isEmpty()){
|
||||
}else if(!attachments.isEmpty()){
|
||||
attachmentsView.setVisibility(View.VISIBLE);
|
||||
for(DraftMediaAttachment att:allAttachments){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
attachmentsView.addView(createMediaAttachmentView(att));
|
||||
}
|
||||
}
|
||||
|
||||
if(editingStatus!=null && editingStatus.visibility!=null) {
|
||||
statusVisibility=editingStatus.visibility;
|
||||
}
|
||||
updateVisibilityIcon();
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
@@ -354,6 +395,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
autocompleteView.setVisibility(View.GONE);
|
||||
mainEditTextWrap.addView(autocompleteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(178), Gravity.TOP));
|
||||
|
||||
creatingView=false;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -455,9 +498,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
updateCharCounter(s);
|
||||
updateCharCounter();
|
||||
}
|
||||
});
|
||||
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
|
||||
if(replyTo!=null){
|
||||
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
@@ -474,46 +518,65 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
|
||||
if(savedInstanceState==null){
|
||||
mainEditText.setText(initialText);
|
||||
ignoreSelectionChanges=true;
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
// TODO: setting for preserving cw always / only when replying to own posts
|
||||
// && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)
|
||||
ignoreSelectionChanges=false;
|
||||
if(!TextUtils.isEmpty(replyTo.spoilerText)){
|
||||
insertSpoiler(replyTo.spoilerText);
|
||||
hasSpoiler=true;
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerEdit.setText(replyTo.spoilerText);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
replyText.setVisibility(View.GONE);
|
||||
}
|
||||
if(savedInstanceState==null){
|
||||
String prefilledText=getArguments().getString("prefilledText");
|
||||
if(!TextUtils.isEmpty(prefilledText)){
|
||||
mainEditText.setText(prefilledText);
|
||||
if(editingStatus!=null){
|
||||
initialText=getArguments().getString("sourceText", "");
|
||||
mainEditText.setText(initialText);
|
||||
ignoreSelectionChanges=true;
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
initialText=prefilledText;
|
||||
}
|
||||
String spoilerText=getArguments().getString("spoilerText");
|
||||
if(!TextUtils.isEmpty(spoilerText)) insertSpoiler(spoilerText);
|
||||
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
|
||||
if(mediaUris!=null && !mediaUris.isEmpty()){
|
||||
for(Uri uri:mediaUris){
|
||||
addMediaAttachment(uri, null);
|
||||
ignoreSelectionChanges=false;
|
||||
if(!editingStatus.mediaAttachments.isEmpty()){
|
||||
attachmentsView.setVisibility(View.VISIBLE);
|
||||
for(Attachment att:editingStatus.mediaAttachments){
|
||||
DraftMediaAttachment da=new DraftMediaAttachment();
|
||||
da.serverAttachment=att;
|
||||
da.description=att.description;
|
||||
da.uri=Uri.parse(att.previewUrl);
|
||||
attachmentsView.addView(createMediaAttachmentView(da));
|
||||
attachments.add(da);
|
||||
}
|
||||
pollBtn.setEnabled(false);
|
||||
}
|
||||
}else{
|
||||
String prefilledText=getArguments().getString("prefilledText");
|
||||
if(!TextUtils.isEmpty(prefilledText)){
|
||||
mainEditText.setText(prefilledText);
|
||||
ignoreSelectionChanges=true;
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
ignoreSelectionChanges=false;
|
||||
initialText=prefilledText;
|
||||
}
|
||||
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
|
||||
if(mediaUris!=null && !mediaUris.isEmpty()){
|
||||
for(Uri uri:mediaUris){
|
||||
addMediaAttachment(uri, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void insertSpoiler(String text) {
|
||||
hasSpoiler=true;
|
||||
if (text!=null) spoilerEdit.setText(text);
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerBtn.setSelected(true);
|
||||
if(editingStatus!=null){
|
||||
updateCharCounter();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
publishButton=new Button(getActivity());
|
||||
publishButton.setText(R.string.publish);
|
||||
publishButton.setText(editingStatus==null ? R.string.publish : R.string.save);
|
||||
publishButton.setOnClickListener(this::onPublishClick);
|
||||
LinearLayout wrap=new LinearLayout(getActivity());
|
||||
wrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||
@@ -536,7 +599,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
wrap.setClipToPadding(false);
|
||||
MenuItem item=menu.add(R.string.publish);
|
||||
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
|
||||
item.setActionView(wrap);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
updatePublishButtonState();
|
||||
@@ -554,7 +617,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private void updateCharCounter(CharSequence text){
|
||||
private void updateCharCounter(){
|
||||
CharSequence text=mainEditText.getText();
|
||||
|
||||
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
||||
MENTION_PATTERN.matcher(
|
||||
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
||||
@@ -566,6 +631,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
charCount++;
|
||||
}
|
||||
|
||||
if(hasSpoiler){
|
||||
charCount+=spoilerEdit.length();
|
||||
}
|
||||
charCounter.setText(String.valueOf(charLimit-charCount));
|
||||
trimmedCharCount=text.toString().trim().length();
|
||||
updatePublishButtonState();
|
||||
@@ -578,11 +646,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(opt.edit.length()>0)
|
||||
nonEmptyPollOptionsCount++;
|
||||
}
|
||||
if(publishButton!=null){
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit
|
||||
&& uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
|
||||
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
if(publishButton==null)
|
||||
return;
|
||||
int nonDoneAttachmentCount=0;
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state!=AttachmentUploadState.DONE)
|
||||
nonDoneAttachmentCount++;
|
||||
}
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
@@ -638,41 +709,59 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
sendProgress.setVisibility(View.VISIBLE);
|
||||
sendError.setVisibility(View.GONE);
|
||||
|
||||
new CreateStatus(req, uuid)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
E.post(new StatusCreatedEvent(result));
|
||||
if(replyTo!=null){
|
||||
replyTo.repliesCount++;
|
||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||
}
|
||||
Nav.finish(ComposeFragment.this);
|
||||
Callback<Status> resCallback=new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null){
|
||||
E.post(new StatusCreatedEvent(result));
|
||||
if(replyTo!=null){
|
||||
replyTo.repliesCount++;
|
||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||
}
|
||||
}else{
|
||||
E.post(new StatusUpdatedEvent(result));
|
||||
}
|
||||
Nav.finish(ComposeFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
sendProgress.setVisibility(View.GONE);
|
||||
sendError.setVisibility(View.VISIBLE);
|
||||
publishButton.setEnabled(true);
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
sendProgress.setVisibility(View.GONE);
|
||||
sendError.setVisibility(View.VISIBLE);
|
||||
publishButton.setEnabled(true);
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
};
|
||||
|
||||
if(editingStatus!=null){
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}else{
|
||||
new CreateStatus(req, uuid)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasDraft(){
|
||||
if(getArguments().getBoolean("hasDraft", false)) return true;
|
||||
if(editingStatus!=null){
|
||||
if(!mainEditText.getText().toString().equals(initialText))
|
||||
return true;
|
||||
List<String> existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList());
|
||||
if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList())))
|
||||
return true;
|
||||
return pollChanged;
|
||||
}
|
||||
boolean pollFieldsHaveContent=false;
|
||||
for(DraftPollOption opt:pollOptions)
|
||||
pollFieldsHaveContent|=opt.edit.length()>0;
|
||||
return getArguments().getBoolean("hasDraft", false)
|
||||
|| (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText))
|
||||
|| !attachments.isEmpty() || uploadingAttachment!=null || !queuedAttachments.isEmpty()
|
||||
|| !failedAttachments.isEmpty() || pollFieldsHaveContent;
|
||||
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty() || pollFieldsHaveContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -716,7 +805,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private void confirmDiscardDraftAndFinish(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discard_draft)
|
||||
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
|
||||
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
@@ -773,7 +862,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
if(size>sizeLimit){
|
||||
float mb=sizeLimit/(float) (1024*1024);
|
||||
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%f" : "%.2f", mb);
|
||||
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%.0f" : "%.2f", mb);
|
||||
showMediaAttachmentError(getString(R.string.media_attachment_too_big, UiUtils.getFileName(uri), sMb));
|
||||
return false;
|
||||
}
|
||||
@@ -782,18 +871,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollBtn.setEnabled(false);
|
||||
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||
draft.uri=uri;
|
||||
draft.mimeType=type;
|
||||
draft.description=description;
|
||||
|
||||
attachmentsView.addView(createMediaAttachmentView(draft));
|
||||
allAttachments.add(draft);
|
||||
attachments.add(draft);
|
||||
attachmentsView.setVisibility(View.VISIBLE);
|
||||
draft.overlay.setVisibility(View.VISIBLE);
|
||||
draft.infoBar.setVisibility(View.GONE);
|
||||
draft.setOverlayVisible(true, false);
|
||||
|
||||
if(uploadingAttachment==null){
|
||||
uploadMediaAttachment(draft);
|
||||
}else{
|
||||
queuedAttachments.add(draft);
|
||||
if(!areThereAnyUploadingAttachments()){
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
updatePublishButtonState();
|
||||
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS)
|
||||
@@ -812,25 +899,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private View createMediaAttachmentView(DraftMediaAttachment draft){
|
||||
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
||||
ImageView img=thumb.findViewById(R.id.thumb);
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
||||
if(draft.serverAttachment!=null){
|
||||
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
|
||||
}else{
|
||||
if(draft.mimeType.startsWith("image/")){
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
||||
}else if(draft.mimeType.startsWith("video/")){
|
||||
loadVideoThumbIntoView(img, draft.uri);
|
||||
}
|
||||
}
|
||||
TextView fileName=thumb.findViewById(R.id.file_name);
|
||||
fileName.setText(UiUtils.getFileName(draft.uri));
|
||||
fileName.setText(UiUtils.getFileName(draft.serverAttachment!=null ? Uri.parse(draft.serverAttachment.url) : draft.uri));
|
||||
|
||||
draft.view=thumb;
|
||||
draft.imageView=img;
|
||||
draft.progressBar=thumb.findViewById(R.id.progress);
|
||||
draft.infoBar=thumb.findViewById(R.id.info_bar);
|
||||
draft.overlay=thumb.findViewById(R.id.overlay);
|
||||
draft.descriptionView=thumb.findViewById(R.id.description);
|
||||
draft.uploadStateTitle=thumb.findViewById(R.id.state_title);
|
||||
draft.uploadStateText=thumb.findViewById(R.id.state_text);
|
||||
ImageButton btn=thumb.findViewById(R.id.remove_btn);
|
||||
btn.setTag(draft);
|
||||
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
||||
btn=thumb.findViewById(R.id.remove_btn2);
|
||||
btn.setTag(draft);
|
||||
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
||||
Button retry=thumb.findViewById(R.id.retry_upload);
|
||||
ImageButton retry=thumb.findViewById(R.id.retry_or_cancel_upload);
|
||||
retry.setTag(draft);
|
||||
retry.setOnClickListener(this::onRetryMediaUploadClick);
|
||||
retry.setVisibility(View.GONE);
|
||||
retry.setOnClickListener(this::onRetryOrCancelMediaUploadClick);
|
||||
draft.retryButton=retry;
|
||||
draft.infoBar.setTag(draft);
|
||||
draft.infoBar.setOnClickListener(this::onEditMediaDescriptionClick);
|
||||
@@ -838,12 +935,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(!TextUtils.isEmpty(draft.description))
|
||||
draft.descriptionView.setText(draft.description);
|
||||
|
||||
if(uploadingAttachment!=draft && !queuedAttachments.contains(draft)){
|
||||
draft.progressBar.setVisibility(View.GONE);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
|
||||
draft.overlay.setBackgroundColor(0xA6000000);
|
||||
}
|
||||
if(failedAttachments.contains(draft)){
|
||||
draft.infoBar.setVisibility(View.GONE);
|
||||
draft.overlay.setVisibility(View.VISIBLE);
|
||||
|
||||
if(draft.state==AttachmentUploadState.UPLOADING || draft.state==AttachmentUploadState.PROCESSING || draft.state==AttachmentUploadState.QUEUED){
|
||||
draft.progressBar.setVisibility(View.GONE);
|
||||
}else if(draft.state==AttachmentUploadState.ERROR){
|
||||
draft.setOverlayVisible(true, false);
|
||||
}
|
||||
|
||||
return thumb;
|
||||
@@ -855,67 +954,92 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
draft.uri=uri;
|
||||
draft.description=description;
|
||||
attachmentsView.addView(createMediaAttachmentView(draft));
|
||||
allAttachments.add(draft);
|
||||
attachments.add(draft);
|
||||
attachmentsView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
||||
if(uploadingAttachment!=null)
|
||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||
uploadingAttachment=attachment;
|
||||
if(areThereAnyUploadingAttachments()){
|
||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||
}
|
||||
attachment.state=AttachmentUploadState.UPLOADING;
|
||||
attachment.progressBar.setVisibility(View.VISIBLE);
|
||||
ObjectAnimator rotationAnimator=ObjectAnimator.ofFloat(attachment.progressBar, View.ROTATION, 0f, 360f);
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
rotationAnimator.start();
|
||||
attachment.progressBarAnimator=rotationAnimator;
|
||||
int maxSize=0;
|
||||
String contentType=getActivity().getContentResolver().getType(attachment.uri);
|
||||
if(contentType!=null && contentType.startsWith("image/")){
|
||||
maxSize=2_073_600; // TODO get this from instance configuration when it gets added there
|
||||
}
|
||||
attachment.uploadStateTitle.setText("");
|
||||
attachment.uploadStateText.setText("");
|
||||
attachment.progressBar.setProgress(0);
|
||||
attachment.speedTracker.reset();
|
||||
attachment.speedTracker.addSample(0);
|
||||
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri, maxSize, attachment.description)
|
||||
.setProgressListener(new ProgressListener(){
|
||||
@Override
|
||||
public void onProgress(long transferred, long total){
|
||||
if(updateUploadEtaRunnable==null){
|
||||
UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100);
|
||||
}
|
||||
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
attachment.progressBar.setProgress(progress, true);
|
||||
else
|
||||
attachment.progressBar.setProgress(progress);
|
||||
|
||||
attachment.speedTracker.setTotalBytes(total);
|
||||
attachment.uploadStateTitle.setText(getString(R.string.file_upload_progress, UiUtils.formatFileSize(getActivity(), transferred, true), UiUtils.formatFileSize(getActivity(), total, true)));
|
||||
attachment.speedTracker.addSample(transferred);
|
||||
}
|
||||
})
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
attachment.serverAttachment=result;
|
||||
attachment.uploadRequest=null;
|
||||
uploadingAttachment=null;
|
||||
attachments.add(attachment);
|
||||
attachment.progressBar.setVisibility(View.GONE);
|
||||
if(!queuedAttachments.isEmpty())
|
||||
uploadMediaAttachment(queuedAttachments.remove(0));
|
||||
updatePublishButtonState();
|
||||
|
||||
rotationAnimator.cancel();
|
||||
V.setVisibilityAnimated(attachment.overlay, View.GONE);
|
||||
V.setVisibilityAnimated(attachment.infoBar, View.VISIBLE);
|
||||
if(TextUtils.isEmpty(result.url)){
|
||||
attachment.state=AttachmentUploadState.PROCESSING;
|
||||
attachment.processingPollingRunnable=()->pollForMediaAttachmentProcessing(attachment);
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
attachment.uploadStateTitle.setText(R.string.upload_processing);
|
||||
attachment.uploadStateText.setText("");
|
||||
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
}else{
|
||||
finishMediaAttachmentUpload(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
attachment.uploadRequest=null;
|
||||
uploadingAttachment=null;
|
||||
failedAttachments.add(attachment);
|
||||
// error.showToast(getActivity());
|
||||
Toast.makeText(getActivity(), R.string.image_upload_failed, Toast.LENGTH_SHORT).show();
|
||||
attachment.progressBarAnimator=null;
|
||||
attachment.state=AttachmentUploadState.ERROR;
|
||||
attachment.uploadStateTitle.setText(R.string.upload_failed);
|
||||
if(error instanceof MastodonErrorResponse er){
|
||||
if(er.underlyingException instanceof SocketException || er.underlyingException instanceof UnknownHostException || er.underlyingException instanceof InterruptedIOException)
|
||||
attachment.uploadStateText.setText(R.string.upload_error_connection_lost);
|
||||
else
|
||||
attachment.uploadStateText.setText(er.error);
|
||||
}else{
|
||||
attachment.uploadStateText.setText("");
|
||||
}
|
||||
attachment.retryButton.setImageResource(R.drawable.ic_fluent_arrow_clockwise_24_filled);
|
||||
attachment.retryButton.setContentDescription(getString(R.string.retry_upload));
|
||||
|
||||
rotationAnimator.cancel();
|
||||
V.setVisibilityAnimated(attachment.retryButton, View.VISIBLE);
|
||||
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
||||
|
||||
if(!queuedAttachments.isEmpty())
|
||||
uploadMediaAttachment(queuedAttachments.remove(0));
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -923,37 +1047,109 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private void onRemoveMediaAttachmentClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att==uploadingAttachment){
|
||||
att.uploadRequest.cancel();
|
||||
uploadingAttachment=null;
|
||||
if(!queuedAttachments.isEmpty())
|
||||
uploadMediaAttachment(queuedAttachments.remove(0));
|
||||
}else{
|
||||
attachments.remove(att);
|
||||
queuedAttachments.remove(att);
|
||||
failedAttachments.remove(att);
|
||||
}
|
||||
allAttachments.remove(att);
|
||||
if(att.isUploadingOrProcessing())
|
||||
att.cancelUpload();
|
||||
attachments.remove(att);
|
||||
uploadNextQueuedAttachment();
|
||||
attachmentsView.removeView(att.view);
|
||||
if(getMediaAttachmentsCount()==0)
|
||||
attachmentsView.setVisibility(View.GONE);
|
||||
updatePublishButtonState();
|
||||
pollBtn.setEnabled(attachments.isEmpty() && queuedAttachments.isEmpty() && failedAttachments.isEmpty() && uploadingAttachment==null);
|
||||
pollBtn.setEnabled(attachments.isEmpty());
|
||||
mediaBtn.setEnabled(true);
|
||||
}
|
||||
|
||||
private void onRetryMediaUploadClick(View v){
|
||||
private void onRetryOrCancelMediaUploadClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(failedAttachments.remove(att)){
|
||||
V.setVisibilityAnimated(att.retryButton, View.GONE);
|
||||
if(att.state==AttachmentUploadState.ERROR){
|
||||
att.retryButton.setImageResource(R.drawable.ic_fluent_dismiss_24_filled);
|
||||
att.retryButton.setContentDescription(getString(R.string.cancel));
|
||||
V.setVisibilityAnimated(att.progressBar, View.VISIBLE);
|
||||
if(uploadingAttachment==null)
|
||||
uploadMediaAttachment(att);
|
||||
else
|
||||
queuedAttachments.add(att);
|
||||
att.state=AttachmentUploadState.QUEUED;
|
||||
if(!areThereAnyUploadingAttachments()){
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
}else{
|
||||
onRemoveMediaAttachmentClick(v);
|
||||
}
|
||||
}
|
||||
|
||||
private void pollForMediaAttachmentProcessing(DraftMediaAttachment attachment){
|
||||
attachment.processingPollingRequest=(GetAttachmentByID) new GetAttachmentByID(attachment.serverAttachment.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
attachment.processingPollingRequest=null;
|
||||
if(!TextUtils.isEmpty(result.url)){
|
||||
attachment.processingPollingRunnable=null;
|
||||
attachment.serverAttachment=result;
|
||||
finishMediaAttachmentUpload(attachment);
|
||||
}else if(getActivity()!=null){
|
||||
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
attachment.processingPollingRequest=null;
|
||||
if(getActivity()!=null)
|
||||
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void finishMediaAttachmentUpload(DraftMediaAttachment attachment){
|
||||
if(attachment.state!=AttachmentUploadState.PROCESSING && attachment.state!=AttachmentUploadState.UPLOADING)
|
||||
throw new IllegalStateException("Unexpected state "+attachment.state);
|
||||
attachment.uploadRequest=null;
|
||||
attachment.state=AttachmentUploadState.DONE;
|
||||
attachment.progressBar.setVisibility(View.GONE);
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
updatePublishButtonState();
|
||||
|
||||
if(attachment.progressBarAnimator!=null){
|
||||
attachment.progressBarAnimator.cancel();
|
||||
attachment.progressBarAnimator=null;
|
||||
}
|
||||
attachment.setOverlayVisible(false, true);
|
||||
}
|
||||
|
||||
private void uploadNextQueuedAttachment(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state==AttachmentUploadState.QUEUED){
|
||||
uploadMediaAttachment(att);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean areThereAnyUploadingAttachments(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state==AttachmentUploadState.UPLOADING)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateUploadETAs(){
|
||||
if(!areThereAnyUploadingAttachments()){
|
||||
UiUtils.removeCallbacks(updateUploadEtaRunnable);
|
||||
updateUploadEtaRunnable=null;
|
||||
return;
|
||||
}
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state==AttachmentUploadState.UPLOADING){
|
||||
long eta=att.speedTracker.updateAndGetETA();
|
||||
// Log.i(TAG, "onProgress: transfer speed "+UiUtils.formatFileSize(getActivity(), Math.round(att.speedTracker.getLastSpeed()), false)+" average "+UiUtils.formatFileSize(getActivity(), Math.round(att.speedTracker.getAverageSpeed()), false)+" eta "+eta);
|
||||
String time=String.format("%d:%02d", eta/60, eta%60);
|
||||
att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time));
|
||||
}
|
||||
}
|
||||
UiUtils.runOnUiThread(updateUploadEtaRunnable, 100);
|
||||
}
|
||||
|
||||
private void onEditMediaDescriptionClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.serverAttachment==null)
|
||||
@@ -996,7 +1192,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollOptionsView.startDragging(option.view);
|
||||
return true;
|
||||
});
|
||||
option.edit.addTextChangedListener(new SimpleTextWatcher(e->updatePublishButtonState()));
|
||||
option.edit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
if(!creatingView)
|
||||
pollChanged=true;
|
||||
updatePublishButtonState();
|
||||
}));
|
||||
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
|
||||
|
||||
pollOptionsView.addView(option.view);
|
||||
@@ -1016,6 +1216,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private void onSwapPollOptions(int oldIndex, int newIndex){
|
||||
pollOptions.add(newIndex, pollOptions.remove(oldIndex));
|
||||
updatePollOptionHints();
|
||||
pollChanged=true;
|
||||
}
|
||||
|
||||
private void showPollDurationMenu(){
|
||||
@@ -1039,6 +1240,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
default -> throw new IllegalStateException("Unexpected value: "+item.getItemId());
|
||||
};
|
||||
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString()));
|
||||
pollChanged=true;
|
||||
return true;
|
||||
});
|
||||
menu.show();
|
||||
@@ -1055,11 +1257,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
spoilerEdit.setText("");
|
||||
spoilerBtn.setSelected(false);
|
||||
mainEditText.requestFocus();
|
||||
updateCharCounter();
|
||||
}
|
||||
}
|
||||
|
||||
private int getMediaAttachmentsCount(){
|
||||
return allAttachments.size();
|
||||
return attachments.size();
|
||||
}
|
||||
|
||||
private void onVisibilityClick(View v){
|
||||
@@ -1095,12 +1298,53 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
menu.show();
|
||||
}
|
||||
|
||||
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
|
||||
if(getArguments().containsKey("replyTo")){
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||
statusVisibility = replyTo.visibility;
|
||||
}
|
||||
|
||||
// A saved privacy setting from a previous compose session wins over the reply visibility
|
||||
if(savedInstanceState !=null){
|
||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
|
||||
new GetPreferences()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Preferences result){
|
||||
// Only override the reply visibility if our preference is more private
|
||||
if (result.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
|
||||
statusVisibility = switch (result.postingDefaultVisibility) {
|
||||
case PUBLIC -> StatusPrivacy.PUBLIC;
|
||||
case UNLISTED -> StatusPrivacy.UNLISTED;
|
||||
case PRIVATE -> StatusPrivacy.PRIVATE;
|
||||
case DIRECT -> StatusPrivacy.DIRECT;
|
||||
};
|
||||
}
|
||||
|
||||
// A saved privacy setting from a previous compose session wins over all
|
||||
if(savedInstanceState !=null){
|
||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
|
||||
updateVisibilityIcon ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.w(TAG, "Unable to get user preferences to set default post privacy");
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void updateVisibilityIcon(){
|
||||
if(statusVisibility==null){ // TODO find out why this happens
|
||||
statusVisibility=StatusPrivacy.PUBLIC;
|
||||
}
|
||||
visibilityBtn.setImageResource(switch(statusVisibility){
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_24_filled;
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
|
||||
case DIRECT -> R.drawable.ic_at_symbol;
|
||||
@@ -1109,6 +1353,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged(int start, int end){
|
||||
if(ignoreSelectionChanges)
|
||||
return;
|
||||
if(start==end && mainEditText.length()>0){
|
||||
ComposeAutocompleteSpan[] spans=mainEditText.getText().getSpans(start, end, ComposeAutocompleteSpan.class);
|
||||
if(spans.length>0){
|
||||
@@ -1179,6 +1425,30 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
finishAutocomplete();
|
||||
}
|
||||
|
||||
private void loadVideoThumbIntoView(ImageView target, Uri uri){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Context context=getActivity();
|
||||
if(context==null)
|
||||
return;
|
||||
try{
|
||||
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
|
||||
mmr.setDataSource(context, uri);
|
||||
Bitmap frame=mmr.getFrameAtTime(3_000_000);
|
||||
mmr.release();
|
||||
int size=Math.max(frame.getWidth(), frame.getHeight());
|
||||
int maxSize=V.dp(250);
|
||||
if(size>maxSize){
|
||||
float factor=maxSize/(float)size;
|
||||
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
|
||||
}
|
||||
Bitmap finalFrame=frame;
|
||||
target.post(()->target.setImageBitmap(finalFrame));
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTitle(){
|
||||
return getString(R.string.new_post);
|
||||
@@ -1199,14 +1469,75 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public Attachment serverAttachment;
|
||||
public Uri uri;
|
||||
public transient UploadAttachment uploadRequest;
|
||||
public transient GetAttachmentByID processingPollingRequest;
|
||||
public String description;
|
||||
public String mimeType;
|
||||
public AttachmentUploadState state=AttachmentUploadState.QUEUED;
|
||||
|
||||
public transient View view;
|
||||
public transient ProgressBar progressBar;
|
||||
public transient TextView descriptionView;
|
||||
public transient View overlay;
|
||||
public transient View infoBar;
|
||||
public transient Button retryButton;
|
||||
public transient ImageButton retryButton;
|
||||
public transient ObjectAnimator progressBarAnimator;
|
||||
public transient Runnable processingPollingRunnable;
|
||||
public transient ImageView imageView;
|
||||
public transient TextView uploadStateTitle, uploadStateText;
|
||||
public transient TransferSpeedTracker speedTracker=new TransferSpeedTracker();
|
||||
|
||||
public void cancelUpload(){
|
||||
switch(state){
|
||||
case UPLOADING -> {
|
||||
if(uploadRequest!=null){
|
||||
uploadRequest.cancel();
|
||||
uploadRequest=null;
|
||||
}
|
||||
}
|
||||
case PROCESSING -> {
|
||||
if(processingPollingRunnable!=null){
|
||||
UiUtils.removeCallbacks(processingPollingRunnable);
|
||||
processingPollingRunnable=null;
|
||||
}
|
||||
if(processingPollingRequest!=null){
|
||||
processingPollingRequest.cancel();
|
||||
processingPollingRequest=null;
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected state "+state);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUploadingOrProcessing(){
|
||||
return state==AttachmentUploadState.UPLOADING || state==AttachmentUploadState.PROCESSING;
|
||||
}
|
||||
|
||||
public void setOverlayVisible(boolean visible, boolean animated){
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
|
||||
if(visible){
|
||||
imageView.setRenderEffect(RenderEffect.createBlurEffect(V.dp(16), V.dp(16), Shader.TileMode.REPEAT));
|
||||
}else{
|
||||
imageView.setRenderEffect(null);
|
||||
}
|
||||
}
|
||||
int infoBarVis=visible ? View.GONE : View.VISIBLE;
|
||||
int overlayVis=visible ? View.VISIBLE : View.GONE;
|
||||
if(animated){
|
||||
V.setVisibilityAnimated(infoBar, infoBarVis);
|
||||
V.setVisibilityAnimated(overlay, overlayVis);
|
||||
}else{
|
||||
infoBar.setVisibility(infoBarVis);
|
||||
overlay.setVisibility(overlayVis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachmentUploadState{
|
||||
QUEUED,
|
||||
UPLOADING,
|
||||
PROCESSING,
|
||||
ERROR,
|
||||
DONE
|
||||
}
|
||||
|
||||
private static class DraftPollOption{
|
||||
|
||||
@@ -2,13 +2,9 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -18,20 +14,14 @@ import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
import org.parceler.Parcels;
|
||||
@@ -41,15 +31,12 @@ import java.util.ArrayList;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.BottomSheet;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||
@@ -141,7 +128,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
}
|
||||
});
|
||||
}
|
||||
}else{
|
||||
}
|
||||
|
||||
return content;
|
||||
|
||||
@@ -23,9 +23,11 @@ import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
@@ -33,6 +35,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -101,6 +104,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.register(this);
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,7 +142,6 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
}
|
||||
@@ -397,4 +404,22 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
scrollToTop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
@@ -16,24 +12,21 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
@@ -84,9 +77,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else{
|
||||
}else if(titleItem!=null){
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
|
||||
return Arrays.asList(titleItem, card);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,91 +155,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private int bgColor=UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground);
|
||||
private int borderColor=UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted);
|
||||
private RectF rect=new RectF();
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
int pos=0;
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
pos=holder.getAbsoluteAdapterPosition();
|
||||
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
||||
if(inset){
|
||||
if(rect.isEmpty()){
|
||||
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
||||
}else{
|
||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
||||
}
|
||||
}else if(!rect.isEmpty()){
|
||||
drawInsetBackground(c);
|
||||
rect.setEmpty();
|
||||
}
|
||||
}
|
||||
if(!rect.isEmpty()){
|
||||
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
|
||||
rect.bottom=parent.getHeight()+V.dp(10);
|
||||
}
|
||||
drawInsetBackground(c);
|
||||
rect.setEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawInsetBackground(Canvas c){
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(bgColor);
|
||||
rect.left=V.dp(12);
|
||||
rect.right=list.getWidth()-V.dp(12);
|
||||
rect.inset(V.dp(4), V.dp(4));
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(borderColor);
|
||||
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
||||
boolean inset=sdi.getItem().inset;
|
||||
int pos=holder.getAbsoluteAdapterPosition();
|
||||
if(inset){
|
||||
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||
int pad;
|
||||
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
||||
pad=V.dp(16);
|
||||
else
|
||||
pad=V.dp(12);
|
||||
boolean insetLeft=true, insetRight=true;
|
||||
if(holder instanceof ImageStatusDisplayItem.Holder<?> img){
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout;
|
||||
PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile;
|
||||
// only inset those items that are on the edges of the layout
|
||||
insetLeft=tile.startCol==0;
|
||||
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
|
||||
// inset all items in the bottom row
|
||||
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
|
||||
bottomSiblingInset=false;
|
||||
}
|
||||
if(insetLeft)
|
||||
outRect.left=pad;
|
||||
if(insetRight)
|
||||
outRect.right=pad;
|
||||
if(!topSiblingInset)
|
||||
outRect.top=pad;
|
||||
if(!bottomSiblingInset)
|
||||
outRect.bottom=pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
@@ -268,4 +179,5 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -421,9 +421,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
if(isSelf){
|
||||
ssb.append('@');
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_fluent_lock_closed_20_filled, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
@@ -431,7 +438,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
username.setText('@'+account.acct);
|
||||
// noinspection SetTextI18n
|
||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||
}
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
@@ -14,15 +15,22 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
@@ -31,11 +39,13 @@ import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
@@ -47,7 +57,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -63,6 +72,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
private TextItem checkForUpdateItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -73,6 +83,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||
items.add(new UpdateItem());
|
||||
}
|
||||
}
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
@@ -102,6 +120,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
|
||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||
items.add(checkForUpdateItem);
|
||||
}
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
@@ -131,7 +153,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
// Add 32dp gaps between sections
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
|
||||
outRect.top=V.dp(32);
|
||||
}
|
||||
});
|
||||
@@ -155,6 +177,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
||||
GlobalUserPreferences.theme=theme;
|
||||
GlobalUserPreferences.save();
|
||||
@@ -294,6 +330,30 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING;
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind();
|
||||
|
||||
UpdateItem updateItem = null;
|
||||
if(items.get(0) instanceof UpdateItem item0) {
|
||||
updateItem = item0;
|
||||
} else if (ev.state != GithubSelfUpdater.UpdateState.CHECKING
|
||||
&& ev.state != GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||
updateItem = new UpdateItem();
|
||||
items.add(0, updateItem);
|
||||
list.setAdapter(new SettingsAdapter());
|
||||
}
|
||||
|
||||
if(updateItem != null && list.findViewHolderForAdapterPosition(0) instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(updateItem);
|
||||
}
|
||||
|
||||
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
@@ -358,10 +418,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
private boolean loading;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick){
|
||||
public TextItem(@StringRes int text, Runnable onClick) {
|
||||
this(text, onClick, false);
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
this.loading=loading;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -395,6 +461,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -408,6 +482,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
case 4 -> new TextViewHolder();
|
||||
case 5 -> new HeaderViewHolder(true);
|
||||
case 6 -> new FooterViewHolder();
|
||||
case 7 -> new UpdateViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
@@ -581,14 +656,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
private final ProgressBar progress;
|
||||
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text=(TextView) itemView;
|
||||
text = itemView.findViewById(R.id.text);
|
||||
progress = itemView.findViewById(R.id.progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
text.setText(item.text);
|
||||
progress.animate().alpha(item.loading ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -609,4 +688,75 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
|
||||
private final TextView text;
|
||||
private final Button button;
|
||||
private final ImageButton cancelBtn;
|
||||
private final ProgressBar progress;
|
||||
|
||||
private ObjectAnimator rotationAnimator;
|
||||
private Runnable progressUpdater=this::updateProgress;
|
||||
|
||||
public UpdateViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_update, list);
|
||||
text=findViewById(R.id.text);
|
||||
button=findViewById(R.id.button);
|
||||
cancelBtn=findViewById(R.id.cancel_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
button.setOnClickListener(v->{
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
switch(updater.getState()){
|
||||
case UPDATE_AVAILABLE -> updater.downloadUpdate();
|
||||
case DOWNLOADED -> updater.installUpdate(getActivity());
|
||||
}
|
||||
});
|
||||
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
|
||||
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(UpdateItem item){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
}else{
|
||||
text.setText(getString(R.string.update_ready, info.version));
|
||||
button.setText(R.string.install_update);
|
||||
}
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
rotationAnimator.start();
|
||||
button.setVisibility(View.INVISIBLE);
|
||||
cancelBtn.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
updateProgress();
|
||||
}else{
|
||||
rotationAnimator.cancel();
|
||||
button.setVisibility(View.VISIBLE);
|
||||
cancelBtn.setVisibility(View.GONE);
|
||||
progress.setVisibility(View.GONE);
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
|
||||
return;
|
||||
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
progress.setProgress(value, true);
|
||||
else
|
||||
progress.setProgress(value);
|
||||
progress.postDelayed(progressUpdater, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
private String id;
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
id=getArguments().getString("id");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.edit_history);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetStatusEditHistory(id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
String action="";
|
||||
if(idx==data.size()-1){
|
||||
action=getString(R.string.edit_original_post);
|
||||
}else{
|
||||
enum StatusEditChangeType{
|
||||
TEXT_CHANGED,
|
||||
SPOILER_ADDED,
|
||||
SPOILER_REMOVED,
|
||||
SPOILER_CHANGED,
|
||||
POLL_ADDED,
|
||||
POLL_REMOVED,
|
||||
POLL_CHANGED,
|
||||
MEDIA_ADDED,
|
||||
MEDIA_REMOVED,
|
||||
MEDIA_REORDERED,
|
||||
MARKED_SENSITIVE,
|
||||
MARKED_NOT_SENSITIVE
|
||||
}
|
||||
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
|
||||
Status prev=data.get(idx+1);
|
||||
|
||||
if(!Objects.equals(s.content, prev.content)){
|
||||
changes.add(StatusEditChangeType.TEXT_CHANGED);
|
||||
}
|
||||
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
|
||||
if(s.spoilerText==null){
|
||||
changes.add(StatusEditChangeType.SPOILER_REMOVED);
|
||||
}else if(prev.spoilerText==null){
|
||||
changes.add(StatusEditChangeType.SPOILER_ADDED);
|
||||
}else{
|
||||
changes.add(StatusEditChangeType.SPOILER_CHANGED);
|
||||
}
|
||||
}
|
||||
if(s.poll!=null || prev.poll!=null){
|
||||
if(s.poll==null){
|
||||
changes.add(StatusEditChangeType.POLL_REMOVED);
|
||||
}else if(prev.poll==null){
|
||||
changes.add(StatusEditChangeType.POLL_ADDED);
|
||||
}else if(!s.poll.id.equals(prev.poll.id)){
|
||||
changes.add(StatusEditChangeType.POLL_CHANGED);
|
||||
}
|
||||
}
|
||||
List<String> newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
|
||||
List<String> prevAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
|
||||
boolean addedOrRemoved=false;
|
||||
if(!newAttachmentIDs.containsAll(prevAttachmentIDs)){
|
||||
changes.add(StatusEditChangeType.MEDIA_REMOVED);
|
||||
addedOrRemoved=true;
|
||||
}
|
||||
if(!prevAttachmentIDs.containsAll(newAttachmentIDs)){
|
||||
changes.add(StatusEditChangeType.MEDIA_ADDED);
|
||||
addedOrRemoved=true;
|
||||
}
|
||||
if(!addedOrRemoved && !newAttachmentIDs.equals(prevAttachmentIDs)){
|
||||
changes.add(StatusEditChangeType.MEDIA_REORDERED);
|
||||
}
|
||||
if(s.sensitive && !prev.sensitive){
|
||||
changes.add(StatusEditChangeType.MARKED_SENSITIVE);
|
||||
}else if(prev.sensitive && !s.sensitive){
|
||||
changes.add(StatusEditChangeType.MARKED_NOT_SENSITIVE);
|
||||
}
|
||||
|
||||
if(changes.size()==1){
|
||||
action=getString(switch(changes.iterator().next()){
|
||||
case TEXT_CHANGED -> R.string.edit_text_edited;
|
||||
case SPOILER_ADDED -> R.string.edit_spoiler_added;
|
||||
case SPOILER_REMOVED -> R.string.edit_spoiler_removed;
|
||||
case SPOILER_CHANGED -> R.string.edit_spoiler_edited;
|
||||
case POLL_ADDED -> R.string.edit_poll_added;
|
||||
case POLL_REMOVED -> R.string.edit_poll_removed;
|
||||
case POLL_CHANGED -> R.string.edit_poll_edited;
|
||||
case MEDIA_ADDED -> R.string.edit_media_added;
|
||||
case MEDIA_REMOVED -> R.string.edit_media_removed;
|
||||
case MEDIA_REORDERED -> R.string.edit_media_reordered;
|
||||
case MARKED_SENSITIVE -> R.string.edit_marked_sensitive;
|
||||
case MARKED_NOT_SENSITIVE -> R.string.edit_marked_not_sensitive;
|
||||
});
|
||||
}else{
|
||||
action=getString(R.string.edit_multiple_changed);
|
||||
}
|
||||
}
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,14 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -61,7 +62,58 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){}
|
||||
|
||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){}
|
||||
protected void onStatusUpdated(StatusUpdatedEvent ev){
|
||||
ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
|
||||
for(int i=0;i<data.size();i++){
|
||||
Status s=data.get(i);
|
||||
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
|
||||
s.reblog=ev.status;
|
||||
statusesForDisplayItems.add(s);
|
||||
}else if(s.id.equals(ev.status.id)){
|
||||
data.set(i, ev.status);
|
||||
statusesForDisplayItems.add(ev.status);
|
||||
}
|
||||
}
|
||||
for(int i=0;i<preloadedData.size();i++){
|
||||
Status s=preloadedData.get(i);
|
||||
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
|
||||
s.reblog=ev.status;
|
||||
}else if(s.id.equals(ev.status.id)){
|
||||
preloadedData.set(i, ev.status);
|
||||
}
|
||||
}
|
||||
|
||||
if(statusesForDisplayItems.isEmpty())
|
||||
return;
|
||||
|
||||
for(Status s:statusesForDisplayItems){
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.parentID.equals(s.id)){
|
||||
int start=i;
|
||||
for(;i<displayItems.size();i++){
|
||||
if(!displayItems.get(i).parentID.equals(s.id))
|
||||
break;
|
||||
}
|
||||
List<StatusDisplayItem> postItems=displayItems.subList(start, i);
|
||||
postItems.clear();
|
||||
postItems.addAll(buildDisplayItems(s));
|
||||
int oldSize=i-start, newSize=postItems.size();
|
||||
if(oldSize==newSize){
|
||||
adapter.notifyItemRangeChanged(start, newSize);
|
||||
}else if(oldSize<newSize){
|
||||
adapter.notifyItemRangeChanged(start, oldSize);
|
||||
adapter.notifyItemRangeInserted(start+oldSize, newSize-oldSize);
|
||||
}else{
|
||||
adapter.notifyItemRangeChanged(start, newSize);
|
||||
adapter.notifyItemRangeRemoved(start+newSize, oldSize-newSize);
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Status getContentStatusByID(String id){
|
||||
Status s=getStatusByID(id);
|
||||
@@ -139,8 +191,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||
StatusListFragment.this.onStatusUnpinned(ev);
|
||||
public void onStatusUpdated(StatusUpdatedEvent ev){
|
||||
StatusListFragment.this.onStatusUpdated(ev);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ThreadFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||
if(s==mainStatus){
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
for(StatusDisplayItem item:items){
|
||||
if(item instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
public class GoogleMadeMeAddThisFragment extends AppKitFragment{
|
||||
private UsableRecyclerView list;
|
||||
private MergeRecyclerAdapter adapter;
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private Instance instance;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private Call currentRequest;
|
||||
private ItemsAdapter itemsAdapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
|
||||
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
|
||||
loadServerPrivacyPolicy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
|
||||
|
||||
list=view.findViewById(R.id.list);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
|
||||
TextView title=headerView.findViewById(R.id.title);
|
||||
TextView subtitle=headerView.findViewById(R.id.subtitle);
|
||||
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
|
||||
title.setText(R.string.privacy_policy_title);
|
||||
subtitle.setText(R.string.privacy_policy_subtitle);
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.setSelector(null);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildViewHolder(view) instanceof ItemViewHolder){
|
||||
outRect.left=outRect.right=V.dp(18.5f);
|
||||
outRect.top=V.dp(16);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), SignupFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadServerPrivacyPolicy(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://"+instance.uri+"/terms")
|
||||
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
|
||||
.build();
|
||||
currentRequest=MastodonAPIController.getHttpClient().newCall(req);
|
||||
currentRequest.enqueue(new Callback(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
currentRequest=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
currentRequest=null;
|
||||
try(ResponseBody body=response.body()){
|
||||
if(!response.isSuccessful())
|
||||
return;
|
||||
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
|
||||
final Item item=new Item(doc.title(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
|
||||
Activity activity=getActivity();
|
||||
if(activity!=null){
|
||||
activity.runOnUiThread(()->{
|
||||
items.add(item);
|
||||
itemsAdapter.notifyItemInserted(items.size()-1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
|
||||
private final TextView domain, title;
|
||||
private final ImageView favicon;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_privacy_policy_link, list);
|
||||
domain=findViewById(R.id.domain);
|
||||
title=findViewById(R.id.title);
|
||||
favicon=findViewById(R.id.favicon);
|
||||
itemView.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
itemView.setClipToOutline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item item){
|
||||
domain.setText(item.domain);
|
||||
title.setText(item.title);
|
||||
|
||||
ViewImageLoader.load(favicon, null, new UrlImageLoaderRequest(item.faviconUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
UiUtils.launchWebBrowser(getActivity(), item.url);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Item{
|
||||
public String title, domain, url, faviconUrl;
|
||||
|
||||
public Item(String title, String domain, String url, String faviconUrl){
|
||||
this.title=title;
|
||||
this.domain=domain;
|
||||
this.url=url;
|
||||
this.faviconUrl=faviconUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class InstanceRulesFragment extends AppKitFragment{
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), SignupFragment.class, args);
|
||||
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -85,7 +85,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.NO_REBLOGS)
|
||||
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
@@ -102,6 +102,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
else
|
||||
selectedIDs.add(id);
|
||||
list.invalidate();
|
||||
btn.setEnabled(!selectedIDs.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum ExpandMedia {
|
||||
@SerializedName("default")
|
||||
DEFAULT,
|
||||
@SerializedName("show_all")
|
||||
SHOW_ALL,
|
||||
@SerializedName("hide_all")
|
||||
HIDE_ALL;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Preferred common behaviors to be shared across clients.
|
||||
*/
|
||||
public class Preferences extends BaseModel {
|
||||
/**
|
||||
* Default visibility for new posts
|
||||
*/
|
||||
@SerializedName("posting:default:visibility")
|
||||
public StatusPrivacy postingDefaultVisibility;
|
||||
|
||||
/**
|
||||
* Default sensitivity flag for new posts
|
||||
*/
|
||||
@SerializedName("posting:default:sensitive")
|
||||
public boolean postingDefaultSensitive;
|
||||
|
||||
/**
|
||||
* Default language for new posts
|
||||
*/
|
||||
@SerializedName("posting:default:language")
|
||||
public String postingDefaultLanguage;
|
||||
|
||||
/**
|
||||
* Whether media attachments should be automatically displayed or blurred/hidden.
|
||||
*/
|
||||
@SerializedName("reading:expand:media")
|
||||
public ExpandMedia readingExpandMedia;
|
||||
|
||||
/**
|
||||
* Whether CWs should be expanded by default.
|
||||
*/
|
||||
@SerializedName("reading:expand:spoilers")
|
||||
public boolean readingExpandSpoilers;
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public int reblogsCount;
|
||||
public int favouritesCount;
|
||||
public int repliesCount;
|
||||
public Instant editedAt;
|
||||
|
||||
public String url;
|
||||
public String inReplyToId;
|
||||
|
||||
@@ -4,11 +4,25 @@ import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum StatusPrivacy{
|
||||
@SerializedName("public")
|
||||
PUBLIC,
|
||||
PUBLIC(0),
|
||||
@SerializedName("unlisted")
|
||||
UNLISTED,
|
||||
UNLISTED(1),
|
||||
@SerializedName("private")
|
||||
PRIVATE,
|
||||
PRIVATE(2),
|
||||
@SerializedName("direct")
|
||||
DIRECT;
|
||||
DIRECT(3);
|
||||
|
||||
private int privacy;
|
||||
|
||||
StatusPrivacy(int privacy) {
|
||||
this.privacy = privacy;
|
||||
}
|
||||
|
||||
public boolean isLessVisibleThan(StatusPrivacy other) {
|
||||
return privacy > other.getPrivacy();
|
||||
}
|
||||
|
||||
public int getPrivacy() {
|
||||
return privacy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
@@ -12,6 +13,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusEditHistoryFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
|
||||
@@ -43,39 +45,35 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<ExtendedFooterStatusDisplayItem>{
|
||||
private final TextView reblogs, favorites, time;
|
||||
private final View buttonsView;
|
||||
private final TextView time, favoritesCount, reblogsCount, lastEditTime;
|
||||
private final View favorites, reblogs, editHistory;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_extended_footer, parent);
|
||||
reblogs=findViewById(R.id.reblogs);
|
||||
favorites=findViewById(R.id.favorites);
|
||||
editHistory=findViewById(R.id.edit_history);
|
||||
time=findViewById(R.id.timestamp);
|
||||
buttonsView=findViewById(R.id.button_bar);
|
||||
favoritesCount=findViewById(R.id.favorites_count);
|
||||
reblogsCount=findViewById(R.id.reblogs_count);
|
||||
lastEditTime=findViewById(R.id.last_edited);
|
||||
|
||||
reblogs.setOnClickListener(v->startAccountListFragment(StatusReblogsListFragment.class));
|
||||
favorites.setOnClickListener(v->startAccountListFragment(StatusFavoritesListFragment.class));
|
||||
editHistory.setOnClickListener(v->startEditHistoryFragment());
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(ExtendedFooterStatusDisplayItem item){
|
||||
Status s=item.status;
|
||||
if(s.favouritesCount>0){
|
||||
favorites.setVisibility(View.VISIBLE);
|
||||
favorites.setText(getFormattedPlural(R.plurals.x_favorites, s.favouritesCount));
|
||||
favoritesCount.setText(String.format("%,d", s.favouritesCount));
|
||||
reblogsCount.setText(String.format("%,d", s.reblogsCount));
|
||||
if(s.editedAt!=null){
|
||||
editHistory.setVisibility(View.VISIBLE);
|
||||
lastEditTime.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt)));
|
||||
}else{
|
||||
favorites.setVisibility(View.GONE);
|
||||
}
|
||||
if(s.reblogsCount>0){
|
||||
reblogs.setVisibility(View.VISIBLE);
|
||||
reblogs.setText(getFormattedPlural(R.plurals.x_reblogs, s.reblogsCount));
|
||||
}else{
|
||||
reblogs.setVisibility(View.GONE);
|
||||
}
|
||||
if(s.favouritesCount==0 && s.reblogsCount==0){
|
||||
buttonsView.setVisibility(View.GONE);
|
||||
}else{
|
||||
buttonsView.setVisibility(View.VISIBLE);
|
||||
editHistory.setVisibility(View.GONE);
|
||||
}
|
||||
String timeStr=TIME_FORMATTER.format(item.status.createdAt.atZone(ZoneId.systemDefault()));
|
||||
if(item.status.application!=null && !TextUtils.isEmpty(item.status.application.name)){
|
||||
@@ -108,5 +106,12 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
Nav.go(item.parentFragment.getActivity(), cls, args);
|
||||
}
|
||||
|
||||
private void startEditHistoryFragment(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("id", item.status.id);
|
||||
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import android.widget.Toast;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -135,7 +137,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||
Account account=item.user;
|
||||
int id=menuItem.getItemId();
|
||||
if(id==R.id.delete){
|
||||
if(id==R.id.edit){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("editStatus", Parcels.wrap(item.status));
|
||||
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}else{
|
||||
new GetStatusSourceText(item.status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(GetStatusSourceText.Response result){
|
||||
args.putString("sourceText", result.text);
|
||||
args.putString("sourceSpoiler", result.spoilerText);
|
||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(item.parentFragment.getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(item.parentFragment.getActivity(), R.string.loading, true)
|
||||
.exec(item.parentFragment.getAccountID());
|
||||
}
|
||||
}else if(id==R.id.delete){
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}else if(id==R.id.delete_and_redraft) {
|
||||
UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
@@ -179,7 +205,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.user.acct);
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
if(item.status==null || item.status.editedAt==null)
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
else
|
||||
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
||||
if(item.hasVisibilityToggle){
|
||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||
@@ -253,6 +282,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
Account account=item.user;
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
@@ -114,7 +115,7 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
if(addFooter){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
if(status.hasGapAfter)
|
||||
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
|
||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
int i=1;
|
||||
|
||||
@@ -129,7 +129,16 @@ public class HtmlParser{
|
||||
}
|
||||
|
||||
public static void parseCustomEmoji(SpannableStringBuilder ssb, List<Emoji> emojis){
|
||||
Map<String, Emoji> emojiByCode=emojis.stream().collect(Collectors.toMap(e->e.shortcode, Function.identity()));
|
||||
Map<String, Emoji> emojiByCode =
|
||||
emojis.stream()
|
||||
.collect(
|
||||
Collectors.toMap(e->e.shortcode, Function.identity(), (emoji1, emoji2) -> {
|
||||
// Ignore duplicate shortcodes and just take the first, it will be
|
||||
// the same emoji anyway
|
||||
return emoji1;
|
||||
})
|
||||
);
|
||||
|
||||
Matcher matcher=EMOJI_CODE_PATTERN.matcher(ssb);
|
||||
int spanCount=0;
|
||||
CustomEmojiSpan lastSpan=null;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.NotificationsListFragment;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||
private final BaseStatusListFragment<?> listFragment;
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private int bgColor;
|
||||
private int borderColor;
|
||||
private RectF rect=new RectF();
|
||||
|
||||
public InsetStatusItemDecoration(BaseStatusListFragment<?> listFragment){
|
||||
this.listFragment=listFragment;
|
||||
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), android.R.attr.colorBackground);
|
||||
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorPollVoted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
|
||||
int pos=0;
|
||||
for(int i=0; i<parent.getChildCount(); i++){
|
||||
View child=parent.getChildAt(i);
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||
pos=holder.getAbsoluteAdapterPosition();
|
||||
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
||||
if(inset){
|
||||
if(rect.isEmpty()){
|
||||
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
||||
}else{
|
||||
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
|
||||
}
|
||||
}else if(!rect.isEmpty()){
|
||||
drawInsetBackground(parent, c);
|
||||
rect.setEmpty();
|
||||
}
|
||||
}
|
||||
if(!rect.isEmpty()){
|
||||
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
|
||||
rect.bottom=parent.getHeight()+V.dp(10);
|
||||
}
|
||||
drawInsetBackground(parent, c);
|
||||
rect.setEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawInsetBackground(RecyclerView list, Canvas c){
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(bgColor);
|
||||
rect.left=V.dp(12);
|
||||
rect.right=list.getWidth()-V.dp(12);
|
||||
rect.inset(V.dp(4), V.dp(4));
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(borderColor);
|
||||
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
|
||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
List<StatusDisplayItem> displayItems=listFragment.getDisplayItems();
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
||||
boolean inset=sdi.getItem().inset;
|
||||
int pos=holder.getAbsoluteAdapterPosition();
|
||||
if(inset){
|
||||
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||
int pad;
|
||||
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
||||
pad=V.dp(16);
|
||||
else
|
||||
pad=V.dp(12);
|
||||
boolean insetLeft=true, insetRight=true;
|
||||
if(holder instanceof ImageStatusDisplayItem.Holder<?> img){
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout;
|
||||
PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile;
|
||||
// only inset those items that are on the edges of the layout
|
||||
insetLeft=tile.startCol==0;
|
||||
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
|
||||
// inset all items in the bottom row
|
||||
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
|
||||
bottomSiblingInset=false;
|
||||
}
|
||||
if(insetLeft)
|
||||
outRect.left=pad;
|
||||
if(insetRight)
|
||||
outRect.right=pad;
|
||||
if(!topSiblingInset)
|
||||
outRect.top=pad;
|
||||
if(!bottomSiblingInset)
|
||||
outRect.bottom=pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
public class TransferSpeedTracker{
|
||||
private final double SMOOTHING_FACTOR=0.05;
|
||||
|
||||
private long lastKnownPos;
|
||||
private long lastKnownPosTime;
|
||||
private double lastSpeed;
|
||||
private double averageSpeed;
|
||||
private long totalBytes;
|
||||
|
||||
public void addSample(long position){
|
||||
if(lastKnownPosTime==0){
|
||||
lastKnownPosTime=SystemClock.uptimeMillis();
|
||||
lastKnownPos=position;
|
||||
}else{
|
||||
long time=SystemClock.uptimeMillis();
|
||||
lastSpeed=(position-lastKnownPos)/((double)(time-lastKnownPosTime)/1000.0);
|
||||
lastKnownPos=position;
|
||||
lastKnownPosTime=time;
|
||||
}
|
||||
}
|
||||
|
||||
public double getLastSpeed(){
|
||||
return lastSpeed;
|
||||
}
|
||||
|
||||
public double getAverageSpeed(){
|
||||
return averageSpeed;
|
||||
}
|
||||
|
||||
public long updateAndGetETA(){ // must be called at a constant interval
|
||||
if(averageSpeed==0.0)
|
||||
averageSpeed=lastSpeed;
|
||||
else
|
||||
averageSpeed=SMOOTHING_FACTOR*lastSpeed+(1.0-SMOOTHING_FACTOR)*averageSpeed;
|
||||
return Math.round((totalBytes-lastKnownPos)/averageSpeed);
|
||||
}
|
||||
|
||||
public void setTotalBytes(long totalBytes){
|
||||
this.totalBytes=totalBytes;
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
lastKnownPos=lastKnownPosTime=0;
|
||||
lastSpeed=averageSpeed=0.0;
|
||||
totalBytes=0;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -98,6 +99,7 @@ import okhttp3.MediaType;
|
||||
public class UiUtils{
|
||||
private static Handler mainHandler=new Handler(Looper.getMainLooper());
|
||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM");
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||
|
||||
private UiUtils(){}
|
||||
|
||||
@@ -142,6 +144,23 @@ public class UiUtils{
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){
|
||||
long t=instant.toEpochMilli();
|
||||
long now=System.currentTimeMillis();
|
||||
long diff=now-t;
|
||||
if(diff<1000L){
|
||||
return context.getString(R.string.time_just_now);
|
||||
}else if(diff<60_000L){
|
||||
int secs=(int)(diff/1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
|
||||
}else if(diff<3600_000L){
|
||||
int mins=(int)(diff/60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
|
||||
}else{
|
||||
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatTimeLeft(Context context, Instant instant){
|
||||
long t=instant.toEpochMilli();
|
||||
long now=System.currentTimeMillis();
|
||||
@@ -195,6 +214,14 @@ public class UiUtils{
|
||||
mainHandler.post(runnable);
|
||||
}
|
||||
|
||||
public static void runOnUiThread(Runnable runnable, long delay){
|
||||
mainHandler.postDelayed(runnable, delay);
|
||||
}
|
||||
|
||||
public static void removeCallbacks(Runnable runnable){
|
||||
mainHandler.removeCallbacks(runnable);
|
||||
}
|
||||
|
||||
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
|
||||
public static int lerp(int startValue, int endValue, float fraction) {
|
||||
return startValue + Math.round(fraction * (endValue - startValue));
|
||||
@@ -212,6 +239,18 @@ public class UiUtils{
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
public static String formatFileSize(Context context, long size, boolean atLeastKB){
|
||||
if(size<1024 && !atLeastKB){
|
||||
return context.getString(R.string.file_size_bytes, size);
|
||||
}else if(size<1024*1024){
|
||||
return context.getString(R.string.file_size_kb, size/1024.0);
|
||||
}else if(size<1024*1024*1024){
|
||||
return context.getString(R.string.file_size_mb, size/(1024.0*1024.0));
|
||||
}else{
|
||||
return context.getString(R.string.file_size_gb, size/(1024.0*1024.0*1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
public static MediaType getFileMediaType(File file){
|
||||
String name=file.getName();
|
||||
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1)));
|
||||
|
||||
@@ -54,12 +54,13 @@ public class ComposeEditText extends EditText{
|
||||
// Support receiving images from keyboards
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs){
|
||||
final var ic = super.onCreateInputConnection(outAttrs);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1){
|
||||
outAttrs.contentMimeTypes=selectionListener.onGetAllowedMediaMimeTypes();
|
||||
inputConnectionWrapper.setTarget(super.onCreateInputConnection(outAttrs));
|
||||
inputConnectionWrapper.setTarget(ic);
|
||||
return inputConnectionWrapper;
|
||||
}
|
||||
return super.onCreateInputConnection(outAttrs);
|
||||
return ic;
|
||||
}
|
||||
|
||||
// Support pasting images
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.joinmastodon.android.updater;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
|
||||
public abstract class GithubSelfUpdater{
|
||||
private static GithubSelfUpdater instance;
|
||||
|
||||
public static GithubSelfUpdater getInstance(){
|
||||
if(instance==null){
|
||||
try{
|
||||
Class<?> c=Class.forName("org.joinmastodon.android.updater.GithubSelfUpdaterImpl");
|
||||
instance=(GithubSelfUpdater) c.newInstance();
|
||||
}catch(IllegalAccessException|InstantiationException|ClassNotFoundException ignored){
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static boolean needSelfUpdating(){
|
||||
return BuildConfig.BUILD_TYPE.equals("githubRelease") || BuildConfig.BUILD_TYPE.equals("debug");
|
||||
}
|
||||
|
||||
public abstract void checkForUpdates();
|
||||
|
||||
public abstract void maybeCheckForUpdates();
|
||||
|
||||
public abstract GithubSelfUpdater.UpdateState getState();
|
||||
|
||||
public abstract GithubSelfUpdater.UpdateInfo getUpdateInfo();
|
||||
|
||||
public abstract void downloadUpdate();
|
||||
|
||||
public abstract void installUpdate(Activity activity);
|
||||
|
||||
public abstract float getDownloadProgress();
|
||||
|
||||
public abstract void cancelDownload();
|
||||
|
||||
public abstract void handleIntentFromInstaller(Intent intent, Activity activity);
|
||||
|
||||
public enum UpdateState{
|
||||
NO_UPDATE,
|
||||
CHECKING,
|
||||
UPDATE_AVAILABLE,
|
||||
DOWNLOADING,
|
||||
DOWNLOADED
|
||||
}
|
||||
|
||||
public static class UpdateInfo{
|
||||
public String version;
|
||||
public long size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16.7096,17.7682C19.4819,17.4391 21.8955,15.7408 22.199,14.1888C22.6769,11.7442 22.6376,8.2231 22.6376,8.2231C22.6376,3.4504 19.4929,2.0516 19.4929,2.0516C17.9073,1.3274 15.1846,1.023 12.356,1H12.2865C9.4579,1.023 6.7369,1.3274 5.1513,2.0516C5.1513,2.0516 2.0066,3.4504 2.0066,8.2231C2.0066,8.5125 2.0051,8.8169 2.0035,9.1339C1.9991,10.0135 1.9943,10.9896 2.0199,12.0083C2.1341,16.6755 2.8805,21.2752 7.2202,22.4175C9.2213,22.944 10.9392,23.0542 12.323,22.9785C14.832,22.8403 16.2406,22.0883 16.2406,22.0883L16.1577,20.2779C16.1577,20.2779 14.3648,20.8402 12.3511,20.7717C10.356,20.7037 8.2496,20.5577 7.9269,18.1221C7.8972,17.9082 7.8823,17.6794 7.8823,17.4391C7.8823,17.4391 9.8408,17.9152 12.323,18.0283C13.8407,18.0974 15.2639,17.9399 16.7096,17.7682ZM18.8747,14.3719V8.5932C18.8747,7.4121 18.5723,6.4736 17.9648,5.7792C17.3382,5.0849 16.518,4.729 15.4997,4.729C14.3212,4.729 13.4291,5.1792 12.8392,6.0799L12.2657,7.0359L11.692,6.0799C11.1023,5.1792 10.21,4.729 9.0316,4.729C8.0134,4.729 7.193,5.0849 6.5664,5.7792C5.9589,6.4736 5.6565,7.4121 5.6565,8.5932V14.3719H7.959V8.763C7.959,7.5805 8.4594,6.9806 9.4602,6.9806C10.5665,6.9806 11.1211,7.6925 11.1211,9.1001V12.1701H13.4101V9.1001C13.4101,7.6925 13.9647,6.9806 15.071,6.9806C16.0718,6.9806 16.5722,7.5805 16.5722,8.763V14.3719H18.8747Z"
|
||||
android:fillColor="#fff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
5
mastodon/src/main/res/drawable/bg_settings_update.xml
Normal file
5
mastodon/src/main/res/drawable/bg_settings_update.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?colorPollVoted"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorForeground">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#18000000"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?android:colorBackground"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
8
mastodon/src/main/res/drawable/bg_upload_progress.xml
Normal file
8
mastodon/src/main/res/drawable/bg_upload_progress.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/highlight_over_dark">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/gray_600"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -0,0 +1,3 @@
|
||||
<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 4.75c-4.004 0-7.25 3.246-7.25 7.25s3.246 7.25 7.25 7.25 7.25-3.246 7.25-7.25c0-0.286-0.017-0.567-0.049-0.844C19.133 10.568 19.56 10 20.151 10c0.515 0 0.968 0.358 1.03 0.87 0.046 0.37 0.069 0.747 0.069 1.13 0 5.109-4.141 9.25-9.25 9.25S2.75 17.109 2.75 12 6.891 2.75 12 2.75c2.173 0 4.171 0.75 5.75 2.004V4.25c0-0.552 0.448-1 1-1s1 0.448 1 1v2.698L19.784 7H19.75v0.25c0 0.552-0.448 1-1 1h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1h0.666c-1.222-0.94-2.754-1.5-4.416-1.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="16" android:viewportHeight="16">
|
||||
<path android:pathData="M2.397 2.554L2.47 2.47c0.266-0.267 0.683-0.29 0.976-0.073L3.53 2.47 8 6.939l4.47-4.47c0.293-0.292 0.767-0.292 1.06 0 0.293 0.294 0.293 0.768 0 1.061L9.061 8l4.47 4.47c0.266 0.266 0.29 0.683 0.072 0.976L13.53 13.53c-0.266 0.267-0.683 0.29-0.976 0.073L12.47 13.53 8 9.061l-4.47 4.47c-0.293 0.292-0.767 0.292-1.06 0-0.293-0.294-0.293-0.768 0-1.061L6.939 8l-4.47-4.47C2.204 3.264 2.18 2.847 2.398 2.554L2.47 2.47 2.397 2.554z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<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.21 4.387l0.083-0.094c0.36-0.36 0.928-0.388 1.32-0.083l0.094 0.083L12 10.585l6.293-6.292c0.39-0.39 1.024-0.39 1.414 0 0.39 0.39 0.39 1.024 0 1.414L13.415 12l6.292 6.293c0.36 0.36 0.388 0.928 0.083 1.32l-0.083 0.094c-0.36 0.36-0.928 0.388-1.32 0.083l-0.094-0.083L12 13.415l-6.293 6.292c-0.39 0.39-1.024 0.39-1.414 0-0.39-0.39-0.39-1.024 0-1.414L10.585 12 4.293 5.707c-0.36-0.36-0.388-0.928-0.083-1.32l0.083-0.094L4.21 4.387z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -1,3 +1,3 @@
|
||||
<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.946 2.047l0.005 0.007C11.296 2.02 11.646 2 12 2c5.522 0 10 4.477 10 10s-4.478 10-10 10c-3.21 0-6.066-1.512-7.896-3.862H4.102v-0.003C2.786 16.441 2 14.312 2 12c0-5.162 3.911-9.41 8.932-9.944l0.014-0.009zM12 3.5c-0.053 0-0.106 0-0.16 0.002 0.123 0.244 0.255 0.532 0.374 0.85 0.347 0.921 0.666 2.28 0.1 3.486-0.522 1.113-1.424 1.4-2.09 1.573L10.14 9.432c-0.657 0.17-0.91 0.235-1.093 0.514-0.17 0.257-0.144 0.582 0.061 1.25l0.046 0.148c0.082 0.258 0.18 0.57 0.23 0.863 0.064 0.364 0.082 0.827-0.152 1.275-0.231 0.444-0.538 0.747-0.9 0.945-0.341 0.185-0.694 0.256-0.958 0.302l-0.093 0.017c-0.515 0.09-0.761 0.134-1 0.39-0.187 0.2-0.307 0.553-0.377 1.079-0.029 0.214-0.046 0.427-0.064 0.646l-0.01 0.117c-0.02 0.242-0.044 0.521-0.099 0.76v0.002c1.554 1.696 3.787 2.76 6.27 2.76 1.576 0 3.053-0.43 4.319-1.178-0.099-0.1-0.205-0.218-0.31-0.35-0.34-0.428-0.786-1.164-0.631-2.033 0.074-0.418 0.298-0.768 0.515-1.036 0.22-0.274 0.486-0.526 0.72-0.74l0.158-0.146c0.179-0.163 0.33-0.301 0.46-0.437 0.172-0.18 0.21-0.262 0.212-0.267 0.068-0.224-0.015-0.384-0.106-0.454-0.046-0.035-0.107-0.06-0.19-0.061-0.084 0-0.22 0.024-0.401 0.14-0.21 0.132-0.515 0.214-0.836 0.085-0.267-0.108-0.415-0.314-0.486-0.432-0.144-0.237-0.225-0.546-0.278-0.772-0.04-0.174-0.08-0.372-0.115-0.553l-0.04-0.206c-0.05-0.25-0.094-0.428-0.134-0.54l-0.02-0.037c-0.014-0.027-0.035-0.062-0.064-0.105-0.058-0.089-0.133-0.192-0.227-0.317l-0.11-0.143c-0.16-0.212-0.353-0.463-0.516-0.712-0.196-0.298-0.417-0.688-0.487-1.104-0.037-0.22-0.036-0.475 0.055-0.734 0.094-0.264 0.265-0.482 0.487-0.649 0.483-0.362 1.193-1.172 1.823-1.959 0.288-0.359 0.544-0.695 0.736-0.95C15.222 3.98 13.667 3.5 12 3.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
<path android:pathData="M10.946 2.047l0.005 0.007C11.296 2.02 11.646 2 12 2c5.522 0 10 4.477 10 10s-4.478 10-10 10c-3.21 0-6.066-1.512-7.896-3.862H4.102v-0.003C2.786 16.441 2 14.312 2 12c0-5.162 3.911-9.41 8.932-9.944l0.014-0.009zM12 3.5c-0.053 0-0.106 0-0.16 0.002 0.123 0.244 0.255 0.532 0.374 0.85 0.347 0.921 0.666 2.28 0.1 3.486-0.522 1.113-1.424 1.4-2.09 1.573L10.14 9.432c-0.657 0.17-0.91 0.235-1.093 0.514-0.17 0.257-0.144 0.582 0.061 1.25l0.046 0.148c0.082 0.258 0.18 0.57 0.23 0.863 0.064 0.364 0.082 0.827-0.152 1.275-0.231 0.444-0.538 0.747-0.9 0.945-0.341 0.185-0.694 0.256-0.958 0.302l-0.093 0.017c-0.515 0.09-0.761 0.134-1 0.39-0.187 0.2-0.307 0.553-0.377 1.079-0.029 0.214-0.046 0.427-0.064 0.646l-0.01 0.117c-0.02 0.242-0.044 0.521-0.099 0.76v0.002c1.554 1.696 3.787 2.76 6.27 2.76 1.576 0 3.053-0.43 4.319-1.178-0.099-0.1-0.205-0.218-0.31-0.35-0.34-0.428-0.786-1.164-0.631-2.033 0.074-0.418 0.298-0.768 0.515-1.036 0.22-0.274 0.486-0.526 0.72-0.74l0.158-0.146c0.179-0.163 0.33-0.301 0.46-0.437 0.172-0.18 0.21-0.262 0.212-0.267 0.068-0.224-0.015-0.384-0.106-0.454-0.046-0.035-0.107-0.06-0.19-0.061-0.084 0-0.22 0.024-0.401 0.14-0.21 0.132-0.515 0.214-0.836 0.085-0.267-0.108-0.415-0.314-0.486-0.432-0.144-0.237-0.225-0.546-0.278-0.772-0.04-0.174-0.08-0.372-0.115-0.553l-0.04-0.206c-0.05-0.25-0.094-0.428-0.134-0.54l-0.02-0.037c-0.014-0.027-0.035-0.062-0.064-0.105-0.058-0.089-0.133-0.192-0.227-0.317l-0.11-0.143c-0.16-0.212-0.353-0.463-0.516-0.712-0.196-0.298-0.417-0.688-0.487-1.104-0.037-0.22-0.036-0.475 0.055-0.734 0.094-0.264 0.265-0.482 0.487-0.649 0.483-0.362 1.193-1.172 1.823-1.959 0.288-0.359 0.544-0.695 0.736-0.95C15.222 3.98 13.667 3.5 12 3.5zm5.727 2.22c-0.197 0.263-0.461 0.608-0.757 0.978-0.602 0.751-1.4 1.685-2.05 2.187 0.026 0.1 0.1 0.262 0.255 0.498 0.131 0.2 0.281 0.397 0.44 0.604l0.129 0.17c0.172 0.229 0.411 0.548 0.52 0.844 0.087 0.234 0.149 0.519 0.198 0.762l0.049 0.246c0.025 0.13 0.049 0.253 0.075 0.37 0.601-0.172 1.201-0.068 1.67 0.294 0.608 0.47 0.862 1.286 0.624 2.074-0.11 0.362-0.364 0.66-0.563 0.869-0.17 0.177-0.372 0.362-0.556 0.53l-0.132 0.12c-0.23 0.212-0.423 0.4-0.568 0.579-0.148 0.184-0.195 0.299-0.205 0.356-0.04 0.219 0.067 0.51 0.328 0.838 0.118 0.148 0.244 0.274 0.341 0.362l0.033 0.03C19.36 16.872 20.5 14.569 20.5 12c0-2.488-1.069-4.726-2.773-6.28zM3.5 12c0 1.398 0.338 2.718 0.936 3.881 0.085-0.557 0.262-1.248 0.748-1.768 0.6-0.642 1.335-0.763 1.798-0.839l0.13-0.021c0.248-0.044 0.391-0.083 0.502-0.143 0.088-0.049 0.188-0.128 0.288-0.321 0.015-0.028 0.042-0.107 0.004-0.325-0.032-0.187-0.093-0.381-0.172-0.636-0.02-0.06-0.04-0.125-0.06-0.192-0.185-0.604-0.48-1.602 0.12-2.515 0.522-0.792 1.36-0.994 1.893-1.123l0.162-0.04c0.563-0.145 0.883-0.28 1.108-0.758 0.295-0.629 0.168-1.485-0.146-2.32-0.15-0.396-0.324-0.744-0.463-0.994-0.043-0.078-0.083-0.146-0.116-0.202C6.386 4.498 3.5 7.912 3.5 12z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<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.03 2.97c1.398 1.397 1.398 3.663 0 5.06L9.062 20c-0.277 0.277-0.621 0.477-0.999 0.58l-5.116 1.395c-0.56 0.153-1.073-0.361-0.92-0.921l1.395-5.116c0.103-0.377 0.302-0.722 0.58-0.999L15.97 2.97c1.397-1.398 3.663-1.398 5.06 0zM15 6.06L5.062 16c-0.092 0.092-0.159 0.207-0.193 0.333l-1.05 3.85 3.85-1.05C7.793 19.096 7.908 19.03 8 18.938L17.94 9 15 6.06zm2.03-2.03L16.06 5 19 7.94l0.97-0.97c0.811-0.812 0.811-2.128 0-2.94-0.812-0.811-2.128-0.811-2.94 0z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
10
mastodon/src/main/res/drawable/ic_settings_24_badged.xml
Normal file
10
mastodon/src/main/res/drawable/ic_settings_24_badged.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_fluent_settings_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||
<shape android:shape="oval">
|
||||
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
|
||||
<solid android:color="@color/primary_600"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
12
mastodon/src/main/res/drawable/update_progress.xml
Normal file
12
mastodon/src/main/res/drawable/update_progress.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/progress">
|
||||
<shape
|
||||
android:innerRadius="16dp"
|
||||
android:shape="ring"
|
||||
android:thickness="4dp"
|
||||
android:useLevel="true">
|
||||
<solid android:color="?colorSearchHint"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -6,7 +6,7 @@
|
||||
android:shape="ring"
|
||||
android:thickness="4dp"
|
||||
android:useLevel="true">
|
||||
<solid android:color="?android:colorAccent"/>
|
||||
<solid android:color="@color/gray_100"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -65,30 +65,68 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#cc000000"
|
||||
android:backgroundTint="?colorWindowBackground"
|
||||
android:padding="8dp"
|
||||
android:clipToPadding="false"
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_gravity="center"
|
||||
android:progressDrawable="@drawable/upload_progress"
|
||||
android:max="1000"
|
||||
android:padding="0dp"
|
||||
android:indeterminateOnly="false"
|
||||
android:indeterminate="false"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/retry_upload"
|
||||
android:layout_width="wrap_content"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|bottom"
|
||||
style="?secondaryButtonStyle"
|
||||
android:text="@string/retry_upload"/>
|
||||
android:layout_gravity="center_vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/retry_or_cancel_upload"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:src="@drawable/ic_fluent_dismiss_24_filled"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:tint="@color/gray_100"
|
||||
android:background="@drawable/bg_upload_progress"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:progressDrawable="@drawable/upload_progress"
|
||||
android:max="1000"
|
||||
android:padding="0dp"
|
||||
android:indeterminateOnly="false"
|
||||
android:indeterminate="false"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/state_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:layout_below="@id/retry_or_cancel_upload"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="@color/gray_200"
|
||||
android:textSize="14dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:includeFontPadding="false"
|
||||
tools:text="Upload failed"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/state_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
android:layout_below="@id/state_title"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_200"
|
||||
android:gravity="center_horizontal|top"
|
||||
android:lines="2"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
tools:text="Your device lost connection to the internet"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/remove_btn2"
|
||||
@@ -98,6 +136,7 @@
|
||||
android:layout_gravity="end|bottom"
|
||||
android:background="?android:selectableItemBackgroundBorderless"
|
||||
android:tint="#D92C2C"
|
||||
android:contentDescription="@string/delete"
|
||||
android:src="@drawable/ic_fluent_delete_20_regular"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -2,49 +2,146 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorBackgroundLightest">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
|
||||
android:id="@+id/button_bar"
|
||||
<RelativeLayout
|
||||
android:id="@+id/reblogs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="64dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<Button
|
||||
android:id="@+id/reblogs"
|
||||
android:layout_width="wrap_content"
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/ic_fluent_arrow_repeat_all_24_regular"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:importantForAccessibility="no"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:textSize="14sp"
|
||||
android:minHeight="36dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:background="@drawable/bg_text_button"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="4 reblogs"/>
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:minHeight="22dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/post_info_reblogs"
|
||||
android:textAppearance="@style/m3_body_large" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/favorites"
|
||||
android:layout_width="wrap_content"
|
||||
<TextView
|
||||
android:id="@+id/reblogs_count"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:textSize="14sp"
|
||||
android:minHeight="36dp"
|
||||
android:layout_below="@id/title"
|
||||
android:layout_alignStart="@id/title"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/m3_body_medium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:background="@drawable/bg_text_button"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="12 favorites"/>
|
||||
tools:text="123 456"/>
|
||||
|
||||
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/favorites"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/ic_fluent_star_24_regular"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:importantForAccessibility="no"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:minHeight="22dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/post_info_favorites"
|
||||
android:textAppearance="@style/m3_body_large" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/favorites_count"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/title"
|
||||
android:layout_alignStart="@id/title"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/m3_body_medium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="123 456"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/edit_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/ic_fluent_edit_24_regular"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:importantForAccessibility="no"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_toEndOf="@id/icon"
|
||||
android:minHeight="22dp"
|
||||
android:singleLine="true"
|
||||
android:text="@string/edit_history"
|
||||
android:textAppearance="@style/m3_body_large" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_edited"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/title"
|
||||
android:layout_alignStart="@id/title"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/m3_body_medium"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="123 456"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_margin="16dp"
|
||||
android:minHeight="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="14sp"
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
android:tint="@color/compose_button"
|
||||
android:tintMode="src_in"
|
||||
android:contentDescription="@string/post_visibility"
|
||||
android:src="@drawable/ic_fluent_earth_24_filled"/>
|
||||
android:src="@drawable/ic_fluent_earth_24_regular"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="0px"
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
android:layout_margin="16dp"
|
||||
android:minWidth="145dp"
|
||||
style="?primaryLargeButtonStyle"
|
||||
android:text="@string/next" />
|
||||
android:text="@string/i_agree" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
44
mastodon/src/main/res/layout/item_privacy_policy_link.xml
Normal file
44
mastodon/src/main/res/layout/item_privacy_policy_link.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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="16dp"
|
||||
android:elevation="3dp"
|
||||
android:background="?colorBackgroundLightest"
|
||||
android:foreground="?android:selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/favicon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:importantForAccessibility="no"
|
||||
tools:src="#0f0"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/domain"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:layout_toEndOf="@id/favicon"
|
||||
android:layout_marginStart="4dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="joinmastodon.org"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp"
|
||||
android:layout_below="@id/domain"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textAppearance="@style/m3_title_medium"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="Mastodon for Android privacy policy"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -1,13 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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="48dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="daffdsa"/>
|
||||
android:layoutDirection="locale">
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="48dp"
|
||||
android:paddingRight="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="Account settings"/>
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:alpha="0"
|
||||
/>
|
||||
</LinearLayout>
|
||||
75
mastodon/src/main/res/layout/item_settings_update.xml
Normal file
75
mastodon/src/main/res/layout/item_settings_update.xml
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="64dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:background="@drawable/bg_settings_update"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textAppearance="@style/m3_body_medium"
|
||||
tools:text="@string/update_available"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:textColor="?colorAccentLight"
|
||||
android:textAllCaps="true"
|
||||
android:textSize="14dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:stateListAnimator="@null"
|
||||
tools:text="@string/install_update"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cancel_btn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/bg_update_download_progress"
|
||||
android:tint="?colorSearchHint"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:visibility="gone"
|
||||
android:src="@drawable/ic_fluent_dismiss_16_filled"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:progressDrawable="@drawable/update_progress"
|
||||
android:max="1000"
|
||||
android:padding="0dp"
|
||||
android:visibility="gone"
|
||||
android:indeterminateOnly="false"
|
||||
android:indeterminate="false"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/vis_public"
|
||||
android:icon="@drawable/ic_fluent_earth_24_filled"
|
||||
android:icon="@drawable/ic_fluent_earth_24_regular"
|
||||
android:title="@string/visibility_public"/>
|
||||
<item android:id="@+id/vis_unlisted"
|
||||
android:icon="@drawable/ic_fluent_people_community_24_regular"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/notify_anyone" android:title="@string/notify_anyone" android:icon="@drawable/ic_fluent_earth_24_filled"/>
|
||||
<item android:id="@+id/notify_anyone" android:title="@string/notify_anyone" android:icon="@drawable/ic_fluent_earth_24_regular"/>
|
||||
<item android:id="@+id/notify_follower" android:title="@string/notify_follower" android:icon="@drawable/ic_fluent_people_checkmark_24_regular"/>
|
||||
<item android:id="@+id/notify_followed" android:title="@string/notify_followed" android:icon="@drawable/ic_fluent_people_checkmark_24_regular"/>
|
||||
<item android:id="@+id/notify_none" android:title="@string/notify_none" android:icon="@drawable/ic_fluent_prohibited_24_regular"/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/edit" android:title="@string/edit"/>
|
||||
<item android:id="@+id/delete" android:title="@string/delete"/>
|
||||
<item android:id="@+id/delete_and_redraft" android:title="@string/delete_and_redraft"/>
|
||||
<item android:id="@+id/pin" android:title="@string/pin_post"/>
|
||||
|
||||
@@ -2,4 +2,9 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome>
|
||||
<layer-list>
|
||||
<item android:drawable="@drawable/ic_launcher_monochrome" android:gravity="center"/>
|
||||
</layer-list>
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
@@ -2,4 +2,9 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome>
|
||||
<layer-list>
|
||||
<item android:drawable="@drawable/ic_launcher_monochrome" android:gravity="center"/>
|
||||
</layer-list>
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="get_started">الخطوات الأولى</string>
|
||||
<string name="log_in">تسجيل الدخول</string>
|
||||
<string name="log_in">تسجيلُ الدخول</string>
|
||||
<string name="next">التالي</string>
|
||||
<string name="loading_instance">يَجري الحُصُول على معلومات المَثيل…</string>
|
||||
<string name="error">خطأ</string>
|
||||
@@ -16,7 +16,7 @@
|
||||
<string name="user_sent_follow_request">أرسَلَ طَلَبًا لِمُتابَعَتِك</string>
|
||||
<string name="user_favorited">فَضَّلَ مَنشُورَك</string>
|
||||
<string name="notification_boosted">أعادَ تَدوينَ مَنشُورَك</string>
|
||||
<string name="poll_ended">انتهى استطلاع الرأي</string>
|
||||
<string name="poll_ended">انتهى استطلاعُ الرأي</string>
|
||||
<string name="time_seconds">%d ثا</string>
|
||||
<string name="time_minutes">%d د</string>
|
||||
<string name="time_hours">%d سا</string>
|
||||
@@ -54,7 +54,7 @@
|
||||
<string name="posts">منشورات</string>
|
||||
<string name="posts_and_replies">مَنشُوراتٌ وَرُدُود</string>
|
||||
<string name="media">وسائط</string>
|
||||
<string name="profile_about">عن</string>
|
||||
<string name="profile_about">حَول</string>
|
||||
<string name="button_follow">تابِع</string>
|
||||
<string name="button_following">يُتابِع</string>
|
||||
<string name="edit_profile">حرّر الملف الشخصي</string>
|
||||
@@ -260,8 +260,6 @@
|
||||
<string name="content_warning">تحذير من المحتوى</string>
|
||||
<string name="add_image_description">أضف وصفًا للصورة…</string>
|
||||
<string name="retry_upload">حاول الرفع مجددًا</string>
|
||||
<string name="image_upload_failed">فشل رفع الصورة</string>
|
||||
<string name="video_upload_failed">فشل رفع الفيديو</string>
|
||||
<string name="edit_image">حرّر الصورة</string>
|
||||
<string name="save">احفظ</string>
|
||||
<string name="add_alt_text">أضف نصًا بديلًا</string>
|
||||
@@ -409,4 +407,32 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s عبر %2$s</string>
|
||||
<string name="time_now">الآن</string>
|
||||
<string name="post_info_reblogs">إعادات التدوين</string>
|
||||
<string name="post_info_favorites">المفضلة</string>
|
||||
<string name="last_edit_at_x">آخر تعديل %s</string>
|
||||
<string name="time_just_now">للتوّ</string>
|
||||
<string name="edited_timestamp">عُدّل في %s</string>
|
||||
<string name="edit_original_post">المنشور الأصلي</string>
|
||||
<string name="edit_text_edited">تم تعديل النص</string>
|
||||
<string name="edit_spoiler_added">تم إضافة تحذير المحتوى</string>
|
||||
<string name="edit_spoiler_edited">تم تعديل تحذير المحتوى</string>
|
||||
<string name="edit_spoiler_removed">تم حذف تحذير المحتوى</string>
|
||||
<string name="edit_poll_added">تمت إضافة استطلاع للرأي</string>
|
||||
<string name="edit_poll_edited">تم تعديل الاستطلاع</string>
|
||||
<string name="edit_poll_removed">تمت إزالة الاستطلاع</string>
|
||||
<string name="edit_media_added">تمت إضافة الوسائط</string>
|
||||
<string name="edit_media_removed">تمت إزالة الوسائط</string>
|
||||
<string name="edit_media_reordered">تمت إعادة ترتيب الوسائط</string>
|
||||
<string name="edit_marked_sensitive">مُعَين كحساس</string>
|
||||
<string name="edit_marked_not_sensitive">مُعَين كمنشور غير حساس</string>
|
||||
<string name="edit_multiple_changed">عُدّل المنشور</string>
|
||||
<string name="edit">تعديل</string>
|
||||
<string name="discard_changes">تجاهل التغييرات؟</string>
|
||||
<string name="upload_failed">فشلت عملية التحميل</string>
|
||||
<string name="file_upload_time_remaining">%s متبقية</string>
|
||||
<string name="upload_error_connection_lost">فقد جهازك الاتصال بالإنترنت</string>
|
||||
<string name="upload_processing">قيد المعالجة…</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -162,8 +162,6 @@
|
||||
<string name="content_warning">Pizanje sadržaja</string>
|
||||
<string name="add_image_description">Dodaj opis slike…</string>
|
||||
<string name="retry_upload">Pokušaj ponovo</string>
|
||||
<string name="image_upload_failed">Slika se nije mogla sačuvati</string>
|
||||
<string name="video_upload_failed">Video se nije mogao sačuvati</string>
|
||||
<string name="edit_image">Uredi sliku</string>
|
||||
<string name="save">Sačuvaj</string>
|
||||
<string name="add_alt_text">Dodaj alt tekst</string>
|
||||
@@ -256,4 +254,7 @@
|
||||
<string name="file_saved">Datoteka sačuvana</string>
|
||||
<string name="downloading">Downloading…</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -199,8 +199,6 @@
|
||||
<string name="content_warning">Avís de contingut</string>
|
||||
<string name="add_image_description">Afegir una descripció a la imatge…</string>
|
||||
<string name="retry_upload">Torna a provar de pujar</string>
|
||||
<string name="image_upload_failed">La imatge no s\'ha pogut penjar</string>
|
||||
<string name="video_upload_failed">No s\'ha pogut penjar el vídeo</string>
|
||||
<string name="edit_image">Editar imatge</string>
|
||||
<string name="save">Desar</string>
|
||||
<string name="add_alt_text">Afegir text alternatiu</string>
|
||||
@@ -319,4 +317,7 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s través de %2$s</string>
|
||||
<string name="time_now">ara</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="get_started">Začínáme</string>
|
||||
<string name="log_in">Přihlásit</string>
|
||||
<string name="get_started">Začít</string>
|
||||
<string name="log_in">Přihlásit se</string>
|
||||
<string name="next">Další</string>
|
||||
<string name="loading_instance">Získávání informací o instanci…</string>
|
||||
<string name="error">Chyba</string>
|
||||
@@ -9,13 +9,13 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="preparing_auth">Příprava na ověřování…</string>
|
||||
<string name="finishing_auth">Dokončení ověřování…</string>
|
||||
<string name="user_boosted">%s sdíleno</string>
|
||||
<string name="user_boosted">%s boostnul</string>
|
||||
<string name="in_reply_to">V odpovědi na %s</string>
|
||||
<string name="notifications">Upozornění</string>
|
||||
<string name="user_followed_you">vás sleduje</string>
|
||||
<string name="user_sent_follow_request">vám poslal žádost o sledování</string>
|
||||
<string name="user_favorited">si oblíbil váš příspěvek</string>
|
||||
<string name="notification_boosted">sdílel váš příspěvek</string>
|
||||
<string name="notification_boosted">boostnul váš příspěvek</string>
|
||||
<string name="poll_ended">anketa skončila</string>
|
||||
<string name="time_seconds">%d s</string>
|
||||
<string name="time_minutes">%d m</string>
|
||||
@@ -27,6 +27,24 @@
|
||||
<string name="discard_draft">Zahodit koncept?</string>
|
||||
<string name="discard">Zahodit</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<plurals name="followers">
|
||||
<item quantity="one">sledující</item>
|
||||
<item quantity="few">sledující</item>
|
||||
<item quantity="many">sledujících</item>
|
||||
<item quantity="other">sledujících</item>
|
||||
</plurals>
|
||||
<plurals name="following">
|
||||
<item quantity="one">sledovaní</item>
|
||||
<item quantity="few">sledování</item>
|
||||
<item quantity="many">sledování</item>
|
||||
<item quantity="other">sledování</item>
|
||||
</plurals>
|
||||
<plurals name="posts">
|
||||
<item quantity="one">příspěvek</item>
|
||||
<item quantity="few">příspěvky</item>
|
||||
<item quantity="many">příspěvků</item>
|
||||
<item quantity="other">příspěvků</item>
|
||||
</plurals>
|
||||
<string name="posts">Příspěvky</string>
|
||||
<string name="posts_and_replies">Příspěvky a odpovědi</string>
|
||||
<string name="media">Média</string>
|
||||
@@ -43,6 +61,12 @@
|
||||
<string name="report_user">Nahlásit %s</string>
|
||||
<string name="block_domain">Blokovat %s</string>
|
||||
<string name="unblock_domain">Odblokovat %s</string>
|
||||
<plurals name="x_posts">
|
||||
<item quantity="one">%,d příspěvek</item>
|
||||
<item quantity="few">%,d příspěvky</item>
|
||||
<item quantity="many">%,d příspěvků</item>
|
||||
<item quantity="other">%,d příspěvků</item>
|
||||
</plurals>
|
||||
<string name="profile_joined">Účet vytvořen</string>
|
||||
<string name="done">Hotovo</string>
|
||||
<string name="loading">Načítání…</string>
|
||||
@@ -169,7 +193,7 @@
|
||||
<string name="report_sent_subtitle">Zatímco to posuzujeme, můžete podniknout kroky proti %s.</string>
|
||||
<string name="unfollow_user">Přestat sledovat %s</string>
|
||||
<string name="unfollow">Přestat sledovat</string>
|
||||
<string name="mute_user_explain">Neuvidíte jejich příspěvky nebo sdílení v domovském kanálu. Nebudou vědět, že jsou skrytí.</string>
|
||||
<string name="mute_user_explain">Neuvidíte jejich příspěvky nebo boostnutí v domovském kanálu. Nebudou vědět, že jsou skrytí.</string>
|
||||
<string name="block_user_explain">Už nebudou moci sledovat nebo vidět vaše příspěvky, ale mohou vidět, že byli blokováni.</string>
|
||||
<string name="report_personal_title">Nechcete tohle vidět?</string>
|
||||
<string name="report_personal_subtitle">Když uvidíte něco, co se vám nelíbí na Mastodonu, můžete odstranit tuto osobu ze svého zážitku.</string>
|
||||
@@ -204,15 +228,182 @@
|
||||
<string name="resend">Poslat znovu</string>
|
||||
<string name="open_email_app">Otevřít e-mailovou aplikaci</string>
|
||||
<string name="resent_email">Potvrzující e-mail odeslán</string>
|
||||
<string name="compose_hint">Napište nebo vložte, co máte na mysli</string>
|
||||
<string name="content_warning">Varování o obsahu</string>
|
||||
<string name="add_image_description">Přidat popis obrázku…</string>
|
||||
<string name="retry_upload">Opakovat nahrání</string>
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
<string name="save">Uložit</string>
|
||||
<string name="add_alt_text">Přidat alt text</string>
|
||||
<string name="alt_text_subtitle">Alt text popisuje obrázky pro lidi se špatným nebo žádným zrakem. Pokuste se zahrnout jen tolik obsahu, kolik je potřeba pro pochopení kontextu.</string>
|
||||
<string name="alt_text_hint">Např. podezřívavě rozhlížející se pes se zúženýma očima namířenýma na kameru.</string>
|
||||
<string name="visibility_public">Veřejný</string>
|
||||
<string name="visibility_followers_only">Pouze sledující</string>
|
||||
<string name="visibility_private">Pouze lidé, které zmíním</string>
|
||||
<string name="search_all">Vše</string>
|
||||
<string name="search_people">Lidé</string>
|
||||
<string name="recent_searches">Nedávná hledání</string>
|
||||
<string name="step_x_of_n">Krok %1$d z %2$d</string>
|
||||
<string name="skip">Přeskočit</string>
|
||||
<string name="notification_type_follow">Noví sledující</string>
|
||||
<string name="notification_type_favorite">Oblíbené</string>
|
||||
<string name="notification_type_reblog">Boostnutí</string>
|
||||
<string name="notification_type_mention">Zmínky</string>
|
||||
<string name="notification_type_poll">Ankety</string>
|
||||
<string name="choose_account">Vybrat účet</string>
|
||||
<string name="err_not_logged_in">Nejprve se přihlaste do Mastodonu</string>
|
||||
<plurals name="cant_add_more_than_x_attachments">
|
||||
<item quantity="one">Nelze přidat více než %d multimediálních příloh</item>
|
||||
<item quantity="few">Nelze přidat více než %d multimediální přílohy</item>
|
||||
<item quantity="many">Nelze přidat více než %d multimediálních příloh</item>
|
||||
<item quantity="other">Nelze přidat více než %d multimediálních příloh</item>
|
||||
</plurals>
|
||||
<string name="media_attachment_unsupported_type">Soubor %s nepatří mezi podporované typy</string>
|
||||
<string name="media_attachment_too_big">Soubor %1$s překračuje limit velikosti %2$s MB</string>
|
||||
<string name="settings_theme">Vizuální podoba</string>
|
||||
<string name="theme_auto">Automatická</string>
|
||||
<string name="theme_light">Světlá</string>
|
||||
<string name="theme_dark">Tmavá</string>
|
||||
<string name="theme_true_black">Režim skutečně černé</string>
|
||||
<string name="settings_behavior">Chování</string>
|
||||
<string name="settings_gif">Přehrávat animované avatary a emoji</string>
|
||||
<string name="settings_custom_tabs">Používat interní prohlížeč</string>
|
||||
<string name="settings_notifications">Upozornění</string>
|
||||
<string name="notify_me_when">Upozornit mě, když</string>
|
||||
<string name="notify_anyone">kdokoliv</string>
|
||||
<string name="notify_follower">sledující</string>
|
||||
<string name="notify_followed">někdo, koho sleduji</string>
|
||||
<string name="notify_none">nikoho</string>
|
||||
<string name="notify_favorites">Oblíbil si můj příspěvek</string>
|
||||
<string name="notify_follow">Sleduje mě</string>
|
||||
<string name="notify_reblog">Sdílel můj příspěvek</string>
|
||||
<string name="notify_reblog">Boostnul můj příspěvek</string>
|
||||
<string name="notify_mention">Zmiňuje mě</string>
|
||||
<string name="settings_boring">Nudná část</string>
|
||||
<string name="settings_account">Nastavení účtu</string>
|
||||
<string name="settings_contribute">Přispějte do Mastodonu</string>
|
||||
<string name="settings_tos">Podmínky používání</string>
|
||||
<string name="settings_privacy_policy">Zásady ochrany osobních údajů</string>
|
||||
<string name="settings_spicy">Ostrá část</string>
|
||||
<string name="settings_clear_cache">Vymazat mezipaměť médií</string>
|
||||
<string name="settings_app_version">Mastodon pro Android v%1$s (%2$d)</string>
|
||||
<string name="media_cache_cleared">Mezipaměť médií vymazána</string>
|
||||
<string name="confirm_log_out">Opravdu se chcete odhlásit?</string>
|
||||
<string name="sensitive_content">Citlivý obsah</string>
|
||||
<string name="sensitive_content_explain">Autor označil toto médium za citlivé. Klepnutím zobrazíte.</string>
|
||||
<string name="media_hidden">Klepnutím zobrazit</string>
|
||||
<string name="avatar_description">Jít na profil %s</string>
|
||||
<string name="more_options">Více možností</string>
|
||||
<string name="reveal_content">Zobrazit obsah</string>
|
||||
<string name="hide_content">Skrýt obsah</string>
|
||||
<string name="new_post">Nový příspěvek</string>
|
||||
<string name="button_reply">Odpovědět</string>
|
||||
<string name="button_reblog">Boostnout</string>
|
||||
<string name="button_favorite">Oblíbit</string>
|
||||
<string name="button_share">Sdílet</string>
|
||||
<string name="media_no_description">Média bez popisu</string>
|
||||
<string name="add_media">Přidat média</string>
|
||||
<string name="add_poll">Přidat anketu</string>
|
||||
<string name="emoji">Emoji</string>
|
||||
<string name="post_visibility">Viditelnost příspěvku</string>
|
||||
<string name="home_timeline">Domovská časová osa</string>
|
||||
<string name="my_profile">Můj profil</string>
|
||||
<string name="media_viewer">Prohlížeč médií</string>
|
||||
<string name="follow_user">Sledovat %s</string>
|
||||
<string name="unfollowed_user">Sledování %s ukončeno</string>
|
||||
<string name="followed_user">Nyní sledujete %s</string>
|
||||
<string name="open_in_browser">Otevřít v prohlížeči</string>
|
||||
<string name="hide_boosts_from_user">Skrýt boosty od %s</string>
|
||||
<string name="show_boosts_from_user">Zobrazit boosty od %s</string>
|
||||
<string name="signup_reason">proč se chcete připojit?</string>
|
||||
<string name="signup_reason_note">Toto nám pomůže posoudit vaši žádost.</string>
|
||||
<string name="clear">Vyčistit</string>
|
||||
<string name="profile_header">Obrázek v záhlaví</string>
|
||||
<string name="profile_picture">Profilová fotografie</string>
|
||||
<string name="reorder">Změnit pořadí</string>
|
||||
<string name="download">Stáhnout</string>
|
||||
<string name="permission_required">Vyžadováno oprávnění</string>
|
||||
<string name="storage_permission_to_download">Aplikace potřebuje přístup k vašemu úložišti, aby mohla uložit tento soubor.</string>
|
||||
<string name="open_settings">Otevřít nastavení</string>
|
||||
<string name="error_saving_file">Chyba při ukládání souboru</string>
|
||||
<string name="file_saved">Soubor uložen</string>
|
||||
<string name="downloading">Stahování…</string>
|
||||
<string name="no_app_to_handle_action">Nebyly nalezeny žádné aplikace pro tuto úlohu</string>
|
||||
<string name="local_timeline">Komunita</string>
|
||||
<string name="trending_posts_info_banner">Toto jsou příspěvky, které získávají pozornost ve vašem koutu Mastodonu.</string>
|
||||
<string name="trending_hashtags_info_banner">Toto jsou hashtagy, které získávají pozornost ve vašem koutu Mastodonu.</string>
|
||||
<string name="trending_links_info_banner">Toto jsou zprávy, které jsou nejvíce sdíleny ve vašem koutu Mastodonu.</string>
|
||||
<string name="local_timeline_info_banner">Toto jsou nejnovější příspěvky od lidí, kteří používají stejný server Mastodonu jako vy.</string>
|
||||
<string name="dismiss">Zavřít</string>
|
||||
<string name="see_new_posts">Zobrazit nové příspěvky</string>
|
||||
<string name="load_missing_posts">Načíst chybějící příspěvky</string>
|
||||
<string name="follow_back">Sledovat zpátky</string>
|
||||
<string name="button_follow_pending">Čekající</string>
|
||||
<string name="follows_you">Sleduje vás</string>
|
||||
<string name="manually_approves_followers">Ručně schvaluje sledující</string>
|
||||
<string name="current_account">Současný účet</string>
|
||||
<string name="log_out_account">Odhlásit %s</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<plurals name="x_followers">
|
||||
<item quantity="one">%,d sledující</item>
|
||||
<item quantity="few">%,d sledující</item>
|
||||
<item quantity="many">%,d sledujících</item>
|
||||
<item quantity="other">%,d sledujících</item>
|
||||
</plurals>
|
||||
<plurals name="x_following">
|
||||
<item quantity="one">%,d sledující</item>
|
||||
<item quantity="few">%,d sledování</item>
|
||||
<item quantity="many">%,d sledování</item>
|
||||
<item quantity="other">%,d sledování</item>
|
||||
</plurals>
|
||||
<plurals name="x_favorites">
|
||||
<item quantity="one">%,d oblíbený</item>
|
||||
<item quantity="few">%,d oblíbené</item>
|
||||
<item quantity="many">%,d oblíbených</item>
|
||||
<item quantity="other">%,d oblíbených</item>
|
||||
</plurals>
|
||||
<plurals name="x_reblogs">
|
||||
<item quantity="one">%,d boostnul</item>
|
||||
<item quantity="few">%,d boostnuli</item>
|
||||
<item quantity="many">%,d boostnulo</item>
|
||||
<item quantity="other">%,d boostnulo</item>
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s přes %2$s</string>
|
||||
<string name="time_now">teď</string>
|
||||
<string name="post_info_reblogs">Reblogy</string>
|
||||
<string name="post_info_favorites">Oblíbené</string>
|
||||
<string name="edit_history">Historie změn</string>
|
||||
<string name="last_edit_at_x">Poslední úprava %s</string>
|
||||
<string name="time_just_now">právě teď</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">Před 1 vteřinou</item>
|
||||
<item quantity="few">Před %d vteřinami</item>
|
||||
<item quantity="many">Před %d vteřinami</item>
|
||||
<item quantity="other">Před %d vteřinami</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">před %d minutou</item>
|
||||
<item quantity="few">před %d minutami</item>
|
||||
<item quantity="many">před %d minutami</item>
|
||||
<item quantity="other">před %d minutami</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">upraveno %s</string>
|
||||
<string name="edit_original_post">Původní příspěvek</string>
|
||||
<string name="edit_text_edited">Text upraven</string>
|
||||
<string name="edit_spoiler_added">Upozornění na obsah bylo přidáno</string>
|
||||
<string name="edit_spoiler_edited">Upozornění na obsah upraveno</string>
|
||||
<string name="edit_spoiler_removed">Upozornění na obsah odstraněno</string>
|
||||
<string name="edit_poll_added">Anketa přidána</string>
|
||||
<string name="edit_poll_edited">Anketa upravena</string>
|
||||
<string name="edit_poll_removed">Anketa odstraněna</string>
|
||||
<string name="edit_media_added">Média přidána</string>
|
||||
<string name="edit_media_removed">Média odstraněna</string>
|
||||
<string name="edit_media_reordered">Média přeřazena</string>
|
||||
<string name="edit_marked_sensitive">Označeno jako citlivé</string>
|
||||
<string name="edit_marked_not_sensitive">Označeno, že není citlivé</string>
|
||||
<string name="edit_multiple_changed">Příspěvek upraven</string>
|
||||
<string name="edit">Upravit</string>
|
||||
<string name="discard_changes">Zrušit změny?</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -3,27 +3,27 @@
|
||||
<string name="get_started">Loslegen</string>
|
||||
<string name="log_in">Anmelden</string>
|
||||
<string name="next">Weiter</string>
|
||||
<string name="loading_instance">Lade Serverinformationen…</string>
|
||||
<string name="loading_instance">Instanzinformationen werden geladen …</string>
|
||||
<string name="error">Fehler</string>
|
||||
<string name="not_a_mastodon_instance">%s scheint keine Mastodon-Instanz zu sein.</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="preparing_auth">Bereite Authentifizierung vor…</string>
|
||||
<string name="finishing_auth">Authentifizierung erfolgt…</string>
|
||||
<string name="user_boosted">%s teilte</string>
|
||||
<string name="in_reply_to">Antwort auf %s</string>
|
||||
<string name="preparing_auth">Authentifizierung wird vorbereitet …</string>
|
||||
<string name="finishing_auth">Authentifizierung abschließen …</string>
|
||||
<string name="user_boosted">%s hat diesen Beitrag geteilt</string>
|
||||
<string name="in_reply_to">Antwort auf den Beitrag von %s</string>
|
||||
<string name="notifications">Benachrichtigungen</string>
|
||||
<string name="user_followed_you">ist dir gefolgt</string>
|
||||
<string name="user_followed_you">folgt dir jetzt</string>
|
||||
<string name="user_sent_follow_request">hat dir eine Folgeanfrage gesendet</string>
|
||||
<string name="user_favorited">hat deinen Beitrag favorisiert</string>
|
||||
<string name="notification_boosted">hat deinen Beitrag geteilt</string>
|
||||
<string name="poll_ended">Abstimmung beendet</string>
|
||||
<string name="time_seconds">%dSek.</string>
|
||||
<string name="time_minutes">%dMin.</string>
|
||||
<string name="time_hours">%d Std.</string>
|
||||
<string name="time_days">%d T</string>
|
||||
<string name="time_seconds">vor %d Sekunden</string>
|
||||
<string name="time_minutes">vor %d Minuten</string>
|
||||
<string name="time_hours">vor %d Stunden</string>
|
||||
<string name="time_days">vor %d Tagen</string>
|
||||
<string name="share_toot_title">Teilen</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="publish">Tröt</string>
|
||||
<string name="publish">Veröffentlichen</string>
|
||||
<string name="discard_draft">Entwurf verwerfen?</string>
|
||||
<string name="discard">Verwerfen</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
@@ -32,15 +32,15 @@
|
||||
<item quantity="other">Follower</item>
|
||||
</plurals>
|
||||
<plurals name="following">
|
||||
<item quantity="one">Folgender</item>
|
||||
<item quantity="other">Folgende</item>
|
||||
<item quantity="one">Folge ich</item>
|
||||
<item quantity="other">Folge ich</item>
|
||||
</plurals>
|
||||
<plurals name="posts">
|
||||
<item quantity="one">Beitrag</item>
|
||||
<item quantity="other">Beiträge</item>
|
||||
</plurals>
|
||||
<string name="posts">Beiträge</string>
|
||||
<string name="posts_and_replies">Beiträge und Antworten</string>
|
||||
<string name="posts_and_replies">Beiträge & Antworten</string>
|
||||
<string name="pinned_posts">Angeheftet</string>
|
||||
<string name="media">Medien</string>
|
||||
<string name="profile_about">Über</string>
|
||||
@@ -48,26 +48,26 @@
|
||||
<string name="button_following">Folge ich</string>
|
||||
<string name="edit_profile">Profil bearbeiten</string>
|
||||
<string name="mention_user">%s erwähen</string>
|
||||
<string name="share_user">Teile das Profil von %s</string>
|
||||
<string name="share_user">%s teilen</string>
|
||||
<string name="mute_user">%s stummschalten</string>
|
||||
<string name="unmute_user">%s nicht mehr stummschalten</string>
|
||||
<string name="block_user">%s blockieren</string>
|
||||
<string name="unblock_user">%s nicht mehr blockieren</string>
|
||||
<string name="report_user">%s melden</string>
|
||||
<string name="block_domain">%s blockieren</string>
|
||||
<string name="unblock_domain">%s nicht mehr blockieren</string>
|
||||
<string name="block_domain">Domain %s blockieren</string>
|
||||
<string name="unblock_domain">Domain %s nicht mehr blockieren</string>
|
||||
<plurals name="x_posts">
|
||||
<item quantity="one">%,d Beitrag</item>
|
||||
<item quantity="other">%,d Beiträge</item>
|
||||
</plurals>
|
||||
<string name="profile_joined">Beigetreten</string>
|
||||
<string name="done">Fertig</string>
|
||||
<string name="loading">Lädt…</string>
|
||||
<string name="loading">wird geladen …</string>
|
||||
<string name="field_label">Beschriftung</string>
|
||||
<string name="field_content">Inhalt</string>
|
||||
<string name="saving">Speichern…</string>
|
||||
<string name="saving">Speichern …</string>
|
||||
<string name="post_from_user">Beitrag von %s</string>
|
||||
<string name="poll_option_hint">Auswahlmöglichkeit %d</string>
|
||||
<string name="poll_option_hint">%d. Auswahl</string>
|
||||
<plurals name="x_minutes">
|
||||
<item quantity="one">%d Minute</item>
|
||||
<item quantity="other">%d Minuten</item>
|
||||
@@ -98,23 +98,23 @@
|
||||
<item quantity="other">%d Tage verbleibend</item>
|
||||
</plurals>
|
||||
<plurals name="x_voters">
|
||||
<item quantity="one">%,d hat gewählt</item>
|
||||
<item quantity="other">%,d haben gewählt</item>
|
||||
<item quantity="one">%,d Stimme</item>
|
||||
<item quantity="other">%,d Stimmen</item>
|
||||
</plurals>
|
||||
<string name="poll_closed">Geschlossen</string>
|
||||
<string name="poll_closed">Beendet</string>
|
||||
<string name="confirm_mute_title">Konto stummschalten</string>
|
||||
<string name="confirm_mute">Bestätigen um %s stummzuschalten</string>
|
||||
<string name="confirm_mute">Bestätigen, um %s stummzuschalten</string>
|
||||
<string name="do_mute">Stummschalten</string>
|
||||
<string name="confirm_unmute_title">Konto nicht mehr stummschalten</string>
|
||||
<string name="confirm_unmute">Bestätigen, um %s nicht mehr stummzuschalten</string>
|
||||
<string name="do_unmute">Nicht mehr stummschalten</string>
|
||||
<string name="confirm_block_title">Konto blockieren</string>
|
||||
<string name="confirm_block_title">Konto sperren</string>
|
||||
<string name="confirm_block_domain_title">Domain blockieren</string>
|
||||
<string name="confirm_block">Bestätigen um %s zu blockieren</string>
|
||||
<string name="do_block">Blockieren</string>
|
||||
<string name="confirm_unblock_title">Konto nicht mehr blockieren</string>
|
||||
<string name="confirm_block">Bestätigen, um %s zu blockieren</string>
|
||||
<string name="do_block">Ja, blockieren</string>
|
||||
<string name="confirm_unblock_title">Konto nicht mehr sperren</string>
|
||||
<string name="confirm_unblock_domain_title">Domain nicht mehr blockieren</string>
|
||||
<string name="confirm_unblock">Bestätigen um %s nicht mehr zu blockieren</string>
|
||||
<string name="confirm_unblock">Bestätigen, um %s nicht mehr zu blockieren</string>
|
||||
<string name="do_unblock">Nicht mehr blockieren</string>
|
||||
<string name="button_muted">Stummgeschaltet</string>
|
||||
<string name="button_blocked">Blockiert</string>
|
||||
@@ -143,7 +143,7 @@
|
||||
<string name="search_hint">Suchen</string>
|
||||
<string name="hashtags">Hashtags</string>
|
||||
<string name="news">Nachrichten</string>
|
||||
<string name="for_you">Für dich</string>
|
||||
<string name="for_you">Vorschläge</string>
|
||||
<string name="all_notifications">Alle</string>
|
||||
<string name="mentions">Erwähnungen</string>
|
||||
<plurals name="x_people_talking">
|
||||
@@ -151,15 +151,15 @@
|
||||
<item quantity="other">%d Personen reden darüber</item>
|
||||
</plurals>
|
||||
<plurals name="discussed_x_times">
|
||||
<item quantity="one">%d mal diskutiert</item>
|
||||
<item quantity="other">%d mal diskutiert</item>
|
||||
<item quantity="one">%d Mal diskutiert</item>
|
||||
<item quantity="other">%d × auf dieser Mastodon-Instanz geteilt</item>
|
||||
</plurals>
|
||||
<string name="report_title">%s melden</string>
|
||||
<string name="report_choose_reason">Sag uns, was mit diesem Beitrag vor sich geht?</string>
|
||||
<string name="report_choose_reason">Was stimmt mit diesem Beitrag nicht?</string>
|
||||
<string name="report_choose_reason_account">Was ist los mit %s?</string>
|
||||
<string name="report_choose_reason_subtitle">Wähle die beste Zugehörigkeit</string>
|
||||
<string name="report_reason_personal">Das gefällt mir nicht</string>
|
||||
<string name="report_reason_personal_subtitle">Das ist nicht etwas, was ihr nicht sehen wollt</string>
|
||||
<string name="report_choose_reason_subtitle">Wähle die bestmögliche Option aus</string>
|
||||
<string name="report_reason_personal">Der Beitrag gefällt mir nicht</string>
|
||||
<string name="report_reason_personal_subtitle">Den Inhalt kann man nicht allen zumuten</string>
|
||||
<string name="report_reason_spam">Das ist Spam</string>
|
||||
<string name="report_reason_spam_subtitle">Bösartige Links, gefälschtes Engagement oder wiederholte Antworten</string>
|
||||
<string name="report_reason_violation">Es verstößt gegen Serverregeln</string>
|
||||
@@ -172,27 +172,27 @@
|
||||
<string name="report_choose_posts_subtitle">Alles Zutreffende auswählen</string>
|
||||
<string name="report_comment_title">Gibt es etwas anderes, was wir wissen sollten?</string>
|
||||
<string name="report_comment_hint">Zusätzliche Kommentare</string>
|
||||
<string name="sending_report">Sende Meldung…</string>
|
||||
<string name="report_sent_title">Vielen Dank für die Berichterstattung, wir werden uns damit befassen.</string>
|
||||
<string name="sending_report">Bericht wird gesendet …</string>
|
||||
<string name="report_sent_title">Vielen Dank für die Meldung, wir werden uns damit befassen.</string>
|
||||
<string name="report_sent_subtitle">Während wir dies überprüfen, kannst du gegen %s vorgehen.</string>
|
||||
<string name="unfollow_user">%s entfolgen</string>
|
||||
<string name="unfollow">Entfolgen</string>
|
||||
<string name="mute_user_explain">Du wirst die Beiträge vom Konto nicht mehr sehen. Das Konto kann dir immer noch folgen, und die Person hinter dem Konto wird deine Beiträge sehen können und nicht wissen, dass du sie stummgeschaltet hast.</string>
|
||||
<string name="mute_user_explain">Du wirst die eigenen und geteilten Beiträge des Kontos nicht mehr sehen können. Dass du das Profil stummgeschaltet hast, erfährt die Person hinter dem Account aber nicht.</string>
|
||||
<string name="block_user_explain">Du wirst die Beiträge von diesem Konto nicht sehen. Das Konto wird nicht in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird wissen, dass du das Konto blockiert hast.</string>
|
||||
<string name="report_personal_title">Du willst das nicht mehr sehen?</string>
|
||||
<string name="report_personal_subtitle">Wenn du etwas auf Mastodon nicht sehen willst, kannst du den Nutzer aus deiner Erfahrung streichen.</string>
|
||||
<string name="back">Zurück</string>
|
||||
<string name="instance_catalog_title">Mastodon besteht aus Benutzern auf verschiedenen Servern.</string>
|
||||
<string name="instance_catalog_subtitle">Wähle einen Server basierend auf deinen Interessen, deiner Region oder einen Allgemeinen. Du kannst trotzdem mit jedem Interagieren, egal auf welchem Server.</string>
|
||||
<string name="search_communities">Nach Server suchen oder URL eingeben</string>
|
||||
<string name="instance_rules_title">Hier ein paar Regeln</string>
|
||||
<string name="instance_rules_subtitle">Nimm dir eine Minute Zeit und gehe kurz durch alle Regeln durch, die %s machen.</string>
|
||||
<string name="instance_catalog_subtitle">Wähle einen Server basierend auf deinen Interessen oder deiner Region – oder einfach einen allgemeinen. Du kannst trotzdem mit jedem interagieren, egal auf welchem Server.</string>
|
||||
<string name="search_communities">Server suchen oder Adresse eingeben</string>
|
||||
<string name="instance_rules_title">Einige Grundregeln</string>
|
||||
<string name="instance_rules_subtitle">Nimm dir eine Minute Zeit, und gehe kurz alle Regeln von %s durch.</string>
|
||||
<string name="signup_title">Okay, lass uns mit %s anfangen</string>
|
||||
<string name="edit_photo">bearbeiten</string>
|
||||
<string name="display_name">anzeigename</string>
|
||||
<string name="username">nutzername</string>
|
||||
<string name="display_name">Anzeigename</string>
|
||||
<string name="username">Nutzername</string>
|
||||
<string name="email">E-Mail</string>
|
||||
<string name="password">passwort</string>
|
||||
<string name="password">Passwort</string>
|
||||
<string name="password_note">Verwende Großbuchstaben, Sonderzeichen und Zahlen, um deine Passwortstärke zu erhöhen.</string>
|
||||
<string name="category_academia">Bildung</string>
|
||||
<string name="category_activism">Aktivismus</string>
|
||||
@@ -214,76 +214,76 @@
|
||||
<string name="resent_email">Bestätigungs-E-Mail gesendet</string>
|
||||
<string name="compose_hint">Was gibt\'s Neues?</string>
|
||||
<string name="content_warning">Inhaltswarnung</string>
|
||||
<string name="add_image_description">Bildbeschreibung hinzufügen…</string>
|
||||
<string name="retry_upload">Hochladen erneut versuchen</string>
|
||||
<string name="image_description">Bildbeschreibung</string>
|
||||
<string name="add_image_description">Füge eine Bildbeschreibung hinzu…</string>
|
||||
<string name="retry_upload">Upload erneut versuchen</string>
|
||||
<string name="image_upload_failed">Fehler beim Hochladen des Bildes</string>
|
||||
<string name="video_upload_failed">Fehler beim Hochladen des Videos</string>
|
||||
<string name="edit_image">Bild bearbeiten</string>
|
||||
<string name="save">Speichern</string>
|
||||
<string name="add_alt_text">Alternativtext hinzufügen</string>
|
||||
<string name="alt_text_subtitle">Alternativtext erscheint für blinde Menschen. Versuche, nur so viele Details einzubeziehen, um den Kontext zu verstehen.</string>
|
||||
<string name="alt_text_hint">z.B. Eine Giraffe auf einem Dreirad während sie eine Banane isst</string>
|
||||
<string name="add_alt_text">Bildbeschreibung hinzufügen</string>
|
||||
<string name="alt_text_subtitle">Die Bildbeschreibung („Alt-Text“) ist eine wichtige Unterstützung für blinde und sehbehinderte Menschen. Beschränke dich bei der Formulierung auf das nötigste, interpretiere nicht und beschreibe nur, was zu sehen ist, damit der Kontext verständlich ist und alle Menschen daran teilhaben können.</string>
|
||||
<string name="alt_text_hint">z. B. „Eine Giraffe auf einem Dreirad, während sie eine Banane isst.“</string>
|
||||
<string name="visibility_public">Öffentlich</string>
|
||||
<string name="visibility_unlisted">Nicht gelistet</string>
|
||||
<string name="visibility_followers_only">Nur Folgende</string>
|
||||
<string name="visibility_private">Nur Leute, die ich erwähne</string>
|
||||
<string name="visibility_followers_only">Nur Follower</string>
|
||||
<string name="visibility_private">Nur erwähnte Profile</string>
|
||||
<string name="search_all">Alle</string>
|
||||
<string name="search_people">Personen</string>
|
||||
<string name="recent_searches">Letzte Suchanfragen</string>
|
||||
<string name="step_x_of_n">Schritt %1$d von %2$d</string>
|
||||
<string name="skip">Überspringen</string>
|
||||
<string name="notification_type_follow">Neue Follower</string>
|
||||
<string name="notification_type_follow">Neue Folgende</string>
|
||||
<string name="notification_type_favorite">Favoriten</string>
|
||||
<string name="notification_type_reblog">Teilungen</string>
|
||||
<string name="notification_type_reblog">Geteilte Beiträge</string>
|
||||
<string name="notification_type_mention">Erwähnungen</string>
|
||||
<string name="notification_type_poll">Umfragen</string>
|
||||
<string name="choose_account">Konto auswählen</string>
|
||||
<string name="err_not_logged_in">Melde dich zuerst mit Mastodon an</string>
|
||||
<string name="err_not_logged_in">Bitte zuerst in Mastodon anmelden</string>
|
||||
<plurals name="cant_add_more_than_x_attachments">
|
||||
<item quantity="one">Du kannst nicht mehr als %d Medienanhang hinzufügen</item>
|
||||
<item quantity="other">Du kannst nicht mehr als %d Medienanhänge hinzufügen</item>
|
||||
<item quantity="one">Du kannst nicht mehr als %d Medien-Datei anhängen</item>
|
||||
<item quantity="other">Du kannst nicht mehr als %d Medien-Dateien anhängen</item>
|
||||
</plurals>
|
||||
<string name="media_attachment_unsupported_type">Datei %s wird nicht unterstützt</string>
|
||||
<string name="media_attachment_too_big">Die Dateigröße %1$s übersteigt das Uploadlimit von %2$s MB</string>
|
||||
<string name="settings_theme">Visuelle Darstellung</string>
|
||||
<string name="theme_auto">Automatisch</string>
|
||||
<string name="settings_theme">Design</string>
|
||||
<string name="theme_auto">Systembedingt</string>
|
||||
<string name="theme_light">Hell</string>
|
||||
<string name="theme_dark">Dunkel</string>
|
||||
<string name="theme_true_black">AMOLED-Modus</string>
|
||||
<string name="settings_behavior">Verhalten</string>
|
||||
<string name="settings_gif">Spiele animierte Avatare und Emojis ab</string>
|
||||
<string name="settings_custom_tabs">Verwende In-App-Browser</string>
|
||||
<string name="settings_behavior">App-Verhalten</string>
|
||||
<string name="settings_gif">Spiele animierte GIFs, Avatare und Emojis ab</string>
|
||||
<string name="settings_custom_tabs">In-App-Browser verwenden</string>
|
||||
<string name="settings_notifications">Benachrichtigungen</string>
|
||||
<string name="notify_me_when">Benachrichtige mich, wenn</string>
|
||||
<string name="notify_anyone">jeder</string>
|
||||
<string name="notify_follower">ein Follower</string>
|
||||
<string name="notify_followed">jemand, dem ich folge</string>
|
||||
<string name="notify_anyone">irgendjemand</string>
|
||||
<string name="notify_follower">ein Follower von mir</string>
|
||||
<string name="notify_followed">jemand, dem ich folge,</string>
|
||||
<string name="notify_none">niemand</string>
|
||||
<string name="notify_favorites">Meinen Beitrag favorisiert</string>
|
||||
<string name="notify_follow">Mir folgt</string>
|
||||
<string name="notify_reblog">Meinen Beitrag teilt</string>
|
||||
<string name="notify_mention">Mich erwähnt</string>
|
||||
<string name="settings_boring">Die Langeweile-Zone</string>
|
||||
<string name="notify_favorites">meinen Beitrag favorisiert</string>
|
||||
<string name="notify_follow">mir folgt</string>
|
||||
<string name="notify_reblog">einen meiner Beiträge geteilt hat</string>
|
||||
<string name="notify_mention">mich erwähnt</string>
|
||||
<string name="settings_boring">Langweiliges</string>
|
||||
<string name="settings_account">Kontoeinstellungen</string>
|
||||
<string name="settings_contribute">Zu Mastodon beitragen</string>
|
||||
<string name="settings_tos">AGBs</string>
|
||||
<string name="settings_privacy_policy">Datenschutzerklärung</string>
|
||||
<string name="settings_spicy">Die gefährliche Zone</string>
|
||||
<string name="settings_contribute">Bei Mastodon unterstützen und mitmachen</string>
|
||||
<string name="settings_tos">Nutzungsbedingungen</string>
|
||||
<string name="settings_privacy_policy">Datenschutzbestimmungen</string>
|
||||
<string name="settings_spicy">Gefährliches</string>
|
||||
<string name="settings_clear_cache">Medien-Cache leeren</string>
|
||||
<string name="settings_app_version">Mastodon für Android v%1$s (%2$d)</string>
|
||||
<string name="settings_app_version">Mastodos v%1$s (%2$d)</string>
|
||||
<string name="media_cache_cleared">Medien-Cache geleert</string>
|
||||
<string name="confirm_log_out">Bist du dir sicher, dass du dich abmelden möchtest?</string>
|
||||
<string name="sensitive_content">NSFW-Inhalt</string>
|
||||
<string name="sensitive_content_explain">Der Autor hat den Inhalt als NSFW markiert. Tippen zum Öffnen.</string>
|
||||
<string name="sensitive_content">Sensibler Inhalt</string>
|
||||
<string name="sensitive_content_explain">Der Autor hat den Inhalt als sensiblen Inhalt markiert. Tippen zum Anzeigen.</string>
|
||||
<string name="media_hidden">Tippen zum Öffnen</string>
|
||||
<string name="avatar_description">Gehe zu %s\'s Profil</string>
|
||||
<string name="avatar_description">Das Profil von %s öffnen</string>
|
||||
<string name="more_options">Mehr Optionen</string>
|
||||
<string name="reveal_content">Inhalt anzeigen</string>
|
||||
<string name="hide_content">Inhalt verstecken</string>
|
||||
<string name="new_post">Neuer Beitrag</string>
|
||||
<string name="button_reply">Antworten</string>
|
||||
<string name="button_reblog">Erneut teilen</string>
|
||||
<string name="button_reblog">Teilen</string>
|
||||
<string name="button_favorite">Favorisieren</string>
|
||||
<string name="button_share">Teilen</string>
|
||||
<string name="media_no_description">Medien ohne Beschreibung</string>
|
||||
@@ -297,54 +297,102 @@
|
||||
<string name="follow_user">%s folgen</string>
|
||||
<string name="unfollowed_user">%s entfolgt</string>
|
||||
<string name="followed_user">Du folgst nun %s</string>
|
||||
<string name="open_in_browser">Im Browser öffnen</string>
|
||||
<string name="hide_boosts_from_user">Verstecke Teilungen von %s</string>
|
||||
<string name="show_boosts_from_user">Zeige Teilungen von %s</string>
|
||||
<string name="signup_reason">Warum möchtest du beitreten?</string>
|
||||
<string name="open_in_browser">Beitrag im Browser öffnen</string>
|
||||
<string name="hide_boosts_from_user">Verberge geteilte Beiträge von %s</string>
|
||||
<string name="show_boosts_from_user">Zeige geteilte Beiträge von %s</string>
|
||||
<string name="signup_reason">Weshalb möchtest du beitreten?</string>
|
||||
<string name="signup_reason_note">Dies wird uns dabei helfen, deine Anmeldungsanfrage besser zu verarbeiten.</string>
|
||||
<string name="clear">Löschen</string>
|
||||
<string name="profile_header">Header-Bild</string>
|
||||
<string name="profile_header">Kopfbild</string>
|
||||
<string name="profile_picture">Profilbild</string>
|
||||
<string name="reorder">Neu sortieren</string>
|
||||
<string name="download">Herunterladen</string>
|
||||
<string name="permission_required">Berechtigung erforderlich</string>
|
||||
<string name="storage_permission_to_download">Die App benötigt Zugriff auf Ihren Speicher, um diese Datei zu speichern.</string>
|
||||
<string name="storage_permission_to_download">Die App benötigt Zugriff auf den Speicher deines Geräts, um diese Datei zu speichern.</string>
|
||||
<string name="open_settings">Einstellungen öffnen</string>
|
||||
<string name="error_saving_file">Fehler beim Speichern der Datei</string>
|
||||
<string name="file_saved">Datei gespeichert</string>
|
||||
<string name="downloading">Herunterladen…</string>
|
||||
<string name="downloading">wird heruntergeladen …</string>
|
||||
<string name="no_app_to_handle_action">Es gibt keine App, um diese Aktion auszuführen</string>
|
||||
<string name="local_timeline">Community</string>
|
||||
<string name="trending_posts_info_banner">Dies sind die Beiträge, die in deiner Mastodon-Bubble beliebter werden.</string>
|
||||
<string name="trending_hashtags_info_banner">Dies sind die Hashtags in deiner Mastodon-Bubble.</string>
|
||||
<string name="trending_links_info_banner">Dies sind die Nachrichten, die am meisten in deiner Mastodon-Bubble geteilt werden.</string>
|
||||
<string name="trending_posts_info_banner">Dies sind Beiträge, die auf deinem Mastodon-Server gerade angesagt sind.</string>
|
||||
<string name="trending_hashtags_info_banner">Dies sind Hashtags, die auf deinem Mastodon-Server gerade angesagt sind.</string>
|
||||
<string name="trending_links_info_banner">Dies sind journalistische Nachrichten, die auf deinem Mastodon-Server gerade am häufigsten geteilt werden.</string>
|
||||
<string name="local_timeline_info_banner">Dies sind die neuesten Beiträge der Leute, die den gleichen Mastodon-Server verwenden wie du.</string>
|
||||
<string name="dismiss">Verwerfen</string>
|
||||
<string name="see_new_posts">Neue Beiträge anzeigen</string>
|
||||
<string name="load_missing_posts">Fehlende Beiträge laden</string>
|
||||
<string name="load_missing_posts">Weitere Beiträge laden</string>
|
||||
<string name="follow_back">Zurück folgen</string>
|
||||
<string name="button_follow_pending">Ausstehend</string>
|
||||
<string name="follows_you">Folgt dir</string>
|
||||
<string name="manually_approves_followers">Genehmigt Folgende manuell</string>
|
||||
<string name="current_account">Aktuelles Konto</string>
|
||||
<string name="log_out_account">Ausloggen</string>
|
||||
<string name="log_out_account">%s abmelden</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<plurals name="x_followers">
|
||||
<item quantity="one">%,d Follower</item>
|
||||
<item quantity="other">%,d Follower</item>
|
||||
</plurals>
|
||||
<plurals name="x_following">
|
||||
<item quantity="one">%,d Gefolgt</item>
|
||||
<item quantity="other">%,d Gefolgt</item>
|
||||
<item quantity="one">%,d folge ich</item>
|
||||
<item quantity="other">%,d folge ich</item>
|
||||
</plurals>
|
||||
<plurals name="x_favorites">
|
||||
<item quantity="one">%,d Favorit</item>
|
||||
<item quantity="one">%,d × favorisiert</item>
|
||||
<item quantity="other">%,d Favoriten</item>
|
||||
</plurals>
|
||||
<plurals name="x_reblogs">
|
||||
<item quantity="one">%,d Reblog</item>
|
||||
<item quantity="other">%,d Reblogs</item>
|
||||
<item quantity="one">%,d × geteilt</item>
|
||||
<item quantity="other">%,d × geteilt</item>
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s via %2$s</string>
|
||||
<string name="time_now">jetzt</string>
|
||||
<string name="post_info_reblogs">Geteilte Beiträge</string>
|
||||
<string name="post_info_favorites">Favoriten</string>
|
||||
<string name="edit_history">Verlauf bearbeiten</string>
|
||||
<string name="last_edit_at_x">Letzte Bearbeitung: %s</string>
|
||||
<string name="time_just_now">gerade eben</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">vor %d Sekunde</item>
|
||||
<item quantity="other">vor %d Sekunden</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">vor %d Minute</item>
|
||||
<item quantity="other">vor %d Minuten</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">bearbeitet %s</string>
|
||||
<string name="edit_original_post">Ursprünglicher Beitrag</string>
|
||||
<string name="edit_text_edited">Text bearbeitet</string>
|
||||
<string name="edit_spoiler_added">Inhaltwarnung hinzugefügt</string>
|
||||
<string name="edit_spoiler_edited">Inhaltwarnung bearbeitet</string>
|
||||
<string name="edit_spoiler_removed">Inhaltwarnung entfernt</string>
|
||||
<string name="edit_poll_added">Umfrage hinzugefügt</string>
|
||||
<string name="edit_poll_edited">Umfrage bearbeitet</string>
|
||||
<string name="edit_poll_removed">Umfrage entfernt</string>
|
||||
<string name="edit_media_added">Medien hinzugefügt</string>
|
||||
<string name="edit_media_removed">Medien entfernt</string>
|
||||
<string name="edit_media_reordered">Medien umsortiert</string>
|
||||
<string name="edit_marked_sensitive">Als NSFW markiert</string>
|
||||
<string name="edit_marked_not_sensitive">Als nicht NSFW markiert</string>
|
||||
<string name="edit_multiple_changed">Beitrag bearbeitet</string>
|
||||
<string name="edit">Bearbeiten</string>
|
||||
<string name="discard_changes">Änderungen verwerfen?</string>
|
||||
<string name="upload_failed">Upload fehlgeschlagen</string>
|
||||
<string name="file_size_bytes">%d bytes</string>
|
||||
<string name="file_size_kb">%.2f KB</string>
|
||||
<string name="file_size_mb">%.2f MB</string>
|
||||
<string name="file_size_gb">%.2f GB</string>
|
||||
<string name="file_upload_progress">%1$s von %2$s</string>
|
||||
<string name="file_upload_time_remaining">%s verbleibend</string>
|
||||
<string name="upload_error_connection_lost">Dein Gerät hat gerade keinen Zugang zum Internet</string>
|
||||
<string name="upload_processing">wird verarbeitet …</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<string name="update_available">Mastodos %s kann nun heruntergeladen werden.</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<string name="update_ready">Mastodos %s wurde heruntergeladen und kann nun installiert werden.</string>
|
||||
<!-- %s is file size -->
|
||||
<string name="download_update">Download (%s)</string>
|
||||
<string name="install_update">Installieren</string>
|
||||
<string name="check_for_update">Auf Update prüfen</string>
|
||||
<string name="no_update_available">Kein Update verfügbar</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -204,8 +204,6 @@
|
||||
<string name="content_warning">Advertencia de contenido</string>
|
||||
<string name="add_image_description">Añadir descripción de la imagen…</string>
|
||||
<string name="retry_upload">Reintentar subida</string>
|
||||
<string name="image_upload_failed">Error al subir la imagen</string>
|
||||
<string name="video_upload_failed">Error al subir el vídeo</string>
|
||||
<string name="edit_image">Editar imagen</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="add_alt_text">Añadir texto alternativo</string>
|
||||
@@ -333,4 +331,37 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s a través de %2$s</string>
|
||||
<string name="time_now">ahora</string>
|
||||
<string name="post_info_reblogs">Reblogueos</string>
|
||||
<string name="post_info_favorites">Favoritos</string>
|
||||
<string name="edit_history">Editar historial</string>
|
||||
<string name="last_edit_at_x">Última edición: %s</string>
|
||||
<string name="time_just_now">justo ahora</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">Hace %d segundo</item>
|
||||
<item quantity="other">Hace %d segundos</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">Hace %d minuto</item>
|
||||
<item quantity="other">Hace %d minutos</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">editado %s</string>
|
||||
<string name="edit_original_post">Publicación original</string>
|
||||
<string name="edit_text_edited">Texto editado</string>
|
||||
<string name="edit_spoiler_added">Advertencia de contenido añadida</string>
|
||||
<string name="edit_spoiler_edited">Advertencia de contenido editada</string>
|
||||
<string name="edit_spoiler_removed">Advertencia de contenido eliminada</string>
|
||||
<string name="edit_poll_added">Encuesta añadida</string>
|
||||
<string name="edit_poll_edited">Encuesta editada</string>
|
||||
<string name="edit_poll_removed">Encuesta eliminada</string>
|
||||
<string name="edit_media_added">Contenido multimedia añadido</string>
|
||||
<string name="edit_media_removed">Contenido multimedia eliminado</string>
|
||||
<string name="edit_media_reordered">Contenido multimedia reordenado</string>
|
||||
<string name="edit_marked_sensitive">Marcado como sensible</string>
|
||||
<string name="edit_marked_not_sensitive">Marcado como no sensible</string>
|
||||
<string name="edit_multiple_changed">Publicación editada</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="discard_changes">¿Descartar cambios?</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -184,8 +184,6 @@
|
||||
<string name="content_warning">Edukiaren abisua</string>
|
||||
<string name="add_image_description">Gehitu irudiaren deskribapena…</string>
|
||||
<string name="retry_upload">Saiatu berriro igotzen</string>
|
||||
<string name="image_upload_failed">Irudia ezin izan da kargatu</string>
|
||||
<string name="video_upload_failed">Bideoa ezin izan da kargatu</string>
|
||||
<string name="edit_image">Editatu irudia</string>
|
||||
<string name="save">Gorde</string>
|
||||
<string name="add_alt_text">Gehitu ordezko testua</string>
|
||||
@@ -258,4 +256,7 @@
|
||||
<string name="file_saved">Fitxategia gorde da</string>
|
||||
<string name="downloading">Jeisten…</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -49,4 +49,7 @@
|
||||
<string name="downloading">Ladataan…</string>
|
||||
<string name="follow_back">Seuraa takaisin</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -204,8 +204,6 @@
|
||||
<string name="content_warning">Avertissement sur le contenu</string>
|
||||
<string name="add_image_description">Ajouter une description de l’image…</string>
|
||||
<string name="retry_upload">Réessayer le téléversement</string>
|
||||
<string name="image_upload_failed">Échec du téléversement de l’image</string>
|
||||
<string name="video_upload_failed">Échec du téléversement de la vidéo</string>
|
||||
<string name="edit_image">Modifier l’image</string>
|
||||
<string name="save">Enregistrer</string>
|
||||
<string name="add_alt_text">Ajouter un texte alternatif</string>
|
||||
@@ -333,4 +331,50 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s via %2$s</string>
|
||||
<string name="time_now">à l’instant</string>
|
||||
<string name="post_info_reblogs">Partages</string>
|
||||
<string name="post_info_favorites">Favoris</string>
|
||||
<string name="edit_history">Historique des éditions</string>
|
||||
<string name="last_edit_at_x">Dernière édition %s</string>
|
||||
<string name="time_just_now">à l’instant</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">Il y a %d seconde</item>
|
||||
<item quantity="other">Il y a %d secondes</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">Il y a %d minute</item>
|
||||
<item quantity="other">Il y a %d minutes</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">édité %s</string>
|
||||
<string name="edit_original_post">Message originel</string>
|
||||
<string name="edit_text_edited">Texte modifié</string>
|
||||
<string name="edit_spoiler_added">Avertissement de contenu ajouté</string>
|
||||
<string name="edit_spoiler_edited">Avertissement de contenu modifié</string>
|
||||
<string name="edit_spoiler_removed">Avertissement de contenu supprimé</string>
|
||||
<string name="edit_poll_added">Sondage ajouté</string>
|
||||
<string name="edit_poll_edited">Sondage édité</string>
|
||||
<string name="edit_poll_removed">Sondage supprimé</string>
|
||||
<string name="edit_media_added">Média ajouté</string>
|
||||
<string name="edit_media_removed">Média supprimé</string>
|
||||
<string name="edit_media_reordered">Média réordonné</string>
|
||||
<string name="edit_marked_sensitive">Marqué comme sensible</string>
|
||||
<string name="edit_marked_not_sensitive">Marqué comme non-sensible</string>
|
||||
<string name="edit_multiple_changed">Message édité</string>
|
||||
<string name="edit">Éditer</string>
|
||||
<string name="discard_changes">Ignorer les modifications ?</string>
|
||||
<string name="upload_failed">Échec de l’envoi</string>
|
||||
<string name="file_size_bytes">%d octets</string>
|
||||
<string name="file_size_kb">%.2f Ko</string>
|
||||
<string name="file_size_mb">%.2f Mo</string>
|
||||
<string name="file_size_gb">%.2f Go</string>
|
||||
<string name="file_upload_progress">%1$s sur %2$s</string>
|
||||
<string name="file_upload_time_remaining">%s restant</string>
|
||||
<string name="upload_error_connection_lost">Votre appareil a perdu la connexion à internet</string>
|
||||
<string name="upload_processing">Traitement en cours…</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<string name="update_available">Mastodon pour Android %s est prêt à être téléchargé.</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<string name="update_ready">Mastodon pour Android %s est téléchargé et prêt à être installé.</string>
|
||||
<!-- %s is file size -->
|
||||
<string name="download_update">Téléchargement (%s)</string>
|
||||
<string name="install_update">Installer</string>
|
||||
</resources>
|
||||
|
||||
@@ -204,8 +204,6 @@
|
||||
<string name="content_warning">Aviso sobre o contido</string>
|
||||
<string name="add_image_description">Engadir descrición da imaxe…</string>
|
||||
<string name="retry_upload">Reintentar a suba</string>
|
||||
<string name="image_upload_failed">Non se puido subir a imaxe</string>
|
||||
<string name="video_upload_failed">Non se puido subir o vídeo</string>
|
||||
<string name="edit_image">Editar imaxe</string>
|
||||
<string name="save">Gardar</string>
|
||||
<string name="add_alt_text">Engadir texto descritivo</string>
|
||||
@@ -333,4 +331,46 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s vía %2$s</string>
|
||||
<string name="time_now">agora</string>
|
||||
<string name="post_info_reblogs">Promocións</string>
|
||||
<string name="post_info_favorites">Favoritos</string>
|
||||
<string name="edit_history">Editar historial</string>
|
||||
<string name="last_edit_at_x">Última edición %s</string>
|
||||
<string name="time_just_now">xusto agora</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">fai %d segundo</item>
|
||||
<item quantity="other">fai %d segundos</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">fai %d minuto</item>
|
||||
<item quantity="other">fai %d minutos</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">editado %s</string>
|
||||
<string name="edit_original_post">Publicación orixinal</string>
|
||||
<string name="edit_text_edited">Texto editado</string>
|
||||
<string name="edit_spoiler_added">Engadido aviso sobre o contido</string>
|
||||
<string name="edit_spoiler_edited">Editado o aviso sobre o contido</string>
|
||||
<string name="edit_spoiler_removed">Eliminado o aviso sobre o contido</string>
|
||||
<string name="edit_poll_added">Enquisa engadida</string>
|
||||
<string name="edit_poll_edited">Enquisa editada</string>
|
||||
<string name="edit_poll_removed">Enquisa eliminada</string>
|
||||
<string name="edit_media_added">Engadido multimedia</string>
|
||||
<string name="edit_media_removed">Engadido eliminado</string>
|
||||
<string name="edit_media_reordered">Multimedia reordenado</string>
|
||||
<string name="edit_marked_sensitive">Marcado como sensible</string>
|
||||
<string name="edit_marked_not_sensitive">Marcado como non sensible</string>
|
||||
<string name="edit_multiple_changed">Publicación editada</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="discard_changes">Descartar cambios?</string>
|
||||
<string name="upload_failed">Fallou a carga</string>
|
||||
<string name="file_size_bytes">%d bytes</string>
|
||||
<string name="file_size_kb">%.2f KB</string>
|
||||
<string name="file_size_mb">%.2f MB</string>
|
||||
<string name="file_size_gb">%.2f GB</string>
|
||||
<string name="file_upload_progress">%1$s de %2$s</string>
|
||||
<string name="file_upload_time_remaining">%s restante</string>
|
||||
<string name="upload_error_connection_lost">O dispositivo perdeu a conexión a internet</string>
|
||||
<string name="upload_processing">Procesando…</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -212,8 +212,6 @@
|
||||
<string name="content_warning">Upozorenje za sadržaj</string>
|
||||
<string name="add_image_description">Dodaj opis slike…</string>
|
||||
<string name="retry_upload">Ponovi prijenos</string>
|
||||
<string name="image_upload_failed">Prijenos slike nije uspio</string>
|
||||
<string name="video_upload_failed">Prijenos videa nije uspio</string>
|
||||
<string name="edit_image">Uredi sliku</string>
|
||||
<string name="save">Spremi</string>
|
||||
<string name="add_alt_text">Dodaj altenativni tekst</string>
|
||||
@@ -307,4 +305,7 @@
|
||||
<string name="downloading">Preuzimanje…</string>
|
||||
<string name="no_app_to_handle_action">Ne postoji aplikacija za rukovanje ovom radnjom</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -51,4 +51,7 @@
|
||||
<string name="unblock_user">Հանել արգելափակումից %s</string>
|
||||
<string name="report_user">Բողոքել %s</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -204,8 +204,6 @@
|
||||
<string name="content_warning">Avviso sul contenuto</string>
|
||||
<string name="add_image_description">Aggiungi descrizione immagine…</string>
|
||||
<string name="retry_upload">Riprova caricamento</string>
|
||||
<string name="image_upload_failed">Impossibile caricare il file</string>
|
||||
<string name="video_upload_failed">Impossibile caricare il video</string>
|
||||
<string name="edit_image">Modifica immagine</string>
|
||||
<string name="save">Salva</string>
|
||||
<string name="add_alt_text">Aggiungi testo alt</string>
|
||||
@@ -333,4 +331,50 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s tramite %2$s</string>
|
||||
<string name="time_now">ora</string>
|
||||
<string name="post_info_reblogs">Condivisioni</string>
|
||||
<string name="post_info_favorites">Preferiti</string>
|
||||
<string name="edit_history">Modifica cronologia</string>
|
||||
<string name="last_edit_at_x">Ultima modifica: %s</string>
|
||||
<string name="time_just_now">proprio ora</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">%d secondo fa</item>
|
||||
<item quantity="other">%d secondi fa</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">%d minuto fa</item>
|
||||
<item quantity="other">%d minuti fa</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">modificato %s</string>
|
||||
<string name="edit_original_post">Post originale</string>
|
||||
<string name="edit_text_edited">Testo modificato</string>
|
||||
<string name="edit_spoiler_added">Avviso sul contenuto aggiunto</string>
|
||||
<string name="edit_spoiler_edited">Avviso sul contenuto modificato</string>
|
||||
<string name="edit_spoiler_removed">Avviso sul contenuto rimosso</string>
|
||||
<string name="edit_poll_added">Sondaggio aggiunto</string>
|
||||
<string name="edit_poll_edited">Sondaggio modificato</string>
|
||||
<string name="edit_poll_removed">Sondaggio rimosso</string>
|
||||
<string name="edit_media_added">Media aggiunto</string>
|
||||
<string name="edit_media_removed">Media rimosso</string>
|
||||
<string name="edit_media_reordered">Media riordinati</string>
|
||||
<string name="edit_marked_sensitive">Contrassegnato come sensibile</string>
|
||||
<string name="edit_marked_not_sensitive">Contrassegnato come non sensibile</string>
|
||||
<string name="edit_multiple_changed">Post modificato</string>
|
||||
<string name="edit">Modifica</string>
|
||||
<string name="discard_changes">Annullare le modifiche?</string>
|
||||
<string name="upload_failed">Caricamento fallito</string>
|
||||
<string name="file_size_bytes">%d bytes</string>
|
||||
<string name="file_size_kb">%.2f KB</string>
|
||||
<string name="file_size_mb">%.2f MB</string>
|
||||
<string name="file_size_gb">%.2f GB</string>
|
||||
<string name="file_upload_progress">%1$s di %2$s</string>
|
||||
<string name="file_upload_time_remaining">%s rimanenti</string>
|
||||
<string name="upload_error_connection_lost">Il tuo dispositivo ha perso la connessione a internet</string>
|
||||
<string name="upload_processing">In elaborazione…</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<string name="update_available">Mastodon per Android %s è pronto per il download.</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<string name="update_ready">Mastodon per Android %s è scaricato e pronto per l\'installazione.</string>
|
||||
<!-- %s is file size -->
|
||||
<string name="download_update">Scarica (%s)</string>
|
||||
<string name="install_update">Installa</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<string name="settings">設定</string>
|
||||
<string name="publish">公開</string>
|
||||
<string name="discard_draft">下書きを削除しますか?</string>
|
||||
<string name="discard">下書き</string>
|
||||
<string name="discard">破棄</string>
|
||||
<string name="cancel">キャンセル</string>
|
||||
<plurals name="followers">
|
||||
<item quantity="other">フォロワー</item>
|
||||
@@ -190,8 +190,6 @@
|
||||
<string name="content_warning">コンテンツ警告</string>
|
||||
<string name="add_image_description">画像の説明を追加</string>
|
||||
<string name="retry_upload">アップロードを再試行</string>
|
||||
<string name="image_upload_failed">画像のアップロードに失敗しました</string>
|
||||
<string name="video_upload_failed">ビデオのアップロードに失敗しました。</string>
|
||||
<string name="edit_image">画像を編集</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="add_alt_text">代替テキストを追加</string>
|
||||
@@ -314,4 +312,7 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$sに%2$s経由</string>
|
||||
<string name="time_now">今</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -199,8 +199,6 @@
|
||||
<string name="content_warning">Alɣu ɣef ugbur</string>
|
||||
<string name="add_image_description">Rnu aglam n tugna…</string>
|
||||
<string name="retry_upload">Ɛreḍ asali i tikkelt-nniḍen</string>
|
||||
<string name="image_upload_failed">Asali n tugna yecceḍ</string>
|
||||
<string name="video_upload_failed">Asali n tvidyut yecceḍ</string>
|
||||
<string name="edit_image">Ẓreg tugna</string>
|
||||
<string name="save">Sekles</string>
|
||||
<string name="add_alt_text">Rnu ɣer uḍris</string>
|
||||
@@ -300,9 +298,15 @@
|
||||
<string name="dismiss">Ffer</string>
|
||||
<string name="see_new_posts">Ẓer tissufaɣ timaynutin</string>
|
||||
<string name="load_missing_posts">Sali tisuffaɣ i iruḥen</string>
|
||||
<string name="follow_back">Ḍfeṛ</string>
|
||||
<string name="button_follow_pending">Yettraǧu</string>
|
||||
<string name="follows_you">Yeṭṭafaṛ-ik·em-id</string>
|
||||
<string name="current_account">Amiḍan amiran</string>
|
||||
<string name="log_out_account">Ffeɣ seg %s</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<string name="timestamp_via_app">%1$s s %2$s</string>
|
||||
<string name="time_now">tura</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -190,8 +190,6 @@
|
||||
<string name="content_warning">열람 주의</string>
|
||||
<string name="add_image_description">이미지 설명 추가…</string>
|
||||
<string name="retry_upload">업로드 재시도</string>
|
||||
<string name="image_upload_failed">이미지 업로드 실패</string>
|
||||
<string name="video_upload_failed">동영상 업로드 실패</string>
|
||||
<string name="edit_image">이미지 편집</string>
|
||||
<string name="save">저장</string>
|
||||
<string name="add_alt_text">대체 텍스트 추가</string>
|
||||
@@ -314,4 +312,7 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s에 %2$s에서</string>
|
||||
<string name="time_now">방금</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -83,4 +83,7 @@
|
||||
<string name="file_saved">Fichièr salvagardat</string>
|
||||
<string name="downloading">Telecargament…</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -232,8 +232,6 @@
|
||||
<string name="content_warning">Ostrzeżenie o zawartości</string>
|
||||
<string name="add_image_description">Dodaj opis obrazu…</string>
|
||||
<string name="retry_upload">Ponów wysyłanie</string>
|
||||
<string name="image_upload_failed">Nie udało się wysłać obrazu</string>
|
||||
<string name="video_upload_failed">Nie udało się wysłać filmu</string>
|
||||
<string name="edit_image">Edytuj obraz</string>
|
||||
<string name="save">Zapisz</string>
|
||||
<string name="add_alt_text">Dodaj tekst alternatywny</string>
|
||||
@@ -371,4 +369,50 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s przez %2$s</string>
|
||||
<string name="time_now">przed chwilą</string>
|
||||
<string name="post_info_reblogs">Podbicia</string>
|
||||
<string name="post_info_favorites">Ulubione</string>
|
||||
<string name="edit_history">Historia edycji</string>
|
||||
<string name="last_edit_at_x">Ostatnia zmiana: %s</string>
|
||||
<string name="time_just_now">przed chwilą</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">%d sekundę temu</item>
|
||||
<item quantity="few">%d sekund temu</item>
|
||||
<item quantity="many">%d sekund temu</item>
|
||||
<item quantity="other">%d sekund temu</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_ago">
|
||||
<item quantity="one">%d minutę temu</item>
|
||||
<item quantity="few">%d minuty temu</item>
|
||||
<item quantity="many">%d minut temu</item>
|
||||
<item quantity="other">%d min temu</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">edytowano %s</string>
|
||||
<string name="edit_original_post">Oryginalny post</string>
|
||||
<string name="edit_text_edited">Tekst edytowany</string>
|
||||
<string name="edit_spoiler_added">Ostrzeżenie o zawartości</string>
|
||||
<string name="edit_spoiler_edited">Ostrzeżenie o zawartości edytowane</string>
|
||||
<string name="edit_spoiler_removed">Ostrzeżenie o treści usunięte</string>
|
||||
<string name="edit_poll_added">Dodano ankietę</string>
|
||||
<string name="edit_poll_edited">Edytowano ankietę</string>
|
||||
<string name="edit_poll_removed">Ankieta usunięta</string>
|
||||
<string name="edit_media_added">Dodano media</string>
|
||||
<string name="edit_media_removed">Media usunięte</string>
|
||||
<string name="edit_media_reordered">Kolejność mediów</string>
|
||||
<string name="edit_marked_sensitive">Oznaczono jako wrażliwe</string>
|
||||
<string name="edit_marked_not_sensitive">Oznaczono jako wrażliwe</string>
|
||||
<string name="edit_multiple_changed">Edytowano post</string>
|
||||
<string name="edit">Edytuj</string>
|
||||
<string name="discard_changes">Odrzucić zmiany?</string>
|
||||
<string name="upload_failed">Przesyłanie nie powiodło się</string>
|
||||
<string name="file_size_bytes">%d bajtów</string>
|
||||
<string name="file_size_kb">%.2f KB</string>
|
||||
<string name="file_size_mb">%.2f MB</string>
|
||||
<string name="file_size_gb">%.2f GB</string>
|
||||
<string name="file_upload_progress">%1$s z %2$s</string>
|
||||
<string name="file_upload_time_remaining">Pozostało %s</string>
|
||||
<string name="upload_error_connection_lost">Twoje urządzenie utraciło połączenie z Internetem</string>
|
||||
<string name="upload_processing">Przetwarzanie…</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
<string name="ok">OK</string>
|
||||
<string name="preparing_auth">Preparando para autenticação…</string>
|
||||
<string name="finishing_auth">Finalizando autenticação…</string>
|
||||
<string name="user_boosted">%s reblogado</string>
|
||||
<string name="in_reply_to">Em resposta à %s</string>
|
||||
<string name="notifications">Notificações</string>
|
||||
<string name="user_followed_you">seguiu você</string>
|
||||
<string name="user_sent_follow_request">enviou uma solicitação para você seguir</string>
|
||||
<string name="user_favorited">favoritou seu post</string>
|
||||
<string name="notification_boosted">reblogado sua postagem</string>
|
||||
<string name="poll_ended">enquete encerrada</string>
|
||||
<string name="time_seconds">%ds</string>
|
||||
<string name="time_minutes">%dm</string>
|
||||
@@ -159,6 +161,7 @@
|
||||
<string name="report_sent_subtitle">Enquanto revisamos isso, você pode tomar medidas contra @%s.</string>
|
||||
<string name="unfollow_user">Deixar de seguir @%s</string>
|
||||
<string name="unfollow">Deixar de seguir</string>
|
||||
<string name="mute_user_explain">Você não verá suas postagens ou reblogs em seu feed inicial. Eles não saberão que foram silenciados.</string>
|
||||
<string name="block_user_explain">Eles não poderão mais seguir ou ver seus post, mas poderão ver se foram bloqueados.</string>
|
||||
<string name="report_personal_title">Não quer ver isto?</string>
|
||||
<string name="report_personal_subtitle">Quando você vê algo que não gosta no Mastodon, pode remover a pessoa da sua experiência.</string>
|
||||
@@ -197,8 +200,6 @@
|
||||
<string name="content_warning">Aviso de conteúdo</string>
|
||||
<string name="add_image_description">Adicionar descrição da imagem…</string>
|
||||
<string name="retry_upload">Tentar enviar novamente</string>
|
||||
<string name="image_upload_failed">Falha ao carregar imagem</string>
|
||||
<string name="video_upload_failed">Falha no envio do vídeo</string>
|
||||
<string name="edit_image">Editar imagem</string>
|
||||
<string name="save">Salvar</string>
|
||||
<string name="add_alt_text">Adicionar texto alternativo</string>
|
||||
@@ -214,6 +215,7 @@
|
||||
<string name="skip">Pular</string>
|
||||
<string name="notification_type_follow">Novos seguidores</string>
|
||||
<string name="notification_type_favorite">Favoritos</string>
|
||||
<string name="notification_type_reblog">Reblogues</string>
|
||||
<string name="notification_type_mention">Menções</string>
|
||||
<string name="notification_type_poll">Enquetes</string>
|
||||
<string name="choose_account">Escolher conta</string>
|
||||
@@ -240,6 +242,7 @@
|
||||
<string name="notify_none">ninguém</string>
|
||||
<string name="notify_favorites">Favoritaram minha publicação</string>
|
||||
<string name="notify_follow">Sigam-me</string>
|
||||
<string name="notify_reblog">Rebloga meu post</string>
|
||||
<string name="notify_mention">Mencione-me</string>
|
||||
<string name="settings_boring">A zona entediante</string>
|
||||
<string name="settings_account">Configurações da conta</string>
|
||||
@@ -260,6 +263,7 @@
|
||||
<string name="hide_content">Ocultar conteúdo</string>
|
||||
<string name="new_post">Novo post</string>
|
||||
<string name="button_reply">Responder</string>
|
||||
<string name="button_reblog">Reblogar</string>
|
||||
<string name="button_favorite">Favoritar</string>
|
||||
<string name="button_share">Compartilhar</string>
|
||||
<string name="media_no_description">Mídia sem descrição</string>
|
||||
@@ -274,6 +278,8 @@
|
||||
<string name="unfollowed_user">Deixar de seguir @%s</string>
|
||||
<string name="followed_user">Você agora está seguindo %s</string>
|
||||
<string name="open_in_browser">Abrir no navegador</string>
|
||||
<string name="hide_boosts_from_user">Ocultar reblogues de %s</string>
|
||||
<string name="show_boosts_from_user">Mostrar reblogues de %s</string>
|
||||
<string name="signup_reason">por que você quer entrar?</string>
|
||||
<string name="signup_reason_note">Isso vai nos ajudar a revisar sua aplicação.</string>
|
||||
<string name="clear">Limpar</string>
|
||||
@@ -317,4 +323,30 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s via %2$s</string>
|
||||
<string name="time_now">agora</string>
|
||||
<string name="post_info_reblogs">Reblogues</string>
|
||||
<string name="post_info_favorites">Favoritos</string>
|
||||
<string name="edit_history">Editar histórico</string>
|
||||
<string name="last_edit_at_x">Última edição %s</string>
|
||||
<string name="time_just_now">agora mesmo</string>
|
||||
<plurals name="x_seconds_ago">
|
||||
<item quantity="one">%d segundo atrás</item>
|
||||
<item quantity="other">%d segundos atrás</item>
|
||||
</plurals>
|
||||
<string name="edited_timestamp">editado %s</string>
|
||||
<string name="edit_original_post">Postagem original</string>
|
||||
<string name="edit_text_edited">Texto editado</string>
|
||||
<string name="edit_spoiler_added">Aviso de conteúdo adicionado</string>
|
||||
<string name="edit_spoiler_edited">Aviso de conteúdo editado</string>
|
||||
<string name="edit_spoiler_removed">Aviso de conteúdo removido</string>
|
||||
<string name="edit_marked_sensitive">Marcadas como sensíveis</string>
|
||||
<string name="edit_marked_not_sensitive">Marcado como não sensível</string>
|
||||
<string name="edit_multiple_changed">Publicação editada</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="discard_changes">Descartar alterações?</string>
|
||||
<string name="upload_failed">Falha no upload</string>
|
||||
<string name="upload_error_connection_lost">Seu dispositivo perdeu a conexão com a internet</string>
|
||||
<string name="upload_processing">Processando…</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -178,8 +178,6 @@
|
||||
<string name="content_warning">Aviso de conteúdo</string>
|
||||
<string name="add_image_description">Adicionar descrição da imagem…</string>
|
||||
<string name="retry_upload">Tentar enviar novamente</string>
|
||||
<string name="image_upload_failed">Falha ao carregar imagem</string>
|
||||
<string name="video_upload_failed">Falha ao carregar vídeo</string>
|
||||
<string name="edit_image">Editar imagem</string>
|
||||
<string name="save">Salvar</string>
|
||||
<string name="add_alt_text">Adicionar texto alternativo</string>
|
||||
@@ -194,4 +192,7 @@
|
||||
<string name="step_x_of_n">Passo %1$d de %2$d</string>
|
||||
<string name="skip">Ignorar</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
@@ -232,8 +232,6 @@
|
||||
<string name="content_warning">Предупреждение о содержании</string>
|
||||
<string name="add_image_description">Добавить описание…</string>
|
||||
<string name="retry_upload">Повторить попытку</string>
|
||||
<string name="image_upload_failed">Не удалось загрузить изображение</string>
|
||||
<string name="video_upload_failed">Не удалось загрузить видео</string>
|
||||
<string name="edit_image">Редактировать изображение</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="add_alt_text">Добавить альтернативный текст</string>
|
||||
@@ -359,4 +357,7 @@
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s через %2$s</string>
|
||||
<string name="time_now">только что</string>
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is version like 1.2.3 -->
|
||||
<!-- %s is file size -->
|
||||
</resources>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user