1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 23:19:09 -05:00
This commit is contained in:
Rafał Borcz 2024-06-11 20:37:31 +02:00 committed by GitHub
parent fc549309d8
commit 3e106d5af0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 192 additions and 7 deletions

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.data package io.github.wulkanowy.data
import io.github.wulkanowy.data.repositories.isEndDateReached
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -267,7 +268,8 @@ inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
emit(Resource.Loading()) emit(Resource.Loading())
val data = query().first() val data = query().first()
if (shouldFetch(data)) { val updatedShouldFetch = if (isEndDateReached) false else shouldFetch(data)
if (updatedShouldFetch) {
emit(Resource.Intermediate(data)) emit(Resource.Intermediate(data))
try { try {

View File

@ -12,9 +12,13 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import timber.log.Timber import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private val endDate = LocalDate.of(2024, 6, 25)
val isEndDateReached = LocalDate.now() >= endDate
@Singleton @Singleton
class WulkanowyRepository @Inject constructor( class WulkanowyRepository @Inject constructor(
private val wulkanowyService: WulkanowyService, private val wulkanowyService: WulkanowyService,
@ -24,7 +28,6 @@ class WulkanowyRepository @Inject constructor(
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val cacheKey = "mapping_refresh_key" private val cacheKey = "mapping_refresh_key"
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> = fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =

View File

@ -4,19 +4,27 @@ import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.work.*
import androidx.work.BackoffPolicy.EXPONENTIAL import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy.KEEP import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType.CONNECTED import androidx.work.NetworkType.CONNECTED
import androidx.work.NetworkType.UNMETERED import androidx.work.NetworkType.UNMETERED
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isHolidays
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import timber.log.Timber import timber.log.Timber
import java.time.LocalDate.now import java.time.LocalDate.now
import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.MINUTES
@ -34,7 +42,9 @@ class SyncManager @Inject constructor(
) { ) {
init { init {
if (now().isHolidays) stopSyncWorker() if (now().isHolidays || isEndDateReached) {
stopSyncWorker()
}
if (SDK_INT >= O) { if (SDK_INT >= O) {
channels.forEach { it.create() } channels.forEach { it.create() }
@ -50,7 +60,7 @@ class SyncManager @Inject constructor(
} }
fun startPeriodicSyncWorker(restart: Boolean = false) { fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) { if (preferencesRepository.isServiceEnabled && !now().isHolidays && isEndDateReached) {
val serviceInterval = preferencesRepository.servicesInterval val serviceInterval = preferencesRepository.servicesInterval
workManager.enqueueUniquePeriodicWork( workManager.enqueueUniquePeriodicWork(
@ -70,6 +80,10 @@ class SyncManager @Inject constructor(
// if quiet, no notifications will be sent // if quiet, no notifications will be sent
fun startOneTimeSyncWorker(quiet: Boolean = false): Flow<WorkInfo?> { fun startOneTimeSyncWorker(quiet: Boolean = false): Flow<WorkInfo?> {
if (isEndDateReached) {
return flowOf(null)
}
val work = OneTimeWorkRequestBuilder<SyncWorker>() val work = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData( .setInputData(
Data.Builder() Data.Builder()

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
@ -42,7 +43,9 @@ class SyncWorker @AssistedInject constructor(
override suspend fun doWork(): Result = withContext(dispatchersProvider.io) { override suspend fun doWork(): Result = withContext(dispatchersProvider.io) {
Timber.i("SyncWorker is starting") Timber.i("SyncWorker is starting")
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() if (!studentRepository.isCurrentStudentSet() || isEndDateReached) {
return@withContext Result.failure()
}
val (student, semester) = try { val (student, semester) = try {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
@ -91,6 +94,7 @@ class SyncWorker @AssistedInject constructor(
.build() .build()
) )
} }
errors.isNotEmpty() -> Result.retry() errors.isNotEmpty() -> Result.retry()
else -> { else -> {
preferencesRepository.lasSyncDate = Instant.now() preferencesRepository.lasSyncDate = Instant.now()

View File

@ -0,0 +1,31 @@
package io.github.wulkanowy.ui.modules.end
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.activity.addCallback
import androidx.core.text.HtmlCompat
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentEndBinding
import io.github.wulkanowy.ui.base.BaseFragment
@AndroidEntryPoint
class EndFragment : BaseFragment<FragmentEndBinding>(R.layout.fragment_end), EndView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentEndBinding.bind(view)
requireActivity().onBackPressedDispatcher.addCallback {
requireActivity().finishAffinity()
}
binding.endClose.setOnClickListener { requireActivity().finishAffinity() }
val message = getString(R.string.end_message)
binding.endDescription.movementMethod = LinkMovementMethod.getInstance()
binding.endDescription.text =
HtmlCompat.fromHtml(message, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
}

View File

@ -0,0 +1,5 @@
package io.github.wulkanowy.ui.modules.end
import io.github.wulkanowy.ui.base.BaseView
interface EndView : BaseView

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.ActivityLoginBinding import io.github.wulkanowy.databinding.ActivityLoginBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.end.EndFragment
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
@ -115,9 +116,14 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
} }
} }
override fun navigateToEnd() {
openFragment(EndFragment(), clearBackStack = true)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
inAppUpdateHelper.onResume() inAppUpdateHelper.onResume()
presenter.updateSdkMappings() presenter.updateSdkMappings()
presenter.checkIfEnd()
} }
} }

View File

@ -2,6 +2,8 @@ package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -11,7 +13,8 @@ import javax.inject.Inject
class LoginPresenter @Inject constructor( class LoginPresenter @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository, private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository,
private val syncManager: SyncManager
) : BasePresenter<LoginView>(errorHandler, studentRepository) { ) : BasePresenter<LoginView>(errorHandler, studentRepository) {
override fun onAttachView(view: LoginView) { override fun onAttachView(view: LoginView) {
@ -26,4 +29,11 @@ class LoginPresenter @Inject constructor(
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }
} }
fun checkIfEnd() {
if (isEndDateReached) {
syncManager.stopSyncWorker()
view?.navigateToEnd()
}
}
} }

View File

@ -5,4 +5,6 @@ import io.github.wulkanowy.ui.base.BaseView
interface LoginView : BaseView { interface LoginView : BaseView {
fun initView() fun initView()
fun navigateToEnd()
} }

View File

@ -14,7 +14,9 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.dataOrThrow import io.github.wulkanowy.data.dataOrThrow
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.ui.modules.splash.SplashActivity
@ -35,6 +37,9 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
@Inject @Inject
lateinit var sharedPref: SharedPrefProvider lateinit var sharedPref: SharedPrefProvider
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object { companion object {
private const val LUCKY_NUMBER_WIDGET_MAX_SIZE = 196 private const val LUCKY_NUMBER_WIDGET_MAX_SIZE = 196
@ -130,6 +135,10 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
} }
private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking { private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking {
if (isEndDateReached) {
return@runBlocking null
}
try { try {
val students = studentRepository.getSavedStudents() val students = studentRepository.getSavedStudents()
val student = students.singleOrNull { it.student.id == studentId }?.student val student = students.singleOrNull { it.student.id == studentId }?.student

View File

@ -33,6 +33,7 @@ import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.end.EndFragment
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
@ -139,6 +140,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
super.onResume() super.onResume()
inAppUpdateHelper.onResume() inAppUpdateHelper.onResume()
presenter.updateSdkMappings() presenter.updateSdkMappings()
presenter.checkIfEnd()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -362,4 +364,10 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState) navController.onSaveInstanceState(outState)
} }
override fun navigateToEnd() {
binding.mainToolbar.isVisible = false
pushView(EndFragment())
onBackCallback?.isEnabled = false
}
} }

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -15,6 +16,7 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.AccountView import io.github.wulkanowy.ui.modules.account.AccountView
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView
import io.github.wulkanowy.ui.modules.end.EndView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
@ -110,6 +112,7 @@ class MainPresenter @Inject constructor(
} }
private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) { private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) {
is EndView,
is AccountView, is AccountView,
is StudentInfoView, is StudentInfoView,
is AccountDetailsView -> false is AccountDetailsView -> false
@ -208,4 +211,11 @@ class MainPresenter @Inject constructor(
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }
} }
fun checkIfEnd() {
if (isEndDateReached) {
syncManager.stopSyncWorker()
view?.navigateToEnd()
}
}
} }

View File

@ -48,6 +48,8 @@ interface MainView : BaseView {
fun openMoreDestination(destination: Destination) fun openMoreDestination(destination: Destination)
fun navigateToEnd()
interface MainChildView { interface MainChildView {
fun onFragmentReselected() fun onFragmentReselected()

View File

@ -22,6 +22,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
@ -71,6 +72,8 @@ class TimetableWidgetFactory(
items = emptyList() items = emptyList()
if (isEndDateReached) return
runBlocking { runBlocking {
runCatching { runCatching {
val student = getStudent(studentId) ?: return@runBlocking val student = getStudent(studentId) ?: return@runBlocking

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/endIcon"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="32dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintBottom_toTopOf="@id/endText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/endText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:text="@string/end_title"
android:textSize="28sp"
app:layout_constraintBottom_toTopOf="@id/endDescription"
app:layout_constraintTop_toBottomOf="@id/endIcon" />
<TextView
android:id="@+id/endDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="16dp"
android:gravity="center"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/endClose"
app:layout_constraintTop_toBottomOf="@id/endText"
tools:maxLines="15"
tools:text="@string/end_message" />
<com.google.android.material.button.MaterialButton
android:id="@+id/endClose"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="48dp"
android:layout_marginBottom="32dp"
android:text="ZAMKNIJ APLIKACJĘ"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/endDescription"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Motiw systemu</item>
<item>Jôsny</item>
<item>Cemny</item>
<item>Cemny (AMOLED)</item>
</string-array>
</resources>

View File

@ -895,4 +895,8 @@
<string name="message_unmute">Unmute</string> <string name="message_unmute">Unmute</string>
<string name="message_mute_success">You have muted this user</string> <string name="message_mute_success">You have muted this user</string>
<string name="message_unmute_success">You have unmuted this user</string> <string name="message_unmute_success">You have unmuted this user</string>
<!--End-->
<string name="end_title">Koniec Wulkanowego</string>
<string name="end_message">Jak zapewne niektórzy z Was się domyślali zbliża się ten moment, aby zakończyć pewien etap. Wraz z końcem tego roku szkolnego zamykamy projekt Wulkanowy. Stworzenie apki było ekscytującym wyzwaniem, ale skala projektu jest tak duża, że nie jesteśmy w stanie odpowiedzialnie utrzymywać aplikacji. Wulkanowy był fajną przygodą, ale sytuacja wymknęła się nam spod kontroli zarówno pod względem technicznym, jak i społecznym. Nie akceptujemy pojawiającego się hejtu wobec nas ani wobec innych, także Vulcana. Nie chcemy brać udziału w tych działaniach i być z nimi utożsamiani. Prosimy Was o powściągliwość i rozwagę w publikowanych komentarzach i nieprzekraczanie dopuszczalnych granic.&lt;br />&lt;br />Prosimy Was też o uszanowanie naszej decyzji, jest ona przemyślana i ostateczna. Wszystkim dotychczasowym użytkownikom Wulkanowego rekomendujemy użycie oficjalnej aplikacji &lt;a href="https://play.google.com/store/apps/details?id=pl.edu.vulcan.hebe&amp;hl=pl">Dzienniczek VULCAN&lt;/a>. Jeszcze raz dziękujemy wszystkim użytkownikom za lata wsparcia, pomoc i miłe słowa!</string>
</resources> </resources>