[UI/WebPush] Implement Web Push pairing fragment and API. Add more templates.

This commit is contained in:
Kuba Szczodrzyński 2019-12-19 21:57:01 +01:00
parent b905283b61
commit 3e4accb82c
18 changed files with 635 additions and 14 deletions

View File

@ -67,6 +67,7 @@ import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
@ -118,6 +119,7 @@ class MainActivity : AppCompatActivity() {
const val TARGET_HELP = 502 const val TARGET_HELP = 502
const val TARGET_FEEDBACK = 120 const val TARGET_FEEDBACK = 120
const val TARGET_MESSAGES_DETAILS = 503 const val TARGET_MESSAGES_DETAILS = 503
const val TARGET_WEB_PUSH = 140
const val HOME_ID = DRAWER_ITEM_HOME const val HOME_ID = DRAWER_ITEM_HOME
@ -209,6 +211,7 @@ class MainActivity : AppCompatActivity() {
list += NavTarget(TARGET_HELP, R.string.menu_help, HelpFragment::class) list += NavTarget(TARGET_HELP, R.string.menu_help, HelpFragment::class)
list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class) list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class)
list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class) list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
list list

View File

@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull
import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
@ -121,4 +123,34 @@ class SzkolnyApi(val app: App) {
eventId = event.id eventId = event.id
)).execute() )).execute()
} }
fun pairBrowser(browserId: String?, pairToken: String?): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest(
action = "pairBrowser",
deviceId = app.deviceId,
browserId = browserId,
pairToken = pairToken
)).execute().body()
return response?.data?.browsers ?: emptyList()
}
fun listBrowsers(): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest(
action = "listBrowsers",
deviceId = app.deviceId
)).execute().body()
return response?.data?.browsers ?: emptyList()
}
fun unpairBrowser(browserId: String): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest(
action = "unpairBrowser",
deviceId = app.deviceId,
browserId = browserId
)).execute().body()
return response?.data?.browsers ?: emptyList()
}
} }

View File

@ -6,8 +6,10 @@ package pl.szczodrzynski.edziennik.data.api.szkolny
import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ServerSyncResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.ServerSyncResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.POST import retrofit2.http.POST
@ -19,4 +21,7 @@ interface SzkolnyService {
@POST("share") @POST("share")
fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Nothing>> fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Nothing>>
@POST("webPush")
fun webPush(@Body request: WebPushRequest): Call<ApiResponse<WebPushResponse>>
} }

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class WebPushRequest(
val action: String,
val deviceId: String,
val browserId: String? = null,
val pairToken: String? = null
)

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.response
data class WebPushResponse(val browsers: List<Browser>) {
data class Browser(
val id: Int,
val browserId: String,
val userAgent: String,
val dateRegistered: String
)
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding
import pl.szczodrzynski.edziennik.onClick
class TemplateAdapter(
val context: Context,
val onItemClick: ((item: TemplateItem) -> Unit)? = null,
val onItemButtonClick: ((item: TemplateItem) -> Unit)? = null
) : RecyclerView.Adapter<TemplateAdapter.ViewHolder>() {
private val app by lazy { context.applicationContext as App }
var items = listOf<TemplateItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = TemplateListItemBinding.inflate(inflater, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
val b = holder.b
onItemClick?.let { listener ->
b.root.onClick { listener(item) }
}
/*b.someButton.visibility = if (buttonVisible) View.VISIBLE else View.GONE
onItemButtonClick?.let { listener ->
b.someButton.onClick { listener(item) }
}*/
}
override fun getItemCount() = items.size
class ViewHolder(val b: TemplateListItemBinding) : RecyclerView.ViewHolder(b.root)
data class TemplateItem(val text: String)
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.ui.dialogs
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import me.dm7.barcodescanner.zxing.ZXingScannerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.dp
import kotlin.coroutines.CoroutineContext
class QrScannerDialog(
val activity: AppCompatActivity,
val onCodeScanned: (text: String) -> Unit,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "QrScannerDialog"
}
private lateinit var app: App
private lateinit var scannerView: ZXingScannerView
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
scannerView = ZXingScannerView(activity)
scannerView.setPadding(0, 16.dp, 0, 0)
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.qr_scanner_dialog_title)
.setView(scannerView)
.setPositiveButton(R.string.close) { dialog, _ ->
dialog.dismiss()
}
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
scannerView.setResultHandler {
scannerView.stopCamera()
dialog.dismiss()
onCodeScanned(it.text)
}
scannerView.startCamera()
}}
}

View File

@ -5,25 +5,34 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope
import androidx.navigation.Navigation import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.FragmentTemplateBinding import pl.szczodrzynski.edziennik.databinding.FragmentTemplateBinding
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
import kotlin.coroutines.CoroutineContext
class TemplateFragment : Fragment() { class TemplateFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "TemplateFragment"
}
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: FragmentTemplateBinding private lateinit var b: FragmentTemplateBinding
private val navController: NavController by lazy { Navigation.findNavController(b.root) }
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
if (context == null) context ?: return null
return null
app = activity.application as App app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true) context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null) if (app.profile == null)

View File

@ -34,8 +34,7 @@ class NotificationsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
if (context == null) context ?: return null
return null
app = activity.application as App app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true) context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null) if (app.profile == null)

View File

@ -53,7 +53,6 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker;
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog; import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog; import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog;
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment; import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment;
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushConfigActivity;
import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.edziennik.utils.Utils; import pl.szczodrzynski.edziennik.utils.Utils;
import pl.szczodrzynski.edziennik.utils.models.Date; import pl.szczodrzynski.edziennik.utils.models.Date;
@ -718,8 +717,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.color(IconicsColor.colorInt(iconColor)) .color(IconicsColor.colorInt(iconColor))
) )
.setOnClickAction(() -> { .setOnClickAction(() -> {
Intent i = new Intent(activity, WebPushConfigActivity.class); activity.loadTarget(MainActivity.TARGET_WEB_PUSH, null);
startActivity(i);
}) })
); );

View File

@ -53,8 +53,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
if (context == null) context ?: return null
return null
app = activity.application as App app = activity.application as App
job = Job() job = Job()
context!!.theme.applyStyle(Themes.appTheme, true) context!!.theme.applyStyle(Themes.appTheme, true)

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.ui.modules.webpush
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.databinding.WebPushBrowserItemBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.setText
class WebPushBrowserAdapter(
val context: Context,
val onItemClick: ((browser: WebPushResponse.Browser) -> Unit)? = null,
val onUnpairButtonClick: ((browser: WebPushResponse.Browser) -> Unit)? = null
) : RecyclerView.Adapter<WebPushBrowserAdapter.ViewHolder>() {
private val app by lazy { context.applicationContext as App }
var items = listOf<WebPushResponse.Browser>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = WebPushBrowserItemBinding.inflate(inflater, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val browser = items[position]
val b = holder.b
onItemClick?.let { listener ->
b.root.onClick { listener(browser) }
}
b.browserName.text = browser.userAgent
b.datePaired.setText(R.string.web_push_date_paired_format, browser.dateRegistered)
onUnpairButtonClick?.let { listener ->
b.unpair.onClick { listener(browser) }
}
}
override fun getItemCount() = items.size
class ViewHolder(val b: WebPushBrowserItemBinding) : RecyclerView.ViewHolder(b.root)
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.ui.modules.webpush
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.databinding.WebPushFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.Themes
import kotlin.coroutines.CoroutineContext
class WebPushFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "TemplateFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: WebPushFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var adapter: WebPushBrowserAdapter
private val api by lazy {
SzkolnyApi(app)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false)
// activity, context and profile is valid
b = WebPushFragmentBinding.inflate(inflater)
return b.root
}
@SuppressLint("DefaultLocale")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
b.scanQrCode.onClick {
val result = ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
if (result == PackageManager.PERMISSION_GRANTED) {
QrScannerDialog(activity, {
b.tokenEditText.setText(it.crc32().toString(36).toUpperCase())
pairBrowser(browserId = it)
})
} else {
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 1)
}
}
b.tokenAccept.onClick {
val pairToken = b.tokenEditText.text.toString().toUpperCase()
if (!"[0-9A-Z]{3,13}".toRegex().matches(pairToken)) {
b.tokenLayout.error = app.getString(R.string.web_push_token_invalid)
return@onClick
}
b.tokenLayout.error = null
b.tokenEditText.setText(pairToken)
pairBrowser(pairToken = pairToken)
}
adapter = WebPushBrowserAdapter(
activity,
onItemClick = null,
onUnpairButtonClick = {
unpairBrowser(it.browserId)
}
)
launch {
val browsers = withContext(Dispatchers.Default) {
api.listBrowsers()
}
updateBrowserList(browsers)
}
}
private fun updateBrowserList(browsers: List<WebPushResponse.Browser>) {
adapter.items = browsers
if (b.browsersView.adapter == null) {
b.browsersView.adapter = adapter
b.browsersView.apply {
isNestedScrollingEnabled = false
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
}
}
adapter.notifyDataSetChanged()
if (browsers.isNotEmpty()) {
b.browsersView.visibility = View.VISIBLE
b.browsersNoData.visibility = View.GONE
} else {
b.browsersView.visibility = View.GONE
b.browsersNoData.visibility = View.VISIBLE
}
}
private fun pairBrowser(browserId: String? = null, pairToken: String? = null) {
b.scanQrCode.isEnabled = false
b.tokenAccept.isEnabled = false
b.tokenEditText.isEnabled = false
b.tokenEditText.clearFocus()
launch {
val browsers = withContext(Dispatchers.Default) {
api.pairBrowser(browserId, pairToken)
}
b.scanQrCode.isEnabled = true
b.tokenAccept.isEnabled = true
b.tokenEditText.isEnabled = true
updateBrowserList(browsers)
}
}
private fun unpairBrowser(browserId: String) {
launch {
val browsers = withContext(Dispatchers.Default) {
api.unpairBrowser(browserId)
}
updateBrowserList(browsers)
}
}
}

View File

@ -0,0 +1,52 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-19.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M56,46.668L8,46.668L8,9.332C8,8.5938 8.5977,8 9.332,8L54.668,8C55.4063,8 56,8.5938 56,9.332Z"
android:fillColor="#455A64"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M10.668,10.668L53.332,10.668L53.332,37.332L10.668,37.332Z"
android:fillColor="#BBDEFB"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M0,53.332L64,53.332L64,56L0,56Z"
android:fillColor="#B0BEC5"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M56,40L8,40L0,53.332L64,53.332Z"
android:fillColor="#CFD8DC"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M10.668,42.668L53.332,42.668L57.332,50.668L6.668,50.668Z"
android:fillColor="#546E7A"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M22.0586,50.668L21.332,53.332L42.668,53.332L41.9414,50.668Z"
android:fillColor="#90A4AE"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M37.332,24C37.332,21.668 38.668,21.332 38.668,21.332L45.332,21.332C44.3047,18.9805 41.3984,17.332 38.668,17.332C35.9336,17.332 33.5938,18.9805 32.5625,21.332L21.332,21.332C19.8594,21.332 18.668,22.5273 18.668,24C18.668,25.4727 19.8594,26.668 21.332,26.668L32.5625,26.668C33.5938,29.0195 35.9336,30.668 38.668,30.668C41.3984,30.668 44.3047,29.0195 45.332,26.668L38.668,26.668C38.668,26.668 37.332,26.332 37.332,24Z"
android:fillColor="#1976D2"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-19.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="vertical"
android:background="?selectableItemBackground">
</LinearLayout>
</layout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-19.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="8dp"
android:orientation="horizontal"
android:background="?selectableItemBackground"
android:baselineAligned="false">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/browserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="Firefox 71.0 @ Windows 7 64-bit"/>
<TextView
android:id="@+id/datePaired"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Połączono 2019-12-12 19:54:09"/>
</LinearLayout>
<Button
android:id="@+id/unpair"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="@string/web_push_unpair_button" />
</LinearLayout>
</layout>

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-12-19.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Przekazywanie powiadomień pozwala na połączenie komputera, na którym będą pokazywane powiadomienia otrzymane w aplikacji Szkolny.eu na telefonie.\n\nDzięki temu będziesz zawsze wiedział wszystko na bieżąco, nie patrząc nawet na swój telefon."/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="Jak połączyć komputer z aplikacją?"
android:textSize="24sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1. Na komputerze otwórz stronę http://szkolny.eu/wp\n2. Zezwól stronie na wyświetlanie powiadomień dot. danych z dziennika.\n3. Kliknij przycisk &quot;Skanuj&quot; poniżej lub przepisz token ze strony."/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="Sparuj przeglądarkę"
android:textSize="24sp" />
<Button
android:id="@+id/scanQrCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Skanuj kod QR" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tokenLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Token">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/tokenEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="1A22IT" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/tokenAccept"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="OK" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="Połączone przeglądarki"
android:textSize="24sp" />
<TextView
android:id="@+id/browsersNoData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:drawableTop="@drawable/ic_web_push_no_browsers"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="Brak połączonych przeglądarek"
android:textSize="18sp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/browsersView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
tools:visibility="visible"
tools:listitem="@layout/web_push_browser_item" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</layout>

View File

@ -1104,4 +1104,9 @@
<string name="event_details_added_by_self_format">%1$s przez Ciebie</string> <string name="event_details_added_by_self_format">%1$s przez Ciebie</string>
<string name="event_details_shared_by_format">{cmd-share-variant} %1$s przez %2$s</string> <string name="event_details_shared_by_format">{cmd-share-variant} %1$s przez %2$s</string>
<string name="event_details_shared_by_self_format">{cmd-share-variant} %1$s przez Ciebie</string> <string name="event_details_shared_by_self_format">{cmd-share-variant} %1$s przez Ciebie</string>
<string name="web_push_token_invalid">Token nie wygląda na prawidłowy</string>
<string name="menu_web_push">Przekazywanie powiadomień</string>
<string name="qr_scanner_dialog_title">Skanuj kod QR</string>
<string name="web_push_unpair_button">Odłącz</string>
<string name="web_push_date_paired_format">Połączono %s</string>
</resources> </resources>