Compare commits

..
10 Commits
Author SHA1 Message Date
megboyzz c9bdb03d5e lc 2026-09-23 01:18:39 +03:00
megboyzz 50f4102b93 last commit 2026-09-23 01:18:04 +03:00
megboyzz 62b7658fb9 fix bottommsheet layout 2025-02-22 19:58:57 +03:00
megboyzz 193ad2d07a filany refactor code 2025-02-22 19:34:07 +03:00
megboyzz 7d313e1c64 cutout voyager 2025-02-22 16:48:26 +03:00
megboyzz 11cafec847 Реализована функция копирования в буфер обмена 2024-12-02 21:30:19 +03:00
megboyzz 589464e69c shit happens 2024-10-02 23:42:55 +03:00
megboyzz e8ed594b9f stable login and check cookies 2024-09-29 15:33:39 +03:00
megboyzz e32b4feeef main funtionality 2024-09-23 01:05:47 +03:00
megboyzz 7d3d3517c5 Search funcs 2024-09-22 22:44:41 +03:00
88 changed files with 1649 additions and 534 deletions
Regular → Executable
View File
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
+3
View File
@@ -5,6 +5,9 @@
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="DropdownSearchPrev">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+1 -2
View File
@@ -4,14 +4,13 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/app/data" />
<option value="$PROJECT_DIR$/app/domain" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
+8
View File
@@ -37,6 +37,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
@@ -45,6 +49,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
+1 -1
View File
@@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+4
View File
@@ -5,8 +5,12 @@
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>
+76
View File
@@ -0,0 +1,76 @@
kotlin version: 2.0.0
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/megboyzz/workdir/Projects/AndroidStudioProjects/LKSPROFILE/app/src/main/java/ru/megboyzz/lks/profile/ui/components/MainNavigation.kt:34:1: java.lang.IllegalStateException: Zero or multiple overrides found for serialize in FirRegularClassSymbol ru/megboyzz/lks/profile/ui/components/SearchScreen
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:56)
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:180)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
at org.jetbrains.kotlin.fir.pipeline.ConvertToIrKt.runPlatformCheckers(convertToIr.kt:62)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:316)
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:116)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:155)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:50)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:104)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:48)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:453)
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:62)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:676)
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1661)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.IllegalStateException: Zero or multiple overrides found for serialize in FirRegularClassSymbol ru/megboyzz/lks/profile/ui/components/SearchScreen
at org.jetbrains.kotlinx.serialization.compiler.fir.SerializationFirResolveExtension.getFromSupertype(SerializationFirResolveExtension.kt:146)
at org.jetbrains.kotlinx.serialization.compiler.fir.SerializationFirResolveExtension.generateFunctions(SerializationFirResolveExtension.kt:177)
at org.jetbrains.kotlin.fir.scopes.impl.FirGeneratedMemberDeclarationsStorage$CallableStorage.generateMemberFunctions(FirGeneratedScopes.kt:198)
at org.jetbrains.kotlin.fir.scopes.impl.FirGeneratedMemberDeclarationsStorage$CallableStorage.access$generateMemberFunctions(FirGeneratedScopes.kt:175)
at org.jetbrains.kotlin.fir.scopes.impl.FirGeneratedMemberDeclarationsStorage$CallableStorage$special$$inlined$createCache$1.invoke(FirCachesFactory.kt:71)
at org.jetbrains.kotlin.fir.scopes.impl.FirGeneratedMemberDeclarationsStorage$CallableStorage$special$$inlined$createCache$1.invoke(FirCachesFactory.kt:69)
at org.jetbrains.kotlin.fir.caches.FirThreadUnsafeCache.getValue(FirThreadUnsafeCachesFactory.kt:40)
at org.jetbrains.kotlin.fir.scopes.impl.FirGeneratedClassDeclaredMemberScope.processFunctionsByName(FirGeneratedScopes.kt:281)
at org.jetbrains.kotlin.fir.scopes.FirCompositeScope.processFunctionsByName(FirCompositeScope.kt:55)
at org.jetbrains.kotlin.fir.scopes.FirNameAwareCompositeScope.processFunctionsByName(FirCompositeScope.kt:82)
at org.jetbrains.kotlin.fir.scopes.impl.AbstractFirUseSiteMemberScope.collectDeclaredFunctions(AbstractFirUseSiteMemberScope.kt:78)
at org.jetbrains.kotlin.fir.scopes.impl.AbstractFirUseSiteMemberScope.collectFunctions(AbstractFirUseSiteMemberScope.kt:72)
at org.jetbrains.kotlin.fir.scopes.impl.AbstractFirUseSiteMemberScope.processFunctionsByName(AbstractFirUseSiteMemberScope.kt:63)
at org.jetbrains.kotlin.fir.scopes.FirContainingNamesAwareScopeKt.processAllFunctions(FirContainingNamesAwareScope.kt:19)
at org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirMultipleDefaultsInheritedFromSupertypesChecker.check(FirMultipleDefaultsInheritedFromSupertypesChecker.kt:44)
at org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirMultipleDefaultsInheritedFromSupertypesChecker$Regular.check(FirMultipleDefaultsInheritedFromSupertypesChecker.kt:32)
at org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirMultipleDefaultsInheritedFromSupertypesChecker$Regular.check(FirMultipleDefaultsInheritedFromSupertypesChecker.kt:29)
at org.jetbrains.kotlin.fir.analysis.collectors.components.DeclarationCheckersDiagnosticComponent.check(DeclarationCheckersDiagnosticComponent.kt:133)
at org.jetbrains.kotlin.fir.analysis.collectors.components.DeclarationCheckersDiagnosticComponent.visitRegularClass(DeclarationCheckersDiagnosticComponent.kt:57)
at org.jetbrains.kotlin.fir.analysis.collectors.components.DeclarationCheckersDiagnosticComponent.visitRegularClass(DeclarationCheckersDiagnosticComponent.kt:19)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.access$checkElement(AbstractDiagnosticCollectorVisitor.kt:29)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:1349)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:86)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:91)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:29)
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:56)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:37)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1124)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:29)
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
... 32 more
Regular → Executable
View File
Regular → Executable
+15 -18
View File
@@ -2,20 +2,21 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("kapt")
alias(libs.plugins.kotlin.serialization)
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
android {
namespace = "ru.megboyzz.lks.profile"
compileSdk = 34
compileSdk = 36
defaultConfig {
applicationId = "ru.megboyzz.lks.profile"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
minSdk = 29
targetSdk = 36
versionCode = 105
versionName = "1.0.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -51,6 +52,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.foundation.layout.android)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -59,19 +61,14 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
implementation(libs.navigation.compose)
implementation(libs.hilt.android)
kapt(libs.hilt.android.compiler)
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.bottom.sheet.navigator)
implementation(libs.voyager.transitions)
implementation(libs.voyager.hilt)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.room.compiler)
implementation(libs.jsoup)
implementation(libs.kotlinx.serialization.json)
}
-1
View File
@@ -1 +0,0 @@
/build
-45
View File
@@ -1,45 +0,0 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "ru.megboyzz.lks.data"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.jsoup)
}
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,24 +0,0 @@
package ru.megboyzz.lks.data
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("ru.megboyzz.lks.data.test", appContext.packageName)
}
}
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -1,17 +0,0 @@
package ru.megboyzz.lks.data
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
-1
View File
@@ -1 +0,0 @@
/build
-15
View File
@@ -1,15 +0,0 @@
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
testImplementation(libs.junit)
}
-69
View File
@@ -1,69 +0,0 @@
import org.junit.Test
import ru.megboyzz.lks.domain.entities.Student
class EntityTest {
@Test
fun testStudent() {
val student = Student(
surname = "Киц",
name = "Михаил",
patronymic = "Андреевич"
)
assert(student.toString() == "Киц Михаил Андреевич")
}
@Test
fun testStudent2() {
val student = Student(
surname = null,
name = "Михаил",
patronymic = "Андреевич"
)
assert(student.toString() == "Михаил Андреевич")
}
@Test
fun testStudent3() {
val student = Student(
surname = "Киц",
name = null,
patronymic = "Андреевич"
)
println(student)
assert(student.toString() == "Киц Андреевич")
}
@Test
fun testStudent4() {
val student = Student(
surname = "Киц",
name = "Михаил",
patronymic = null
)
assert(student.toString() == "Киц Михаил")
}
@Test
fun testStudent5() {
val student = Student(
surname = null,
name = null,
patronymic = null
)
assert(student.toString().isEmpty())
}
}
Vendored Regular → Executable
View File
Regular → Executable
View File
+105
View File
@@ -0,0 +1,105 @@
{
"ИДО": "Институт дополнительного образования",
"ИМО": "Институт международного образования",
"ИДП": "Институт довузовской подготовки",
"ИТХТ": "Институт тонких химических технологий имени М.В. Ломоносова",
"ИИИ": "Институт искусственного интеллекта",
"БК250": "Базовая кафедра № 250 - математического обеспечения информационных систем",
"ИПТ": "Институт перспективных технологий и индустриального программирования",
"БК235": "Базовая кафедра № 235 - цифровых устройств и систем защиты информации",
"КСТУ": "Кафедра современных технологий управления",
"БК536": "Базовая кафедра № 536 - программного обеспечения систем РЭА",
"ИКДТ": "Институт кибербезопасности и цифровых технологий",
"УПО": "Управление платных образовательных услуг",
"КВМ3": "Кафедра высшей математики-3",
"КФТМ": "Кафедра физики и технической механики",
"УОРТ": "Учебный отдел Института радиотехнических и телекоммуникационных систем",
"БК333": "Базовая кафедра № 333 имени профессора Лобанова Б.С. - систем радиоэлектронной борьбы",
"БК334": "Базовая кафедра № 334 - дальней радиосвязи",
"БК338": "Базовая кафедра № 338 - космической радиоэлектроники",
"КИЭТ": "Кафедра инженерной экологии техносферы",
"КИЯ1": "Кафедра иностранных языков 1",
"КРЭСК": "Кафедра радиоэлектронных систем и комплексов",
"КБПФ": "Кафедра биотехнологии и промышленной фармации",
"КДГП": "Кафедра документоведения, истории государства и права",
"БК340": "Базовая кафедра № 340 - информационных космических систем",
"БК343": "Базовая кафедра № 343 - моделирования радиофизических процессов",
"ЦДО": "Центр дистанционного обучения",
"КФТМ2024": "Кафедра физики и технической механики (от 19.06.2024 № 26-ст)",
"КИТГУ": "Кафедра информационных технологий в государственном управлении",
"КМОСИТ": "Кафедра математического обеспечения и стандартизации информационных технологий",
"КПК": "Колледж программирования и кибербезопасности",
"КВТ": "Кафедра вычислительной техники",
"ИИТ": "Институт информационных технологий",
"КИГ": "Кафедра инженерной графики",
"КЭТ": "Кафедра экономической теории",
"КОИ": "Кафедра общей информатики",
"ККПРЭС": "Кафедра конструирования и производства радиоэлектронных средств",
"КСИ": "Кафедра системной инженерии",
"Аспирантура": "Аспирантура",
"ПОДГ": "Подготовительное отделение для иностранных граждан",
"Ректорат": "Ректорат",
"ИРИ": "Институт радиоэлектроники и информатики",
"ЮО": "Юридический отдел",
"УМУ": "Учебно-методическое управление",
"УК": "Управление кадров",
"ФФГБОУВОС": "Филиал федерального государственного бюджетного образовательного учреждения высшего образования «МИРЭА - Российский технологический университет» в г. Ставрополе",
"УГОЧС": "Управление гражданской обороны и чрезвычайных ситуаций",
"КН": "Кафедра наноэлектроники",
"Бухгалтерия": "Бухгалтерия",
"УОИМО": "Учебный отдел института международного образования",
"СПВ86": "Сектор «Проспект Вернадского, 86»",
"СИАО": "Студенческий информационно-архивный отдел",
"УЭФ": "Управление экономики и финансов",
"УМС": "Управление международных связей",
"УВСР": "Управление по воспитательной и социальной работе",
"ПР": "Первый отдел",
"СГИ": "Служба главного инженера",
"НТБ": "Научно-техническая библиотека",
"УИ": "Управление информатизации",
"КМС": "Кафедра метрологии и стандартизации",
"ССт20": "Сектор «Стромынка, 20»",
"ИАО": "Информационно-аналитический отдел",
"ОСПР": "Отдел судебно-претензионной работы",
"БК140": "Базовая кафедра № 140 - фотоники",
"УД": "Управление делами",
"УМП": "Управление мобилизационной подготовки",
"БК130": "Базовая кафедра № 130 - твердотельной электроники",
"НОЦТЦ": "Научно-образовательный центр «Технологический центр»",
"БК131": "Базовая кафедра № 131 - оптико-физических измерений",
"БК132": "Базовая кафедра № 132 - лазерной техники",
"БК136": "Базовая кафедра № 136 - СВЧ приборов и устройств",
"ФМШ": "Физико-математическая школа",
"КПВВ": "Курсы по подготовке к поступлению в вуз",
"ОМКО": "Отдел менеджмента качества образования",
"ОТСО": "Отдел технических средств обучения",
"ОЗЗИ": "Отдел обеспечения защиты информации",
"ОТК": "Отдел технического контроля",
"Автобаза": "Автобаза",
"Академия ТВ": "Академия ТВ",
"ГИВЦ": "Главный информационно-вычислительный центр",
"УНИР": "Управление научно-исследовательских разработок",
"КДЦ": "Культурно-досуговый центр",
"МНТЦИС": "Межотраслевой научно-технический центр интеллектуальных технологий обработки информации и управления «Интеллектуальные системы»",
"НТЦН": "Научно-технологический центр «Наука»",
"НИЦРСВТ": "Научно-исследовательский центр разработки специальной вычислительной техники",
"НИЦИПМ": "Научно-исследовательский центр «Инновационные полимерные материалы и изделия»",
"ОИТОСБ": "Отдел инженерно-технического обеспечения средств безопасности",
"ЦПИС": "Центр проектирования интегральных схем, устройств наноэлектроники и микросистем",
"ЦЯП": "Центр языковой подготовки",
"ИМПМО": "Институт молодежной политики и международных отношений",
"НИИМФП": "Научно-исследовательский институт медицинской и фармацевтической промышленности",
"ИПЦСНК": "Инженерно-научный производственный центр «Средства неразрушающего контроля» МИРЭА",
"УЦВ": "Учебный центр подготовки водителей",
"ИТП": "Институт технологического предпринимательства",
"СУЛСДФ": "Специализированная учебно-научная лаборатория сверхбыстрой динамики ферроиков",
"УНИИСУ": "Учебно-научный институт информатики и систем управления",
"УМЦИТ": "Учебно-методический центр интерактивных технологий",
"ОКБМИРЭА": "Особое конструкторское бюро МИРЭА",
"ИУС": "Институт учета и статистики",
"НИЦПНТП": "Научно-инжиниринговый центр «Перспективные научно-технические проекты»",
"Наноцентр МИРЭА": "Наноцентр МИРЭА",
"НТЦИРФ": "Научно-технологический центр «Интегральная радиофотоника»",
"РИО": "Редакционно-издательский отдел",
"ПЛМА": "Проблемная лаборатория молекулярной активации"
}
+5 -11
View File
@@ -4,25 +4,19 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import cafe.adriel.voyager.navigator.Navigator
import dagger.hilt.android.AndroidEntryPoint
import ru.megboyzz.lks.profile.ui.screens.SignInScreen
import ru.megboyzz.lks.profile.ui.theme.MainTheme
import ru.megboyzz.lks.profile.ui.components.MainNavHost
import ru.megboyzz.lks.profile.ui.provider.ActivityProvider
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
Navigator(SignInScreen())
MainNavHost()
}
}
}
View File
+16 -9
View File
@@ -1,14 +1,14 @@
package ru.megboyzz.lks.profile.data
import ru.megboyzz.lks.profile.domain.entities.LksRequest
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Connection
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.domain.entities.LksResponse
private typealias Cookies = Map<String, String>
@@ -17,6 +17,7 @@ private fun Connection.dataMap(vararg pairs: Pair<String, String>) = this.data(m
suspend fun signInLks(username: String, password: String) = withContext(Dispatchers.IO) {
val cookies = Jsoup
.connect("https://lk.mirea.ru/auth.php?login=yes")
//.userAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
.dataMap(
"AUTH_FORM" to "Y",
"TYPE" to "AUTH",
@@ -35,33 +36,39 @@ suspend fun signInLks(username: String, password: String) = withContext(Dispatch
return@withContext mapOf<String, String>()
}
suspend fun fakeConnect(cookies: Cookies) = withContext(Dispatchers.IO) {
fun fakeConnect(cookies: Cookies) {
Jsoup
.connect("https://lk.mirea.ru/")
//.userAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
.cookies(cookies)
.referrer("https://lk.mirea.ru/")
.execute()
}
suspend fun getPage(payload: LksRequest, cookies: Cookies, page: Int) = withContext(Dispatchers.IO) {
fun getProfilePage(payload: LksRequest, cookies: Cookies, page: Int): Document {
val sesid = cookies["BITRIX_SM_UIDH"]
fakeConnect(cookies)
Jsoup
val conn = Jsoup
.connect("https://lk.mirea.ru/staff/students/?${payload.getSubmitUrlPath(sesid ?: "")}&PAGEN_1=$page")
.cookies(cookies)
.referrer("https://lk.mirea.ru/")
.execute().parse()
.execute()
val doc = conn.parse()
return doc
}
fun Document.getTotalPages() = kotlin.runCatching {
this.select("body > div.container > div.bx-pagination > div > ul > li:nth-child(6) > a > span").text().toInt()
}.getOrNull() ?: 0
fun Document.findStudentsInPage(): LksResponse {
fun findStudentsInPage(document: Document): LksResponse {
val table = this.select("body > div.container > table > tbody") ?: return LksResponse()
val table = document.select("body > div.container > table > tbody") ?: return LksResponse()
val students = table.select("tr").map {
@@ -88,7 +95,7 @@ fun Document.findStudentsInPage(): LksResponse {
return LksResponse(
students = students,
availablePages = this.getTotalPages()
availablePages = document.getTotalPages()
)
}
View File
+2 -2
View File
@@ -31,8 +31,8 @@ class DataModule {
fun providesDefaultCookies() = mapOf(
"PHPSESSID" to "deleted",
"BITRIX_SM_SOUND_LOGIN_PLAYED" to "Y",
"BITRIX_SM_UIDH" to "00000000000000000000000000000000",
"BITRIX_SM_UIDL" to "ivachev.a.a%40edu.mirea.ru"
"BITRIX_SM_UIDH" to "22f459c5c83cff9cd62c99693b00c361",
"BITRIX_SM_UIDL" to "kits.m.a%40edu.mirea.ru"
)
}
+21 -4
View File
@@ -1,18 +1,22 @@
package ru.megboyzz.lks.profile.data.repositories
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import ru.megboyzz.lks.profile.data.findStudentsInPage
import ru.megboyzz.lks.profile.data.getProfilePage
import ru.megboyzz.lks.profile.data.signInLks
import ru.megboyzz.lks.profile.domain.CookiesPrefs
import ru.megboyzz.lks.profile.domain.entities.LksRequest
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.domain.repositories.LksRepository
import javax.inject.Inject
class LksRepositoryImpl @Inject constructor(
@CookiesPrefs
private val sharedPreferences: SharedPreferences
private val sharedPreferences: SharedPreferences,
) : LksRepository {
private val cookieNamesKey = "cookie_names"
@@ -23,7 +27,16 @@ class LksRepositoryImpl @Inject constructor(
): Map<String, String> = signInLks(username, password)
override suspend fun cookiesIsValid(cookie: Map<String, String>): Boolean {
return true
if(cookie.isEmpty()) return false
val document = getProfilePage(LksRequest(), cookie, 1)
val pages =
document.select("body > div.container > div.bx-pagination > div > ul > li:nth-child(6) > a > span")
.text()
return pages.isNotEmpty()
}
@@ -47,9 +60,13 @@ class LksRepositoryImpl @Inject constructor(
override suspend fun getPage(lksRequest: LksRequest, cookie: Map<String, String>, pageNumber: Int): Result<LksResponse> {
val document = ru.megboyzz.lks.profile.data.getPage(lksRequest, cookie, pageNumber)
Log.i("TEXT", "Its begin doc!")
val document = getProfilePage(lksRequest, cookie, pageNumber)
Log.i("TEXT", "Its begin end!")
val page = document.findStudentsInPage()
Log.i("TEXT", "Its begin!")
val page = findStudentsInPage(document)
Log.i("TEXT", "Its end!")
return if(page == LksResponse())
Result.failure(Exception())
@@ -1,18 +1,18 @@
package ru.megboyzz.lks.profile.data.repositories
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.domain.repositories.StudentRepository
class StudentRepositoryImpl : StudentRepository {
override fun getAllFavStudents(): List<LksResponse> {
override fun getAllFavStudents(): List<LksStudent> {
TODO("Not yet implemented")
}
override fun addStudent(lksResponse: LksResponse) {
override fun likeStudent(lksResponse: LksStudent) {
TODO("Not yet implemented")
}
override fun deleteFormFav(lksResponse: LksResponse) {
override fun deleteFormFav(lksResponse: LksStudent) {
TODO("Not yet implemented")
}
}
View File
+1 -1
View File
@@ -12,5 +12,5 @@ data class LksRequest(
* Метод для получения пути после https://lk.mirea.ru/staff/students? ...
*/
fun getSubmitUrlPath(sesid: String) =
"sessid=$sesid&findname=${student}&findgroup=${group ?: ""}&finddepartment=${department ?: ""}&submit-filter=Найти"
"sessid=$sesid&findname=${student ?: ""}&findgroup=${group ?: ""}&finddepartment=${department ?: ""}&submit-filter=Найти"
}
View File
View File
+3 -1
View File
@@ -2,8 +2,10 @@ package ru.megboyzz.lks.profile.domain.repositories
import ru.megboyzz.lks.profile.domain.entities.LksRequest
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import java.io.Serializable
interface LksRepository {
interface LksRepository: Serializable {
suspend fun signInAndGetCookies(username: String, password: String): Map<String, String>
@@ -1,13 +1,13 @@
package ru.megboyzz.lks.profile.domain.repositories
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.entities.LksStudent
interface StudentRepository {
fun getAllFavStudents(): List<LksResponse>
fun getAllFavStudents(): List<LksStudent>
fun addStudent(lksResponse: LksResponse)
fun likeStudent(lksResponse: LksStudent)
fun deleteFormFav(lksResponse: LksResponse)
fun deleteFormFav(lksResponse: LksStudent)
}
@@ -1,32 +0,0 @@
package ru.megboyzz.lks.profile.ui
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.hilt.ScreenModelFactory
import cafe.adriel.voyager.hilt.ScreenModelFactoryKey
import cafe.adriel.voyager.hilt.ScreenModelKey
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.multibindings.IntoMap
import ru.megboyzz.lks.profile.ui.model.SignInScreenModel
@Module
@InstallIn(ActivityComponent::class)
interface HiltModule {
/*@Binds
@IntoMap
@ScreenModelKey(SignInScreenModel::class)
abstract fun bindSignInScreenModel(
signInScreenModel: SignInScreenModel
) : ScreenModel*/
@Binds
@IntoMap
@ScreenModelFactoryKey(SignInScreenModel.Factory::class)
fun bindSignInScreenModelFactory(
factory: SignInScreenModel.Factory
): ScreenModelFactory
}
+72
View File
@@ -0,0 +1,72 @@
package ru.megboyzz.lks.profile.ui
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlin.concurrent.thread
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Modifier.clickableWithNormalRipple(
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
) = this.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = true),
onClick = onClick,
onLongClick = onLongClick
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Modifier.clickableWithNoEffect(
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
) = this.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onClick,
onLongClick = onLongClick
)
@Composable
fun Int.asPainter() = painterResource(this)
fun Context.copyToClipboard(content: String) {
val clipboardManager = this.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("label", content)
clipboardManager.setPrimaryClip(clip)
Toast.makeText(this, "Текст скопирован в буфер обмена", Toast.LENGTH_SHORT).show()
}
fun Context.goToVK(request: String) {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://vk.com/search?q=$request")
)
this.startActivity(browserIntent)
}
fun Context.getDepartmentDict() : Map<String, String> {
val jsonString = assets.open("dict.json").bufferedReader().use { it.readText() }
return Json.decodeFromString(jsonString)
}
@@ -0,0 +1,95 @@
package ru.megboyzz.lks.profile.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun SimpleDropdownMenu(
expanded: Boolean,
onDismiss: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) = DropdownMenu(
expanded = expanded,
onDismissRequest = onDismiss,
content = content
)
@Composable
fun SimpleDropdownMenuItem(
text: String,
onClick: () -> Unit
) = DropdownMenuItem(
text = { Text(text) },
onClick = onClick
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropdownSelectableSearch(
) {
var expanded by remember { mutableStateOf(false) }
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var selectedOptionText by remember { mutableStateOf(options[0]) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded != expanded }
) {
TextField(
readOnly = true,
modifier = Modifier.clickable {
expanded != expanded
},
value = selectedOptionText,
onValueChange = { },
label = { Text("Label") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {}
) {
options.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(text = selectionOption) },
onClick = {
selectedOptionText = selectionOption
}
)
}
}
}
}
@Preview
@Composable
private fun DropdownSearchPrev() {
Box(Modifier.padding(top =100.dp)) {
DropdownSelectableSearch()
}
}
@@ -0,0 +1,52 @@
package ru.megboyzz.lks.profile.ui.components
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import ru.megboyzz.lks.profile.ui.model.SignInViewModel
import ru.megboyzz.lks.profile.ui.provider.ActivityProvider
import ru.megboyzz.lks.profile.ui.provider.LocalMainActivity
import ru.megboyzz.lks.profile.ui.screens.SearchScreenContent
import ru.megboyzz.lks.profile.ui.screens.SignInScreenContent
/**
* Место где лучше всего применять все CompositionProvider
*/
@Composable
fun MainNavHost() = ActivityProvider {
val navHostController = rememberNavController()
val viewModel by LocalMainActivity.current.viewModels<SignInViewModel>()
val state by viewModel.state.collectAsState()
if(state is SignInViewModel.SignInState.Success) {
navHostController.navigate(SearchScreen)
}
NavHost(
navController = navHostController,
startDestination = SignInScreen
) {
composable<SignInScreen> {
SignInScreenContent()
}
composable<SearchScreen> {
SearchScreenContent()
}
}
}
@Serializable
object SearchScreen
@Serializable
object SignInScreen
@@ -0,0 +1,358 @@
package ru.megboyzz.lks.profile.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import ru.megboyzz.lks.profile.R
import ru.megboyzz.lks.profile.domain.entities.LksRequest
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.domain.entities.Student
import ru.megboyzz.lks.profile.ui.asPainter
import ru.megboyzz.lks.profile.ui.clickableWithNormalRipple
@Composable
fun ContextMenuContainer(
content: @Composable ColumnScope.() -> Unit
) {
Column(
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 8.dp)
.fillMaxWidth()
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(32.dp, 4.dp)
.clip(RoundedCornerShape(80))
.background(MaterialTheme.colorScheme.onBackground)
)
Spacer(Modifier.height(16.dp))
Column (
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.padding(bottom = 12.dp),
content = content
)
}
}
@Preview
@Composable
private fun ContextMenuContainerPrev() {
ContextMenuContainer {
Text("Hello!")
Text("Hello!")
}
}
@Composable
fun SearchTextField(
value: String,
onValueChange: (String) -> Unit,
suggestions: List<String>? = null,
label: String? = null
) {
var expanded by remember { mutableStateOf(false) }
var dropDownWidth by remember { mutableIntStateOf(0) }
val icon = if (expanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown
Column {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { dropDownWidth = it.width },
label = { Text(label ?: "Label") },
trailingIcon = {
if(suggestions != null)
Icon(
icon,
"contentDescription",
Modifier.clickableWithNormalRipple{ expanded = !expanded }
)
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(with(LocalDensity.current){dropDownWidth.toDp()})
) {
suggestions?.forEach { label ->
DropdownMenuItem(
onClick = { onValueChange(label) },
text = { Text(text = label) }
)
}
}
}
}
@Preview
@Composable
fun SearchTextFieldPrev() {
val suggestions = listOf("Item1","Item2","Item3")
var value by remember { mutableStateOf("") }
SearchTextField(
value = value,
onValueChange = { value = it },
suggestions = suggestions
)
}
@Composable
fun SearchContextMenu(
initialState: LksRequest,
onSearch: (LksRequest) -> Unit
) {
var name by remember { mutableStateOf(initialState.student?.name ?: "") }
var surname by remember { mutableStateOf(initialState.student?.patronymic ?: "") }
var family by remember { mutableStateOf(initialState.student?.surname ?: "") }
var group by remember { mutableStateOf(initialState.group ?: "") }
var departamnet by remember { mutableStateOf("") }
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 24.dp)
) {
SearchTextField(
value = family,
onValueChange = {family = it},
label = "Фамилия"
)
SearchTextField(
value = name,
onValueChange = {name = it},
label = "Имя"
)
SearchTextField(
value = surname,
onValueChange = {surname = it},
label = "Отчество"
)
Spacer(Modifier.height(1.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.onBackground)
SearchTextField(
value = group,
onValueChange = {group = it},
label = "Учебная группа"
)
SearchTextField(
value = departamnet,
onValueChange = { departamnet = it },
label = "Подразделение"
)
Button(
shape = OutlinedTextFieldDefaults.shape,
modifier = Modifier.fillMaxWidth(),
onClick = {
val lksRequest = LksRequest(
student = Student(
name = name.ifEmpty { null },
surname = family.ifEmpty { null },
patronymic = surname.ifEmpty { null },
),
group = group.ifEmpty { null },
department = departamnet.ifEmpty { null }
)
onSearch(lksRequest)
//bottomSheetNavigator?.hide()
}
) {
Text("Поиск")
}
}
}
@Preview
@Composable
private fun SearchContextMenuPrev() {
SearchContextMenu(
initialState = LksRequest()
) {
}
}
@Composable
fun ContextMenuButton(
icon: Painter,
label: String,
onClick: () -> Unit
) {
Box(
modifier = Modifier
.padding(0.dp)
.clickableWithNormalRipple(onClick = onClick)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
painter = icon,
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.size(36.dp)
)
Text(text = label)
}
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
)
}
}
}
@Preview
@Composable
fun ContextMenuButtonPrev() {
ContextMenuButton(
icon = R.drawable.vk_ic.asPainter(),
label = "Переидти в VK",
onClick = {}
)
}
@Composable
fun LongPressCardContextMenu(
lksStudent: LksStudent,
onGoToVk: () -> Unit,
onCopyName: (String) -> Unit,
onCopyGroup: (String) -> Unit
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(
"${lksStudent.studentFullName} ${lksStudent.group}"
)
HorizontalDivider(color = MaterialTheme.colorScheme.onBackground)
ContextMenuButton(
icon = R.drawable.vk_ic.asPainter(),
label = "Переидти в VK",
onClick = onGoToVk
)
ContextMenuButton(
icon = rememberVectorPainter(Icons.Outlined.AccountCircle),
label = "Скопировать имя пользователя",
onClick = { onCopyName.invoke(lksStudent.studentFullName ?: "") }
)
ContextMenuButton(
icon = rememberVectorPainter(Icons.Outlined.Person),
label = "Скопировать группу",
onClick = { onCopyGroup.invoke(lksStudent.group ?: "") }
)
}
}
@Preview
@Composable
private fun LongPressCardContextMenuPrev() {
val lksStudent = LksStudent(
studentFullName = "Киц Михаил Андреевич",
group = "ИКБО-03-20"
)
LongPressCardContextMenu(
lksStudent = lksStudent,
onGoToVk = {},
onCopyName = {},
onCopyGroup = {}
)
}
+243 -3
View File
@@ -1,14 +1,254 @@
package ru.megboyzz.lks.profile.ui.components
import android.provider.ContactsContract.Profile
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
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.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.ui.clickableWithNoEffect
import ru.megboyzz.lks.profile.ui.clickableWithNormalRipple
import ru.megboyzz.lks.profile.ui.theme.MainTheme
@Composable
fun ProfileCard(lksStudent: LksStudent) {
fun ProfileCard(
lksStudent: LksStudent,
onLongPress: () -> Unit
) {
var toggled by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.border(
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
shape = RoundedCornerShape(16.dp)
)
.defaultMinSize(minWidth = 240.dp)
.clip(RoundedCornerShape(16.dp))
.clickableWithNormalRipple(
onClick = { toggled = !toggled },
onLongClick = onLongPress,
),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = lksStudent.studentFullName ?: "<Нет имени>"
)
Text(
text = lksStudent.group ?: "<Нет группы>"
)
AnimatedVisibility(toggled) {
Column(
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary
)
Text(
text = lksStudent.department ?: "<Нет подразделения>"
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScaffold(
onFloatingButtonClick: () -> Unit,
onTopClick: () -> Unit = {},
content: @Composable (PaddingValues) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
Scaffold(
topBar = {
Surface(
shadowElevation = 8.dp,
modifier = Modifier.clickableWithNoEffect(onTopClick)
) {
TopAppBar(
title = { Text("LKS Search") },
actions = {
IconButton(onClick = { expanded =! expanded }) {
Icon(Icons.Default.MoreVert, contentDescription = "More options")
}
SimpleDropdownMenu(
expanded = expanded,
onDismiss = { expanded =! expanded },
) {
SimpleDropdownMenuItem("История поиска") {
expanded =! expanded
}
SimpleDropdownMenuItem("Избранное") {
expanded =! expanded
}
}
}
)
}
},
floatingActionButton = {
FloatingActionButton(onFloatingButtonClick) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
}
},
content = content
)
}
@Preview
@Composable
private fun ProfileCardPrev() {
val student = LksStudent(
studentFullName = "Киц Михаил Андреевич",
department = "ИИТ",
group = "ИКБО-03-20"
)
ProfileCard(
lksStudent = student
) { }
}
@Preview
@Composable
private fun Main() {
val student = LksStudent(
studentFullName = "Киц Михаил Андреевич",
department = "ИИТ",
group = "ИКБО-03-20"
)
MainTheme {
MainScaffold(
onFloatingButtonClick = {}
) {
Column(modifier = Modifier.padding(it)) {
ProfileCard(
lksStudent = student
) { }
}
}
}
}
@Composable
fun CardsColumn(
modifier: Modifier = Modifier,
lksStudents: List<LksStudent>,
onLongPress: (LksStudent) -> Unit,
onColumnOverflow: suspend () -> Unit,
) {
val buffer = 1
var isLoading by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
// observe list scrolling
val reachedBottom: Boolean by remember {
derivedStateOf {
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - buffer
}
}
// load more if scrolled to bottom
LaunchedEffect(reachedBottom) {
if (reachedBottom) {
isLoading = true
withContext(Dispatchers.IO) {
onColumnOverflow()
}
isLoading = false
}
}
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LazyColumn(
state = listState,
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(lksStudents) { student ->
ProfileCard(
lksStudent = student,
onLongPress = { onLongPress(student) }
)
}
if(isLoading)
item {
CircularProgressIndicator()
}
}
}
}
@@ -0,0 +1,52 @@
package ru.megboyzz.lks.profile.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LoadingShield(title: String? = null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
if(title != null)
Text(title)
}
}
}
@Composable
fun ErrorShield(title: String? = null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null
)
if(title != null)
Text(title)
}
}
}
@@ -1,56 +0,0 @@
package ru.megboyzz.lks.profile.ui.model
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import ru.megboyzz.lks.profile.domain.DefaultCookies
import ru.megboyzz.lks.profile.domain.entities.LksRequest
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.repositories.LksRepository
import javax.inject.Inject
class SearchScreenModel @Inject constructor(
private val lksRepository: LksRepository,
@DefaultCookies
private val cookies: Map<String, String>
) : ScreenModel {
private val _request = MutableStateFlow(LksRequest())
val request = _request.asStateFlow()
private val _curPageNumber = MutableStateFlow(1)
val curPageNumber = _curPageNumber.asStateFlow()
private val _responseList = MutableStateFlow(LksResponse())
val responseList = _responseList.asStateFlow()
init {
_curPageNumber.onEach {
update()
}
}
fun nextPage() = screenModelScope.launch { _curPageNumber.emit(_curPageNumber.value++) }
private fun update() = screenModelScope.launch {
val result = lksRepository.getPage(_request.value, cookies, curPageNumber.value)
val list = result.getOrElse { LksResponse() }
_responseList.emit(list)
}
fun newSearch() = screenModelScope.launch {
_curPageNumber.emit(1)
update()
}
}
@@ -0,0 +1,82 @@
package ru.megboyzz.lks.profile.ui.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.megboyzz.lks.profile.domain.entities.LksRequest
import ru.megboyzz.lks.profile.domain.entities.LksResponse
import ru.megboyzz.lks.profile.domain.repositories.LksRepository
import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
private val lksRepository: LksRepository,
) : ViewModel() {
private val _request = MutableStateFlow(LksRequest())
val request = _request.asStateFlow()
private val _curPageNumber = MutableStateFlow(1)
val curPageNumber = _curPageNumber.asStateFlow()
private val _responseList = MutableStateFlow(LksResponse())
val responseList = _responseList.asStateFlow()
sealed class UiState(val status: String? = null) {
data object Loading: UiState()
//data object Success: SignInState("Успешный вход")
data class Error(val _status: String) : UiState(_status)
data object Idle: UiState()
}
val state = MutableStateFlow<UiState>(UiState.Idle)
suspend fun nextPage() {
if(_responseList.value.availablePages > _curPageNumber.value) {
_curPageNumber.emit(_curPageNumber.value + 1)
update(_request.value)
}
}
suspend fun resetStudents() {
_curPageNumber.value = 1
_responseList.emit(LksResponse())
}
fun newSearch(request: LksRequest) {
viewModelScope.launch(Dispatchers.IO) {
state.emit(UiState.Loading)
resetStudents()
state.emit(update(request))
}
}
suspend fun update(request: LksRequest): UiState {
_request.emit(request)
val repoCookies = lksRepository.restoreCooke()
val result = lksRepository.getPage(request, repoCookies, curPageNumber.value)
if(result.isFailure)
return UiState.Error("Ошибка :-(")
val list = result.getOrElse { LksResponse() }
val updatedStudents = LksResponse(
students = _responseList.value.students + list.students,
availablePages = list.availablePages
)
_responseList.emit(updatedStudents)
return UiState.Idle
}
}
@@ -1,26 +1,28 @@
package ru.megboyzz.lks.profile.ui.model
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.hilt.ScreenModelFactory
import cafe.adriel.voyager.navigator.Navigator
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import ru.megboyzz.lks.profile.domain.DefaultCookies
import ru.megboyzz.lks.profile.domain.repositories.LksRepository
import ru.megboyzz.lks.profile.ui.screens.SearchScreen
import javax.inject.Inject
class SignInScreenModel @AssistedInject constructor(
@HiltViewModel
class SignInViewModel @Inject constructor(
@ApplicationContext val context: Context,
private val lksRepository: LksRepository,
@Assisted private val navigator: Navigator
) : ScreenModel {
@DefaultCookies
private val defaultCookies: Map<String, String>
) : ViewModel() {
private val _username = MutableStateFlow("")
val username = _username.asStateFlow()
@@ -37,19 +39,40 @@ class SignInScreenModel @AssistedInject constructor(
val state = MutableStateFlow<SignInState>(SignInState.Idle)
private val _loadingScreen = MutableStateFlow(false)
val loadingScreen = _loadingScreen.asStateFlow()
private var cookie: Map<String, String> = mapOf()
init {
private fun makeSuspendToast(text: String) = viewModelScope.launch(Dispatchers.Main) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
screenModelScope.launch {
private var isUseDefaultCookie = false
init {
viewModelScope.launch(Dispatchers.IO) {
state.emit(SignInState.Loading)
cookie = lksRepository.restoreCooke()
if(lksRepository.cookiesIsValid(cookie))
navigator.push(SearchScreen())
if(cookie.isEmpty()) {
isUseDefaultCookie = true
cookie = defaultCookies
}
if(lksRepository.cookiesIsValid(cookie)) {
state.emit(SignInState.Success)
if(isUseDefaultCookie)
makeSuspendToast("Успех с дефолтными куками!")
else
makeSuspendToast("Успех!")
}else {
state.emit(SignInState.Error("Error!"))
makeSuspendToast("Не успех!")
}
}
state.onEach {
if(it == SignInState.Success) navigator.push(SearchScreen())
}.launchIn(screenModelScope)
//if(it == SignInState.Success) navigator.push(SearchScreen())
}.launchIn(viewModelScope)
}
@@ -62,7 +85,7 @@ class SignInScreenModel @AssistedInject constructor(
}
fun signIn() {
screenModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) {
state.emit(SignInState.Loading)
cookie = lksRepository.signInAndGetCookies(_username.value, _password.value)
if(cookie.isEmpty())
@@ -73,10 +96,4 @@ class SignInScreenModel @AssistedInject constructor(
}
}
}
@AssistedFactory
interface Factory : ScreenModelFactory {
fun create(navigator: Navigator): SignInScreenModel
}
}
@@ -0,0 +1,21 @@
package ru.megboyzz.lks.profile.ui.provider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalContext
import ru.megboyzz.lks.profile.MainActivity
val LocalMainActivity = compositionLocalOf<MainActivity> {
error("No MainActivity provided")
}
@Composable
fun ActivityProvider(
content: @Composable () -> Unit
) {
val mainActivity = LocalContext.current as MainActivity
CompositionLocalProvider(LocalMainActivity provides mainActivity) {
content()
}
}
+48 -30
View File
@@ -1,45 +1,63 @@
package ru.megboyzz.lks.profile.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.activity.viewModels
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerIcon.Companion.Text
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import ru.megboyzz.lks.profile.ui.model.SearchScreenModel
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.ui.components.CardsColumn
import ru.megboyzz.lks.profile.ui.components.ErrorShield
import ru.megboyzz.lks.profile.ui.components.LoadingShield
import ru.megboyzz.lks.profile.ui.components.MainScaffold
import ru.megboyzz.lks.profile.ui.model.SearchViewModel
import ru.megboyzz.lks.profile.ui.provider.LocalMainActivity
import ru.megboyzz.lks.profile.ui.screens.sub.LongPressMenu
import ru.megboyzz.lks.profile.ui.screens.sub.SearchBottomSheet
class SearchScreen : Screen {
@Composable
override fun Content() {
@Composable
fun SearchScreenContent() {
val screenModel = getScreenModel<SearchScreenModel>()
val screenModel by LocalMainActivity.current.viewModels<SearchViewModel>()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Spacer(Modifier.height(20.dp))
val navigator = LocalNavigator.current!!
val state by screenModel.state.collectAsState()
val list by screenModel.responseList.collectAsState()
var searchMenuExpanded by remember { mutableStateOf(false) }
var isLongPressedStudent by remember {
mutableStateOf<LksStudent?>(null)
}
MainScaffold(
onFloatingButtonClick = { searchMenuExpanded =! searchMenuExpanded },
onTopClick = { }
) {
when (state) {
is SearchViewModel.UiState.Loading -> LoadingShield("Загрузка челиков")
is SearchViewModel.UiState.Error -> ErrorShield(state.status)
else -> {
CardsColumn(
modifier = Modifier.padding(it),
lksStudents = list.students,
onLongPress = { student -> isLongPressedStudent = student },
onColumnOverflow = {
screenModel.nextPage()
}
)
}
}
}
if(searchMenuExpanded)
SearchBottomSheet { searchMenuExpanded =! searchMenuExpanded }
if(isLongPressedStudent != null)
LongPressMenu(isLongPressedStudent!!) { isLongPressedStudent = null }
}
@@ -1,74 +0,0 @@
package ru.megboyzz.lks.profile.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import ru.megboyzz.lks.profile.ui.model.SignInScreenModel
class SignInScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.current!!
val viewModel = getScreenModel<SignInScreenModel, SignInScreenModel.Factory> { assistedFactory ->
assistedFactory.create(navigator)
}
val username by viewModel.username.collectAsState()
val password by viewModel.password.collectAsState()
val state by viewModel.state.collectAsState()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = username,
onValueChange = { viewModel.setUsername(it) }
)
OutlinedTextField(
value = password,
onValueChange = { viewModel.setPassword(it) }
)
OutlinedButton(
onClick = viewModel::signIn
) {
Text("Войти")
}
when(state) {
is SignInScreenModel.SignInState.Idle -> Text("")
is SignInScreenModel.SignInState.Success -> Text(state.status ?: "")
is SignInScreenModel.SignInState.Loading -> CircularProgressIndicator()
is SignInScreenModel.SignInState.Error -> Text(state.status ?: "Ошибка")
}
}
}
}
}
@@ -0,0 +1,74 @@
package ru.megboyzz.lks.profile.ui.screens
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ru.megboyzz.lks.profile.ui.components.LoadingShield
import ru.megboyzz.lks.profile.ui.model.SignInViewModel
import ru.megboyzz.lks.profile.ui.provider.LocalMainActivity
import ru.megboyzz.lks.profile.ui.theme.MainTheme
@Composable
fun SignInScreenContent() = MainTheme {
val viewModel by LocalMainActivity.current.viewModels<SignInViewModel>()
val username by viewModel.username.collectAsState()
val password by viewModel.password.collectAsState()
val state by viewModel.state.collectAsState()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
when(state) {
is SignInViewModel.SignInState.Loading, SignInViewModel.SignInState.Success -> LoadingShield("Проверка твоих куки файлов :-)")
else -> {
OutlinedTextField(
value = username,
onValueChange = { viewModel.setUsername(it) }
)
OutlinedTextField(
value = password,
onValueChange = { viewModel.setPassword(it) }
)
OutlinedButton(
onClick = viewModel::signIn
) {
Text("Войти")
}
when (state) {
is SignInViewModel.SignInState.Idle -> Text("")
is SignInViewModel.SignInState.Success -> Text(state.status ?: "")
is SignInViewModel.SignInState.Loading -> CircularProgressIndicator()
is SignInViewModel.SignInState.Error -> Text(state.status ?: "Ошибка")
else -> {}
}
}
}
}
}
}
@@ -0,0 +1,50 @@
package ru.megboyzz.lks.profile.ui.screens.sub
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import ru.megboyzz.lks.profile.domain.entities.LksStudent
import ru.megboyzz.lks.profile.ui.components.LongPressCardContextMenu
import ru.megboyzz.lks.profile.ui.copyToClipboard
import ru.megboyzz.lks.profile.ui.goToVK
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LongPressMenu(
lksStudent: LksStudent,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val sheetState = rememberModalBottomSheetState()
val coroutineScope = rememberCoroutineScope()
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss
) {
LongPressCardContextMenu(
lksStudent = lksStudent,
onCopyName = context::copyToClipboard,
onCopyGroup = context::copyToClipboard,
onGoToVk = {
Toast.makeText(context, "Переход на VK", Toast.LENGTH_SHORT).show()
val searchString = lksStudent
.studentFullName
?.split(" ")
?.dropLast(1)
?.joinToString(" ") ?: ""
context.goToVK(searchString)
}
)
}
}
@@ -0,0 +1,76 @@
package ru.megboyzz.lks.profile.ui.screens.sub
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.ime
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
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.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import kotlinx.coroutines.launch
import ru.megboyzz.lks.profile.ui.components.SearchContextMenu
import ru.megboyzz.lks.profile.ui.model.SearchViewModel
import ru.megboyzz.lks.profile.ui.provider.LocalMainActivity
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBottomSheet(onDismiss: () -> Unit) {
val viewModel by LocalMainActivity.current.viewModels<SearchViewModel>()
val request by viewModel.request.collectAsState()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val coroutineScope = rememberCoroutineScope()
val view = LocalView.current
var isImeVisible by remember { mutableStateOf(false) }
// Observe keyboard visibility using ViewTreeObserver
DisposableEffect(Unit) {
val listener = ViewTreeObserver.OnPreDrawListener {
// Check if the IME (keyboard) is visible
isImeVisible = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) == true
true
}
view.viewTreeObserver.addOnPreDrawListener(listener)
onDispose {
view.viewTreeObserver.removeOnPreDrawListener(listener)
}
}
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss,
//windowInsets = WindowInsets.ime,
dragHandle = { BottomSheetDefaults.DragHandle() },
modifier = Modifier.then(
if (isImeVisible)
Modifier.fillMaxHeight(1.0F) // Full height when keyboard is visible
else
Modifier.fillMaxHeight(0.73F) // Partial height when keyboard is hidden
)
) {
SearchContextMenu(request) { lksRequest ->
viewModel.newSearch(lksRequest)
onDismiss()
coroutineScope.launch { sheetState.hide() }
}
}
}
View File
+24 -26
View File
@@ -8,50 +8,48 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
primary = Color(0xFFB0BEC5), // Серый с голубым оттенком
secondary = Color(0xFF90A4AE), // Более мягкий серо-голубой
tertiary = Color(0xFFB39DDB), // Нежный фиолетовый
background = Color(0xFF121212), // Темный фон для контраста
surface = Color(0xFF1E1E1E), // Темная поверхность
onPrimary = Color.Black,
onSecondary = Color.Black,
onTertiary = Color.Black,
onBackground = Color(0xFFECEFF1), // Светлый текст на темном фоне
onSurface = Color(0xFFECEFF1) // Светлый текст на поверхностях
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
primary = Color(0xFF207AEE), // Голубой с пастельным оттенком
secondary = Color(0xFFB3E5FC), // Мягкий голубой
tertiary = Color(0xFFC5CAE9), // Светлый фиолетовый
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
background = Color(0xFFF5F5F5), // Очень светлый фон
surface = Color(0xFFFFFFFF), // Чисто белая поверхность
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
onSecondary = Color.Black,
onTertiary = Color.Black,
onBackground = Color(0xFF212121), // Темный текст на светлом фоне
onSurface = Color(0xFF212121) // Темный текст на поверхностях
)
@Composable
fun MainTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
dynamicColor: Boolean = false, // Отключаем динамические цвета для четкости палитры
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val colorScheme = LightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
}
+3 -4
View File
@@ -14,11 +14,11 @@ val Typography = Typography(
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
),
// Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontWeight = FontWeight.Medium,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
@@ -30,5 +30,4 @@ val Typography = Typography(
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
View File
View File
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,840v-240h80v80h320v80L520,760v80h-80ZM120,760v-80h240v80L120,760ZM280,600v-80L120,520v-80h160v-80h80v240h-80ZM440,520v-80h400v80L440,520ZM600,360v-240h80v80h160v80L680,280v80h-80ZM120,280v-80h400v80L120,280Z"
android:fillColor="#e8eaed"/>
</vector>
+14
View File
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M42,37c0,2.762 -2.238,5 -5,5H11c-2.761,0 -5,-2.238 -5,-5V11c0,-2.762 2.239,-5 5,-5h26c2.762,0 5,2.238 5,5V37z"
android:fillColor="#1976d2"/>
<path
android:pathData="M35.937,18.041c0.046,-0.151 0.068,-0.291 0.062,-0.416C35.984,17.263 35.735,17 35.149,17h-2.618c-0.661,0 -0.966,0.4 -1.144,0.801c0,0 -1.632,3.359 -3.513,5.574c-0.61,0.641 -0.92,0.625 -1.25,0.625C26.447,24 26,23.786 26,23.199v-5.185C26,17.32 25.827,17 25.268,17h-4.649C20.212,17 20,17.32 20,17.641c0,0.667 0.898,0.827 1,2.696v3.623C21,24.84 20.847,25 20.517,25c-0.89,0 -2.642,-3 -3.815,-6.932C16.448,17.294 16.194,17 15.533,17h-2.643C12.127,17 12,17.374 12,17.774c0,0.721 0.6,4.619 3.875,9.101C18.25,30.125 21.379,32 24.149,32c1.678,0 1.85,-0.427 1.85,-1.094v-2.972C26,27.133 26.183,27 26.717,27c0.381,0 1.158,0.25 2.658,2c1.73,2.018 2.044,3 3.036,3h2.618c0.608,0 0.957,-0.255 0.971,-0.75c0.003,-0.126 -0.015,-0.267 -0.056,-0.424c-0.194,-0.576 -1.084,-1.984 -2.194,-3.326c-0.615,-0.743 -1.222,-1.479 -1.501,-1.879C32.062,25.36 31.991,25.176 32,25c0.009,-0.185 0.105,-0.361 0.249,-0.607C32.223,24.393 35.607,19.642 35.937,18.041z"
android:fillColor="#fff"
tools:ignore="VectorPath" />
</vector>
View File
View File
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 982 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File
View File
View File
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
View File
Regular → Executable
+2 -1
View File
@@ -5,5 +5,6 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.android.library) apply false
id("com.google.dagger.hilt.android") version "2.51.1" apply false
id("com.google.devtools.ksp") version "2.2.0-2.0.2" apply false
id("com.google.dagger.hilt.android") version "2.57" apply false
}
Regular → Executable
View File
Regular → Executable
+22 -21
View File
@@ -1,21 +1,25 @@
[versions]
agp = "8.5.1"
hiltAndroidCompiler = "2.51.1"
jsoup = "1.18.1"
kotlin = "2.0.0"
coreKtx = "1.13.1"
agp = "8.11.1"
hiltAndroidCompiler = "2.57"
jsoup = "1.21.1"
kotlin = "2.2.0"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.4"
activityCompose = "1.9.1"
composeBom = "2024.04.01"
jetbrainsKotlinJvm = "2.0.0"
appcompat = "1.7.0"
material = "1.12.0"
voyagerVersion = "1.1.0-beta02"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.2"
activityCompose = "1.10.1"
composeBom = "2025.07.00"
jetbrainsKotlinJvm = "2.2.0"
composeNavigation = "2.9.3"
room_version = "2.7.2"
serialization = "1.9.0"
foundationLayoutAndroid = "1.8.3"
[libraries]
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
@@ -33,13 +37,9 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
voyager-bottom-sheet-navigator = { module = "cafe.adriel.voyager:voyager-bottom-sheet-navigator", version.ref = "voyagerVersion" }
voyager-hilt = { module = "cafe.adriel.voyager:voyager-hilt", version.ref = "voyagerVersion" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyagerVersion" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyagerVersion" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyagerVersion" }
navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "composeNavigation" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization"}
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundationLayoutAndroid" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -47,4 +47,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Vendored Regular → Executable
View File
Vendored Regular → Executable
+2 -2
View File
@@ -1,6 +1,6 @@
#Wed Aug 21 19:37:41 MSK 2024
#Fri Aug 01 15:44:12 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Regular → Executable
View File
Regular → Executable
-2
View File
@@ -21,5 +21,3 @@ dependencyResolutionManagement {
rootProject.name = "LKS PROFILE"
include(":app")
include(":app:domain")
include(":app:data")