diff --git a/app/build.gradle b/app/build.gradle index 6e133f07..270d3ffa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,14 +131,14 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' /** * AndroidX dependencies: */ implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-splashscreen:1.0.0' - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-splashscreen:1.0.1' + implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' @@ -156,8 +156,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1" implementation "androidx.annotation:annotation:1.6.0" implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation "androidx.activity:activity-ktx:1.7.0" - implementation 'androidx.fragment:fragment-ktx:1.5.6' + implementation "androidx.activity:activity-ktx:1.7.1" + implementation 'androidx.fragment:fragment-ktx:1.5.7' implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.media2:media2-widget:1.2.1' implementation 'androidx.media2:media2-player:1.2.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index beb4a316..92a39fe5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,9 @@ android:name=".posts.ReportActivity" android:screenOrientation="sensorPortrait" tools:ignore="LockedOrientationActivity" /> + + when (position){ 1 -> launchActivity(ProfileActivity()) 2 -> launchActivity(SettingsActivity()) 3 -> logOut() + 4 -> launchActivity(StoriesActivity()) } false } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 8cd7a3a8..b3ad94c0 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -195,8 +195,10 @@ class PostCreationViewModel( * and display it. */ val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - cursor.getLong(sizeIndex) + if(sizeIndex >= 0) { + cursor.moveToFirst() + cursor.getLong(sizeIndex) + } else null } ?: 0 } else { uri.toFile().length() diff --git a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt index e56e64fe..01da0f5b 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt @@ -130,7 +130,7 @@ fun parseHTMLText( } -fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean, context: Context) { +fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean) { val now = Date.from(Instant.now()).time try { @@ -140,7 +140,7 @@ fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Bool android.text.format.DateUtils.SECOND_IN_MILLIS, android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString() - textView.text = if(absoluteTime) context.getString(R.string.posted_on).format(date) + textView.text = if(absoluteTime) textView.context.getString(R.string.posted_on).format(date) else formattedDate } catch (e: ParseException) { diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt index 39898014..376ab1c4 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt @@ -139,8 +139,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold setTextViewFromISO8601( status?.created_at!!, binding.postDate, - isActivity, - binding.root.context + isActivity ) binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context) diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt index 7dd5beea..b1469fe3 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt @@ -221,8 +221,7 @@ class NotificationsFragment : CachedFeedFragment() { setTextViewFromISO8601( it, notificationTime, - false, - itemView.context + false ) } diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt new file mode 100644 index 00000000..8a6e90d6 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -0,0 +1,110 @@ +package org.pixeldroid.app.stories + +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import kotlinx.coroutines.launch +import org.pixeldroid.app.databinding.ActivityStoriesBinding +import org.pixeldroid.app.posts.setTextViewFromISO8601 +import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity + + +class StoriesActivity: BaseThemedWithoutBarActivity() { + + private lateinit var binding: ActivityStoriesBinding + + private lateinit var model: StoriesViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityStoriesBinding.inflate(layoutInflater) + setContentView(binding.root) + + val _model: StoriesViewModel by viewModels { + StoriesViewModelFactory(application) + } + model = _model + + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + model.uiState.collect { uiState -> + binding.pause.isSelected = uiState.paused + + uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) } + + uiState.profilePicture?.let { + Glide.with(binding.storyAuthorProfilePicture) + .load(it) + .apply(RequestOptions.circleCropTransform()) + .into(binding.storyAuthorProfilePicture) + } + + binding.storyAuthor.text = uiState.username + + uiState.imageList.getOrNull(uiState.currentImage)?.let { + Glide.with(binding.storyImage) + .load(it) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean = false + + override fun onResourceReady( + resource: Drawable?, + m: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean, + ): Boolean { + model.imageLoaded() + return false + } + }) + .into(binding.storyImage) + } + } + } + } + + model.count.observe(this) { state -> + // Render state in UI + model.uiState.value.durationList.getOrNull(model.uiState.value.currentImage)?.let { + val percent = 100 - ((state/it.toFloat())*100).toInt() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.progressBarStory.setProgress(percent, true) + } else { + binding.progressBarStory.progress = percent + } + } + } + + binding.pause.setOnClickListener { + //Set the button's appearance + it.isSelected = !it.isSelected + if (it.isSelected) { + //Handle selected state change + } else { + //Handle de-select state change + } + } + + binding.storyImage.setOnClickListener { + model.pause() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt new file mode 100644 index 00000000..18382e18 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -0,0 +1,143 @@ +package org.pixeldroid.app.stories + +import android.app.Application +import android.os.CountDownTimer +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.pixeldroid.app.utils.PixelDroidApplication +import org.pixeldroid.app.utils.api.objects.StoryCarousel +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import java.time.Instant +import javax.inject.Inject + +data class StoriesUiState( + val profilePicture: String? = null, + val username: String? = null, + val age: Instant? = null, + val currentImage: Int = 0, + val imageList: List = emptyList(), + val durationList: List = emptyList(), + val paused: Boolean = false, + val errorMessage: String? = null, +) + +class StoriesViewModel( + application: Application, +) : AndroidViewModel(application) { + + @Inject + lateinit var apiHolder: PixelfedAPIHolder + + private val _uiState: MutableStateFlow = MutableStateFlow(StoriesUiState()) + + val uiState: StateFlow = _uiState + + var carousel: StoryCarousel? = null + + val count = MutableLiveData() + + private var timer: CountDownTimer? = null + + init { + (application as PixelDroidApplication).getAppComponent().inject(this) + loadStories() + } + + private fun setTimer(timerLength: Long) { + count.value = timerLength + timer = object: CountDownTimer(timerLength * 1000, 500){ + + override fun onTick(millisUntilFinished: Long) { + count.value = millisUntilFinished / 1000 + Log.e("Timer second", "${count.value}") + } + + override fun onFinish() { + goToNext() + } + } + } + + private fun goToNext(){ + _uiState.update { currentUiState -> + currentUiState.copy( + currentImage = currentUiState.currentImage + 1, + //TODO don't just take the first here, choose from activity input somehow? + age = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(currentUiState.currentImage + 1)?.created_at + ) + } + //TODO when done with viewing all stories, close activity and move to profile (?) + timer?.cancel() + startTimerForCurrent() + } + + private fun loadStories() { + viewModelScope.launch { + try{ + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + carousel = api.carousel() + + //TODO don't just take the first here, choose from activity input somehow? + val chosenAccount = carousel?.nodes?.firstOrNull() + + _uiState.update { currentUiState -> + currentUiState.copy( + profilePicture = chosenAccount?.user?.avatar, + age = chosenAccount?.nodes?.getOrNull(0)?.created_at, + username = chosenAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option? + errorMessage = null, + currentImage = 0, + imageList = chosenAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(), + durationList = chosenAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList() + ) + } + startTimerForCurrent() + } catch (exception: Exception){ + _uiState.update { currentUiState -> + currentUiState.copy(errorMessage = "Something went wrong fetching the carousel") + } + } + } + } + + private fun startTimerForCurrent(){ + uiState.value.let { + it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time -> + setTimer(time) + timer?.start() + } + } + } + + fun imageLoaded() {/* + _uiState.update { currentUiState -> + currentUiState.copy(currentImage = currentUiState.currentImage + 1) + }*/ + } + + fun pause() { + if(_uiState.value.paused){ + timer?.start() + } else { + timer?.cancel() + count.value?.let { setTimer(it) } + } + _uiState.update { currentUiState -> + currentUiState.copy(paused = !currentUiState.paused) + } + } +} + +class StoriesViewModelFactory(val application: Application) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.getConstructor(Application::class.java).newInstance(application) + } +} diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index 4e8db5fa..f008e992 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -231,6 +231,20 @@ interface PixelfedAPI { @Query("post_id") post_id: String, ) + @GET("/api/v1.1/stories/carousel") + suspend fun carousel( + ): StoryCarousel + + @POST("/api/v1.1/stories/seen") + suspend fun carouselSeen( + @Query("id") id: String //TODO figure out if this is the id of post or of user? + ) + + @POST("/api/v1.1/stories/self-expire/{id}") + suspend fun deleteCarousel( + @Path("id") storyId: String + ) + //Used in our case to retrieve comments for a given status @GET("/api/v1/statuses/{id}/context") suspend fun statusComments( diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt new file mode 100644 index 00000000..12d5a799 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt @@ -0,0 +1,35 @@ +package org.pixeldroid.app.utils.api.objects + +import java.time.Instant + +data class StoryCarousel( + val self: CarouselUserContainer?, + val nodes: List? +) + +data class CarouselUser( + val id: String?, + val username: String?, + val username_acct: String?, + val avatar: String?, // URL to account avatar + val local: Boolean?, // Is this story from the local instance? + val is_author: Boolean?, // Is this me? (seems redundant with id) +) + +/** + * Container with a description of the [user] and a list of stories ([nodes]) + */ +data class CarouselUserContainer( + val user: CarouselUser?, + val nodes: List?, +) + +data class Story( + val id: String?, + val pid: String?, // id of author + val type: String?, //TODO make enum of this? examples: "photo", ??? + val src: String?, // URL to photo of story + val duration: Int?, //Time in seconds that the Story should be shown + val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen + val created_at: Instant?, //ISO 8601 Datetime +) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt index b9a779bb..23aca907 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt @@ -9,6 +9,7 @@ import org.pixeldroid.app.utils.BaseFragment import dagger.Component import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.profile.EditProfileViewModel +import org.pixeldroid.app.stories.StoriesViewModel import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import javax.inject.Singleton @@ -22,6 +23,7 @@ interface ApplicationComponent { fun inject(notificationsWorker: NotificationsWorker) fun inject(postCreationViewModel: PostCreationViewModel) fun inject(editProfileViewModel: EditProfileViewModel) + fun inject(storiesViewModel: StoriesViewModel) val context: Context? val application: Application? diff --git a/app/src/main/res/drawable/pause.xml b/app/src/main/res/drawable/pause.xml new file mode 100644 index 00000000..f701d6f8 --- /dev/null +++ b/app/src/main/res/drawable/pause.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml new file mode 100644 index 00000000..0870be8f --- /dev/null +++ b/app/src/main/res/drawable/play.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/play_pause.xml b/app/src/main/res/drawable/play_pause.xml new file mode 100644 index 00000000..9c956cb2 --- /dev/null +++ b/app/src/main/res/drawable/play_pause.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml new file mode 100644 index 00000000..b61152df --- /dev/null +++ b/app/src/main/res/layout/activity_stories.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + \ 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 2d56dece..e47bf398 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,4 +330,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Contains NSFW media Switch accounts NSFW/CW posts will not be blurred, and will be shown by default. + Story image diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml deleted file mode 100644 index fd01fe5f..00000000 --- a/gradle/verification-metadata.xml +++ /dev/null @@ -1,6874 +0,0 @@ - - - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -