From f20ce1fc690788273bb779663a4f3211f47a0973 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sat, 23 Mar 2024 09:40:40 +0100 Subject: Move first batch of preferences code to :ui:preferences (#7010) --- ui/preferences/README.md | 3 + ui/preferences/build.gradle | 47 ++++ ui/preferences/src/main/AndroidManifest.xml | 5 + ui/preferences/src/main/assets/.gitignore | 4 + .../src/main/assets/LICENSE_APACHE-2.0.txt | 202 +++++++++++++++ ui/preferences/src/main/assets/LICENSE_GLIDE.txt | 94 +++++++ ui/preferences/src/main/assets/LICENSE_JSOUP.txt | 21 ++ ui/preferences/src/main/assets/LICENSE_OKHTTP.txt | 13 + .../src/main/assets/LICENSE_PICTOGRAMMERS.txt | 20 ++ .../src/main/assets/LICENSE_SEARCHPREFERENCE.txt | 21 ++ ui/preferences/src/main/assets/developers.csv | 235 +++++++++++++++++ ui/preferences/src/main/assets/licenses.xml | 123 +++++++++ ui/preferences/src/main/assets/special_thanks.csv | 6 + ui/preferences/src/main/assets/translators.csv | 51 ++++ .../src/main/assets/website-languages.txt | 7 + .../preference/MasterSwitchPreference.java | 42 ++++ .../preference/MaterialListPreference.java | 43 ++++ .../MaterialMultiSelectListPreference.java | 47 ++++ .../ui/preferences/preference/ThemePreference.java | 53 ++++ .../screen/AutoDownloadPreferencesFragment.java | 202 +++++++++++++++ .../screen/NotificationPreferencesFragment.java | 28 +++ .../screen/about/ContributorsPagerFragment.java | 87 +++++++ .../screen/about/DevelopersFragment.java | 56 +++++ .../preferences/screen/about/LicensesFragment.java | 125 +++++++++ .../screen/about/SimpleIconListAdapter.java | 59 +++++ .../screen/about/SpecialThanksFragment.java | 55 ++++ .../screen/about/TranslatorsFragment.java | 55 ++++ .../screen/downloads/ChooseDataFolderDialog.java | 36 +++ .../screen/downloads/DataFolderAdapter.java | 139 ++++++++++ .../synchronization/AuthenticationDialog.java | 54 ++++ .../GpodderAuthenticationFragment.java | 279 +++++++++++++++++++++ .../NextcloudAuthenticationFragment.java | 111 ++++++++ .../SynchronizationPreferencesFragment.java | 220 ++++++++++++++++ .../main/res/drawable-nodpi/theme_preview_dark.png | Bin 0 -> 18079 bytes .../res/drawable-nodpi/theme_preview_light.png | Bin 0 -> 17760 bytes .../res/drawable-nodpi/theme_preview_system.png | Bin 0 -> 34076 bytes .../src/main/res/layout/about_teaser.xml | 9 + .../layout/alertdialog_sync_provider_chooser.xml | 24 ++ .../src/main/res/layout/authentication_dialog.xml | 61 +++++ .../main/res/layout/choose_data_folder_dialog.xml | 12 + .../res/layout/choose_data_folder_dialog_entry.xml | 46 ++++ .../main/res/layout/dialog_switch_preference.xml | 15 ++ .../main/res/layout/gpodnetauth_credentials.xml | 83 ++++++ .../src/main/res/layout/gpodnetauth_device.xml | 67 +++++ .../src/main/res/layout/gpodnetauth_device_row.xml | 13 + .../src/main/res/layout/gpodnetauth_dialog.xml | 31 +++ .../src/main/res/layout/gpodnetauth_finish.xml | 29 +++ .../src/main/res/layout/gpodnetauth_host.xml | 38 +++ .../src/main/res/layout/nextcloud_auth_dialog.xml | 75 ++++++ .../src/main/res/layout/proxy_settings.xml | 90 +++++++ .../src/main/res/layout/simple_icon_list_item.xml | 41 +++ .../src/main/res/layout/theme_preference.xml | 123 +++++++++ .../src/main/res/menu/bug_report_options.xml | 7 + ui/preferences/src/main/res/xml/preferences.xml | 71 ++++++ .../src/main/res/xml/preferences_about.xml | 28 +++ .../src/main/res/xml/preferences_autodownload.xml | 34 +++ .../src/main/res/xml/preferences_downloads.xml | 69 +++++ .../src/main/res/xml/preferences_import_export.xml | 45 ++++ .../src/main/res/xml/preferences_notifications.xml | 19 ++ .../src/main/res/xml/preferences_playback.xml | 108 ++++++++ .../src/main/res/xml/preferences_swipe.xml | 28 +++ .../main/res/xml/preferences_synchronization.xml | 31 +++ .../main/res/xml/preferences_user_interface.xml | 95 +++++++ 63 files changed, 3835 insertions(+) create mode 100644 ui/preferences/README.md create mode 100644 ui/preferences/build.gradle create mode 100644 ui/preferences/src/main/AndroidManifest.xml create mode 100644 ui/preferences/src/main/assets/.gitignore create mode 100644 ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt create mode 100644 ui/preferences/src/main/assets/LICENSE_GLIDE.txt create mode 100644 ui/preferences/src/main/assets/LICENSE_JSOUP.txt create mode 100644 ui/preferences/src/main/assets/LICENSE_OKHTTP.txt create mode 100644 ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt create mode 100644 ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt create mode 100644 ui/preferences/src/main/assets/developers.csv create mode 100644 ui/preferences/src/main/assets/licenses.xml create mode 100644 ui/preferences/src/main/assets/special_thanks.csv create mode 100644 ui/preferences/src/main/assets/translators.csv create mode 100644 ui/preferences/src/main/assets/website-languages.txt create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java create mode 100644 ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java create mode 100644 ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png create mode 100644 ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png create mode 100644 ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png create mode 100644 ui/preferences/src/main/res/layout/about_teaser.xml create mode 100644 ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml create mode 100644 ui/preferences/src/main/res/layout/authentication_dialog.xml create mode 100644 ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml create mode 100644 ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml create mode 100644 ui/preferences/src/main/res/layout/dialog_switch_preference.xml create mode 100644 ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml create mode 100644 ui/preferences/src/main/res/layout/gpodnetauth_device.xml create mode 100644 ui/preferences/src/main/res/layout/gpodnetauth_device_row.xml create mode 100644 ui/preferences/src/main/res/layout/gpodnetauth_dialog.xml create mode 100644 ui/preferences/src/main/res/layout/gpodnetauth_finish.xml create mode 100644 ui/preferences/src/main/res/layout/gpodnetauth_host.xml create mode 100644 ui/preferences/src/main/res/layout/nextcloud_auth_dialog.xml create mode 100644 ui/preferences/src/main/res/layout/proxy_settings.xml create mode 100644 ui/preferences/src/main/res/layout/simple_icon_list_item.xml create mode 100644 ui/preferences/src/main/res/layout/theme_preference.xml create mode 100644 ui/preferences/src/main/res/menu/bug_report_options.xml create mode 100644 ui/preferences/src/main/res/xml/preferences.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_about.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_autodownload.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_downloads.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_import_export.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_notifications.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_playback.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_swipe.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_synchronization.xml create mode 100644 ui/preferences/src/main/res/xml/preferences_user_interface.xml (limited to 'ui/preferences') diff --git a/ui/preferences/README.md b/ui/preferences/README.md new file mode 100644 index 000000000..341e8e52e --- /dev/null +++ b/ui/preferences/README.md @@ -0,0 +1,3 @@ +# :ui:preferences + +This module provides the settings screen. diff --git a/ui/preferences/build.gradle b/ui/preferences/build.gradle new file mode 100644 index 000000000..b91158fb9 --- /dev/null +++ b/ui/preferences/build.gradle @@ -0,0 +1,47 @@ +plugins { + id("com.android.library") +} +apply from: "../../common.gradle" +apply from: "../../playFlavor.gradle" + +android { + namespace "de.danoeh.antennapod.ui.preferences" +} + +dependencies { + implementation project(":core") + implementation project(":event") + implementation project(":model") + implementation project(":net:common") + implementation project(":net:sync:model") + implementation project(":net:sync:gpoddernet") + implementation project(":storage:preferences") + implementation project(":storage:importexport") + implementation project(":ui:common") + implementation project(":ui:glide") + implementation project(":ui:i18n") + + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "com.google.android.material:material:$googleMaterialVersion" + implementation "androidx.preference:preference:$preferenceVersion" + implementation "androidx.work:work-runtime:$workManagerVersion" + + implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" + implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" + implementation "com.github.bumptech.glide:glide:$glideVersion" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion" + implementation "org.greenrobot:eventbus:$eventbusVersion" + implementation 'com.github.ByteHamster:SearchPreference:v2.5.0' +} + +tasks.register('copyLicense', Copy) { + from "../../LICENSE" + into "src/main/assets/" + rename { String fileName -> + fileName + ".txt" + } +} + +preBuild.dependsOn copyLicense diff --git a/ui/preferences/src/main/AndroidManifest.xml b/ui/preferences/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7af8da301 --- /dev/null +++ b/ui/preferences/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + diff --git a/ui/preferences/src/main/assets/.gitignore b/ui/preferences/src/main/assets/.gitignore new file mode 100644 index 000000000..f4de63f77 --- /dev/null +++ b/ui/preferences/src/main/assets/.gitignore @@ -0,0 +1,4 @@ +# this file is generated automatically +about.html +LICENSE.txt +CONTRIBUTORS.txt diff --git a/ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt b/ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ui/preferences/src/main/assets/LICENSE_GLIDE.txt b/ui/preferences/src/main/assets/LICENSE_GLIDE.txt new file mode 100644 index 000000000..f5111eeab --- /dev/null +++ b/ui/preferences/src/main/assets/LICENSE_GLIDE.txt @@ -0,0 +1,94 @@ +License for everything not in third_party and not otherwise marked: + +Copyright 2014 Google, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Google, Inc. +--------------------------------------------------------------------------------------------- +License for third_party/disklrucache: + +Copyright 2012 Jake Wharton +Copyright 2011 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--------------------------------------------------------------------------------------------- +License for third_party/gif_decoder: + +Copyright (c) 2013 Xcellent Creations, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------------------------------------------- +License for third_party/gif_encoder/AnimatedGifEncoder.java and +third_party/gif_encoder/LZWEncoder.java: + +No copyright asserted on the source code of this class. May be used for any +purpose, however, refer to the Unisys LZW patent for restrictions on use of +the associated LZWEncoder class. Please forward any corrections to +kweiner@fmsware.com. + +----------------------------------------------------------------------------- +License for third_party/gif_encoder/NeuQuant.java + +Copyright (c) 1994 Anthony Dekker + +NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See +"Kohonen neural networks for optimal colour quantization" in "Network: +Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of +the algorithm. + +Any party obtaining a copy of these files from the author, directly or +indirectly, is granted, free of charge, a full and unrestricted irrevocable, +world-wide, paid up, royalty-free, nonexclusive right and license to deal in +this software and documentation files (the "Software"), including without +limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons who +receive copies from any such party to do so, with the only requirement being +that this copyright notice remain intact. diff --git a/ui/preferences/src/main/assets/LICENSE_JSOUP.txt b/ui/preferences/src/main/assets/LICENSE_JSOUP.txt new file mode 100644 index 000000000..fef8197fe --- /dev/null +++ b/ui/preferences/src/main/assets/LICENSE_JSOUP.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009-2022 Jonathan Hedley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ui/preferences/src/main/assets/LICENSE_OKHTTP.txt b/ui/preferences/src/main/assets/LICENSE_OKHTTP.txt new file mode 100644 index 000000000..48164b3fc --- /dev/null +++ b/ui/preferences/src/main/assets/LICENSE_OKHTTP.txt @@ -0,0 +1,13 @@ +Copyright 2019 Square, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt b/ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt new file mode 100644 index 000000000..382f8a138 --- /dev/null +++ b/ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt @@ -0,0 +1,20 @@ +Pictogrammers Free License +-------------------------- + +This icon collection is released as free, open source, and GPL friendly by +the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it +for commercial projects, open source projects, or anything really. + +# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) +Some of the icons are redistributed under the Apache 2.0 license. All other +icons are either redistributed under their respective licenses or are +distributed under the Apache 2.0 license. + +# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) +All web and desktop fonts are distributed under the Apache 2.0 license. Web +and desktop fonts contain some icons that are redistributed under the Apache +2.0 license. All other icons are either redistributed under their respective +licenses or are distributed under the Apache 2.0 license. + +# Code: MIT (https://opensource.org/licenses/MIT) +The MIT license applies to all non-font and non-icon files. diff --git a/ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt b/ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt new file mode 100644 index 000000000..2e1603517 --- /dev/null +++ b/ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 ByteHamster + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ui/preferences/src/main/assets/developers.csv b/ui/preferences/src/main/assets/developers.csv new file mode 100644 index 000000000..da0fa6093 --- /dev/null +++ b/ui/preferences/src/main/assets/developers.csv @@ -0,0 +1,235 @@ +ByteHamster;5811634;Maintainer +danieloeh;968613;Original creator of AntennaPod (retired) +mfietz;6860662;Maintainer (retired) +TomHennen;5216560;Maintainer (retired) +orionlee;250644;Contributor +domingos86;9538859;Contributor +TacoTheDank;32376686;Contributor +tonytamsf;149837;Contributor +damoasda;46045854;Contributor +andersonvom;69922;Contributor +ebraminio;833473;Contributor +shortspider;5712543;Contributor +spacecowboy;223655;Contributor +asdoi;36813904;Contributor +keunes;11229646;Maintainer +patheticpat;16046;Contributor +brad;1614;Contributor +maxbechtold;9162198;Contributor +Cj-Malone;10121513;Contributor +gaul;848247;Contributor +qkolj;6667105;Contributor +pachecosf;46357909;Contributor +gerardolgvr;20119298;Contributor +johnjohndoe;144518;Contributor +bws9000;262625;Contributor +hannesa2;3314607;Contributor +ahangarha;11241315;Contributor +rharriso;570910;Contributor +xgouchet;818706;Contributor +ueen;5067479;Contributor +peakvalleytech;65185819;Contributor +vbh;56578479;Contributor +mueller-ma;22525368;Contributor +gitstart;1501599;Contributor +terminalmage;328598;Contributor +TheRealFalcon;153674;Contributor +Slinger;75751;Contributor +udif;809640;Contributor +malockin;12814657;Contributor +jas14;569991;Contributor +jonasburian;15125616;Contributor +dirkmueller;1029152;Contributor +jatinkumarg;20503830;Contributor +peschmae0;4450993;Contributor +orelogo;15976578;Contributor +txtd;7108931;Contributor +ydinath;4193331;Contributor +two-heart;12869538;Contributor +binarytoto;75904760;Contributor +saqura;1935380;Contributor +drabux;10663142;Contributor +dethstar;1239177;Contributor +mchelen;30691;Contributor +CedricCabessa;365097;Contributor +matejdro;507922;Contributor +jhenninger;197274;Contributor +Xeitor;8825715;Contributor +ligi;111600;Contributor +egsavage;126165;Contributor +cketti;218061;Contributor +MeirAtIMDDE;4421079;Contributor +deandreamatias;21011641;Contributor +hzulla;1705654;Contributor +bibz;5141956;Contributor +HaBaLeS;730902;Contributor +JessieVela;33134794;Contributor +volhol;11587858;Contributor +michaelmwhite;28901334;Contributor +twiceyuan;2619800;Contributor +thrillfall;15801468;Contributor +rezanejati;16049370;Contributor +beijingling;13600573;Contributor +VishnuSanal;50027064;Contributor +nereocystis;2257107;Contributor +liesen;26872;Contributor +dreiss;4121;Contributor +Thom-Merrilin;76849828;Contributor +archibishop;36948493;Contributor +alroborol;24603829;Contributor +avirajrsingh;69088913;Contributor +caoilte;1500358;Contributor +toggles;14695;Contributor +connectety;26038710;Contributor +matdb;48329535;Contributor +SosoTughushi;19908097;Contributor +Lukmannudin;32972299;Contributor +24hours;650407;Contributor +LatinSuD;451487;Contributor +katrinleinweber;9948149;Contributor +jmue;898577;Contributor +xisberto;1914956;Contributor +HolgerJeromin;2410353;Contributor +HrBDev;25826502;Contributor +CameronBanga;611354;Contributor +mohitshah3111999;42018918;Contributor +markamaze;17114678;Contributor +femmdi;47671383;Contributor +datavizard;44409076;Contributor +wseemann;2296196;Contributor +vinodpatildev;61724808;Contributor +liutng;8223139;Contributor +moralesg;14352147;Contributor +mr-intj;6268767;Contributor +tamizh143;50977879;Contributor +tuxayo;2678215;Contributor +alimemonzx;44647595;Contributor +dev-darrell;52300159;Contributor +jmdouglas;10855634;Contributor +olivoto;15932680;Contributor +PtilopsisLeucotis;54054883;Contributor +dsmith47;14109426;Contributor +kingargyle;177042;Contributor +FarzanKh;14272565;Contributor +damlayildiz;56313500;Contributor +hannesaa2;18496079;Contributor +myslok;2098329;Contributor +jhunnius;9149031;Contributor +Jared234;26669009;Contributor +skitt;2128935;Contributor +mamehacker;16738348;Contributor +raghulrm;5362986;Contributor +raghulj;57007;Contributor +Niffler;8172446;Contributor +JonathanZopf;47294759;Contributor +a1291762;327162;Contributor +ShadowIce;59123;Contributor +victorhaggqvist;1887628;Contributor +Toover;8531603;Contributor +atrus6;357881;Contributor +edent;837136;Contributor +Carrajaula;25173082;Contributor +vimsick;20211590;Contributor +corecode;177979;Contributor +Pinkolik;26690061;Contributor +danners;116551;Contributor +Silverwarriorin;46795935;Contributor +shombando;42972338;Contributor +shantanahardy;26757164;Contributor +sethoscope;534043;Contributor +sonnayasomnambula;7716779;Contributor +selivan;1208989;Contributor +SebiderSushi;23618858;Contributor +SamWhited;512573;Contributor +bobrippling;205673;Contributor +ricardoborgesjr;2378440;Contributor +rahmatrmdn;43070505;Contributor +RafaelBod;77226971;Contributor +panoreak;25068506;Contributor +ortylp;470439;Contributor +patrickdemers6;12687723;Contributor +pganssle;1377457;Contributor +patrickjkennedy;8617261;Contributor +zawad2221;32180355;Contributor +trevortabaka;1552990;Contributor +thomasdomingos;16108830;Contributor +tamizh138;26201258;Contributor +struggggle;19150666;Contributor +silansuslu;72400543;Contributor +satis-fy;33289586;Contributor +s3lph;5564491;Contributor +hiasr;22374542;Contributor +quails4Eva;16786857;Contributor +NWuensche;15856197;Contributor +minusf;3632883;Contributor +loucasal;25279797;Contributor +lightonflux;1377943;Contributor +gregoryjtom;32783177;Contributor +sak96;26397224;Contributor +fossterer;4236021;Contributor +e-t-l;40775958;Contributor +cliambrown;17516840;Contributor +chrk2205;44704035;Contributor +blairun;1585872;Contributor +axq;5077221;Contributor +andrewc1;19559401;Contributor +amhokies;3124968;Contributor +agibault;15703733;Contributor +yarons;406826;Contributor +waylife;3348620;Contributor +heyyviv;56256802;Contributor +oliver;2344;Contributor +IordanisKokk;72551397;Contributor +harshad1;1940940;Contributor +Geist5000;37940313;Contributor +eerden;277513;Contributor +eirikv;4076243;Contributor +edwinhere;19705425;Contributor +dhruvpatidar359;103873587;Contributor +cdhiraj40;75211982;Contributor +brettle;118192;Contributor +ariedov;958646;Contributor +danielm5;66779;Contributor +CWftw;1498303;Contributor +cszucko;1810383;Contributor +britiger;2057760;Contributor +chrissicool;232590;Contributor +chetan882777;36985543;Contributor +BoJacobs;25435640;Contributor +bhaskarblur;85757758;Contributor +arantius;84729;Contributor +andweg;30474752;Contributor +andrey-krutov;1488973;Contributor +ASGusev;5954975;Contributor +awbooze;42682253;Contributor +alexte;7724992;Contributor +alanorth;191754;Contributor +adrns;13379985;Contributor +abhinavg1997;60095795;Contributor +nproth;48482306;Contributor +nikhil097;35090769;Contributor +nicoolasj;63880378;Contributor +mounirlamouri;573590;Contributor +MolarAmbiguity;10541979;Contributor +Mengshi24;58278376;Contributor +max-wittig;6639323;Contributor +mschuetz;108637;Contributor +Gaffen;718125;Contributor +Mchoi8;45410115;Contributor +mdeveloper20;2319126;Contributor +mo;7117;Contributor +mgborowiec;29843126;Contributor +M-arcel;56698158;Contributor +schwedenmut;9077622;Contributor +mlasson;5814258;Contributor +mjydv4548;92643506;Contributor +MStrecke;5202211;Contributor +LukasBrilla5;114982148;Contributor +luiscruz;1080714;Contributor +kvithayathil;1056073;Contributor +Kaligule;3586246;Contributor +CreamyCookie;3063858;Contributor +JonOfUs;11487762;Contributor +Foso;5015532;Contributor +jannic;232606;Contributor +jklippel;8657220;Contributor diff --git a/ui/preferences/src/main/assets/licenses.xml b/ui/preferences/src/main/assets/licenses.xml new file mode 100644 index 000000000..e6745e4f5 --- /dev/null +++ b/ui/preferences/src/main/assets/licenses.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/preferences/src/main/assets/special_thanks.csv b/ui/preferences/src/main/assets/special_thanks.csv new file mode 100644 index 000000000..5758d48c0 --- /dev/null +++ b/ui/preferences/src/main/assets/special_thanks.csv @@ -0,0 +1,6 @@ +ByteHamster;Project lead;https://avatars2.githubusercontent.com/u/5811634?s=60&v=4 +Keunes;Project lead;https://avatars2.githubusercontent.com/u/11229646?s=60&v=4 +Femmdi;Translations coordinator;https://avatars2.githubusercontent.com/u/47671383?s=60&v=4 +Ryan Gorley (Freehive);2023 brand design;https://avatars2.githubusercontent.com/u/12849958?s=60&v=4 +221 Pixels;2020 brand design;https://avatars2.githubusercontent.com/u/58243143?s=60&v=4 +Anxhelo Lushka;2020 website design;https://avatars2.githubusercontent.com/u/25004151?s=60&v=4 diff --git a/ui/preferences/src/main/assets/translators.csv b/ui/preferences/src/main/assets/translators.csv new file mode 100644 index 000000000..28d659c99 --- /dev/null +++ b/ui/preferences/src/main/assets/translators.csv @@ -0,0 +1,51 @@ +Arabic;abuzar3.khalid, AhmedHll, Ammar99, badarotti, fake4K, HeshamTB, keunes, mars_amn, Mehyar, mh.abdelhay, mhamade, moftasa, mohmans, MustafaAlgurabi, nabilMaghura, rex07, shubbar, vernandos +Asturian (ast_ES);enolp, keunes +Azerbaijani;5NOER227O, xxmn77 +Basque;a_mento, Asier_Iturralde_Sarasola, bipoza, gaztainalde, IngrownMink4, keunes, Osoitz, pospolos +Bengali;laggybird +Breton;Belvar, Eorn, EwenKorr, FlorentTroer, Iriep, keunes, technozuzici +Bulgarian;keunes, ma4ko, mihainov, ppk89, solusitor, x7ype +Catalan;and_dapo, arseru, badlop, bluegeekgh, carles.llacer, dvd1985, elcamilet, exort12, IvanAmarante, javiercoll, josep2, keunes, Kintu, lambdani, marcmetallextrem, prova, sandandmercury, selmins, xc70 +Chinese (zh_CN);135e2, aihenry2980, Biacke, brnme, claybiockiller, clong289734997, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, JY3, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, weylinn, whiye.hust, wongsyrone, Xrodo, yangyang, yiqiok +Chinese (zh_TW);bobchao, BWsix, ijliao, keunes, LNDDYL, mapobi, pggdt, ymhuang0808 +Czech (cs_CZ);anotheranonymoususer, befeleme, Benda, elich, Hanzmeister, jjh, JStrange, kudlav, McLenin666, md.share, ShimonH, svetlemodry, Thomaash, viotalJiplk +Danish;deusdenton, ERYpTION, Grooty12, JFreak, jhertel, keunes, mikini, petterbejo, SebastianKiwiDk, soelvraeven +Dutch;daerts, e2jk, fvbommel, keunes, Kleurenregen, mijnheer, oldblue, rwv, twijg, Vistaus, y33per +Estonian;beez276, Eraser, keunes, mahfiaz, Rots +Finnish;Ban3, keunes, ktstmu, Kuutar, noppa, Sahtor, scop, teemue +French;5moufl, 5NOER227O, AX.AGD, ayiniho, ChaoticMind, clombion, Cornegidouille, Daremo, e2jk, ebouaziz, keunes, klintom, Kuscoo, lacouture, LouFex, manuelleduc, Matth78, paolovador, petterbejo, PierreLaville, Poussinou, RomainTT, sterylmreep, teamon, Thoscellen +Galician;antiparvos, pikamoku, Raichely, Sirgo +German;23Ba1l598, 5NOER227O, _Er, axre, ByteHamster, Ceekay, ceving, dadosch, datesastick, Delvo, DerSilly, elkangaroo, enz, Erc187, F462, f_grubm, femmdi, finsterwalder, forght, hbilke, HolgerJeromin, JMAN, JoeMcFly, jokap, JoniArida, JonOfUs, kalei, keunes, klyneloud, Kostas_F, L.D.A., Macusercom, MahdiMoradi, max.wittig, mfietz, Michael_Strecke, mkida, muellerma, Nickname, petterbejo, pudeeh, Quiss42, repat, sadfgdf, Sargon_Isa, teamon, thetrash23, thiesrappen, timo.rohwedder, toaskoas, Tobiasff3200, tomte, Tonne11, ttick, tweimer, VfBFan, vrifox, Willhelm, ypid +Hebrew (he_IL);amir.dafnyman, E1i9, eldaryiftach, mongoose4004, pinkasey, rellieberman, Yaron +Hindi (hi_IN);Agyat009, itforchange, keunes, PrestigiousBeat6355, purple.coder, siddhusengar, singhrishi245021, sohailmangal72, techiethakkar, thelazyoxymoron +Hu;hurrikan, keunes, lna91, lomapur, marthynw, mc.transifex, meskobalazs, MMate2007, naren93, Remboo +Icelandic;keunes, marthjod +Indonesian;awmpawl, dbrw, justch, keunes, levirs565, liimee, Matyeyev +Italian (it_IT);aalex70, allin, alvami, atilluF, Bonnee, datesastick, dontknowcris, giulia.iuppa, giuseppep, Guybrush88, ilmanzo, juanjom, keunes, lu.por, m.chinni, marco_pag, mat650, micael_27, mircocau, neonsoftware, niccord, salorock, theloca95 +Japanese;atsukotominaga, ayiniho, giulia.iuppa, guyze, keunes, kirameister, KotaKato, Naofumi, sh3llc4t, tko_cactus, TranslatorG, Xrodo +Kannada (kn_IN);chethanhs, chiraag.nataraj, deepu2, itforchange, keunes, thejeshgn, yogi +Ko;changwoo, eshc123, keunes, libliboom, shinwookim +Latin;nivaca +Lithuanian;keunes, Sharper +Macedonian;krisfremen +Malayalam;joice, keunes, KiranS, rashivkp +Modern Greek (1453-);AnimaRain, antonist, bufetr, Fotispel, Ioannis_D, keunes, Kostas_F, pavlosv, pcguy23 +Norwegian Bokmål (nb_NO);abstrakct, ahysing, bablecopherye, corkie, forteller, Gauteweb, halibut, heraldo, jakobkg, Jamiera, keunes, kongk, sevenmaster, tc5, timbast, TrymSan, ttick +Persian;ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, K2latmanesh, keunes, khersi, MahdiMoradi, mmehdishafiee, sinamoghaddas, zarinisalman62 +Polish (pl_PL);ad.szczepanski, befeleme, ewm, Gadzinisko, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, M4SK1N, mandlus, maniexx, Medzik, Mephistofeles, millup, portonus, Rakowy_Manaska, scooby250319888, shark103, TheName, tyle +Portuguese;emansije, jmelo461, keunes, lecalam, smarquespt, WalkerPt +Portuguese (pt_BR);alexupits, alysonborges, amalvarenga, andersonvom, aracnus, arua, bandreghetti, brasileiro, caioau, carlo_valente, castrors, denisdl, diecavallax, fnogcps, jmelo461, keunes, lipefire, mbaltar, olivoto, philosp, ricardo_ramos, rogervezaro, RubeensVinicius, SamWilliam, tepadilha, tschertel, Xandefex, ziul123 +Romanian (ro_RO);AdrianMirica, andreh, eRadical, fuzzmz, Hiumee, keunes, mozartro, ralienpp +Russian (ru_RU);ashed, btimofeev, Duke_Raven, flexagoon, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, nachoman, null, overmind88, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea, yako +Sardinian;prova +Slovak;ati3, jose1711, keunes, marulinko, McLenin666, real_name, tiborepcek +Slovenian (sl_SI);asovic, filomena.pzn, keunes, panter23, TheFireFighter, trus2 +Spanish;3argueta3, 5NOER227O, AleksSyntek, andersonvom, andrespelaezp, arseru, Atreyu94, badlop, CaeM0R, carlos.levy, cartojo, deandreamatias, delthia, devarops, dvd1985, elcamilet, elojodepajaro, Fitoschido, frandavid100, Gomerick, hard_ware, Ioannis_D, israelem, javiercoll, keunes, kiekie, LatinSuD, leogrignafini, meanderingDot, Nickname, nivaca, rafael.osuna, technozuzici, tldevelopbit, tres.14159, vfmatzkin, victorzequeida96, wakutiteo, ziul123 +Swahili (macrolanguage);1silvester, keunes, kmtra +Swedish (sv_SE);aiix, Ainali, bittin, bpnilsson, gustavkj, jrosdahl, keunes, LinAGKar, nilso, TwoD, victorhggqvst +Tatar;seber +Telugu;keunes, veeven +Turkish;AhmedDuran, alianilkocak, alierdogan7, AliGaygisiz, androtuna, archixe, brsata, Erdy, firatsoygul, ibo90p, kabaqtepeli, keunes, overbite, Piryus, samsamsamsam, sismantolga, Slsdem, TZVS, xe1st +Ukrainian (uk_UA);amatra, aserbovets, balaraz, hishak, keunes, koorool, older, paul_sm, sergiyr, voinovich_vyacheslav, zhenya97 +Uzbek;Usmon +Vietnamese;abnvolk, bruhwut, keunes, ppanhh diff --git a/ui/preferences/src/main/assets/website-languages.txt b/ui/preferences/src/main/assets/website-languages.txt new file mode 100644 index 000000000..64361314b --- /dev/null +++ b/ui/preferences/src/main/assets/website-languages.txt @@ -0,0 +1,7 @@ +en +fr +nl +it +da +de +es diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java new file mode 100644 index 000000000..cab3d3fc3 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.ui.preferences.preference; + +import android.content.Context; +import android.graphics.Typeface; +import androidx.preference.SwitchPreferenceCompat; +import androidx.preference.PreferenceViewHolder; +import android.util.AttributeSet; +import android.widget.TextView; + +import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.preferences.R; + +public class MasterSwitchPreference extends SwitchPreferenceCompat { + + public MasterSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public MasterSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public MasterSwitchPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public MasterSwitchPreference(Context context) { + super(context); + } + + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + holder.itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSurfaceVariant)); + TextView title = (TextView) holder.findViewById(android.R.id.title); + if (title != null) { + title.setTypeface(title.getTypeface(), Typeface.BOLD); + } + } +} \ No newline at end of file diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java new file mode 100644 index 000000000..a1264c569 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java @@ -0,0 +1,43 @@ +package de.danoeh.antennapod.ui.preferences.preference; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.preference.ListPreference; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +public class MaterialListPreference extends ListPreference { + + public MaterialListPreference(Context context) { + super(context); + } + + public MaterialListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onClick() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(getTitle()); + builder.setIcon(getDialogIcon()); + builder.setNegativeButton(getNegativeButtonText(), null); + + CharSequence[] values = getEntryValues(); + int selected = -1; + for (int i = 0; i < values.length; i++) { + if (values[i].toString().equals(getValue())) { + selected = i; + } + } + builder.setSingleChoiceItems(getEntries(), selected, (dialog, which) -> { + dialog.dismiss(); + if (which >= 0 && getEntryValues() != null) { + String value = getEntryValues()[which].toString(); + if (callChangeListener(value)) { + setValue(value); + } + } + }); + builder.show(); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java new file mode 100644 index 000000000..1cf1ee170 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.ui.preferences.preference; + +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; +import androidx.preference.MultiSelectListPreference; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.util.HashSet; +import java.util.Set; + +public class MaterialMultiSelectListPreference extends MultiSelectListPreference { + + public MaterialMultiSelectListPreference(Context context) { + super(context); + } + + public MaterialMultiSelectListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onClick() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(getTitle()); + builder.setIcon(getDialogIcon()); + builder.setNegativeButton(getNegativeButtonText(), null); + + boolean[] selected = new boolean[getEntries().length]; + CharSequence[] values = getEntryValues(); + for (int i = 0; i < values.length; i++) { + selected[i] = getValues().contains(values[i].toString()); + } + builder.setMultiChoiceItems(getEntries(), selected, + (DialogInterface dialog, int which, boolean isChecked) -> selected[which] = isChecked); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + Set selectedValues = new HashSet<>(); + for (int i = 0; i < values.length; i++) { + if (selected[i]) { + selectedValues.add(getEntryValues()[i].toString()); + } + } + setValues(selectedValues); + }); + builder.show(); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java new file mode 100644 index 000000000..9fdf591c5 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.ui.preferences.preference; + +import android.content.Context; +import android.util.AttributeSet; +import androidx.cardview.widget.CardView; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; +import com.google.android.material.elevation.SurfaceColors; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.preferences.R; +import de.danoeh.antennapod.ui.preferences.databinding.ThemePreferenceBinding; + +public class ThemePreference extends Preference { + ThemePreferenceBinding viewBinding; + + public ThemePreference(Context context) { + super(context); + setLayoutResource(R.layout.theme_preference); + } + + public ThemePreference(Context context, AttributeSet attrs) { + super(context, attrs); + setLayoutResource(R.layout.theme_preference); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + viewBinding = ThemePreferenceBinding.bind(holder.itemView); + updateUi(); + } + + void updateThemeCard(CardView card, UserPreferences.ThemePreference theme) { + float density = getContext().getResources().getDisplayMetrics().density; + int surfaceColor = SurfaceColors.getColorForElevation(getContext(), 1 * density); + int surfaceColorActive = SurfaceColors.getColorForElevation(getContext(), 32 * density); + UserPreferences.ThemePreference activeTheme = UserPreferences.getTheme(); + card.setCardBackgroundColor(theme == activeTheme ? surfaceColorActive : surfaceColor); + card.setOnClickListener(v -> { + UserPreferences.setTheme(theme); + if (getOnPreferenceChangeListener() != null) { + getOnPreferenceChangeListener().onPreferenceChange(this, UserPreferences.getTheme()); + } + updateUi(); + }); + } + + void updateUi() { + updateThemeCard(viewBinding.themeSystemCard, UserPreferences.ThemePreference.SYSTEM); + updateThemeCard(viewBinding.themeLightCard, UserPreferences.ThemePreference.LIGHT); + updateThemeCard(viewBinding.themeDarkCard, UserPreferences.ThemePreference.DARK); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java new file mode 100644 index 000000000..7c0c3ed4c --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java @@ -0,0 +1,202 @@ +package de.danoeh.antennapod.ui.preferences.screen; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.CheckBoxPreference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.ui.preferences.R; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { + private static final String TAG = "AutoDnldPrefFragment"; + + private CheckBoxPreference[] selectedNetworks; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_autodownload); + + setupAutoDownloadScreen(); + buildAutodownloadSelectedNetworksPreference(); + setSelectedNetworksEnabled(UserPreferences.isEnableAutodownloadWifiFilter()); + buildEpisodeCleanupPreference(); + } + + @Override + public void onStart() { + super.onStart(); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.pref_automatic_download_title); + } + + @Override + public void onResume() { + super.onResume(); + checkAutodownloadItemVisibility(UserPreferences.isEnableAutodownload()); + } + + private void setupAutoDownloadScreen() { + findPreference(UserPreferences.PREF_ENABLE_AUTODL).setOnPreferenceChangeListener( + (preference, newValue) -> { + if (newValue instanceof Boolean) { + checkAutodownloadItemVisibility((Boolean) newValue); + } + return true; + }); + if (Build.VERSION.SDK_INT >= 29) { + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setVisible(false); + } + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + if (newValue instanceof Boolean) { + setSelectedNetworksEnabled((Boolean) newValue); + return true; + } else { + return false; + } + } + ); + } + + private void checkAutodownloadItemVisibility(boolean autoDownload) { + findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setEnabled(autoDownload); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY).setEnabled(autoDownload); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled(autoDownload); + findPreference(UserPreferences.PREF_EPISODE_CLEANUP).setEnabled(autoDownload); + setSelectedNetworksEnabled(autoDownload && UserPreferences.isEnableAutodownloadWifiFilter()); + } + + private static String blankIfNull(String val) { + return val == null ? "" : val; + } + + @SuppressLint("MissingPermission") // getConfiguredNetworks needs location permission starting with API 29 + private void buildAutodownloadSelectedNetworksPreference() { + if (Build.VERSION.SDK_INT >= 29) { + return; + } + + final Activity activity = getActivity(); + + if (selectedNetworks != null) { + clearAutodownloadSelectedNetworsPreference(); + } + // get configured networks + WifiManager wifiservice = (WifiManager) activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + List networks = wifiservice.getConfiguredNetworks(); + + if (networks == null) { + Log.e(TAG, "Couldn't get list of configure Wi-Fi networks"); + return; + } + Collections.sort(networks, (x, y) -> + blankIfNull(x.SSID).compareToIgnoreCase(blankIfNull(y.SSID))); + selectedNetworks = new CheckBoxPreference[networks.size()]; + List prefValues = Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()); + PreferenceScreen prefScreen = getPreferenceScreen(); + Preference.OnPreferenceClickListener clickListener = preference -> { + if (preference instanceof CheckBoxPreference) { + String key = preference.getKey(); + List prefValuesList = new ArrayList<>( + Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()) + ); + boolean newValue = ((CheckBoxPreference) preference) + .isChecked(); + Log.d(TAG, "Selected network " + key + ". New state: " + newValue); + + int index = prefValuesList.indexOf(key); + if (index >= 0 && !newValue) { + // remove network + prefValuesList.remove(index); + } else if (index < 0 && newValue) { + prefValuesList.add(key); + } + + UserPreferences.setAutodownloadSelectedNetworks(prefValuesList.toArray(new String[0])); + return true; + } else { + return false; + } + }; + // create preference for each known network. attach listener and set + // value + for (int i = 0; i < networks.size(); i++) { + WifiConfiguration config = networks.get(i); + + CheckBoxPreference pref = new CheckBoxPreference(activity); + String key = Integer.toString(config.networkId); + pref.setTitle(config.SSID); + pref.setKey(key); + pref.setOnPreferenceClickListener(clickListener); + pref.setPersistent(false); + pref.setChecked(prefValues.contains(key)); + selectedNetworks[i] = pref; + prefScreen.addPreference(pref); + } + } + + private void clearAutodownloadSelectedNetworsPreference() { + if (selectedNetworks != null) { + PreferenceScreen prefScreen = getPreferenceScreen(); + + for (CheckBoxPreference network : selectedNetworks) { + if (network != null) { + prefScreen.removePreference(network); + } + } + } + } + + private void buildEpisodeCleanupPreference() { + final Resources res = getActivity().getResources(); + + ListPreference pref = findPreference(UserPreferences.PREF_EPISODE_CLEANUP); + String[] values = res.getStringArray( + R.array.episode_cleanup_values); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + int v = Integer.parseInt(values[x]); + if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) { + entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { + entries[x] = res.getString(R.string.episode_cleanup_queue_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ + entries[x] = res.getString(R.string.episode_cleanup_never); + } else if (v == 0) { + entries[x] = res.getString(R.string.episode_cleanup_after_listening); + } else if (v > 0 && v < 24) { + entries[x] = res.getQuantityString(R.plurals.episode_cleanup_hours_after_listening, v, v); + } else { + int numDays = v / 24; // assume underlying value will be NOT fraction of days, e.g., 36 (hours) + entries[x] = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, numDays, numDays); + } + } + pref.setEntries(entries); + } + + private void setSelectedNetworksEnabled(boolean b) { + if (selectedNetworks != null) { + for (Preference p : selectedNetworks) { + p.setEnabled(b); + } + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java new file mode 100644 index 000000000..221ea5da1 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.ui.preferences.screen; + +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.ui.preferences.R; + +public class NotificationPreferencesFragment extends PreferenceFragmentCompat { + + private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_notifications); + setUpScreen(); + } + + @Override + public void onStart() { + super.onStart(); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.notification_pref_fragment); + } + + private void setUpScreen() { + findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(SynchronizationSettings.isProviderConnected()); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java new file mode 100644 index 000000000..912d09880 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java @@ -0,0 +1,87 @@ +package de.danoeh.antennapod.ui.preferences.screen.about; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import de.danoeh.antennapod.ui.preferences.R; + +/** + * Displays the 'about->Contributors' pager screen. + */ +public class ContributorsPagerFragment extends Fragment { + private static final int POS_DEVELOPERS = 0; + private static final int POS_TRANSLATORS = 1; + private static final int POS_SPECIAL_THANKS = 2; + private static final int TOTAL_COUNT = 3; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + View rootView = inflater.inflate(R.layout.pager_fragment, container, false); + ViewPager2 viewPager = rootView.findViewById(R.id.viewpager); + viewPager.setAdapter(new StatisticsPagerAdapter(this)); + // Give the TabLayout the ViewPager + TabLayout tabLayout = rootView.findViewById(R.id.sliding_tabs); + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + switch (position) { + case POS_DEVELOPERS: + tab.setText(R.string.developers); + break; + case POS_TRANSLATORS: + tab.setText(R.string.translators); + break; + case POS_SPECIAL_THANKS: + tab.setText(R.string.special_thanks); + break; + default: + break; + } + }).attach(); + + rootView.findViewById(R.id.toolbar).setVisibility(View.GONE); + + return rootView; + } + + @Override + public void onStart() { + super.onStart(); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.contributors); + } + + public static class StatisticsPagerAdapter extends FragmentStateAdapter { + + StatisticsPagerAdapter(@NonNull Fragment fragment) { + super(fragment); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + switch (position) { + case POS_TRANSLATORS: + return new TranslatorsFragment(); + case POS_SPECIAL_THANKS: + return new SpecialThanksFragment(); + default: + case POS_DEVELOPERS: + return new DevelopersFragment(); + } + } + + @Override + public int getItemCount() { + return TOTAL_COUNT; + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java new file mode 100644 index 000000000..de5a21bc0 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.ui.preferences.screen.about; + +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class DevelopersFragment extends ListFragment { + private Disposable developersLoader; + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + getListView().setDivider(null); + getListView().setSelector(android.R.color.transparent); + + developersLoader = Single.create((SingleOnSubscribe>) emitter -> { + ArrayList developers = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + getContext().getAssets().open("developers.csv"), "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + String[] info = line.split(";"); + developers.add(new SimpleIconListAdapter.ListItem(info[0], info[2], + "https://avatars2.githubusercontent.com/u/" + info[1] + "?s=60&v=4")); + } + emitter.onSuccess(developers); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + developers -> setListAdapter(new SimpleIconListAdapter<>(getContext(), developers)), + error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show() + ); + + } + + @Override + public void onStop() { + super.onStop(); + if (developersLoader != null) { + developersLoader.dispose(); + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java new file mode 100644 index 000000000..85badcefc --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java @@ -0,0 +1,125 @@ +package de.danoeh.antennapod.ui.preferences.screen.about; + +import android.os.Bundle; +import android.view.View; +import android.widget.ListView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.fragment.app.ListFragment; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.ui.preferences.R; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class LicensesFragment extends ListFragment { + private Disposable licensesLoader; + private final ArrayList licenses = new ArrayList<>(); + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + getListView().setDivider(null); + + licensesLoader = Single.create((SingleOnSubscribe>) emitter -> { + licenses.clear(); + InputStream stream = getContext().getAssets().open("licenses.xml"); + DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + NodeList libraryList = docBuilder.parse(stream).getElementsByTagName("library"); + for (int i = 0; i < libraryList.getLength(); i++) { + NamedNodeMap lib = libraryList.item(i).getAttributes(); + licenses.add(new LicenseItem( + lib.getNamedItem("name").getTextContent(), + String.format("By %s, %s license", + lib.getNamedItem("author").getTextContent(), + lib.getNamedItem("license").getTextContent()), + null, + lib.getNamedItem("website").getTextContent(), + lib.getNamedItem("licenseText").getTextContent())); + } + emitter.onSuccess(licenses); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + developers -> setListAdapter(new SimpleIconListAdapter(getContext(), developers)), + error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show() + ); + + } + + private static class LicenseItem extends SimpleIconListAdapter.ListItem { + final String licenseUrl; + final String licenseTextFile; + + LicenseItem(String title, String subtitle, String imageUrl, String licenseUrl, String licenseTextFile) { + super(title, subtitle, imageUrl); + this.licenseUrl = licenseUrl; + this.licenseTextFile = licenseTextFile; + } + } + + @Override + public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) { + super.onListItemClick(l, v, position, id); + + LicenseItem item = licenses.get(position); + CharSequence[] items = {"View website", "View license"}; + new MaterialAlertDialogBuilder(getContext()) + .setTitle(item.title) + .setItems(items, (dialog, which) -> { + if (which == 0) { + IntentUtils.openInBrowser(getContext(), item.licenseUrl); + } else if (which == 1) { + showLicenseText(item.licenseTextFile); + } + }).show(); + } + + private void showLicenseText(String licenseTextFile) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader( + getContext().getAssets().open(licenseTextFile), "UTF-8")); + StringBuilder licenseText = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + licenseText.append(line).append("\n"); + } + + new MaterialAlertDialogBuilder(getContext()) + .setMessage(licenseText) + .show(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (licensesLoader != null) { + licensesLoader.dispose(); + } + } + + @Override + public void onStart() { + super.onStart(); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.licenses); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java new file mode 100644 index 000000000..a63b54e5a --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java @@ -0,0 +1,59 @@ +package de.danoeh.antennapod.ui.preferences.screen.about; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.ui.preferences.R; + +import java.util.List; + +/** + * Displays a list of items that have a subtitle and an icon. + */ +public class SimpleIconListAdapter extends ArrayAdapter { + private final Context context; + private final List listItems; + + public SimpleIconListAdapter(Context context, List listItems) { + super(context, R.layout.simple_icon_list_item, listItems); + this.context = context; + this.listItems = listItems; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + view = View.inflate(context, R.layout.simple_icon_list_item, null); + } + + ListItem item = listItems.get(position); + ((TextView) view.findViewById(R.id.title)).setText(item.title); + ((TextView) view.findViewById(R.id.subtitle)).setText(item.subtitle); + Glide.with(context) + .load(item.imageUrl) + .apply(new RequestOptions() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .fitCenter() + .dontAnimate()) + .into(((ImageView) view.findViewById(R.id.icon))); + return view; + } + + public static class ListItem { + public final String title; + public final String subtitle; + public final String imageUrl; + + public ListItem(String title, String subtitle, String imageUrl) { + this.title = title; + this.subtitle = subtitle; + this.imageUrl = imageUrl; + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java new file mode 100644 index 000000000..7e9860036 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.ui.preferences.screen.about; + +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class SpecialThanksFragment extends ListFragment { + private Disposable translatorsLoader; + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + getListView().setDivider(null); + getListView().setSelector(android.R.color.transparent); + + translatorsLoader = Single.create((SingleOnSubscribe>) emitter -> { + ArrayList translators = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + getContext().getAssets().open("special_thanks.csv"), "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + String[] info = line.split(";"); + translators.add(new SimpleIconListAdapter.ListItem(info[0], info[1], info[2])); + } + emitter.onSuccess(translators); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + translators -> setListAdapter(new SimpleIconListAdapter<>(getContext(), translators)), + error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show() + ); + + } + + @Override + public void onStop() { + super.onStop(); + if (translatorsLoader != null) { + translatorsLoader.dispose(); + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java new file mode 100644 index 000000000..3d2079fce --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.ui.preferences.screen.about; + +import android.os.Bundle; +import android.view.View; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.ListFragment; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; + +public class TranslatorsFragment extends ListFragment { + private Disposable translatorsLoader; + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + getListView().setDivider(null); + getListView().setSelector(android.R.color.transparent); + + translatorsLoader = Single.create((SingleOnSubscribe>) emitter -> { + ArrayList translators = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + getContext().getAssets().open("translators.csv"), "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + String[] info = line.split(";"); + translators.add(new SimpleIconListAdapter.ListItem(info[0], info[1], null)); + } + emitter.onSuccess(translators); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + translators -> setListAdapter(new SimpleIconListAdapter<>(getContext(), translators)), + error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show() + ); + + } + + @Override + public void onStop() { + super.onStop(); + if (translatorsLoader != null) { + translatorsLoader.dispose(); + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java new file mode 100644 index 000000000..b43866cc0 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.ui.preferences.screen.downloads; + +import android.content.Context; + +import android.view.View; +import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.ui.preferences.R; + +public class ChooseDataFolderDialog { + + public static void showDialog(final Context context, Consumer handlerFunc) { + + View content = View.inflate(context, R.layout.choose_data_folder_dialog, null); + AlertDialog dialog = new MaterialAlertDialogBuilder(context) + .setView(content) + .setTitle(R.string.choose_data_directory) + .setMessage(R.string.choose_data_directory_message) + .setNegativeButton(R.string.cancel_label, null) + .create(); + ((RecyclerView) content.findViewById(R.id.recyclerView)).setLayoutManager(new LinearLayoutManager(context)); + + DataFolderAdapter adapter = new DataFolderAdapter(context, path -> { + dialog.dismiss(); + handlerFunc.accept(path); + }); + ((RecyclerView) content.findViewById(R.id.recyclerView)).setAdapter(adapter); + + if (adapter.getItemCount() != 0) { + dialog.show(); + } + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java new file mode 100644 index 000000000..bd6a75503 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java @@ -0,0 +1,139 @@ +package de.danoeh.antennapod.ui.preferences.screen.downloads; + +import android.content.Context; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.storage.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.ui.preferences.R; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class DataFolderAdapter extends RecyclerView.Adapter { + private final Consumer selectionHandler; + private final String currentPath; + private final List entries; + private final String freeSpaceString; + + public DataFolderAdapter(Context context, @NonNull Consumer selectionHandler) { + this.entries = getStorageEntries(context); + this.currentPath = getCurrentPath(); + this.selectionHandler = selectionHandler; + this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false); + return new ViewHolder(entryView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + StoragePath storagePath = entries.get(position); + Context context = holder.root.getContext(); + String freeSpace = Formatter.formatShortFileSize(context, storagePath.getAvailableSpace()); + String totalSpace = Formatter.formatShortFileSize(context, storagePath.getTotalSpace()); + + holder.path.setText(storagePath.getShortPath()); + holder.size.setText(String.format(freeSpaceString, freeSpace, totalSpace)); + holder.progressBar.setProgress(storagePath.getUsagePercentage()); + View.OnClickListener selectListener = v -> selectionHandler.accept(storagePath.getFullPath()); + holder.root.setOnClickListener(selectListener); + holder.radioButton.setOnClickListener(selectListener); + + if (storagePath.getFullPath().equals(currentPath)) { + holder.radioButton.toggle(); + } + } + + @Override + public int getItemCount() { + return entries.size(); + } + + private String getCurrentPath() { + File dataFolder = UserPreferences.getDataFolder(null); + if (dataFolder != null) { + return dataFolder.getAbsolutePath(); + } + return null; + } + + private List getStorageEntries(Context context) { + File[] mediaDirs = context.getExternalFilesDirs(null); + final List entries = new ArrayList<>(mediaDirs.length); + for (File dir : mediaDirs) { + if (!isWritable(dir)) { + continue; + } + entries.add(new StoragePath(dir.getAbsolutePath())); + } + if (entries.isEmpty() && isWritable(context.getFilesDir())) { + entries.add(new StoragePath(context.getFilesDir().getAbsolutePath())); + } + return entries; + } + + private boolean isWritable(File dir) { + return dir != null && dir.exists() && dir.canRead() && dir.canWrite(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final View root; + private final TextView path; + private final TextView size; + private final RadioButton radioButton; + private final ProgressBar progressBar; + + ViewHolder(View itemView) { + super(itemView); + root = itemView.findViewById(R.id.root); + path = itemView.findViewById(R.id.path); + size = itemView.findViewById(R.id.size); + radioButton = itemView.findViewById(R.id.radio_button); + progressBar = itemView.findViewById(R.id.used_space); + } + } + + static class StoragePath { + private final String path; + + StoragePath(String path) { + this.path = path; + } + + String getShortPath() { + int prefixIndex = path.indexOf("Android"); + return (prefixIndex > 0) ? path.substring(0, prefixIndex) : path; + } + + String getFullPath() { + return this.path; + } + + long getAvailableSpace() { + return StorageUtils.getFreeSpaceAvailable(path); + } + + long getTotalSpace() { + return StorageUtils.getTotalSpaceAvailable(path); + } + + int getUsagePercentage() { + return 100 - (int) (100 * getAvailableSpace() / (float) getTotalSpace()); + } + } +} \ No newline at end of file diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java new file mode 100644 index 000000000..a91afe78d --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.ui.preferences.screen.synchronization; + +import android.content.Context; +import android.text.method.HideReturnsTransformationMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.LayoutInflater; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.ui.preferences.R; +import de.danoeh.antennapod.ui.preferences.databinding.AuthenticationDialogBinding; + +/** + * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. + */ +public abstract class AuthenticationDialog extends MaterialAlertDialogBuilder { + boolean passwordHidden = true; + + public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, + String usernameInitialValue, String passwordInitialValue) { + super(context); + setTitle(titleRes); + AuthenticationDialogBinding viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context)); + setView(viewBinding.getRoot()); + + viewBinding.usernameEditText.setEnabled(enableUsernameField); + if (usernameInitialValue != null) { + viewBinding.usernameEditText.setText(usernameInitialValue); + } + if (passwordInitialValue != null) { + viewBinding.passwordEditText.setText(passwordInitialValue); + } + viewBinding.showPasswordButton.setOnClickListener(v -> { + if (passwordHidden) { + viewBinding.passwordEditText.setTransformationMethod(HideReturnsTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(1.0f); + } else { + viewBinding.passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + viewBinding.showPasswordButton.setAlpha(0.6f); + } + passwordHidden = !passwordHidden; + }); + + setOnCancelListener(dialog -> onCancelled()); + setNegativeButton(R.string.cancel_label, (dialog, which) -> onCancelled()); + setPositiveButton(R.string.confirm_label, (dialog, which) + -> onConfirmed(viewBinding.usernameEditText.getText().toString(), + viewBinding.passwordEditText.getText().toString())); + } + + protected void onCancelled() { + + } + + protected abstract void onConfirmed(String username, String password); +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java new file mode 100644 index 000000000..d28355dad --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java @@ -0,0 +1,279 @@ +package de.danoeh.antennapod.ui.preferences.screen.synchronization; + +import android.app.Dialog; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.ViewFlipper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.fragment.app.DialogFragment; +import com.google.android.material.button.MaterialButton; +import de.danoeh.antennapod.net.common.AntennapodHttpClient; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.SynchronizationCredentials; +import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData; +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; +import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.ui.preferences.R; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Guides the user through the authentication process. + */ +public class GpodderAuthenticationFragment extends DialogFragment { + public static final String TAG = "GpodnetAuthActivity"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_HOSTNAME = 0; + private static final int STEP_LOGIN = 1; + private static final int STEP_DEVICE = 2; + private static final int STEP_FINISH = 3; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + private List devices; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(getContext()); + dialog.setTitle(R.string.gpodnetauth_login_butLabel); + dialog.setNegativeButton(R.string.cancel_label, null); + dialog.setCancelable(false); + this.setCancelable(false); + + View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null); + viewFlipper = root.findViewById(R.id.viewflipper); + advance(); + dialog.setView(root); + + return dialog.create(); + } + + private void setupHostView(View view) { + final Button selectHost = view.findViewById(R.id.chooseHostButton); + final EditText serverUrlText = view.findViewById(R.id.serverUrlText); + selectHost.setOnClickListener(v -> { + if (serverUrlText.getText().length() == 0) { + return; + } + SynchronizationCredentials.clear(getContext()); + SynchronizationCredentials.setHosturl(serverUrlText.getText().toString()); + service = new GpodnetService(AntennapodHttpClient.getHttpClient(), + SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(), + SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword()); + getDialog().setTitle(SynchronizationCredentials.getHosturl()); + advance(); + }); + } + + private void setupLoginView(View view) { + final EditText username = view.findViewById(R.id.etxtUsername); + final EditText password = view.findViewById(R.id.etxtPassword); + final Button login = view.findViewById(R.id.butLogin); + final TextView txtvError = view.findViewById(R.id.credentialsError); + final ProgressBar progressBar = view.findViewById(R.id.progBarLogin); + final TextView createAccountWarning = view.findViewById(R.id.createAccountWarning); + + if (SynchronizationCredentials.getHosturl().startsWith("http://")) { + createAccountWarning.setVisibility(View.VISIBLE); + } + password.setOnEditorActionListener((v, actionID, event) -> + actionID == EditorInfo.IME_ACTION_GO && login.performClick()); + + login.setOnClickListener(v -> { + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (usernameHasUnwantedChars(usernameStr)) { + txtvError.setText(R.string.gpodnetsync_username_characters_error); + txtvError.setVisibility(View.VISIBLE); + return; + } + + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + InputMethodManager inputManager = (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + + Completable.fromAction(() -> { + service.setCredentials(usernameStr, passwordStr); + service.login(); + devices = service.getDevices(); + GpodderAuthenticationFragment.this.username = usernameStr; + GpodderAuthenticationFragment.this.password = passwordStr; + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(() -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + advance(); + }, error -> { + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getCause().getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + + }); + } + + private void setupDeviceView(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer); + deviceName.setText(generateDeviceName()); + + MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton); + createDeviceButton.setOnClickListener(v -> createDevice(view)); + + for (GpodnetDevice device : devices) { + View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null); + Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton); + selectDeviceButton.setOnClickListener(v -> { + selectedDevice = device; + advance(); + }); + selectDeviceButton.setText(device.getCaption()); + devicesContainer.addView(row); + } + } + + private void createDevice(View view) { + final EditText deviceName = view.findViewById(R.id.deviceName); + final TextView txtvError = view.findViewById(R.id.deviceSelectError); + final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice); + + String deviceNameStr = deviceName.getText().toString(); + if (isDeviceInList(deviceNameStr)) { + return; + } + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + deviceName.setEnabled(false); + + Observable.fromCallable(() -> { + String deviceId = generateDeviceId(deviceNameStr); + service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(device -> { + progBarCreateDevice.setVisibility(View.GONE); + selectedDevice = device; + advance(); + }, error -> { + deviceName.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + }); + } + + private String generateDeviceName() { + String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL); + String name = baseName; + int num = 1; + while (isDeviceInList(name)) { + name = baseName + " (" + num + ")"; + num++; + } + return name; + } + + private String generateDeviceId(String name) { + // devices names must be of a certain form: + // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices + return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US); + } + + private boolean isDeviceInList(String name) { + if (devices == null) { + return false; + } + String id = generateDeviceId(name); + for (GpodnetDevice device : devices) { + if (device.getId().equals(id) || device.getCaption().equals(name)) { + return true; + } + } + return false; + } + + private void setupFinishView(View view) { + final Button sync = view.findViewById(R.id.butSyncNow); + + sync.setOnClickListener(v -> { + dismiss(); + SyncService.sync(getContext()); + }); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + View view = viewFlipper.getChildAt(currentStep + 1); + if (currentStep == STEP_DEFAULT) { + setupHostView(view); + } else if (currentStep == STEP_HOSTNAME) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET); + SynchronizationCredentials.setUsername(username); + SynchronizationCredentials.setPassword(password); + SynchronizationCredentials.setDeviceID(selectedDevice.getId()); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + dismiss(); + } + } + + private boolean usernameHasUnwantedChars(String username) { + Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); + Matcher containsUnwantedChars = special.matcher(username); + return containsUnwantedChars.find(); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java new file mode 100644 index 000000000..b73ee2453 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.ui.preferences.screen.synchronization; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.fragment.app.DialogFragment; +import de.danoeh.antennapod.net.common.AntennapodHttpClient; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.SynchronizationCredentials; +import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData; +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.net.sync.nextcloud.NextcloudLoginFlow; +import de.danoeh.antennapod.ui.preferences.R; +import de.danoeh.antennapod.ui.preferences.databinding.NextcloudAuthDialogBinding; + +/** + * Guides the user through the authentication process. + */ +public class NextcloudAuthenticationFragment extends DialogFragment + implements NextcloudLoginFlow.AuthenticationCallback { + public static final String TAG = "NextcloudAuthenticationFragment"; + private static final String EXTRA_LOGIN_FLOW = "LoginFlow"; + private NextcloudAuthDialogBinding viewBinding; + private NextcloudLoginFlow nextcloudLoginFlow; + private boolean shouldDismiss = false; + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(getContext()); + dialog.setTitle(R.string.gpodnetauth_login_butLabel); + dialog.setNegativeButton(R.string.cancel_label, null); + dialog.setCancelable(false); + this.setCancelable(false); + + viewBinding = NextcloudAuthDialogBinding.inflate(getLayoutInflater()); + dialog.setView(viewBinding.getRoot()); + + viewBinding.chooseHostButton.setOnClickListener(v -> { + nextcloudLoginFlow = new NextcloudLoginFlow(AntennapodHttpClient.getHttpClient(), + viewBinding.serverUrlText.getText().toString(), getContext(), this); + startLoginFlow(); + }); + if (savedInstanceState != null && savedInstanceState.getStringArrayList(EXTRA_LOGIN_FLOW) != null) { + nextcloudLoginFlow = NextcloudLoginFlow.fromInstanceState(AntennapodHttpClient.getHttpClient(), + getContext(), this, savedInstanceState.getStringArrayList(EXTRA_LOGIN_FLOW)); + startLoginFlow(); + } + return dialog.create(); + } + + private void startLoginFlow() { + viewBinding.errorText.setVisibility(View.GONE); + viewBinding.chooseHostButton.setVisibility(View.GONE); + viewBinding.loginProgressContainer.setVisibility(View.VISIBLE); + viewBinding.serverUrlText.setEnabled(false); + nextcloudLoginFlow.start(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (nextcloudLoginFlow != null) { + outState.putStringArrayList(EXTRA_LOGIN_FLOW, nextcloudLoginFlow.saveInstanceState()); + } + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + if (nextcloudLoginFlow != null) { + nextcloudLoginFlow.cancel(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (shouldDismiss) { + dismiss(); + } + } + + @Override + public void onNextcloudAuthenticated(String server, String username, String password) { + SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.NEXTCLOUD_GPODDER); + SynchronizationCredentials.clear(getContext()); + SynchronizationCredentials.setPassword(password); + SynchronizationCredentials.setHosturl(server); + SynchronizationCredentials.setUsername(username); + SyncService.fullSync(getContext()); + if (isResumed()) { + dismiss(); + } else { + shouldDismiss = true; + } + } + + @Override + public void onNextcloudAuthError(String errorMessage) { + viewBinding.loginProgressContainer.setVisibility(View.GONE); + viewBinding.errorText.setVisibility(View.VISIBLE); + viewBinding.errorText.setText(errorMessage); + viewBinding.chooseHostButton.setVisibility(View.VISIBLE); + viewBinding.serverUrlText.setEnabled(true); + } +} diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java new file mode 100644 index 000000000..3c6461272 --- /dev/null +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java @@ -0,0 +1,220 @@ +package de.danoeh.antennapod.ui.preferences.screen.synchronization; + +import android.app.Activity; +import android.os.Bundle; +import android.text.Spanned; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.core.text.HtmlCompat; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.google.android.material.snackbar.Snackbar; + +import de.danoeh.antennapod.ui.preferences.R; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import de.danoeh.antennapod.event.SyncServiceEvent; +import de.danoeh.antennapod.core.sync.SynchronizationCredentials; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData; +import de.danoeh.antennapod.core.sync.SynchronizationSettings; + +public class SynchronizationPreferencesFragment extends PreferenceFragmentCompat { + private static final String PREFERENCE_SYNCHRONIZATION_DESCRIPTION = "preference_synchronization_description"; + private static final String PREFERENCE_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; + private static final String PREFERENCE_SYNC = "pref_synchronization_sync"; + private static final String PREFERENCE_FORCE_FULL_SYNC = "pref_synchronization_force_full_sync"; + private static final String PREFERENCE_LOGOUT = "pref_synchronization_logout"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_synchronization); + setupScreen(); + updateScreen(); + } + + @Override + public void onStart() { + super.onStart(); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.synchronization_pref); + updateScreen(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(""); + } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + public void syncStatusChanged(SyncServiceEvent event) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + updateScreen(); + if (event.getMessageResId() == R.string.sync_status_error + || event.getMessageResId() == R.string.sync_status_success) { + updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful(), + SynchronizationSettings.getLastSyncAttempt()); + } else { + ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(event.getMessageResId()); + } + } + + private void setupScreen() { + final Activity activity = getActivity(); + findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION) + .setOnPreferenceClickListener(preference -> { + AuthenticationDialog dialog = new AuthenticationDialog(activity, + R.string.pref_gpodnet_setlogin_information_title, + false, SynchronizationCredentials.getUsername(), null) { + @Override + protected void onConfirmed(String username, String password) { + SynchronizationCredentials.setPassword(password); + } + }; + dialog.show(); + return true; + }); + findPreference(PREFERENCE_SYNC).setOnPreferenceClickListener(preference -> { + SyncService.syncImmediately(getActivity().getApplicationContext()); + return true; + }); + findPreference(PREFERENCE_FORCE_FULL_SYNC).setOnPreferenceClickListener(preference -> { + SyncService.fullSync(getContext()); + return true; + }); + findPreference(PREFERENCE_LOGOUT).setOnPreferenceClickListener(preference -> { + SynchronizationCredentials.clear(getContext()); + Snackbar.make(getView(), R.string.pref_synchronization_logout_toast, Snackbar.LENGTH_LONG).show(); + SynchronizationSettings.setSelectedSyncProvider(null); + updateScreen(); + return true; + }); + } + + private void updateScreen() { + final boolean loggedIn = SynchronizationSettings.isProviderConnected(); + Preference preferenceHeader = findPreference(PREFERENCE_SYNCHRONIZATION_DESCRIPTION); + if (loggedIn) { + SynchronizationProviderViewData selectedProvider = + SynchronizationProviderViewData.fromIdentifier(getSelectedSyncProviderKey()); + preferenceHeader.setTitle(""); + preferenceHeader.setSummary(selectedProvider.getSummaryResource()); + preferenceHeader.setIcon(selectedProvider.getIconResource()); + preferenceHeader.setOnPreferenceClickListener(null); + } else { + preferenceHeader.setTitle(R.string.synchronization_choose_title); + preferenceHeader.setSummary(R.string.synchronization_summary_unchoosen); + preferenceHeader.setIcon(R.drawable.ic_cloud); + preferenceHeader.setOnPreferenceClickListener((preference) -> { + chooseProviderAndLogin(); + return true; + }); + } + + Preference gpodnetSetLoginPreference = findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION); + gpodnetSetLoginPreference.setVisible(isProviderSelected(SynchronizationProviderViewData.GPODDER_NET)); + gpodnetSetLoginPreference.setEnabled(loggedIn); + findPreference(PREFERENCE_SYNC).setEnabled(loggedIn); + findPreference(PREFERENCE_FORCE_FULL_SYNC).setEnabled(loggedIn); + findPreference(PREFERENCE_LOGOUT).setEnabled(loggedIn); + if (loggedIn) { + String summary = getString(R.string.synchronization_login_status, + SynchronizationCredentials.getUsername(), SynchronizationCredentials.getHosturl()); + Spanned formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY); + findPreference(PREFERENCE_LOGOUT).setSummary(formattedSummary); + updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful(), + SynchronizationSettings.getLastSyncAttempt()); + } else { + findPreference(PREFERENCE_LOGOUT).setSummary(null); + ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(null); + } + } + + private void chooseProviderAndLogin() { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setTitle(R.string.dialog_choose_sync_service_title); + + SynchronizationProviderViewData[] providers = SynchronizationProviderViewData.values(); + ListAdapter adapter = new ArrayAdapter( + getContext(), R.layout.alertdialog_sync_provider_chooser, providers) { + + ViewHolder holder; + + class ViewHolder { + ImageView icon; + TextView title; + } + + public View getView(int position, View convertView, ViewGroup parent) { + final LayoutInflater inflater = LayoutInflater.from(getContext()); + if (convertView == null) { + convertView = inflater.inflate( + R.layout.alertdialog_sync_provider_chooser, null); + + holder = new ViewHolder(); + holder.icon = (ImageView) convertView.findViewById(R.id.icon); + holder.title = (TextView) convertView.findViewById(R.id.title); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + SynchronizationProviderViewData synchronizationProviderViewData = getItem(position); + holder.title.setText(synchronizationProviderViewData.getSummaryResource()); + holder.icon.setImageResource(synchronizationProviderViewData.getIconResource()); + return convertView; + } + }; + + builder.setAdapter(adapter, (dialog, which) -> { + switch (providers[which]) { + case GPODDER_NET: + new GpodderAuthenticationFragment() + .show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG); + break; + case NEXTCLOUD_GPODDER: + new NextcloudAuthenticationFragment() + .show(getChildFragmentManager(), NextcloudAuthenticationFragment.TAG); + break; + default: + break; + } + updateScreen(); + }); + + builder.show(); + } + + private boolean isProviderSelected(@NonNull SynchronizationProviderViewData provider) { + String selectedSyncProviderKey = getSelectedSyncProviderKey(); + return provider.getIdentifier().equals(selectedSyncProviderKey); + } + + private String getSelectedSyncProviderKey() { + return SynchronizationSettings.getSelectedSyncProviderKey(); + } + + private void updateLastSyncReport(boolean successful, long lastTime) { + String status = String.format("%1$s (%2$s)", getString(successful + ? R.string.gpodnetsync_pref_report_successful : R.string.gpodnetsync_pref_report_failed), + DateUtils.getRelativeDateTimeString(getContext(), + lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME)); + ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(status); + } +} diff --git a/ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png new file mode 100644 index 000000000..b4e1e0376 Binary files /dev/null and b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png differ diff --git a/ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png new file mode 100644 index 000000000..39ef47b4f Binary files /dev/null and b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png differ diff --git a/ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png new file mode 100644 index 000000000..cc6403a98 Binary files /dev/null and b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png differ diff --git a/ui/preferences/src/main/res/layout/about_teaser.xml b/ui/preferences/src/main/res/layout/about_teaser.xml new file mode 100644 index 000000000..4e7f0454f --- /dev/null +++ b/ui/preferences/src/main/res/layout/about_teaser.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml b/ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml new file mode 100644 index 000000000..9b4d62804 --- /dev/null +++ b/ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/ui/preferences/src/main/res/layout/authentication_dialog.xml b/ui/preferences/src/main/res/layout/authentication_dialog.xml new file mode 100644 index 000000000..0d54420d4 --- /dev/null +++ b/ui/preferences/src/main/res/layout/authentication_dialog.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml b/ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml new file mode 100644 index 000000000..bac14a108 --- /dev/null +++ b/ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml b/ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml new file mode 100644 index 000000000..addc63f4d --- /dev/null +++ b/ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + diff --git a/ui/preferences/src/main/res/layout/dialog_switch_preference.xml b/ui/preferences/src/main/res/layout/dialog_switch_preference.xml new file mode 100644 index 000000000..45fe21a90 --- /dev/null +++ b/ui/preferences/src/main/res/layout/dialog_switch_preference.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml b/ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml new file mode 100644 index 000000000..a5b8c594d --- /dev/null +++ b/ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + +