diff --git a/.gitignore b/.gitignore index aa724b7..38841db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,211 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/androidstudio,kotlin,android,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,kotlin,android,gradle + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ *.iml +.idea/ + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Kotlin ### +# Compiled class file +*.class + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Gradle ### .gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +bin/ +gen/ +out/ + +# Gradle files + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later .externalNativeBuild -.cxx -local.properties + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/androidstudio,kotlin,android,gradle \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 034a122..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Twitimer \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 26ec138..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index f861765..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 625b371..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index e34606c..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index ca38e7f..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b5c8082..d6c1e99 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.mouredev.twitimer" minSdkVersion 23 targetSdkVersion 30 - versionCode 15 - versionName "1.2.3" + versionCode 16 + versionName "1.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/mouredev/twitimer/model/domain/DatabaseUser.kt b/app/src/main/java/com/mouredev/twitimer/model/domain/DatabaseUser.kt index c98f734..ea5d40a 100644 --- a/app/src/main/java/com/mouredev/twitimer/model/domain/DatabaseUser.kt +++ b/app/src/main/java/com/mouredev/twitimer/model/domain/DatabaseUser.kt @@ -56,6 +56,8 @@ data class DatabaseUserSchedule( } data class DatabaseUserSettings( + + var onHolidays: Int? = null, val discord: String? = null, val youtube: String? = null, val twitter: String? = null, @@ -64,7 +66,7 @@ data class DatabaseUserSettings( ) { fun toUserSettings(): UserSettings { - return UserSettings(discord, youtube, twitter, instagram, tiktok) + return UserSettings(onHolidays == 1, discord, youtube, twitter, instagram, tiktok) } } \ No newline at end of file diff --git a/app/src/main/java/com/mouredev/twitimer/model/domain/User.kt b/app/src/main/java/com/mouredev/twitimer/model/domain/User.kt index 2443760..111b0f0 100644 --- a/app/src/main/java/com/mouredev/twitimer/model/domain/User.kt +++ b/app/src/main/java/com/mouredev/twitimer/model/domain/User.kt @@ -76,7 +76,7 @@ data class User( return scheduleJSON } - fun settingsToJSON(): MutableMap { + fun settingsToJSON(): MutableMap { return settings?.toJSON() ?: mutableMapOf() } @@ -280,6 +280,7 @@ enum class WeekdayType(val index: Int) { } data class UserSettings( + var onHolidays: Boolean? = null, var discord: String? = null, var youtube: String? = null, var twitter: String? = null, @@ -287,9 +288,10 @@ data class UserSettings( var tiktok: String? = null ) { - fun toJSON(): MutableMap { + fun toJSON(): MutableMap { return mutableMapOf( + DatabaseField.ON_HOLIDAYS.key to (if (onHolidays == true) 1 else 0), DatabaseField.DISCORD.key to (discord ?: ""), DatabaseField.YOUTUBE.key to (youtube ?: ""), DatabaseField.TWITTER.key to (twitter ?: ""), diff --git a/app/src/main/java/com/mouredev/twitimer/model/session/Session.kt b/app/src/main/java/com/mouredev/twitimer/model/session/Session.kt index 7c962d2..50627b7 100644 --- a/app/src/main/java/com/mouredev/twitimer/model/session/Session.kt +++ b/app/src/main/java/com/mouredev/twitimer/model/session/Session.kt @@ -125,6 +125,17 @@ class Session { } } + fun delete(context: Context, success: () -> Unit) { + + token?.accessToken?.let { accessToken -> + TwitchService.revoke(accessToken, { + remove(context, success) + }, { + remove(context, success) + }) + } + } + fun save(context: Context, schedule: MutableList) { val savedSchedule = savedSchedule(context) @@ -178,6 +189,9 @@ class Session { user?.followedUsers = mutableListOf() } user?.followedUsers?.add(login) + if (streamers == null) { + streamers = mutableListOf() + } streamers?.add(followedUser) setupNotification(true, login) @@ -268,22 +282,25 @@ class Session { streamers.forEach { streamer -> - var nextSchedule: UserSchedule? = null + if (streamer.settings?.onHolidays == false) { + + var nextSchedule: UserSchedule? = null - streamer.schedule?.forEach { schedule -> + streamer.schedule?.forEach { schedule -> - if (schedule.enable) { + if (schedule.enable) { - val weekDate = schedule.weekDate() + val weekDate = schedule.weekDate() - if ((nextSchedule == null && weekDate > currentDate) || (weekDate > currentDate && weekDate < nextSchedule!!.date)) { - nextSchedule = schedule + if ((nextSchedule == null && weekDate > currentDate) || (weekDate > currentDate && weekDate < nextSchedule!!.date)) { + nextSchedule = schedule + } } } - } - nextSchedule?.let { - sortedStreamings.add(SortedStreaming(streamer, it)) + nextSchedule?.let { + sortedStreamings.add(SortedStreaming(streamer, it)) + } } } @@ -507,6 +524,17 @@ class Session { } } + private fun remove(context: Context, completion: (() -> Unit)) { + + user?.let { user -> + FirebaseRDBService.delete(user, { + clear(context, completion) + }, { + clear(context, completion) + }) + } + } + private fun clear(context: Context, completion: (() -> Unit)) { user?.followedUsers?.forEach { user -> diff --git a/app/src/main/java/com/mouredev/twitimer/provider/services/firebase/FirebaseRDBService.kt b/app/src/main/java/com/mouredev/twitimer/provider/services/firebase/FirebaseRDBService.kt index 51c7470..57e4efb 100644 --- a/app/src/main/java/com/mouredev/twitimer/provider/services/firebase/FirebaseRDBService.kt +++ b/app/src/main/java/com/mouredev/twitimer/provider/services/firebase/FirebaseRDBService.kt @@ -28,7 +28,7 @@ enum class DatabaseField(val key: String) { FOLLOWED_USERS("followedUsers"), // Settings - SETTINGS("settings"), DISCORD("discord"), YOUTUBE("youtube"), TWITTER("twitter"), INSTAGRAM("instagram"), TIKTOK("tiktok") + SETTINGS("settings"), ON_HOLIDAYS("onHolidays"), DISCORD("discord"), YOUTUBE("youtube"), TWITTER("twitter"), INSTAGRAM("instagram"), TIKTOK("tiktok") } @@ -109,7 +109,7 @@ object FirebaseRDBService { }.addOnFailureListener { failure() } - }?: kotlin.run { + } ?: run { failure() } } @@ -148,6 +148,23 @@ object FirebaseRDBService { } } + + + fun delete(user: User, success: () -> Unit, failure: () -> Unit) { + + user.login?.let { login -> + (if (user.streamer == true) streamersRef else usersRef).child(login).removeValue { error, _ -> + if (error != null) { + failure() + } else { + success() + } + } + } ?: run { + failure() + } + } + // MARK: Private private fun checkStreamersSearch(ids: List, finishedRequests: Int, streamers: List?, completion: (streamers: List?) -> Unit) { diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserFragment.kt b/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserFragment.kt index b9d9fc7..081179b 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserFragment.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserFragment.kt @@ -37,6 +37,8 @@ class UserFragment : Fragment() { private lateinit var viewModel: UserViewModel private var infoFragment: InfoFragment? = null + private var schedules: MutableList? = null + // Initialization override fun onCreateView( @@ -75,7 +77,7 @@ class UserFragment : Fragment() { super.onResume() // Setup - setupHeader() + setupContent() } // Private @@ -89,15 +91,7 @@ class UserFragment : Fragment() { private fun setup() { - val transaction = activity?.supportFragmentManager?.beginTransaction() - - val schedules = viewModel.getFilterSchedule() - infoFragment = if (schedules.isNullOrEmpty()) InfoRouter().fragment(InfoViewType.SCHEDULE) else InfoRouter().fragment(InfoViewType.STREAMER) - infoFragment?.let { - transaction?.replace(R.id.frameLayoutInfo, it) - } - transaction?.disallowAddToBackStack() - transaction?.commit() + schedules = viewModel.getFilterSchedule() context?.let { context -> @@ -116,9 +110,12 @@ class UserFragment : Fragment() { binding.switchStreamer.setOnCheckedChangeListener { _, isChecked -> if (viewModel.isStreamer != isChecked) { viewModel.save(context, isChecked) - setupBody(schedules) - setupHeader() - setupButtons() + setupContent() + if (isChecked) { + UIUtil.showAlert(context, getString(viewModel.syncInfoAlertTitleText), getString(viewModel.syncInfoAlertBodyText), getString(viewModel.okText), { + checkShowScheduleAlert(context) + }) + } } } @@ -139,20 +136,11 @@ class UserFragment : Fragment() { // Recycler view binding.recyclerViewSchedule.layoutManager = LinearLayoutManager(context) - binding.recyclerViewSchedule.adapter = ScheduleRecyclerViewAdapter(context, schedules, viewModel.readOnly) { schedule -> + binding.recyclerViewSchedule.adapter = ScheduleRecyclerViewAdapter(context, schedules!!, viewModel.readOnly) { schedule -> checkEnableSave(context, schedule) } } - - // Sync - if (viewModel.isStreamer && !viewModel.readOnly && !viewModel.firstSync(context)) { - PreferencesProvider.set(context, PreferencesKey.FIRST_SYNC, true) - syncSchedule(context) - } } - - setupBody(schedules) - setupButtons() } private fun setupHeader() { @@ -165,20 +153,48 @@ class UserFragment : Fragment() { transaction?.commit() } - private fun setupBody(schedule: List?) { + private fun setupContent() { + + setupHeader() + setupInfo() + setupBody() + setupButtons() + } + + private fun setupInfo() { + + val transaction = activity?.supportFragmentManager?.beginTransaction() + + if (!viewModel.isStreamer) { + infoFragment = InfoRouter().fragment(InfoViewType.STREAMER) + } else if (viewModel.onHolidays) { + infoFragment = if (viewModel.readOnly) InfoRouter().fragment(InfoViewType.USER_HOLIDAYS) else InfoRouter().fragment(InfoViewType.HOLIDAY) + } else if (schedules.isNullOrEmpty()) { + infoFragment = InfoRouter().fragment(InfoViewType.SCHEDULE) + } + + infoFragment?.let { + transaction?.replace(R.id.frameLayoutInfo, it) + } + transaction?.disallowAddToBackStack() + transaction?.commit() + } + + private fun setupBody() { binding.imageButtonSync.visibility = View.GONE + binding.textViewSchedule.visibility = View.VISIBLE if (viewModel.isStreamer) { binding.switchStreamer.isChecked = true - if (viewModel.readOnly && schedule.isNullOrEmpty()) { + if (viewModel.onHolidays || (viewModel.readOnly && schedules.isNullOrEmpty())) { infoFragment?.view?.visibility = View.VISIBLE binding.recyclerViewSchedule.visibility = View.GONE } else { infoFragment?.view?.visibility = View.GONE binding.recyclerViewSchedule.visibility = View.VISIBLE } - if (!viewModel.readOnly) { + if (!viewModel.onHolidays && !viewModel.readOnly) { binding.imageButtonSync.visibility = View.VISIBLE } } else { @@ -191,13 +207,17 @@ class UserFragment : Fragment() { binding.textViewStreamer.visibility = View.GONE binding.switchStreamer.visibility = View.GONE } + + if (viewModel.onHolidays || !viewModel.isStreamer) { + binding.textViewSchedule.visibility = View.INVISIBLE + } } private fun setupButtons() { binding.buttonSaveSchedule.visibility = if (viewModel.isStreamer) View.VISIBLE else View.GONE - if (viewModel.readOnly) { + if (viewModel.onHolidays || viewModel.readOnly) { binding.layoutButtons.visibility = View.GONE } } @@ -229,4 +249,13 @@ class UserFragment : Fragment() { binding.buttonSaveSchedule.enable(viewModel.checkEnableSave(context, schedule)) } + private fun checkShowScheduleAlert(context: Context) { + + // Sync + if (viewModel.isStreamer && !viewModel.readOnly && !viewModel.firstSync(context)) { + PreferencesProvider.set(context, PreferencesKey.FIRST_SYNC, true) + syncSchedule(context) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserViewModel.kt b/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserViewModel.kt index 0de269d..38e761a 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserViewModel.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/account/user/UserViewModel.kt @@ -18,6 +18,7 @@ class UserViewModel : ViewModel() { private var user: User? = null // Read only user val readOnly get() = user != null val isStreamer get() = getUser()?.streamer ?: false + val onHolidays get() = getUser()?.settings?.onHolidays ?: false // Localization @@ -27,6 +28,8 @@ class UserViewModel : ViewModel() { val streamerText = R.string.user_streamer val syncAlertTitleText = R.string.user_syncschedule_alert_title val syncAlertBodyText = R.string.user_syncschedule_alert_body + val syncInfoAlertTitleText = R.string.user_scheduleinfo_alert_title + val syncInfoAlertBodyText = R.string.user_scheduleinfo_alert_body val okText = R.string.accept val cancelText = R.string.cancel diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/common/rows/SearchQueryRecyclerViewAdapter.kt b/app/src/main/java/com/mouredev/twitimer/usecases/common/rows/SearchQueryRecyclerViewAdapter.kt index 4212723..8e79f26 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/common/rows/SearchQueryRecyclerViewAdapter.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/common/rows/SearchQueryRecyclerViewAdapter.kt @@ -88,6 +88,7 @@ class SearchQueryRecyclerViewAdapter(val context: Context, var users: List, val // User binding.textViewUser.text = user.displayName ?: "" + binding.imageViewHoliday.visibility = if (user.settings?.onHolidays == true) { View.VISIBLE } else { View.GONE } // Avatar UIUtil.loadAvatar(context, user.profileImageUrl, user.login, binding.imageViewAvatar) @@ -119,7 +120,7 @@ class SearchRecyclerViewAdapter(val context: Context, var users: List, val val binding = viewHolder.binding binding.textViewUser.font(FontSize.BUTTON, FontType.BOLD, ContextCompat.getColor(context, R.color.light)) - binding.textViewUser.maxLines = 1 + binding.textViewUser.maxLines = 2 binding.buttonChannel.background = ContextCompat.getDrawable(context, R.drawable.channel_button_round_dark) binding.textViewChannelTitle.font(FontSize.CAPTION, FontType.LIGHT, ContextCompat.getColor(context, R.color.light)) diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoFragment.kt b/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoFragment.kt index 206d22d..95c542b 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoFragment.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoFragment.kt @@ -151,6 +151,17 @@ class InfoFragment : Fragment() { } InfoViewType.AUTH -> binding.linearLayoutFooter.visibility = View.GONE + + InfoViewType.HOLIDAY, InfoViewType.USER_HOLIDAYS -> { + binding.buttonAction.visibility = View.GONE + binding.textViewFooterFirst.text = getString(viewModel.advice(1)) + context?.let { context -> + viewModel.icon(1)?.let { icon -> + binding.imageViewFooterFirst.setImageDrawable(ContextCompat.getDrawable(context, icon)) + } + binding.linearLayoutFooterSecond.visibility = View.GONE + } + } } } diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoViewModel.kt b/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoViewModel.kt index 2dae7d6..60ce075 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoViewModel.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/common/views/info/InfoViewModel.kt @@ -12,7 +12,7 @@ import java.util.* enum class InfoViewType { - COUNTDOWN, SEARCH, CHANNEL, STREAMER, AUTH, SCHEDULE + COUNTDOWN, SEARCH, CHANNEL, STREAMER, AUTH, SCHEDULE, HOLIDAY, USER_HOLIDAYS } class InfoViewModel : ViewModel() { @@ -33,6 +33,7 @@ class InfoViewModel : ViewModel() { InfoViewType.STREAMER -> R.drawable.radio_microphone InfoViewType.AUTH -> R.drawable.secure_connection InfoViewType.SCHEDULE -> R.drawable.schedule + InfoViewType.HOLIDAY, InfoViewType.USER_HOLIDAYS -> R.drawable.vacation } } @@ -45,6 +46,7 @@ class InfoViewModel : ViewModel() { InfoViewType.STREAMER -> R.string.info_streamer_title InfoViewType.AUTH -> R.string.info_auth_title InfoViewType.SCHEDULE -> R.string.info_schedule_title + InfoViewType.HOLIDAY, InfoViewType.USER_HOLIDAYS -> R.string.info_holiday_title } } @@ -57,6 +59,8 @@ class InfoViewModel : ViewModel() { InfoViewType.STREAMER -> R.string.info_streamer_body InfoViewType.AUTH -> R.string.info_auth_body InfoViewType.SCHEDULE -> R.string.info_schedule_body + InfoViewType.HOLIDAY -> R.string.info_holiday_body + InfoViewType.USER_HOLIDAYS -> R.string.info_userholidays_body } } @@ -68,15 +72,18 @@ class InfoViewModel : ViewModel() { InfoViewType.STREAMER -> R.string.info_streamer_advice_1 InfoViewType.AUTH -> R.string.info_auth_advice_1 InfoViewType.SCHEDULE -> R.string.info_schedule_advice_1 + InfoViewType.HOLIDAY -> R.string.info_holiday_advice_1 + InfoViewType.USER_HOLIDAYS -> R.string.info_userholidays_advice_1 } } fun icon(number: Int): Int? { return when (type) { InfoViewType.SEARCH -> if (number == 1) R.drawable.calendar_add else R.drawable.calendar_remove - InfoViewType.CHANNEL -> R.drawable.megaphone + InfoViewType.CHANNEL, InfoViewType.USER_HOLIDAYS -> R.drawable.megaphone InfoViewType.STREAMER -> R.drawable.calendar InfoViewType.SCHEDULE -> R.drawable.time_clock_circle + InfoViewType.HOLIDAY -> R.drawable.settings InfoViewType.COUNTDOWN, InfoViewType.AUTH -> null } } diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/countdown/CountdownFragment.kt b/app/src/main/java/com/mouredev/twitimer/usecases/countdown/CountdownFragment.kt index 2ec9485..37802e9 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/countdown/CountdownFragment.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/countdown/CountdownFragment.kt @@ -33,6 +33,7 @@ class CountdownFragment : Fragment(), InfoFragmentListener { private lateinit var viewModel: CountdownViewModel private var listener: CountdownFragmentListener? = null private var loaded = false + private var resumed = false // Initialization @@ -63,6 +64,16 @@ class CountdownFragment : Fragment(), InfoFragmentListener { load() } + override fun onResume() { + super.onResume() + if (resumed) { + context?.let { context -> + viewModel.load(context) + } + } + resumed = true + } + // Public fun load() { diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/menu/MenuActivity.kt b/app/src/main/java/com/mouredev/twitimer/usecases/menu/MenuActivity.kt index a9344a3..06971fc 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/menu/MenuActivity.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/menu/MenuActivity.kt @@ -1,11 +1,7 @@ package com.mouredev.twitimer.usecases.menu -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import com.mouredev.twitimer.R import com.mouredev.twitimer.databinding.ActivityMenuBinding diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsActivity.kt b/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsActivity.kt index 2672115..22480c2 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsActivity.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsActivity.kt @@ -60,6 +60,8 @@ class SettingsActivity : AppCompatActivity() { private fun localize() { + binding.textViewHolidayMode.text = getText(viewModel.holidayTitleText) + binding.textViewHolidayModeDetail.text = getText(viewModel.holidayBodyText) binding.textViewSocialMedia.text = getText(viewModel.socialMediaText) binding.editTextDiscord.hint = getText(viewModel.discordPlaceholder) binding.editTextYouTube.hint = getText(viewModel.youtubePlaceholder) @@ -68,6 +70,7 @@ class SettingsActivity : AppCompatActivity() { binding.editTextTikTok.hint = getText(viewModel.tiktokPlaceholder) binding.buttonCloseSession.text = getText(viewModel.closeText) binding.buttonSaveSettings.text = getText(viewModel.saveText) + binding.buttonDeleteAccount.text = getText(viewModel.deleteButtonText) } private fun setup() { @@ -77,6 +80,9 @@ class SettingsActivity : AppCompatActivity() { // Header addClose() + // Holiday mode + setupHolidayMode() + // Social media setupSocialMedia() @@ -85,8 +91,24 @@ class SettingsActivity : AppCompatActivity() { } + private fun setupHolidayMode() { + + // Holiday mode + + binding.textViewHolidayMode.font(FontSize.HEAD, color = ContextCompat.getColor(this, R.color.text)) + binding.textViewHolidayModeDetail.font(FontSize.SUBHEAD, color = ContextCompat.getColor(this, R.color.text)) + + binding.switchHolidayMode.isChecked = viewModel.settings.onHolidays == true + binding.switchHolidayMode.setOnCheckedChangeListener { _, isChecked -> + viewModel.settings.onHolidays = isChecked + checkEnableSave() + } + } + private fun setupSocialMedia() { + // Social media + binding.textViewSocialMedia.font(FontSize.HEAD, color = ContextCompat.getColor(this, R.color.text)) // Discord @@ -254,6 +276,17 @@ class SettingsActivity : AppCompatActivity() { } }) + // Delete account + + binding.textViewDeleteAccount.font(FontSize.HEAD, color = ContextCompat.getColor(this, R.color.text)) + + binding.buttonDeleteAccount.destroy { + UIUtil.showAlert(this, getString(viewModel.deleteButtonText), getString(viewModel.deleteAlertText), getString(viewModel.deleteTitleText), { + hideSoftInput() + viewModel.delete(this) + currentEditText?.clearFocus() + }, getString(viewModel.cancelText), true) + } } private fun setupFooter() { @@ -270,8 +303,13 @@ class SettingsActivity : AppCompatActivity() { binding.buttonSaveSettings.enable(false) binding.buttonSaveSettings.primary { hideSoftInput() - viewModel.save(this) - binding.buttonSaveSettings.enable(false) + if (viewModel.saveHolidays(this)) { + UIUtil.showAlert(this, getString(viewModel.holidayTitleText), getString(viewModel.holidayAlertText), getString(viewModel.okText), { + save() + }, getString(viewModel.cancelText), true) + } else { + save() + } currentEditText?.clearFocus() } } @@ -301,8 +339,12 @@ class SettingsActivity : AppCompatActivity() { } private fun checkEnableSave() { - binding.buttonSaveSettings.enable(viewModel.checkEnableSave(this)) + binding.buttonSaveSettings.enable(viewModel.enableSave(this)) } + private fun save() { + viewModel.save(this) + binding.buttonSaveSettings.enable(false) + } } \ No newline at end of file diff --git a/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsViewModel.kt b/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsViewModel.kt index 08df413..e4f2496 100644 --- a/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/mouredev/twitimer/usecases/settings/SettingsViewModel.kt @@ -19,6 +19,9 @@ class SettingsViewModel : ViewModel() { // Localization + val holidayTitleText = R.string.settings_holiday_title + val holidayBodyText = R.string.settings_holiday_body + val holidayAlertText = R.string.settings_holiday_alert val socialMediaText = R.string.settings_socialmedia val discordPlaceholder = R.string.settings_discord_placeholder val youtubePlaceholder = R.string.settings_youtube_placeholder @@ -28,34 +31,42 @@ class SettingsViewModel : ViewModel() { val closeText = R.string.user_closesession val closeAlertText = R.string.user_closesession_alert_body val saveText = R.string.settings_savesettings + val deleteTitleText = R.string.settings_deleteaccount_title + val deleteButtonText = R.string.settings_deleteaccount_button + val deleteAlertText = R.string.settings_deleteaccount_alert val okText = R.string.accept val cancelText = R.string.cancel // Public - fun checkEnableSave(context: Context): Boolean { - - val savedSettings = Session.instance.savedSettings(context) + fun close(context: SettingsActivity) { - if (savedSettings != settings) { - return true + Session.instance.revoke(context) { + context.onBackPressed() } - return false } fun save(context: Context) { Session.instance.save(context, settings) } - fun close(context: SettingsActivity) { + fun enableSave(context: Context): Boolean { + return Session.instance.savedSettings(context) != settings + } - Session.instance.revoke(context) { - context.onBackPressed() - } + fun saveHolidays(context: Context): Boolean { + return settings.onHolidays == true && (settings.onHolidays != Session.instance.savedSettings(context)?.onHolidays) } fun restoreSaveSettings(context: Context) { Session.instance.user?.settings = Session.instance.savedSettings(context) } + fun delete(context: SettingsActivity) { + + Session.instance.delete(context) { + context.onBackPressed() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/mouredev/twitimer/util/UIUtil.kt b/app/src/main/java/com/mouredev/twitimer/util/UIUtil.kt index 8cd5cc9..6245365 100644 --- a/app/src/main/java/com/mouredev/twitimer/util/UIUtil.kt +++ b/app/src/main/java/com/mouredev/twitimer/util/UIUtil.kt @@ -24,7 +24,7 @@ object UIUtil { // Alert - fun showAlert(context: Context, title: String, message: String, positive: String, positiveAction: (() -> Unit)? = null, negative: String? = null) { + fun showAlert(context: Context, title: String, message: String, positive: String, positiveAction: (() -> Unit)? = null, negative: String? = null, destroy: Boolean = false) { val builder = AlertDialog.Builder(context, R.style.CustomDialogTheme) builder.setTitle(title) @@ -42,7 +42,7 @@ object UIUtil { val dialog: AlertDialog = builder.create() dialog.show() - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(context, R.color.light)) + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(context, if (destroy) R.color.live else R.color.light)) dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(ContextCompat.getColor(context, R.color.dark)) } diff --git a/app/src/main/java/com/mouredev/twitimer/util/extension/ButtonExtension.kt b/app/src/main/java/com/mouredev/twitimer/util/extension/ButtonExtension.kt index e9939f0..46395aa 100644 --- a/app/src/main/java/com/mouredev/twitimer/util/extension/ButtonExtension.kt +++ b/app/src/main/java/com/mouredev/twitimer/util/extension/ButtonExtension.kt @@ -51,6 +51,17 @@ fun AppCompatButton.secondary(listener: View.OnClickListener) { setOnClickListener(listener) } +fun AppCompatButton.destroy(listener: View.OnClickListener) { + + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + setTextSize(TypedValue.COMPLEX_UNIT_SP, FontSize.BUTTON.size.toFloat()) + setTextColor(ContextCompat.getColor(context, R.color.light)) + setTypeface(Typeface.createFromAsset(context.assets, FontType.BOLD.path), Typeface.NORMAL) + background = ContextCompat.getDrawable(context, R.drawable.destroy_button_round) + setOnClickListener(listener) +} + fun Button.picker(color: Int = ContextCompat.getColor(context, R.color.dark)) { setTextSize(TypedValue.COMPLEX_UNIT_SP, FontSize.SUBHEAD.size.toFloat()) diff --git a/app/src/main/res/drawable-hdpi/holiday.png b/app/src/main/res/drawable-hdpi/holiday.png new file mode 100644 index 0000000..24dd120 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/holiday.png differ diff --git a/app/src/main/res/drawable-mdpi/vacation.xml b/app/src/main/res/drawable-mdpi/vacation.xml new file mode 100644 index 0000000..8a3ba5b --- /dev/null +++ b/app/src/main/res/drawable-mdpi/vacation.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/holiday.png b/app/src/main/res/drawable-xhdpi/holiday.png new file mode 100644 index 0000000..693cc42 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/holiday.png differ diff --git a/app/src/main/res/drawable/destroy_button_round.xml b/app/src/main/res/drawable/destroy_button_round.xml new file mode 100644 index 0000000..0372625 --- /dev/null +++ b/app/src/main/res/drawable/destroy_button_round.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/holiday.png b/app/src/main/res/drawable/holiday.png new file mode 100644 index 0000000..c5e1e16 Binary files /dev/null and b/app/src/main/res/drawable/holiday.png differ diff --git a/app/src/main/res/drawable/schedule.xml b/app/src/main/res/drawable/schedule.xml index d025dce..35fe642 100644 --- a/app/src/main/res/drawable/schedule.xml +++ b/app/src/main/res/drawable/schedule.xml @@ -1,6 +1,6 @@ + + + + + + + + + + @@ -186,6 +224,27 @@ + + + + diff --git a/app/src/main/res/layout/info_fragment.xml b/app/src/main/res/layout/info_fragment.xml index f9727fb..d8ee9de 100644 --- a/app/src/main/res/layout/info_fragment.xml +++ b/app/src/main/res/layout/info_fragment.xml @@ -106,6 +106,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="16dp" + android:gravity="center_vertical" android:orientation="horizontal"> + + + + + app:layout_constraintTop_toTopOf="@+id/textViewStreamer" /> Soy streamer Sincronizar horario de Twitch ¿Quieres sincronizar tu calendario recurrente de Twitch? Se sobrescribirá la programación de Twitimer. + Sincronización de horarios + Los horarios guardados desde Twitimer no sobrescribirán los de Twitch para evitar interferir en la plataforma y permitirte gestionarlos de forma más intuitiva y ágil. @@ -88,6 +90,13 @@ El streamer no tiene horarios disponibles. Puede que aún no los haya añadido desde Twitimer (o quizá se está tomando unas vacaciones). Prueba a revisarlos más tarde. Cuando tenga algún horario aparecerá una cuenta atrás en el listado de próximas emisiones. + Modo vacaciones activado + El modo vacaciones oculta tus horarios de transmisión a tus seguidores y notifica a tu audiencia. + Desactiva el modo vacaciones desde los ajustes de tu usuario. + + Este usuario se encuentra de vacaciones. Los horarios se mostrarán en el momento que el streamer regrese. + Si tienes las notificaciones activas, recibirás un aviso. + por ¡Hola! Mi nombre es Brais Moure, soy el creador de Twitimer.\n\nEsta App se ha desarrollado para ayudar a usuarios de Twitch, pero sobre todo pensando en generar contenido formativo para toda la comunidad de programadores y programadoras interesada en el mundo del desarrollo de apps para dispositivos móviles. Ellos han hecho posible el proyecto (ante todo quiero que sea una App gratuita y en constante evolución).\n\nSi te apetece unirte a nuestra comunidad, dejarme algún tipo de sugerencia para mejorar, o apoyar la continuidad del proyecto (unos bits nunca vienen mal para pagar el servidor…), puedes encontrarme en Twitch y resto de redes sociales como @mouredev.\n\n¡Muchas gracias! @@ -97,6 +106,9 @@ + Modo vacaciones + El modo vacaciones oculta tus horarios de transmisión a tus seguidores y notifica a tu audiencia. + ¿Quieres activar el modo vacaciones? Se notificará a tus seguidores. Redes sociales Código de invitación de Discord Usuario de YouTube @@ -104,5 +116,8 @@ Usuario de Instagram Usuario de TikTok Guardar ajustes + Eliminar cuenta + Eliminar mi cuenta de Twitimer + Se eliminará tu cuenta de Twitimer y todos sus datos asociados. Tu cuenta de Twitch no se verá afectada. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3985c9..f6f446b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,8 @@ I\'m a streamer Synchronize Twitch schedule Do you want to sync your recurring Twitch calendar? The Twitimer schedule will be overwritten. + Schedules synchronization + The schedules saved from Twitimer will not overwrite those of Twitch to avoid interfering with the platform and allow you to manage them in a more intuitive and agile way. @@ -88,6 +90,13 @@ The streamer does not have available schedules. He may not have added them Twitimer yet (or he may be on holidays). Try checking them out later. When the streamer has a schedule, a countdown will appear in the list of upcoming broadcasts. + Holiday mode activated + Holiday mode hides your broadcast schedules from your followers and notifies your audience. + Deactivate holiday mode from your user settings. + + This user is on holidays. Schedules will be displayed at the moment that the streamer returns. + If you have active notifications, you will receive a notification. + by @@ -99,6 +108,9 @@ + Holiday mode + Holiday mode hides your broadcast schedules from your followers and notifies your audience. + Do you want to activate the holiday mode? Your followers will be notified. Social media Discord invitation link code YouTube user @@ -106,5 +118,8 @@ Instagram user TikTok user Save settings + Delete account + Delete my Twitimer account + Your Twitimer account and all their associated data will be deleted. Your Twitch account will not be affected. \ No newline at end of file diff --git a/build.gradle b/build.gradle index b36efd2..b3fbc5e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'