This commit is contained in:
Stranni15k 2024-01-08 21:59:29 +04:00
parent 71f82033ec
commit e4a08474a5
28 changed files with 1647 additions and 168 deletions

View File

@ -1,4 +1,3 @@
package ru.ulstu.is.pmu.api package ru.ulstu.`is`.pmu.api
class ApiStatus { enum class ApiStatus { LOADING, ERROR, DONE }
}

View File

@ -21,9 +21,16 @@ import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
import ru.ulstu.`is`.pmu.api.model.UserRemote import ru.ulstu.`is`.pmu.api.model.UserRemote
import ru.ulstu.`is`.pmu.api.model.TaskRemote import ru.ulstu.`is`.pmu.api.model.TaskRemote
import ru.ulstu.`is`.pmu.api.report.ReportRemote
interface MyServerService { interface MyServerService {
@GET("report")
suspend fun getReportInfo(
@Query("fromDate") fromDate: String,
@Query("toDate") toDate: String
): List<ReportRemote>
@GET("users") @GET("users")
suspend fun getUsers(): List<UserRemote> suspend fun getUsers(): List<UserRemote>

View File

@ -9,8 +9,8 @@ data class TaskRemote(
val name: String = "", val name: String = "",
val description: String = "", val description: String = "",
val endDate: String = "", val endDate: String = "",
val favorite: Boolean, val favorite: Boolean = false, // Значение по умолчанию
val userId: Int, val userId: Int = 1, // Значение по умолчанию
) )
fun TaskRemote.toTask(): Task = Task( fun TaskRemote.toTask(): Task = Task(
@ -18,7 +18,7 @@ fun TaskRemote.toTask(): Task = Task(
name, name,
description, description,
endDate, endDate,
favorite = false, favorite,
userId userId
) )
@ -27,6 +27,6 @@ fun Task.toTaskRemote(): TaskRemote = TaskRemote(
name, name,
description, description,
endDate, endDate,
favorite = false, favorite,
userId = 1 userId = 1
) )

View File

@ -1,4 +1,11 @@
package ru.ulstu.is.pmu.api.report package ru.ulstu.`is`.pmu.api.report
class ReportRemote { import kotlinx.serialization.Serializable
}
@Serializable
data class ReportRemote(
val id: Int = 0,
val name: String = "",
val description: String = "",
val endDate: String = ""
)

View File

@ -10,12 +10,16 @@ import ru.ulstu.`is`.pmu.api.MyServerService
import ru.ulstu.`is`.pmu.api.user.RestUserRepository import ru.ulstu.`is`.pmu.api.user.RestUserRepository
import ru.ulstu.`is`.pmu.api.model.toTask import ru.ulstu.`is`.pmu.api.model.toTask
import ru.ulstu.`is`.pmu.api.model.toTaskRemote import ru.ulstu.`is`.pmu.api.model.toTaskRemote
import ru.ulstu.`is`.pmu.api.report.ReportRemote
import ru.ulstu.`is`.pmu.common.AppContainer import ru.ulstu.`is`.pmu.common.AppContainer
import ru.ulstu.`is`.pmu.common.TaskRepository import ru.ulstu.`is`.pmu.common.TaskRepository
import ru.ulstu.`is`.pmu.database.AppDatabase import ru.ulstu.`is`.pmu.database.AppDatabase
import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository
import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.database.task.repository.OfflineTaskRepository import ru.ulstu.`is`.pmu.database.task.repository.OfflineTaskRepository
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class RestTaskRepository( class RestTaskRepository(
private val service: MyServerService, private val service: MyServerService,
@ -46,6 +50,50 @@ class RestTaskRepository(
).flow ).flow
} }
override fun getAllFavoriteTasks(): Flow<PagingData<Task>> {
Log.d(RestTaskRepository::class.simpleName, "Get tasks")
val pagingSourceFactory = { dbTaskRepository.getAllTasksFavoritePagingSource() }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = TaskRemoteMediator(
service,
dbTaskRepository,
dbRemoteKeyRepository,
userRestRepository,
database,
),
pagingSourceFactory = pagingSourceFactory
).flow
}
override fun getAllDateTasks(): Flow<PagingData<Task>> {
Log.d(RestTaskRepository::class.simpleName, "Get tasks")
val pagingSourceFactory = { dbTaskRepository.getAllTasksDatePagingSource() }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = TaskRemoteMediator(
service,
dbTaskRepository,
dbRemoteKeyRepository,
userRestRepository,
database,
),
pagingSourceFactory = pagingSourceFactory
).flow
}
override suspend fun getTask(uid: Int): Task = override suspend fun getTask(uid: Int): Task =
service.getTask(uid).toTask() service.getTask(uid).toTask()
@ -60,4 +108,19 @@ class RestTaskRepository(
override suspend fun deleteTask(task: Task) { override suspend fun deleteTask(task: Task) {
service.deleteTask(task.uid).toTask() service.deleteTask(task.uid).toTask()
} }
override suspend fun favoriteTask(task: Task) {
task.favorite = true
service.updateTask(task.uid, task.toTaskRemote()).toTask()
}
override suspend fun deletefavoriteTask(task: Task) {
task.favorite = false
service.updateTask(task.uid, task.toTaskRemote()).toTask()
}
suspend fun getReport(fromDate: String, toDate: String):List<ReportRemote>
{
return service.getReportInfo(fromDate,toDate)
}
} }

View File

@ -6,8 +6,11 @@ import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import ru.ulstu.`is`.pmu.TaskApplication import ru.ulstu.`is`.pmu.TaskApplication
import ru.ulstu.`is`.pmu.ui.ReportViewModel
import ru.ulstu.`is`.pmu.ui.task.edit.UserDropDownViewModel import ru.ulstu.`is`.pmu.ui.task.edit.UserDropDownViewModel
import ru.ulstu.`is`.pmu.ui.task.edit.TaskEditViewModel import ru.ulstu.`is`.pmu.ui.task.edit.TaskEditViewModel
import ru.ulstu.`is`.pmu.ui.task.list.FavoriteTaskList
import ru.ulstu.`is`.pmu.ui.task.list.FavoriteTaskListViewModel
import ru.ulstu.`is`.pmu.ui.task.list.TaskListViewModel import ru.ulstu.`is`.pmu.ui.task.list.TaskListViewModel
object AppViewModelProvider { object AppViewModelProvider {
@ -24,6 +27,14 @@ object AppViewModelProvider {
initializer { initializer {
UserDropDownViewModel(taskApplication().container.userRestRepository) UserDropDownViewModel(taskApplication().container.userRestRepository)
} }
initializer {
FavoriteTaskListViewModel(taskApplication().container.taskRestRepository)
}
initializer {
ReportViewModel(
taskApplication().container.taskRestRepository,
)
}
} }
} }

View File

@ -6,8 +6,12 @@ import ru.ulstu.`is`.pmu.database.task.model.Task
interface TaskRepository { interface TaskRepository {
fun getAllTasks(): Flow<PagingData<Task>> fun getAllTasks(): Flow<PagingData<Task>>
fun getAllFavoriteTasks(): Flow<PagingData<Task>>
fun getAllDateTasks(): Flow<PagingData<Task>>
suspend fun getTask(uid: Int): Task suspend fun getTask(uid: Int): Task
suspend fun insertTask(task: Task) suspend fun insertTask(task: Task)
suspend fun updateTask(task: Task) suspend fun updateTask(task: Task)
suspend fun deleteTask(task: Task) suspend fun deleteTask(task: Task)
suspend fun favoriteTask(task: Task)
suspend fun deletefavoriteTask(task: Task)
} }

View File

@ -5,9 +5,11 @@ import androidx.room.PrimaryKey
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.database.task.model.User
enum class RemoteKeyType(private val type: String) { enum class RemoteKeyType(private val type: String) {
STUDENT(Task::class.simpleName ?: "Task"); STUDENT(Task::class.simpleName ?: "Task"),
USER(User::class.simpleName ?: "User");
@TypeConverter @TypeConverter
fun toRemoteKeyType(value: String) = RemoteKeyType.values().first { it.type == value } fun toRemoteKeyType(value: String) = RemoteKeyType.values().first { it.type == value }

View File

@ -14,6 +14,12 @@ interface TaskDao {
@Query("select * from tasks order by name collate nocase asc") @Query("select * from tasks order by name collate nocase asc")
fun getAll(): PagingSource<Int, Task> fun getAll(): PagingSource<Int, Task>
@Query("SELECT * FROM tasks WHERE favorite = 1")
fun getFavoriteTasks(): PagingSource<Int, Task>
@Query("SELECT * FROM tasks ORDER BY DATE(SUBSTR(endDate, 7, 4) || '-' || SUBSTR(endDate, 4, 2) || '-' || SUBSTR(endDate, 1, 2)) ASC")
fun getTasksSortedByDate(): PagingSource<Int, Task>
@Query("select * from tasks where tasks.uid = :uid") @Query("select * from tasks where tasks.uid = :uid")
fun getByUid(uid: Int): Flow<Task> fun getByUid(uid: Int): Flow<Task>
@ -26,6 +32,9 @@ interface TaskDao {
@Delete @Delete
suspend fun delete(task: Task) suspend fun delete(task: Task)
@Query("UPDATE tasks SET favorite = :favorite WHERE uid = :taskId")
suspend fun updateFavorite(taskId: Int, favorite: Boolean)
@Query("DELETE FROM tasks") @Query("DELETE FROM tasks")
suspend fun deleteAll() suspend fun deleteAll()
} }

View File

@ -1,23 +1,27 @@
package ru.ulstu.`is`.pmu.database.task.dao package ru.ulstu.`is`.pmu.database.task.dao
import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Update import androidx.room.Update
import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.database.task.model.User import ru.ulstu.`is`.pmu.database.task.model.User
@Dao @Dao
interface UserDao { interface UserDao {
@Query("select * from users order by login collate nocase asc") @Query("select * from users order by login collate nocase asc")
suspend fun getAll(): List<User> suspend fun getAll(): List<User>
@Insert @Insert
suspend fun insert(user: User) suspend fun insert(vararg user: User)
@Update @Update
suspend fun update(user: User) suspend fun update(user: User)
@Delete @Delete
suspend fun delete(user: User) suspend fun delete(user: User)
@Query("DELETE FROM users")
suspend fun deleteAll()
} }

View File

@ -39,7 +39,7 @@ data class Task(
endDate: String, endDate: String,
favorite: Boolean, favorite: Boolean,
user: User, user: User,
) : this(0, name, description, endDate, favorite, user.uid) ) : this(0, name, description, endDate, favorite, 1)
companion object { companion object {

View File

@ -20,6 +20,22 @@ class OfflineTaskRepository(private val taskDao: TaskDao) : TaskRepository {
pagingSourceFactory = taskDao::getAll pagingSourceFactory = taskDao::getAll
).flow ).flow
override fun getAllFavoriteTasks(): Flow<PagingData<Task>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = taskDao::getFavoriteTasks
).flow
override fun getAllDateTasks(): Flow<PagingData<Task>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = taskDao::getTasksSortedByDate
).flow
override suspend fun getTask(uid: Int): Task = taskDao.getByUid(uid).first() override suspend fun getTask(uid: Int): Task = taskDao.getByUid(uid).first()
override suspend fun insertTask(task: Task) = taskDao.insert(task) override suspend fun insertTask(task: Task) = taskDao.insert(task)
@ -27,12 +43,21 @@ class OfflineTaskRepository(private val taskDao: TaskDao) : TaskRepository {
override suspend fun updateTask(task: Task) = taskDao.update(task) override suspend fun updateTask(task: Task) = taskDao.update(task)
override suspend fun deleteTask(task: Task) = taskDao.delete(task) override suspend fun deleteTask(task: Task) = taskDao.delete(task)
override suspend fun favoriteTask(task: Task) { override suspend fun favoriteTask(task: Task) {
TODO("Not yet implemented") taskDao.updateFavorite(task.uid, !task.favorite)
}
override suspend fun deletefavoriteTask(task: Task) {
taskDao.updateFavorite(task.uid, !task.favorite)
} }
fun getAllTasksPagingSource(): PagingSource<Int, Task> = taskDao.getAll() fun getAllTasksPagingSource(): PagingSource<Int, Task> = taskDao.getAll()
fun getAllTasksFavoritePagingSource(): PagingSource<Int, Task> = taskDao.getFavoriteTasks()
fun getAllTasksDatePagingSource(): PagingSource<Int, Task> = taskDao.getTasksSortedByDate()
suspend fun insertTasks(tasks: List<Task>) = suspend fun insertTasks(tasks: List<Task>) =
taskDao.insert(*tasks.toTypedArray()) taskDao.insert(*tasks.toTypedArray())

View File

@ -1,11 +1,23 @@
package ru.ulstu.`is`.pmu.database.task.repository package ru.ulstu.`is`.pmu.database.task.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import kotlinx.coroutines.flow.Flow
import ru.ulstu.`is`.pmu.common.AppContainer
import ru.ulstu.`is`.pmu.common.UserRepository import ru.ulstu.`is`.pmu.common.UserRepository
import ru.ulstu.`is`.pmu.database.task.dao.UserDao import ru.ulstu.`is`.pmu.database.task.dao.UserDao
import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.database.task.model.User import ru.ulstu.`is`.pmu.database.task.model.User
class OfflineUserRepository(private val userDao: UserDao) : UserRepository { class OfflineUserRepository(private val userDao: UserDao) : UserRepository {
override suspend fun getAllUsers(): List<User> = userDao.getAll() override suspend fun getAllUsers(): List<User> = userDao.getAll()
suspend fun insertUsers(users: List<User>) =
userDao.insert(*users.toTypedArray())
suspend fun createUser(user: User) = userDao.insert(user) suspend fun createUser(user: User) = userDao.insert(user)
suspend fun updateUser(user: User) = userDao.update(user) suspend fun updateUser(user: User) = userDao.update(user)
suspend fun clearUsers() = userDao.deleteAll()
} }

View File

@ -1,4 +1,362 @@
package ru.ulstu.is.pmu.ui package ru.ulstu.`is`.pmu.ui
class ReportPage { import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.pdf.PdfDocument
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.widget.CalendarView
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.api.report.ReportRemote
import ru.ulstu.`is`.pmu.common.AppViewModelProvider
import ru.ulstu.`is`.pmu.ui.ReportViewModel
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportPage(navController: NavController?, viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory)) {
val coroutineScope = rememberCoroutineScope()
var showStartDatePicker by remember { mutableStateOf(false) }
var showEndDatePicker by remember { mutableStateOf(false) }
val context = LocalContext.current
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Button(
onClick = { showStartDatePicker = true },
modifier = Modifier.width(350.dp)
) {
Text("Начальная дата")
}
}
if (showStartDatePicker) {
DatePicker(
onDateSelected = { selectedDate ->
viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(startDate = selectedDate))
showStartDatePicker = false
},
onDismissRequest = { showStartDatePicker = false }
)
}
Button(
onClick = { showEndDatePicker = true },
modifier = Modifier.width(350.dp)
) {
Text("Конечная дата")
}
if (showEndDatePicker) {
DatePicker(
onDateSelected = { selectedDate ->
viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(endDate = selectedDate))
showEndDatePicker = false
if (viewModel.reportPageUiState.reportDetails.startDate != null) {
coroutineScope.launch {
viewModel.getReport()
createPdfFile(context, "Отчет.pdf", viewModel.reportResultPageUiState.resReport)
}
}
},
onDismissRequest = { showEndDatePicker = false }
)
}
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Результат",
style = MaterialTheme.typography.headlineLarge
)
ReportTable(reportData = viewModel.reportResultPageUiState.resReport)
}
}
@Composable
fun ReportTable(reportData: List<ReportRemote>) {
Column(modifier = Modifier.padding(16.dp)) {
reportData.forEach { report ->
ReportCard(report)
}
}
}
@Composable
fun ReportCard(report: ReportRemote) {
val (taskId, taskName, taskDescription, taskEndDate) = report
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) // Исправлено здесь
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("ID: ${taskId}", style = MaterialTheme.typography.bodyMedium)
Text("Название: $taskName", style = MaterialTheme.typography.bodyMedium)
Text("Описание: $taskDescription", style = MaterialTheme.typography.bodyMedium)
Text("Дата завершения: $taskEndDate", style = MaterialTheme.typography.bodyMedium)
}
}
}
@Composable
fun CustomCalendarView(onDateSelected: (Date) -> Unit) {
AndroidView(
modifier = Modifier.wrapContentSize(),
factory = { context ->
val calendarView = CalendarView(context)
// Настройки CalendarView
val calendar = Calendar.getInstance()
calendarView.setOnDateChangeListener { _, year, month, dayOfMonth ->
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, month)
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
onDateSelected(calendar.time)
}
calendarView
}
)
}
@Composable
fun DatePicker(onDateSelected: (Date) -> Unit, onDismissRequest: () -> Unit) {
val selDate = remember { mutableStateOf(Date()) }
//todo - add strings to resource after POC
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties()) {
Column(
modifier = Modifier
.wrapContentSize()
.background(
color = Color.White
)
) {
Column(
Modifier
.defaultMinSize(minHeight = 72.dp)
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.primary
)
.padding(16.dp)
) {
Text(
text = "Выберите дату"
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()).format(selDate.value)
)
Spacer(modifier = Modifier.size(16.dp))
}
CustomCalendarView(onDateSelected = {
selDate.value = it
})
Spacer(modifier = Modifier.size(8.dp))
Row(
modifier = Modifier
.align(Alignment.End)
.padding(bottom = 16.dp, end = 16.dp)
.background(
color = Color.White,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
) {
TextButton(
onClick = onDismissRequest
) {
//TODO - hardcode string
Text(
text = "Отмена"
)
}
TextButton(
onClick = {
onDateSelected(selDate.value)
onDismissRequest()
}
) {
//TODO - hardcode string
Text(
text = "Подтвердить"
)
}
}
}
}
}
suspend fun createPdfFile(context: Context, fileName: String, reportData: List<ReportRemote>) {
withContext(Dispatchers.IO) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS)
}
val contentResolver: ContentResolver = context.contentResolver
val uri: Uri? = contentResolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
uri?.let {
try {
contentResolver.openOutputStream(uri)?.use { outputStream ->
createPdfContent(outputStream, reportData)
}
context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
private fun createPdfContent(outputStream: OutputStream, reportData: List<ReportRemote>) {
val pdfDocument = PdfDocument()
val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
val page = pdfDocument.startPage(pageInfo)
val canvas = page.canvas
val paint = Paint()
paint.color = android.graphics.Color.BLACK
paint.textSize = 24f
// Отображаем заголовок
val title = "Отчет"
val xTitle = (pageInfo.pageWidth - paint.measureText(title)) / 2
val yTitle = 40f
canvas.drawText(title, xTitle, yTitle, paint)
// Определение структуры таблицы
val tableWidth = pageInfo.pageWidth - 80f
val columnCount = 4
val columnWidth = tableWidth / columnCount
val rowHeight = 40f
// Отображаем заголовки столбцов
val columnHeaderPaint = Paint()
columnHeaderPaint.color = android.graphics.Color.BLACK
columnHeaderPaint.textSize = 18f
val columnHeaderTitles = listOf("ID", "Название", "Описание", "Дата окончания")
for (i in 0 until columnCount) {
val x = 40f + i * columnWidth
val y = yTitle + 80f
canvas.drawText(columnHeaderTitles[i], x, y, columnHeaderPaint)
}
// Отображаем данные в таблице
val rowDataPaint = Paint()
rowDataPaint.color = android.graphics.Color.BLACK
rowDataPaint.textSize = 18f
for ((index, report) in reportData.withIndex()) {
val x = 40f
val y = yTitle + 120f + index * rowHeight
// Отображаем данные каждого столбца
canvas.drawText(report.id.toString(), x, y, rowDataPaint)
canvas.drawText(report.name, x + columnWidth, y, rowDataPaint)
canvas.drawText(report.description, x + 2 * columnWidth, y, rowDataPaint)
canvas.drawText(report.endDate, x + 3 * columnWidth, y, rowDataPaint)
}
pdfDocument.finishPage(page)
pdfDocument.writeTo(outputStream)
pdfDocument.close()
}
private fun drawCell(canvas: Canvas, paint: Paint, text: String, x: Float, y: Float, width: Int) {
canvas.drawText(text, x + (width - paint.measureText(text)) / 2, y + paint.textSize, paint)
} }

View File

@ -9,8 +9,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.ulstu.`is`.pmu.api.model.TaskRemote import ru.ulstu.`is`.pmu.api.model.TaskRemote
import ru.ulstu.`is`.pmu.api.model.toTask
import ru.ulstu.`is`.pmu.api.report.ReportRemote import ru.ulstu.`is`.pmu.api.report.ReportRemote
import ru.ulstu.`is`.pmu.api.task.RestTaskRepository import ru.ulstu.`is`.pmu.api.task.RestTaskRepository
import ru.ulstu.`is`.pmu.database.task.model.Task
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale

View File

@ -29,8 +29,10 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import ru.ulstu.`is`.pmu.R import ru.ulstu.`is`.pmu.R
import ru.ulstu.`is`.pmu.ui.ReportPage
import ru.ulstu.`is`.pmu.ui.about.About import ru.ulstu.`is`.pmu.ui.about.About
import ru.ulstu.`is`.pmu.ui.task.edit.TaskEdit import ru.ulstu.`is`.pmu.ui.task.edit.TaskEdit
import ru.ulstu.`is`.pmu.ui.task.list.FavoriteTaskList
import ru.ulstu.`is`.pmu.ui.task.list.TaskList import ru.ulstu.`is`.pmu.ui.task.list.TaskList
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -109,6 +111,8 @@ fun Navhost(
) { ) {
TaskEdit(navController) TaskEdit(navController)
} }
composable(Screen.TaskFavoriteList.route) { FavoriteTaskList(navController) }
composable(Screen.Report.route) { ReportPage(navController = navController) }
} }
} }

View File

@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Send
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import ru.ulstu.`is`.pmu.R import ru.ulstu.`is`.pmu.R
@ -20,6 +21,12 @@ enum class Screen(
About( About(
"about", R.string.about_title, Icons.Filled.Info "about", R.string.about_title, Icons.Filled.Info
), ),
TaskFavoriteList(
"task-favorite", R.string.task_favorite_view_title
),
Report(
"report", R.string.report, Icons.Filled.Send
),
TaskEdit( TaskEdit(
"task-edit/{id}", R.string.task_view_title, showInBottomBar = false "task-edit/{id}", R.string.task_view_title, showInBottomBar = false
); );
@ -27,7 +34,9 @@ enum class Screen(
companion object { companion object {
val bottomBarItems = listOf( val bottomBarItems = listOf(
TaskList, TaskList,
TaskFavoriteList,
About, About,
Report
) )
fun getItem(route: String): Screen? { fun getItem(route: String): Screen? {

View File

@ -1,11 +1,19 @@
package ru.ulstu.`is`.pmu.ui.task.edit package ru.ulstu.`is`.pmu.ui.task.edit
import android.content.res.Configuration import android.content.res.Configuration
import android.widget.CalendarView
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -16,6 +24,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -23,12 +32,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -37,6 +50,10 @@ import ru.ulstu.`is`.pmu.database.task.model.User
import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.common.AppViewModelProvider import ru.ulstu.`is`.pmu.common.AppViewModelProvider
import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@Composable @Composable
fun TaskEdit( fun TaskEdit(
@ -44,17 +61,14 @@ fun TaskEdit(
viewModel: TaskEditViewModel = viewModel(factory = AppViewModelProvider.Factory), viewModel: TaskEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
userViewModel: UserDropDownViewModel = viewModel(factory = AppViewModelProvider.Factory) userViewModel: UserDropDownViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val coroutineScope = rememberCoroutineScope()
userViewModel.setCurrentUser(viewModel.taskUiState.taskDetails.userId) userViewModel.setCurrentUser(viewModel.taskUiState.taskDetails.userId)
TaskEdit( TaskEdit(
taskUiState = viewModel.taskUiState, taskUiState = viewModel.taskUiState,
userUiState = userViewModel.userUiState, userUiState = userViewModel.userUiState,
usersListUiState = userViewModel.usersListUiState, usersListUiState = userViewModel.usersListUiState,
onClick = { onClick = {
coroutineScope.launch {
viewModel.saveTask() viewModel.saveTask()
navController.popBackStack() navController.popBackStack()
}
}, },
onUpdate = viewModel::updateUiState, onUpdate = viewModel::updateUiState,
onUserUpdate = userViewModel::updateUiState onUserUpdate = userViewModel::updateUiState
@ -121,6 +135,7 @@ private fun TaskEdit(
onUpdate: (TaskDetails) -> Unit, onUpdate: (TaskDetails) -> Unit,
onUserUpdate: (User) -> Unit onUserUpdate: (User) -> Unit
) { ) {
var showInvalidDateDialog by remember { mutableStateOf(false) }
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -140,48 +155,166 @@ private fun TaskEdit(
label = { Text(stringResource(id = R.string.task_lastname)) }, label = { Text(stringResource(id = R.string.task_lastname)) },
singleLine = true singleLine = true
) )
UserDropDown( var showDatePicker by remember { mutableStateOf(false) }
userUiState = userUiState, if (showDatePicker) {
usersListUiState = usersListUiState, DatePicker(
onUserUpdate = { onDateSelected = { selectedDate ->
onUpdate(taskUiState.taskDetails.copy(userId = it.uid)) onUpdate(taskUiState.taskDetails.copy(endDate = SimpleDateFormat("dd.MM.yyyy").format(selectedDate)))
onUserUpdate(it) showDatePicker = false
},
onDismissRequest = {
showDatePicker = false
} }
) )
}
Button(
onClick = { showDatePicker = true },
modifier = Modifier.fillMaxWidth()
) {
Text("Выбрать дату окончания")
}
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = taskUiState.taskDetails.endDate, value = taskUiState.taskDetails.endDate,
onValueChange = { onUpdate(taskUiState.taskDetails.copy(endDate = it)) }, onValueChange = { onUpdate(taskUiState.taskDetails.copy(endDate = it)) },
label = { Text(stringResource(id = R.string.task_phone)) }, label = { Text(stringResource(id = R.string.task_phone)) },
singleLine = true singleLine = true ,
enabled = false
) )
Button( Button(
onClick = onClick, onClick = {
if (!isValidDate(taskUiState.taskDetails.endDate)) {
showInvalidDateDialog = true
} else {
onClick()
}
},
enabled = taskUiState.isEntryValid, enabled = taskUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text(text = stringResource(R.string.task_save_button)) Text(text = stringResource(R.string.task_save_button))
} }
} }
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) if (showInvalidDateDialog) {
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) AlertDialog(
@Composable onDismissRequest = { showInvalidDateDialog = false },
fun TaskEditPreview() { title = { Text("Неверный формат даты") },
PmudemoTheme { text = { Text("Введите дату по шаблону: 01.12.2023") },
Surface( confirmButton = {
color = MaterialTheme.colorScheme.background Button(onClick = { showInvalidDateDialog = false }) {
) { Text("Подтвердить")
TaskEdit( }
taskUiState = Task.getTask().toUiState(true), }
userUiState = User.DEMO_User.toUiState(),
usersListUiState = UsersListUiState(listOf()),
onClick = {},
onUpdate = {},
onUserUpdate = {}
) )
} }
} }
fun isValidDate(date: String): Boolean {
val regex = Regex("""^\d{2}\.\d{2}\.\d{4}$""")
return regex.matches(date)
}
@Composable
fun CustomCalendarView(onDateSelected: (Date) -> Unit) {
AndroidView(
modifier = Modifier.wrapContentSize(),
factory = { context ->
val calendarView = CalendarView(context)
// Настройки CalendarView
val calendar = Calendar.getInstance()
// Устанавливаем минимальную дату выбора на сегодняшний день
calendarView.minDate = calendar.timeInMillis
calendarView.setOnDateChangeListener { _, year, month, dayOfMonth ->
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, month)
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
onDateSelected(calendar.time)
}
calendarView
}
)
}
@Composable
fun DatePicker(onDateSelected: (Date) -> Unit, onDismissRequest: () -> Unit) {
val selDate = remember { mutableStateOf(Date()) }
//todo - add strings to resource after POC
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties()) {
Column(
modifier = Modifier
.wrapContentSize()
.background(
color = Color.White
)
) {
Column(
Modifier
.defaultMinSize(minHeight = 72.dp)
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.primary
)
.padding(16.dp)
) {
Text(
text = "Выберите дату"
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()).format(selDate.value)
)
Spacer(modifier = Modifier.size(16.dp))
}
CustomCalendarView(onDateSelected = {
selDate.value = it
})
Spacer(modifier = Modifier.size(8.dp))
Row(
modifier = Modifier
.align(Alignment.End)
.padding(bottom = 16.dp, end = 16.dp)
.background(
color = Color.White,
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
) {
TextButton(
onClick = onDismissRequest
) {
//TODO - hardcode string
Text(
text = "Отмена"
)
}
TextButton(
onClick = {
onDateSelected(selDate.value)
onDismissRequest()
}
) {
//TODO - hardcode string
Text(
text = "Подтвердить"
)
}
}
}
}
} }

View File

@ -7,13 +7,14 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.ulstu.`is`.pmu.common.MyViewModel
import ru.ulstu.`is`.pmu.common.TaskRepository import ru.ulstu.`is`.pmu.common.TaskRepository
import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.database.task.model.Task
class TaskEditViewModel( class TaskEditViewModel(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val taskRepository: TaskRepository private val taskRepository: TaskRepository
) : ViewModel() { ) : MyViewModel() {
var taskUiState by mutableStateOf(TaskUiState()) var taskUiState by mutableStateOf(TaskUiState())
private set private set
@ -21,11 +22,16 @@ class TaskEditViewModel(
private val taskUid: Int = checkNotNull(savedStateHandle["id"]) private val taskUid: Int = checkNotNull(savedStateHandle["id"])
init { init {
viewModelScope.launch {
if (taskUid > 0) { if (taskUid > 0) {
runInScope(
actionSuccess = {
taskUiState = taskRepository.getTask(taskUid) taskUiState = taskRepository.getTask(taskUid)
.toUiState(true) .toUiState(true)
},
actionError = {
taskUiState = TaskUiState()
} }
)
} }
} }
@ -35,10 +41,9 @@ class TaskEditViewModel(
isEntryValid = validateInput(taskDetails) isEntryValid = validateInput(taskDetails)
) )
} }
fun saveTask() {
suspend fun saveTask() {
if (validateInput()) { if (validateInput()) {
if (taskUid > 0) { runInScope ( actionSuccess = { if (taskUid > 0) {
taskRepository.updateTask( taskRepository.updateTask(
taskUiState.taskDetails.toTask(taskUid) taskUiState.taskDetails.toTask(taskUid)
) )
@ -47,6 +52,7 @@ class TaskEditViewModel(
taskUiState.taskDetails.toTask() taskUiState.taskDetails.toTask()
) )
} }
} )
} }
} }
@ -55,7 +61,6 @@ class TaskEditViewModel(
name.isNotBlank() name.isNotBlank()
&& description.isNotBlank() && description.isNotBlank()
&& endDate.isNotBlank() && endDate.isNotBlank()
&& userId > 0
} }
} }
} }

View File

@ -1,4 +1,176 @@
package ru.ulstu.is.pmu.ui.student.list package ru.ulstu.`is`.pmu.ui.task.list
class FavoriteTaskList { import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissState
import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import ru.ulstu.`is`.pmu.common.AppViewModelProvider
import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.ui.navigation.Screen
import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme
@Composable
fun FavoriteTaskList(
navController: NavController,
viewModel: FavoriteTaskListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val taskListUiState = viewModel.taskListUiState.collectAsLazyPagingItems()
Scaffold(
topBar = {}
) { innerPadding ->
FavoriteTaskList(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
taskList = taskListUiState
)
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
private fun SwipeToDelete(
task: Task
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 7.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = String.format("%s%n%s%n%s", task.name, task.description, task.endDate),
modifier = Modifier.weight(1f)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
private fun FavoriteTaskList(
modifier: Modifier = Modifier,
taskList: LazyPagingItems<Task>
) {
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
fun refresh() = refreshScope.launch {
refreshing = true
taskList.refresh()
refreshing = false
}
val state = rememberPullRefreshState(refreshing, ::refresh)
Box(
modifier = modifier.pullRefresh(state)
) {
Column(
modifier = modifier.fillMaxSize()
) {
LazyColumn(modifier = Modifier.padding(all = 10.dp)) {
items(
count = taskList.itemCount,
key = taskList.itemKey(),
contentType = taskList.itemContentType()
) { index ->
val task = taskList[index]
task?.let {
SwipeToDelete(
task = task
)
}
}
}
PullRefreshIndicator(
refreshing, state,
Modifier
.align(CenterHorizontally)
.zIndex(100f)
)
}
}
}
@Composable
private fun TaskListItem(
task: Task, modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = modifier.padding(all = 10.dp)
) {
Text(
text = String.format("%s %s", task.name, task.description)
)
}
}
} }

View File

@ -1,4 +1,15 @@
package ru.ulstu.is.pmu.ui.student.list package ru.ulstu.`is`.pmu.ui.task.list
class FavoriteTaskListViewModel { import androidx.lifecycle.ViewModel
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import ru.ulstu.`is`.pmu.common.MyViewModel
import ru.ulstu.`is`.pmu.common.TaskRepository
import ru.ulstu.`is`.pmu.database.task.model.Task
class FavoriteTaskListViewModel(
private val taskRepository: TaskRepository
) : MyViewModel() {
val taskListUiState: Flow<PagingData<Task>> = taskRepository.getAllFavoriteTasks()
} }

View File

@ -16,11 +16,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -66,13 +68,20 @@ import ru.ulstu.`is`.pmu.common.AppViewModelProvider
import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.database.task.model.Task
import ru.ulstu.`is`.pmu.ui.navigation.Screen import ru.ulstu.`is`.pmu.ui.navigation.Screen
import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.remember
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
@Composable @Composable
fun TaskList( fun TaskList(
navController: NavController, navController: NavController,
viewModel: TaskListViewModel = viewModel(factory = AppViewModelProvider.Factory) viewModel: TaskListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val coroutineScope = rememberCoroutineScope()
val taskListUiState = viewModel.taskListUiState.collectAsLazyPagingItems() val taskListUiState = viewModel.taskListUiState.collectAsLazyPagingItems()
Scaffold( Scaffold(
topBar = {}, topBar = {},
@ -92,21 +101,15 @@ fun TaskList(
.padding(innerPadding) .padding(innerPadding)
.fillMaxSize(), .fillMaxSize(),
taskList = taskListUiState, taskList = taskListUiState,
onClick = { uid: Int ->
val route = Screen.TaskEdit.route.replace("{id}", uid.toString())
navController.navigate(route)
},
onDeleteClick = { task: Task -> onDeleteClick = { task: Task ->
// Обработка удаления задачи // Обработка удаления задачи
coroutineScope.launch {
viewModel.deleteTask(task) viewModel.deleteTask(task)
} taskListUiState.refresh()
}, },
onAddToFavoritesClick = { task: Task -> onAddToFavoritesClick = { task: Task ->
// Обработка добавления задачи в избранное // Обработка добавления задачи в избранное
coroutineScope.launch { if(task.favorite == false) { viewModel.favoriteTask(task) } else viewModel.deletefavoriteTask(task)
viewModel.favoriteTask(task) taskListUiState.refresh()
}
}, },
onEditClick = { task: Task -> onEditClick = { task: Task ->
// Обработка редактирования задачи // Обработка редактирования задачи
@ -145,15 +148,66 @@ fun DismissBackground(dismissState: DismissState) {
} }
} }
fun calculateDaysDifference(endDate: Date, currentDate: Date): Int {
try {
val timeDifference = endDate.time - currentDate.time
return (timeDifference / (24 * 60 * 60 * 1000)).toInt()
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
private fun SwipeToDelete( private fun SwipeToDelete(
task: Task, task: Task,
onClick: (uid: Int) -> Unit,
onDeleteClick: (task: Task) -> Unit, onDeleteClick: (task: Task) -> Unit,
onAddToFavoritesClick: (task: Task) -> Unit, onAddToFavoritesClick: (task: Task) -> Unit,
onEditClick: (task: Task) -> Unit onEditClick: (task: Task) -> Unit
) { ) {
var showDialog by remember { mutableStateOf(false) }
// Добавляем диалоговое окно для подтверждения удаления задачи
if (showDialog) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f)) // Меняем уровень прозрачности и цвет
) {
// Диалоговое окно
AlertDialog(
onDismissRequest = { showDialog = false },
title = {
Text(text = "Удаление задачи")
},
text = {
Text(text = "Вы уверены, что хотите удалить эту задачу?")
},
confirmButton = {
TextButton(
onClick = {
onDeleteClick(task)
showDialog =
false // Закрываем диалоговое окно после подтверждения удаления
}
) {
Text(text = "Да")
}
},
dismissButton = {
TextButton(
onClick = {
showDialog = false // Закрываем диалоговое окно без удаления задачи
}
) {
Text(text = "Отмена")
}
}
)
}
}
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -165,8 +219,35 @@ private fun SwipeToDelete(
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
val currentDateString = SimpleDateFormat("dd.MM.yyyy").format(Date())
val currentDate = SimpleDateFormat("dd.MM.yyyy").parse(currentDateString)
val taskName = task.name
val taskDescription = task.description
val endDate = SimpleDateFormat("dd.MM.yyyy").parse(task.endDate)
val isOverdue = currentDate.after(endDate)
val overdueText = if (isOverdue) {
val daysDifference = calculateDaysDifference(currentDate, endDate)
val daysWord = if (daysDifference < 23) "дня" else "дней"
"Задача просрочена на $daysDifference $daysWord"
} else {
task.endDate
}
Text( Text(
text = String.format("%s %s", task.name, task.description), text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Black)) {
append("$taskName\n$taskDescription\n")
}
if (isOverdue) {
withStyle(style = SpanStyle(color = Color.Red)) {
append(overdueText)
}
} else {
append(overdueText)
}
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@ -177,18 +258,18 @@ private fun SwipeToDelete(
Icon( Icon(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.Edit,
contentDescription = "Редактировать", contentDescription = "Редактировать",
tint = Color.Blue tint = Color.Black
) )
} }
IconButton( IconButton(
onClick = { onDeleteClick(task) }, onClick = { showDialog = true }, // Показываем диалоговое окно при нажатии на кнопку удаления
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = "Удалить", contentDescription = "Удалить",
tint = Color.Red tint = Color.Black
) )
} }
@ -197,8 +278,9 @@ private fun SwipeToDelete(
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
) { ) {
Icon( Icon(
imageVector = Icons.Default.Favorite, imageVector = if (task.favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = "Добавить в избранное" contentDescription = "Добавить в избранное",
tint = Color.Black
) )
} }
} }
@ -210,7 +292,6 @@ private fun SwipeToDelete(
private fun TaskList( private fun TaskList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
taskList: LazyPagingItems<Task>, taskList: LazyPagingItems<Task>,
onClick: (uid: Int) -> Unit,
onDeleteClick: (task: Task) -> Unit, onDeleteClick: (task: Task) -> Unit,
onAddToFavoritesClick: (task: Task) -> Unit, onAddToFavoritesClick: (task: Task) -> Unit,
onEditClick: (task: Task) -> Unit onEditClick: (task: Task) -> Unit
@ -240,7 +321,6 @@ private fun TaskList(
task?.let { task?.let {
SwipeToDelete( SwipeToDelete(
task = task, task = task,
onClick = onClick,
onDeleteClick = onDeleteClick, onDeleteClick = onDeleteClick,
onAddToFavoritesClick = onAddToFavoritesClick, onAddToFavoritesClick = onAddToFavoritesClick,
onEditClick = onEditClick onEditClick = onEditClick
@ -257,23 +337,3 @@ private fun TaskList(
} }
} }
} }
@Composable
private fun TaskListItem(
task: Task, modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = modifier.padding(all = 10.dp)
) {
Text(
text = String.format("%s %s", task.name, task.description)
)
}
}
}

View File

@ -3,17 +3,25 @@ package ru.ulstu.`is`.pmu.ui.task.list
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.paging.PagingData import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ru.ulstu.`is`.pmu.common.MyViewModel
import ru.ulstu.`is`.pmu.common.TaskRepository import ru.ulstu.`is`.pmu.common.TaskRepository
import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.database.task.model.Task
class TaskListViewModel( class TaskListViewModel(
private val taskRepository: TaskRepository private val taskRepository: TaskRepository
) : ViewModel() { ) : MyViewModel() {
val taskListUiState: Flow<PagingData<Task>> = taskRepository.getAllTasks() val taskListUiState: Flow<PagingData<Task>> = taskRepository.getAllDateTasks()
suspend fun deleteTask(task: Task) { fun deleteTask(task: Task) {
taskRepository.deleteTask(task) runInScope ( actionSuccess = {taskRepository.deleteTask(task)})
} }
fun favoriteTask(task: Task) {
runInScope ( actionSuccess = {taskRepository.favoriteTask(task)})
}
fun deletefavoriteTask(task: Task) {
runInScope ( actionSuccess = {taskRepository.deletefavoriteTask(task)})
}
} }

View File

@ -1,16 +1,22 @@
<resources> <resources>
<string name="app_name">pmu-demo</string> <string name="app_name">pmu-demo</string>
<string name="task_firstname">Имя</string> <string name="task_firstname">Название задачи</string>
<string name="task_lastname">Фамилия</string> <string name="task_lastname">Описание задачи</string>
<string name="task_user">Группа</string> <string name="task_user">Пользователь</string>
<string name="task_phone">Телефон</string> <string name="task_phone">Дата окончания</string>
<string name="task_email">e-mail</string> <string name="task_email">e-mail</string>
<string name="task_main_title">Список студентов</string> <string name="task_main_title">Список задач</string>
<string name="task_view_title">Профиль студента</string> <string name="task_view_title">Профиль студента</string>
<string name="task_favorite_view_title">Избранные</string>
<string name="task_empty_description">Записи о студентах отсутствуют</string> <string name="task_empty_description">Записи о студентах отсутствуют</string>
<string name="task_user_not_select">Группа не указана</string> <string name="task_user_not_select">Группа не указана</string>
<string name="task_save_button">Сохранить</string> <string name="task_save_button">Сохранить</string>
<string name="about_title">О нас</string> <string name="about_title">О нас</string>
<string name="back">Назад</string>
<string name="loading">Загрузка…</string>
<string name="report">Отчет</string>
<string name="startDate">Дата начала</string>
<string name="endDate">Дата конца</string>
<string name="about_text"> <string name="about_text">
<p>Это текст <b>о нас</b>!</p>\n\n <p>Это текст <b>о нас</b>!</p>\n\n
<p>Здесь могла быть Ваша реклама!</p>\n\n <p>Здесь могла быть Ваша реклама!</p>\n\n

42
server/.gitignore vendored
View File

@ -1,42 +0,0 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@ -14,11 +14,15 @@
"tasks": [ "tasks": [
{ {
"id": 1, "id": 1,
"name": "123456", "name": "dsads",
"description": "32155", "description": "12312312",
"endDate": "55", "endDate": "02.11.2023"
"favorite": false, },
"userId": 1 {
"id": 3,
"name": "test",
"description": "test",
"endDate": "05.12.2023"
} }
] ]
} }

629
server/package-lock.json generated
View File

@ -7,10 +7,26 @@
"": { "": {
"name": "fake-db", "name": "fake-db",
"version": "1.0.0", "version": "1.0.0",
"dependencies": {
"pdfkit": "^0.14.0"
},
"devDependencies": { "devDependencies": {
"json-server": "0.17.4" "json-server": "0.17.4"
} }
}, },
"node_modules/@swc/helpers": {
"version": "0.3.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz",
"integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@swc/helpers/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -48,12 +64,54 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/array-buffer-byte-length": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
"integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
"dependencies": {
"call-bind": "^1.0.2",
"is-array-buffer": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-flatten": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"dev": true "dev": true
}, },
"node_modules/available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/basic-auth": { "node_modules/basic-auth": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@ -90,6 +148,14 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -103,7 +169,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1", "get-intrinsic": "^1.2.1",
@ -143,6 +208,14 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -278,6 +351,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/debug": { "node_modules/debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -287,11 +365,46 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/deep-equal": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
"integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.5",
"es-get-iterator": "^1.1.3",
"get-intrinsic": "^1.2.2",
"is-arguments": "^1.1.1",
"is-array-buffer": "^3.0.2",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"isarray": "^2.0.5",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.5.1",
"side-channel": "^1.0.4",
"which-boxed-primitive": "^1.0.2",
"which-collection": "^1.0.1",
"which-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deep-equal/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"node_modules/define-data-property": { "node_modules/define-data-property": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dev": true,
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.1", "get-intrinsic": "^1.2.1",
"gopd": "^1.0.1", "gopd": "^1.0.1",
@ -301,6 +414,22 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -320,6 +449,11 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -354,6 +488,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/es-get-iterator": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
"integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
"has-symbols": "^1.0.3",
"is-arguments": "^1.1.1",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.7",
"isarray": "^2.0.5",
"stop-iteration-iterator": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-get-iterator/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -516,6 +674,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/fontkit": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz",
"integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==",
"dependencies": {
"@swc/helpers": "^0.3.13",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"deep-equal": "^2.0.5",
"dfa": "^1.2.0",
"restructure": "^2.0.1",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.3.1",
"unicode-trie": "^2.0.0"
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
"dependencies": {
"is-callable": "^1.1.3"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -538,7 +720,14 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true, "funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -556,7 +745,6 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.2", "function-bind": "^1.1.2",
"has-proto": "^1.0.1", "has-proto": "^1.0.1",
@ -571,7 +759,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"dependencies": { "dependencies": {
"get-intrinsic": "^1.1.3" "get-intrinsic": "^1.1.3"
}, },
@ -585,6 +772,14 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true "dev": true
}, },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
"integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -598,7 +793,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dev": true,
"dependencies": { "dependencies": {
"get-intrinsic": "^1.2.2" "get-intrinsic": "^1.2.2"
}, },
@ -610,7 +804,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -622,7 +815,20 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true, "engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"dependencies": {
"has-symbols": "^1.0.2"
},
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -634,7 +840,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@ -676,6 +881,19 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"node_modules/internal-slot": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
"integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
"dependencies": {
"get-intrinsic": "^1.2.2",
"hasown": "^2.0.0",
"side-channel": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -685,6 +903,85 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
"integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.0",
"is-typed-array": "^1.1.10"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-bigint": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
"dependencies": {
"has-bigints": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
"integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": { "node_modules/is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -694,12 +991,130 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-map": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
"integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-number-object": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
"integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-promise": { "node_modules/is-promise": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
"dev": true "dev": true
}, },
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-set": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
"integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
"integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
"dependencies": {
"call-bind": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-string": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
"integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-symbol": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
"integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
"dependencies": {
"has-symbols": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typed-array": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
"dependencies": {
"which-typed-array": "^1.1.11"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakmap": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
"integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakset": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
"integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@ -754,6 +1169,23 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -940,7 +1372,46 @@
"version": "1.13.1", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"dev": true, "funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.assign": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
"integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
"dependencies": {
"call-bind": "^1.0.5",
"define-properties": "^1.2.1",
"has-symbols": "^1.0.3",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -981,6 +1452,17 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
"dev": true "dev": true
}, },
"node_modules/pdfkit": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.14.0.tgz",
"integrity": "sha512-Hnor8/78jhHm6ONrxWhrqOwAVALlBnFyWOF8sstBZMiqHZgZ5A6RU+Q3yahhw82plxpT7LOfH3b3qcOX6rzMQg==",
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^1.8.1",
"linebreak": "^1.0.2",
"png-js": "^1.0.0"
}
},
"node_modules/pify": { "node_modules/pify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@ -1008,6 +1490,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1060,6 +1547,22 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
"integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"set-function-name": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -1069,6 +1572,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/restructure": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz",
"integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg=="
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -1142,7 +1650,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"dev": true,
"dependencies": { "dependencies": {
"define-data-property": "^1.1.1", "define-data-property": "^1.1.1",
"get-intrinsic": "^1.2.1", "get-intrinsic": "^1.2.1",
@ -1153,6 +1660,19 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/set-function-name": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
"integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
"dependencies": {
"define-data-property": "^1.0.1",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -1163,7 +1683,6 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": { "dependencies": {
"call-bind": "^1.0.0", "call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2", "get-intrinsic": "^1.0.2",
@ -1191,6 +1710,17 @@
"graceful-fs": "^4.1.3" "graceful-fs": "^4.1.3"
} }
}, },
"node_modules/stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
"integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
"dependencies": {
"internal-slot": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -1229,6 +1759,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1251,6 +1786,29 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -1278,6 +1836,53 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/which-boxed-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
"integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
"dependencies": {
"is-bigint": "^1.0.1",
"is-boolean-object": "^1.1.0",
"is-number-object": "^1.0.4",
"is-string": "^1.0.5",
"is-symbol": "^1.0.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-collection": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
"integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
"dependencies": {
"is-map": "^2.0.1",
"is-set": "^2.0.1",
"is-weakmap": "^2.0.1",
"is-weakset": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-typed-array": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
"integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
"dependencies": {
"available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.4",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@ -2,9 +2,10 @@
"name": "fake-db", "name": "fake-db",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"start": "json-server --watch data.json --host 0.0.0.0 -p 8079" "start": "json-server --watch data.json --middlewares ./server.js --host 0.0.0.0 -p 8079"
}, },
"dependencies": { "dependencies": {
"pdfkit": "^0.14.0"
}, },
"devDependencies": { "devDependencies": {
"json-server": "0.17.4" "json-server": "0.17.4"