Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a775c2799c
|
|||
|
3d61474774
|
|||
|
ae08758277
|
|||
|
26c4efedc7
|
|||
|
937c6104d1
|
|||
|
31c81021ce
|
|||
|
82d62132c0
|
|||
|
1a1b3677f9
|
|||
|
790a5fabcd
|
|||
|
1286860d55
|
|||
|
654acf1b77
|
|||
|
0cad16ac74
|
|||
|
0c57d0b7e7
|
|||
|
bc7e2b4100
|
|||
|
7f613a2106
|
|||
|
99effd1e52
|
|||
|
764eedc837
|
|||
|
90d1f69f0d
|
|||
|
7bfcb3d3c9
|
|||
|
57bca56d2b
|
|||
|
ad8ef6dca8
|
|||
|
33959936d6
|
|||
|
e9a200d543
|
|||
|
282c93b5d4
|
|||
| c63ca8c1da | |||
| 4c1a9c8765 | |||
| 5b6660aa8d | |||
| 46987c6fa5 | |||
| 3138afb0de | |||
| 455a3cae4c | |||
| 172f997af6 | |||
| dd88d4a81a | |||
| 8b57881e28 | |||
| 9e1e64f3fc | |||
| 83759f5a37 | |||
| da7498bf02 | |||
| a417d74154 | |||
| c09947865d | |||
| d4bb804f24 | |||
| fd49b30efb | |||
| 6e85ca4bdc | |||
| 3cf7885d9e | |||
| 0d4ba49111 | |||
| 6efb5998ae | |||
| 284e841e6c | |||
| 524bab9d6e | |||
| 983249091a |
@@ -1,15 +1,11 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
secrets.properties
|
||||
@@ -1,3 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@@ -1,123 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="GreetingPreview">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="GreetingPreview2">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="LoginActivity">
|
||||
<State />
|
||||
</entry>
|
||||
<entry key="app">
|
||||
<State />
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<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" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,9 +0,0 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
<UNN HUB - unofficial app providing schedule, news and document access for students>
|
||||
Copyright (C) 2025 Gleb Zaharov
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,70 @@
|
||||
# NOTICE
|
||||
|
||||
This project incorporates components from the following third-party libraries:
|
||||
|
||||
## AndroidX Libraries
|
||||
- **androidx.*** (Core, Room, Navigation, Compose, etc.)
|
||||
Copyright The Android Open Source Project
|
||||
Licensed under Apache License 2.0
|
||||
[https://developer.android.com](https://developer.android.com)
|
||||
|
||||
## Coil
|
||||
- **io.coil-kt:coil-compose**
|
||||
Copyright Coil Contributors
|
||||
Licensed under Apache License 2.0
|
||||
[https://github.com/coil-kt/coil](https://github.com/coil-kt/coil)
|
||||
|
||||
## Compose Calendar
|
||||
- **com.kizitonwose.calendar:compose**
|
||||
Copyright Kizito Nwose
|
||||
Licensed under MIT License
|
||||
[https://github.com/kizitonwose/Calendar](https://github.com/kizitonwose/Calendar)
|
||||
|
||||
## Desugar JDK Libs
|
||||
- **com.android.tools:desugar_jdk_libs**
|
||||
Copyright Google LLC
|
||||
Licensed under Apache License 2.0
|
||||
|
||||
## Ktor
|
||||
- **io.ktor:ktor-***
|
||||
Copyright JetBrains s.r.o.
|
||||
Licensed under Apache License 2.0
|
||||
[https://ktor.io](https://ktor.io)
|
||||
|
||||
## Material Components
|
||||
- **com.google.android.material:material**
|
||||
Copyright Google LLC
|
||||
Licensed under Apache License 2.0
|
||||
|
||||
## Splitties
|
||||
- **com.louiscad.splitties:***
|
||||
Copyright Louis CAD
|
||||
Licensed under Apache License 2.0
|
||||
[https://github.com/LouisCAD/Splitties](https://github.com/LouisCAD/Splitties)
|
||||
|
||||
## Secrets Gradle Plugin
|
||||
- **com.google.android.libraries.mapsplatform.secrets-gradle-plugin**
|
||||
Copyright Google LLC
|
||||
Licensed under Apache License 2.0
|
||||
|
||||
## Sentry Android
|
||||
- **io.sentry:sentry-android**
|
||||
Copyright Sentry and contributors
|
||||
Licensed under MIT License
|
||||
[https://github.com/getsentry/sentry-java](https://github.com/getsentry/sentry-java)
|
||||
|
||||
## Build Tools
|
||||
- **Gradle Plugins** (Android, Kotlin)
|
||||
Copyright Google LLC / JetBrains s.r.o.
|
||||
Licensed under Apache License 2.0
|
||||
|
||||
---
|
||||
|
||||
### License Notices
|
||||
This product includes software developed by:
|
||||
- The Android Open Source Project (AOSP)
|
||||
- JetBrains s.r.o.
|
||||
- Google LLC
|
||||
- And other contributors listed above
|
||||
|
||||
Full license texts are available in the [LICENSE](LICENSE) file and respective library repositories.
|
||||
@@ -1,16 +1,31 @@
|
||||
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
|
||||
id("com.google.devtools.ksp")
|
||||
id("io.sentry.android.gradle") version "5.5.0"
|
||||
}
|
||||
|
||||
secrets {
|
||||
propertiesFileName = "secrets.properties"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "ru.sweetbread.unn"
|
||||
compileSdk = 34
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.sweetbread.unn"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
@@ -18,24 +33,53 @@ android {
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
val secretProperties = Properties().apply {
|
||||
val secretFile = rootProject.file("secrets.properties")
|
||||
if (secretFile.exists())
|
||||
secretFile.inputStream().use { load(it) }
|
||||
else
|
||||
println("Warning: secrets.properties not found!")
|
||||
}
|
||||
manifestPlaceholders["sentry_url"] = secretProperties.getProperty("SENTRY_URL")!!
|
||||
|
||||
// javaCompileOptions {
|
||||
// annotationProcessorOptions {
|
||||
// arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
resValue("string", "app_name", "@string/app_name_reg")
|
||||
manifestPlaceholders["sentry_env"] = "production"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix =
|
||||
"-debug+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}"
|
||||
resValue("string", "app_name", "@string/app_name_dev")
|
||||
manifestPlaceholders["sentry_env"] = "debug"
|
||||
}
|
||||
create("beta") {
|
||||
versionNameSuffix =
|
||||
"-beta+${LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)}"
|
||||
resValue("string", "app_name", "@string/app_name_beta")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
manifestPlaceholders["sentry_env"] = "beta"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
@@ -47,10 +91,22 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_1_8
|
||||
}
|
||||
}
|
||||
|
||||
base {
|
||||
archivesName = "ru.sweetbread.unn-v${android.defaultConfig.versionCode}(${android.defaultConfig.versionName})"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.5.0")
|
||||
implementation(libs.androidx.material.icons.core.android)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
@@ -71,16 +127,21 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation("com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl:3.0.0")
|
||||
implementation(libs.splitties.base)
|
||||
implementation(libs.splitties.room)
|
||||
|
||||
implementation("com.kizitonwose.calendar:compose:2.5.0")
|
||||
implementation(libs.sentry)
|
||||
|
||||
}
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "ru.sweetbread.unn",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "ru.sweetbread.unn-v1(1.0)-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/ru.sweetbread.unn-v1(1.0)-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/ru.sweetbread.unn-v1(1.0)-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 26
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,20 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".UNNApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.UNN"
|
||||
tools:targetApi="31">
|
||||
|
||||
<meta-data
|
||||
android:name="io.sentry.dsn"
|
||||
android:value="${sentry_url}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.environment"
|
||||
android:value="${sentry_env}" />
|
||||
<meta-data
|
||||
android:name="io.sentry.send-default-pii"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.sample-rate"
|
||||
android:value="1.0" />
|
||||
<meta-data
|
||||
android:name="io.sentry.traces.user-interaction.enable"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-screenshot"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.attach-view-hierarchy"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="io.sentry.session-replay.on-error-sample-rate"
|
||||
android:value="1.0" />
|
||||
<meta-data
|
||||
android:name="io.sentry.session-replay.session-sample-rate"
|
||||
android:value="0.1" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.layout.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.UNN" />
|
||||
<activity
|
||||
android:name=".ui.layout.LoginActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.UNN">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -22,10 +58,6 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.layout.LoginActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.UNN"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
|
||||
class UNNApp : Application() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.api
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.sweetbread.unn.db.cacheSchedule
|
||||
import ru.sweetbread.unn.db.loadSchedule
|
||||
import ru.sweetbread.unn.ui.layout.LoginData
|
||||
import java.time.LocalDate
|
||||
|
||||
|
||||
lateinit var PHPSESSID: String
|
||||
lateinit var CSRF: String
|
||||
lateinit var ME: User
|
||||
|
||||
|
||||
/**
|
||||
* Authorize user by [login] and [password]
|
||||
*
|
||||
* Also defines local vars [PHPSESSID] and [ME]
|
||||
*/
|
||||
suspend fun auth(
|
||||
login: String = LoginData.login,
|
||||
password: String = LoginData.password,
|
||||
forced: Boolean = false
|
||||
): Boolean {
|
||||
if (!forced) {
|
||||
if (::PHPSESSID.isInitialized and ::ME.isInitialized)
|
||||
return true
|
||||
}
|
||||
val id = getToken(login, password)
|
||||
if (id != null) {
|
||||
PHPSESSID = id
|
||||
getMyself(login)
|
||||
getCSRF()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Save info about current [User] in memory
|
||||
*/
|
||||
private suspend fun getMyself(login: String) = coroutineScope {
|
||||
val idDeferred = async { getId(login) }
|
||||
val userDeferred = async { getUser() }
|
||||
|
||||
val id = idDeferred.await()
|
||||
val user = userDeferred.await()
|
||||
|
||||
ME = User(
|
||||
unnId = id,
|
||||
userId = user.userId,
|
||||
type = Type.student,
|
||||
email = user.email,
|
||||
nameRu = user.nameRu,
|
||||
nameEn = user.nameEn,
|
||||
isMale = user.isMale,
|
||||
birthday = user.birthday,
|
||||
avatar = user.avatar
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getScheduleDay(
|
||||
type: Type = ME.type,
|
||||
id: Int = ME.unnId!!,
|
||||
date: LocalDate
|
||||
): ArrayList<ScheduleUnit> {
|
||||
|
||||
if ((type == ME.type) and (id == ME.unnId!!)) {
|
||||
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
|
||||
Log.d("Schedule", schedule.joinToString())
|
||||
if (schedule.isNotEmpty())
|
||||
return schedule
|
||||
}
|
||||
|
||||
return getSchedule(type, id, date, date)
|
||||
}
|
||||
|
||||
suspend fun getSchedule(
|
||||
type: Type = ME.type,
|
||||
id: Int = ME.unnId!!,
|
||||
start: LocalDate,
|
||||
finish: LocalDate
|
||||
): ArrayList<ScheduleUnit> {
|
||||
val schedule = downloadSchedule(type, id, start, finish)
|
||||
|
||||
if ((type == ME.type) and (id == ME.unnId!!))
|
||||
cacheSchedule(schedule)
|
||||
|
||||
return schedule
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.api
|
||||
|
||||
import android.util.Log
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.android.Android
|
||||
import io.ktor.client.plugins.HttpRequestRetry
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.cache.HttpCache
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.request.forms.submitForm
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.parameters
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import ru.sweetbread.unn.db.cacheUser
|
||||
import ru.sweetbread.unn.db.loadUserByBitrixId
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
|
||||
const val portalURL = "https://portal.unn.ru"
|
||||
const val ruzapiURL = "$portalURL/ruzapi"
|
||||
const val vuzapiURL = "$portalURL/bitrix/vuz/api"
|
||||
const val prtl2URL = "$portalURL/portal2/api"
|
||||
const val restURL = "$portalURL/rest"
|
||||
|
||||
|
||||
val client = HttpClient(Android) {
|
||||
install(HttpCache)
|
||||
install(Logging) {
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
Log.i("Ktor", message)
|
||||
}
|
||||
}
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
connectTimeoutMillis = 5000
|
||||
}
|
||||
install(HttpRequestRetry) {
|
||||
retryOnException(maxRetries = 3, retryOnTimeout = true)
|
||||
exponentialDelay()
|
||||
modifyRequest { request ->
|
||||
request.headers.append("x-retry-count", retryCount.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getToken(login: String, password: String): String? {
|
||||
val r = client.submitForm("$portalURL/auth/?login=yes",
|
||||
formParameters = parameters {
|
||||
append("AUTH_FORM", "Y")
|
||||
append("TYPE", "AUTH")
|
||||
append("backurl", "/")
|
||||
append("USER_LOGIN", login)
|
||||
append("USER_PASSWORD", password)
|
||||
}
|
||||
)
|
||||
|
||||
if (r.status.value == 302)
|
||||
return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun getId(login: String): Int {
|
||||
return JSONObject(client.get("$ruzapiURL/studentinfo/") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
parameter("uns", login.drop(1))
|
||||
}.bodyAsText()).getString("id").toInt()
|
||||
}
|
||||
|
||||
suspend fun getUser(userId: Int? = null): User {
|
||||
// WARNING: trailing / is important, 'cuz API devs are eating shit
|
||||
// TODO: make up another way to get unnId: this is not useful for lectures
|
||||
val json = JSONObject(
|
||||
client.get("$vuzapiURL/user/${userId ?: ""}") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
}.bodyAsText()
|
||||
)
|
||||
|
||||
return User(
|
||||
unnId = null,
|
||||
userId = json.getInt("id"),
|
||||
type = if (json.getJSONArray("profiles").getJSONObject(0)
|
||||
.getString("type") == "employee"
|
||||
) Type.employee else Type.student,
|
||||
email = json.getString("email"),
|
||||
nameRu = json.getString("fullname"),
|
||||
nameEn = json.getString("fullname_en"),
|
||||
isMale = json.getString("sex") == "M",
|
||||
birthday = Instant
|
||||
.parse(json.getString("birthdate"))
|
||||
.atZone(ZoneId.of("Europe/Moscow"))
|
||||
.toLocalDate(),
|
||||
avatar = json.getJSONObject("photo").let {
|
||||
AvatarSet(
|
||||
it.getString("orig"),
|
||||
it.getString("thumbnail"),
|
||||
it.getString("small"),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun downloadSchedule(
|
||||
type: Type = ME.type,
|
||||
id: Int = ME.unnId!!,
|
||||
start: LocalDate,
|
||||
finish: LocalDate
|
||||
): ArrayList<ScheduleUnit> {
|
||||
val unnDatePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
|
||||
val r = client.get("$ruzapiURL/schedule/${type.name}/$id") {
|
||||
parameter("start", start.format(unnDatePattern))
|
||||
parameter("finish", finish.format(unnDatePattern))
|
||||
parameter("lng", "1")
|
||||
}
|
||||
|
||||
val json = JSONArray(r.bodyAsText())
|
||||
val out = arrayListOf<ScheduleUnit>()
|
||||
for (i in 0 until json.length()) {
|
||||
val unit = json.getJSONObject(i)
|
||||
val lecturesJson = unit.getJSONArray("listOfLecturers")
|
||||
val lecturers = arrayListOf<Lecturer>()
|
||||
|
||||
for (j in 0 until lecturesJson.length()) {
|
||||
val lecturer = lecturesJson.getJSONObject(j)
|
||||
lecturers.add(
|
||||
Lecturer(
|
||||
name = lecturer.getString("lecturer"),
|
||||
email = lecturer.getString("lecturerEmail"),
|
||||
unnId = lecturer.getInt("lecturerOid"),
|
||||
uid = lecturer.getString("lecturerUID"),
|
||||
rank = when (lecturer.getString("lecturer_rank")) {
|
||||
"АССИСТ" -> LecturerRank.Assistant
|
||||
"СТПРЕП" -> LecturerRank.SLecturer
|
||||
"ДОЦЕНТ" -> LecturerRank.AProfessor
|
||||
else -> LecturerRank.Lecturer
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
out.add(
|
||||
ScheduleUnit(
|
||||
oid = unit.getInt("lessonOid"),
|
||||
auditorium = Auditorium(
|
||||
name = unit.getString("auditorium"),
|
||||
oid = unit.getInt("auditoriumOid"),
|
||||
floor = unit.getInt("auditoriumfloor"),
|
||||
building = Building(
|
||||
name = unit.getString("building"),
|
||||
gid = unit.getInt("buildingGid"),
|
||||
oid = unit.getInt("buildingOid")
|
||||
)
|
||||
),
|
||||
date = LocalDate.parse(unit.getString("date"), unnDatePattern),
|
||||
discipline = Discipline(
|
||||
name = unit.getString("discipline"),
|
||||
oid = unit.getInt("disciplineOid"),
|
||||
type = unit.getInt("disciplinetypeload")
|
||||
),
|
||||
kindOfWork = KindOfWork(
|
||||
name = unit.getString("kindOfWork"),
|
||||
complexity = unit.getInt("kindOfWorkComplexity"),
|
||||
oid = unit.getInt("kindOfWorkOid"),
|
||||
uid = unit.getString("kindOfWorkUid")
|
||||
),
|
||||
lecturers = lecturers,
|
||||
stream = unit.getString("stream"),
|
||||
begin = LocalTime.parse(
|
||||
unit.getString("beginLesson"),
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
),
|
||||
end = LocalTime.parse(
|
||||
unit.getString("endLesson"),
|
||||
DateTimeFormatter.ofPattern("HH:mm")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
suspend fun getCSRF() {
|
||||
val r = client.get("$restURL/log.blogpost.get") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
parameter("sessid", "")
|
||||
}
|
||||
CSRF = JSONObject(r.bodyAsText()).getString("sessid")
|
||||
}
|
||||
|
||||
suspend fun getUserByBitrixId(id: Int): User {
|
||||
withContext(Dispatchers.IO) {
|
||||
loadUserByBitrixId(id)
|
||||
}?.let { return it }
|
||||
|
||||
val userId = JSONObject(client.get("$vuzapiURL/user/bx/$id") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
}.bodyAsText()).getInt("id")
|
||||
|
||||
getUser(userId).let { user ->
|
||||
withContext(Dispatchers.IO) {
|
||||
cacheUser(user)
|
||||
}
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBlogposts(): ArrayList<Post> {
|
||||
val r = client.get("$prtl2URL/news.php") {
|
||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||
header("x-bitrix-sessid-token", CSRF)
|
||||
}
|
||||
val result = JSONArray(r.bodyAsText())
|
||||
|
||||
val out = arrayListOf<Post>()
|
||||
for (i in 0 until result.length()) {
|
||||
val el = result.getJSONObject(i)
|
||||
out.add(
|
||||
Post(
|
||||
id = el.getString("id").toInt(),
|
||||
authorId = el.getJSONObject("author").getInt("id").toInt(),
|
||||
enableComments = true, // FIXME: Delete the field or get correct value
|
||||
numComments = el.getString("commentsnum").toInt(),
|
||||
date = LocalDateTime.parse(
|
||||
el.getString("time"),
|
||||
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
|
||||
),
|
||||
content = el.getString("fulltext")
|
||||
)
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.api
|
||||
|
||||
import ru.sweetbread.unn.R
|
||||
import splitties.resources.appStr
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
enum class Type(val s: String) {
|
||||
student(appStr(R.string.student)),
|
||||
group("group"),
|
||||
lecturer(appStr(R.string.lecturer)),
|
||||
auditorium("auditorium"),
|
||||
employee(appStr(R.string.employee))
|
||||
}
|
||||
|
||||
enum class LecturerRank(val id: Int) {
|
||||
Assistant(R.string.assistant),
|
||||
Lecturer(R.string.lecturer),
|
||||
SLecturer(R.string.slecturer),
|
||||
AProfessor(R.string.aprofessor)
|
||||
}
|
||||
|
||||
class ScheduleUnit(
|
||||
val oid: Int,
|
||||
val auditorium: Auditorium,
|
||||
val date: LocalDate,
|
||||
val discipline: Discipline,
|
||||
val kindOfWork: KindOfWork,
|
||||
val lecturers: ArrayList<Lecturer>,
|
||||
val stream: String,
|
||||
val begin: LocalTime,
|
||||
val end: LocalTime
|
||||
)
|
||||
|
||||
class Auditorium(
|
||||
val name: String,
|
||||
val oid: Int,
|
||||
val floor: Int,
|
||||
val building: Building
|
||||
)
|
||||
|
||||
class Building(
|
||||
val name: String,
|
||||
val gid: Int,
|
||||
val oid: Int
|
||||
)
|
||||
|
||||
class Discipline(
|
||||
val name: String,
|
||||
val oid: Int,
|
||||
val type: Int
|
||||
)
|
||||
|
||||
class KindOfWork(
|
||||
val name: String,
|
||||
val oid: Int,
|
||||
val uid: String,
|
||||
val complexity: Int
|
||||
)
|
||||
|
||||
class Lecturer(
|
||||
val name: String,
|
||||
val rank: LecturerRank,
|
||||
val email: String,
|
||||
val unnId: Int,
|
||||
val uid: String
|
||||
)
|
||||
|
||||
class User(
|
||||
val unnId: Int?,
|
||||
val userId: Int,
|
||||
val type: Type,
|
||||
val email: String,
|
||||
val nameRu: String,
|
||||
val nameEn: String,
|
||||
val isMale: Boolean,
|
||||
val birthday: LocalDate,
|
||||
val avatar: AvatarSet
|
||||
)
|
||||
|
||||
class Post(
|
||||
val id: Int,
|
||||
val authorId: Int,
|
||||
val enableComments: Boolean,
|
||||
val numComments: Int,
|
||||
val date: LocalDateTime,
|
||||
val content: String
|
||||
)
|
||||
|
||||
class AvatarSet(
|
||||
val original: String,
|
||||
val thumbnail: String,
|
||||
val small: String
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
val cacheDb = roomDb<AppDatabase>(name = "cache") {
|
||||
fallbackToDestructiveMigration(dropAllTables = true)
|
||||
}
|
||||
|
||||
@Database(entities = [
|
||||
UserDB::class,
|
||||
BuildingDB::class,
|
||||
AuditoriumDB::class,
|
||||
DisciplineDB::class,
|
||||
KindOfWorkDB::class,
|
||||
LecturerDB::class,
|
||||
ScheduleUnitDB::class], version = 1)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): UserDao
|
||||
abstract fun scheduleDao(): ScheduleItemDao
|
||||
abstract fun auditoriumDao(): AuditoriumDao
|
||||
abstract fun buildingDao(): BuildingDao
|
||||
abstract fun disciplineDao(): DisciplineDao
|
||||
abstract fun kindOfWorkDao(): KindOfWorkDao
|
||||
abstract fun lecturerDao(): LecturerDao
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.db
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import ru.sweetbread.unn.api.Auditorium
|
||||
import ru.sweetbread.unn.api.Building
|
||||
import ru.sweetbread.unn.api.Discipline
|
||||
import ru.sweetbread.unn.api.KindOfWork
|
||||
import ru.sweetbread.unn.api.Lecturer
|
||||
import ru.sweetbread.unn.api.LecturerRank
|
||||
import ru.sweetbread.unn.api.ScheduleUnit
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Entity
|
||||
data class BuildingDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val gid: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface BuildingDao {
|
||||
@Query("SELECT * FROM buildingdb WHERE oid = :oid LIMIT 1")
|
||||
fun get(oid: Int): BuildingDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(building: BuildingDB)
|
||||
|
||||
@Delete
|
||||
fun delete(building: BuildingDB)
|
||||
}
|
||||
|
||||
fun cacheBuilding(building: Building) {
|
||||
cacheDb.buildingDao().insert(
|
||||
BuildingDB(
|
||||
building.oid,
|
||||
building.name,
|
||||
building.gid,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadBuilding(oid: Int): Building? {
|
||||
return cacheDb.buildingDao().get(oid)?.let {
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) > LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.buildingDao().delete(it)
|
||||
return null
|
||||
}
|
||||
Building(
|
||||
it.name,
|
||||
it.gid,
|
||||
it.oid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class AuditoriumDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val floor: Int?,
|
||||
@ColumnInfo val buildingOid: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface AuditoriumDao {
|
||||
@Query("SELECT * FROM auditoriumdb WHERE oid = :oid LIMIT 1")
|
||||
fun get(oid: Int): AuditoriumDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(auditorium: AuditoriumDB)
|
||||
|
||||
@Delete
|
||||
fun delete(auditorium: AuditoriumDB)
|
||||
}
|
||||
|
||||
fun cacheAuditorium(auditorium: Auditorium) {
|
||||
cacheBuilding(auditorium.building)
|
||||
cacheDb.auditoriumDao().insert(
|
||||
AuditoriumDB(
|
||||
auditorium.oid,
|
||||
auditorium.name,
|
||||
auditorium.floor,
|
||||
auditorium.building.oid,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadAuditorium(oid: Int): Auditorium? {
|
||||
return cacheDb.auditoriumDao().get(oid)?.let {
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) > LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.auditoriumDao().delete(it)
|
||||
return null
|
||||
}
|
||||
val building = loadBuilding(it.buildingOid) ?: return null
|
||||
return Auditorium(
|
||||
it.name,
|
||||
it.oid,
|
||||
it.floor ?: 0,
|
||||
building
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class DisciplineDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val type: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface DisciplineDao {
|
||||
@Query("SELECT * FROM disciplinedb WHERE oid = :oid LIMIT 1")
|
||||
fun get(oid: Int): DisciplineDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(discipline: DisciplineDB)
|
||||
|
||||
@Delete
|
||||
fun delete(discipline: DisciplineDB)
|
||||
}
|
||||
|
||||
fun cacheDiscipline(discipline: Discipline) {
|
||||
cacheDb.disciplineDao().insert(
|
||||
DisciplineDB(
|
||||
discipline.oid,
|
||||
discipline.name,
|
||||
discipline.type,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadDiscipline(oid: Int): Discipline? {
|
||||
return cacheDb.disciplineDao().get(oid)?.let {
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) > LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.disciplineDao().delete(it)
|
||||
return null
|
||||
}
|
||||
|
||||
return Discipline(
|
||||
it.name,
|
||||
it.oid,
|
||||
it.type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class KindOfWorkDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val uid: String,
|
||||
@ColumnInfo val complexity: Int,
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface KindOfWorkDao {
|
||||
@Query("SELECT * FROM kindofworkdb WHERE oid = :oid LIMIT 1")
|
||||
fun get(oid: Int): KindOfWorkDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(kindOfWork: KindOfWorkDB)
|
||||
|
||||
@Delete
|
||||
fun delete(kindOfWork: KindOfWorkDB)
|
||||
}
|
||||
|
||||
fun cacheKindOfWork(kindOfWork: KindOfWork) {
|
||||
cacheDb.kindOfWorkDao().insert(
|
||||
KindOfWorkDB(
|
||||
kindOfWork.oid,
|
||||
kindOfWork.name,
|
||||
kindOfWork.uid,
|
||||
kindOfWork.complexity,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadKindOfWork(oid: Int): KindOfWork? {
|
||||
return cacheDb.kindOfWorkDao().get(oid)?.let {
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) > LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.kindOfWorkDao().delete(it)
|
||||
return null
|
||||
}
|
||||
|
||||
return KindOfWork(
|
||||
it.name,
|
||||
it.oid,
|
||||
it.uid,
|
||||
it.complexity
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class LecturerDB(
|
||||
@PrimaryKey val unnId: Int,
|
||||
@ColumnInfo val name: String,
|
||||
@ColumnInfo val rank: LecturerRank,
|
||||
@ColumnInfo val email: String,
|
||||
@ColumnInfo val uid: String,
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface LecturerDao {
|
||||
@Query("SELECT * FROM lecturerdb WHERE unnId = :unnId LIMIT 1")
|
||||
fun get(unnId: Int): LecturerDB?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(lecturer: LecturerDB)
|
||||
|
||||
@Delete
|
||||
fun delete(lecturer: LecturerDB)
|
||||
}
|
||||
|
||||
fun cacheLecturer(lecturer: Lecturer) {
|
||||
cacheDb.lecturerDao().insert(
|
||||
LecturerDB(
|
||||
lecturer.unnId,
|
||||
lecturer.name,
|
||||
lecturer.rank,
|
||||
lecturer.email,
|
||||
lecturer.uid,
|
||||
LocalDateTime.now().plusMonths(1).format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadLecturer(unnId: Int): Lecturer? {
|
||||
return cacheDb.lecturerDao().get(unnId)?.let {
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) > LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.lecturerDao().delete(it)
|
||||
return null
|
||||
}
|
||||
|
||||
return Lecturer(
|
||||
it.name,
|
||||
it.rank,
|
||||
it.email,
|
||||
it.unnId,
|
||||
it.uid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class ScheduleUnitDB(
|
||||
@PrimaryKey val oid: Int,
|
||||
@ColumnInfo val date: String,
|
||||
@ColumnInfo val stream: String,
|
||||
@ColumnInfo val begin: String,
|
||||
@ColumnInfo val end: String,
|
||||
|
||||
@ColumnInfo val auditoriumOid: Int,
|
||||
@ColumnInfo val disciplineOid: Int,
|
||||
@ColumnInfo val kindOfWorkOid: Int,
|
||||
@ColumnInfo val lecturerId: Int, // TODO: many-to-many
|
||||
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
@Dao
|
||||
interface ScheduleItemDao {
|
||||
@Query("SELECT * FROM scheduleUnitDB WHERE oid = :oid LIMIT 1")
|
||||
fun getSchedule(oid: Int): ScheduleUnitDB?
|
||||
|
||||
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
|
||||
fun getSchedule(date: String): List<ScheduleUnitDB>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(user: ScheduleUnitDB)
|
||||
|
||||
@Delete
|
||||
fun delete(user: ScheduleUnitDB)
|
||||
}
|
||||
|
||||
fun cacheSchedule(item: ScheduleUnit) {
|
||||
cacheAuditorium(item.auditorium)
|
||||
cacheDiscipline(item.discipline)
|
||||
cacheKindOfWork(item.kindOfWork)
|
||||
cacheLecturer(item.lecturers[0])
|
||||
|
||||
cacheDb.scheduleDao().insert(
|
||||
ScheduleUnitDB(
|
||||
item.oid,
|
||||
item.date.format(DateTimeFormatter.ISO_DATE),
|
||||
item.stream,
|
||||
item.begin.format(DateTimeFormatter.ISO_TIME),
|
||||
item.end.format(DateTimeFormatter.ISO_TIME),
|
||||
item.auditorium.oid,
|
||||
item.discipline.oid,
|
||||
item.kindOfWork.oid,
|
||||
item.lecturers[0].unnId,
|
||||
when {
|
||||
(LocalDate.now() > item.date) -> LocalDateTime.now().plusWeeks(2)
|
||||
(LocalDate.now().plusWeeks(1) > item.date) -> LocalDateTime.now().plusDays(1)
|
||||
(LocalDate.now().plusWeeks(2) > item.date) -> LocalDateTime.now().plusWeeks(1)
|
||||
else -> LocalDateTime.now().plusWeeks(2)
|
||||
}.format(DateTimeFormatter.ISO_DATE_TIME)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun cacheSchedule(items: ArrayList<ScheduleUnit>) {
|
||||
for (item in items)
|
||||
cacheSchedule(item)
|
||||
}
|
||||
|
||||
fun loadSchedule(oid: Int): ScheduleUnit? {
|
||||
cacheDb.scheduleDao().getSchedule(oid)?.let {
|
||||
Log.d("load", it.oid.toString())
|
||||
if (LocalDateTime.parse(
|
||||
it.expiredAt,
|
||||
DateTimeFormatter.ISO_DATE_TIME
|
||||
) < LocalDateTime.now()
|
||||
) {
|
||||
Log.d("delete", it.oid.toString())
|
||||
cacheDb.scheduleDao().delete(it)
|
||||
return null
|
||||
}
|
||||
|
||||
return ScheduleUnit(
|
||||
it.oid,
|
||||
loadAuditorium(it.auditoriumOid) ?: return null,
|
||||
LocalDate.parse(it.date, DateTimeFormatter.ISO_DATE),
|
||||
loadDiscipline(it.disciplineOid) ?: return null,
|
||||
loadKindOfWork(it.kindOfWorkOid) ?: return null,
|
||||
arrayListOf(loadLecturer(it.lecturerId) ?: return null),
|
||||
it.stream,
|
||||
LocalTime.parse(it.begin, DateTimeFormatter.ISO_TIME),
|
||||
LocalTime.parse(it.end, DateTimeFormatter.ISO_TIME)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
|
||||
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
|
||||
return ArrayList(
|
||||
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||
.mapNotNull { loadSchedule(it.oid) }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.db
|
||||
|
||||
import android.util.Log
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import ru.sweetbread.unn.api.AvatarSet
|
||||
import ru.sweetbread.unn.api.Type
|
||||
import ru.sweetbread.unn.api.User
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Entity
|
||||
data class UserDB(
|
||||
@PrimaryKey val userId: Int,
|
||||
@ColumnInfo val unnId: Int?,
|
||||
@ColumnInfo val type: Type,
|
||||
@ColumnInfo val email: String,
|
||||
@ColumnInfo val nameRu: String,
|
||||
@ColumnInfo val nameEn: String,
|
||||
@ColumnInfo val isMale: Boolean,
|
||||
@ColumnInfo val birthday: String,
|
||||
@ColumnInfo val origAvatar: String,
|
||||
@ColumnInfo val thumbAvatar: String,
|
||||
@ColumnInfo val smallAvatar: String,
|
||||
@ColumnInfo val expiredAt: String
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface UserDao {
|
||||
@Query("SELECT * FROM userDB WHERE userId = :userId LIMIT 1")
|
||||
fun getUserById(userId: Int): UserDB?
|
||||
|
||||
@Insert
|
||||
fun insert(user: UserDB)
|
||||
|
||||
@Delete
|
||||
fun delete(user: UserDB)
|
||||
}
|
||||
|
||||
|
||||
fun cacheUser(user: User) {
|
||||
try {
|
||||
cacheDb.userDao().insert(
|
||||
UserDB(
|
||||
user.userId,
|
||||
user.unnId,
|
||||
user.type,
|
||||
user.email,
|
||||
user.nameRu,
|
||||
user.nameEn,
|
||||
user.isMale,
|
||||
user.birthday.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
user.avatar.original,
|
||||
user.avatar.thumbnail,
|
||||
user.avatar.small,
|
||||
LocalDateTime.now().plusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
)
|
||||
)
|
||||
} catch (_: android.database.sqlite.SQLiteConstraintException) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUserByBitrixId(bitrixId: Int): User? {
|
||||
val user = cacheDb.userDao().getUserById(bitrixId)
|
||||
Log.d("UserDB", user?.nameEn ?: "None")
|
||||
if (user == null) return null
|
||||
if (LocalDateTime.parse(
|
||||
user.expiredAt,
|
||||
DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
||||
) < LocalDateTime.now()
|
||||
) {
|
||||
cacheDb.userDao().delete(user)
|
||||
return null
|
||||
} else {
|
||||
return User(
|
||||
user.unnId,
|
||||
user.userId,
|
||||
user.type,
|
||||
user.email,
|
||||
user.nameRu,
|
||||
user.nameEn,
|
||||
user.isMale,
|
||||
LocalDate.parse(user.birthday, DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
AvatarSet(
|
||||
user.origAvatar,
|
||||
user.thumbAvatar,
|
||||
user.smallAvatar
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package ru.sweetbread.unn.ui
|
||||
|
||||
import android.util.Log
|
||||
import io.ktor.client.request.forms.submitForm
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.parameters
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import ru.sweetbread.unn.ui.layout.LoginData
|
||||
import ru.sweetbread.unn.ui.layout.client
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
private lateinit var PHPSESSID: String
|
||||
lateinit var ME: User
|
||||
|
||||
const val portalURL = "https://portal.unn.ru"
|
||||
const val ruzapiURL = "$portalURL/ruzapi"
|
||||
|
||||
enum class Type(val s: String) {
|
||||
Student("student"),
|
||||
Group("group"),
|
||||
Lecturer("lecturer"),
|
||||
Auditorium("auditorium")
|
||||
}
|
||||
|
||||
enum class LecturerRank(val s: String) {
|
||||
Lecturer("Lecturer"),
|
||||
SLecturer("Senior Lecturer")
|
||||
}
|
||||
|
||||
class ScheduleUnit(
|
||||
val auditorium: Auditorium,
|
||||
val date: LocalDate,
|
||||
val discipline: Discipline,
|
||||
val kindOfWork: KindOfWork,
|
||||
val lecturers: ArrayList<Lecturer>,
|
||||
val stream: String,
|
||||
val begin: LocalTime,
|
||||
val end: LocalTime)
|
||||
|
||||
class Auditorium (val name: String,
|
||||
val oid: Int,
|
||||
val floor: Int,
|
||||
val building: Building)
|
||||
class Building(val name: String,
|
||||
val gid: Int,
|
||||
val oid: Int)
|
||||
|
||||
class Discipline (val name: String,
|
||||
val oid: Int,
|
||||
val type: Int)
|
||||
|
||||
class KindOfWork (val name: String,
|
||||
val oid: Int,
|
||||
val uid: String,
|
||||
val complexity: Int)
|
||||
|
||||
class Lecturer (val name: String,
|
||||
val rank: LecturerRank,
|
||||
val email: String,
|
||||
val oid: Int,
|
||||
val uid: String)
|
||||
|
||||
class User (val id: String,
|
||||
val uns: String,
|
||||
val type: Type,
|
||||
val email: String,
|
||||
val name: String,
|
||||
val info: String)
|
||||
|
||||
/**
|
||||
* Authorize user by [login] and [password]
|
||||
*
|
||||
* Also defines local vars [PHPSESSID] and [ME.id]
|
||||
*/
|
||||
suspend fun auth(login: String = LoginData.login, password: String = LoginData.password, forced: Boolean = false): Boolean {
|
||||
if (!forced) {
|
||||
if (::PHPSESSID.isInitialized and ::ME.isInitialized)
|
||||
return true
|
||||
}
|
||||
val r = client.submitForm("$portalURL/auth/?login=yes",
|
||||
formParameters = parameters {
|
||||
append("AUTH_FORM", "Y")
|
||||
append("TYPE", "AUTH")
|
||||
append("backurl", "/")
|
||||
append("USER_LOGIN", login)
|
||||
append("USER_PASSWORD", password)
|
||||
}
|
||||
)
|
||||
if (r.status.value == 302) {
|
||||
PHPSESSID = """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
|
||||
getMyself(login)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun getMyself(login: String) {
|
||||
val r = client.get("$ruzapiURL/studentinfo") {
|
||||
parameter("uns", login.substring(1))
|
||||
}
|
||||
val json = JSONObject(r.bodyAsText())
|
||||
ME = User(
|
||||
id = json.getString("id"),
|
||||
uns = json.getString("uns"),
|
||||
type = when(json.getString("type")) {
|
||||
"lecturer" -> Type.Lecturer // ig,,,
|
||||
else -> Type.Student
|
||||
},
|
||||
email = json.getString("email"),
|
||||
name = json.getString("fio"),
|
||||
info = json.getString("info")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getSchedule(type: Type = Type.Student, id: String = ME.id, start: LocalDate, finish: LocalDate): ArrayList<ScheduleUnit> {
|
||||
val r = client.get("$ruzapiURL/schedule/${type.s}/$id") {
|
||||
parameter("start", start.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")))
|
||||
parameter("finish", finish.format(DateTimeFormatter.ofPattern("yyyy.MM.dd")))
|
||||
parameter("lng", "1")
|
||||
}
|
||||
val json = JSONArray(r.bodyAsText())
|
||||
val out = arrayListOf<ScheduleUnit>()
|
||||
for (i in 0 until json.length()) {
|
||||
val unit = json.getJSONObject(i)
|
||||
val lecturesJson = unit.getJSONArray("listOfLecturers")
|
||||
val lecturers = arrayListOf<Lecturer>()
|
||||
|
||||
for (j in 0 until lecturesJson.length()) {
|
||||
val lecturer = lecturesJson.getJSONObject(j)
|
||||
lecturers.add(
|
||||
Lecturer(
|
||||
name = lecturer.getString("lecturer"),
|
||||
email = lecturer.getString("lecturerEmail"),
|
||||
oid = lecturer.getInt("lecturerOid"),
|
||||
uid = lecturer.getString("lecturerUID"),
|
||||
rank = when (lecturer.getString("lecturer_rank")) {
|
||||
"СТПРЕП" -> LecturerRank.SLecturer
|
||||
else -> LecturerRank.Lecturer
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
out.add(
|
||||
ScheduleUnit(
|
||||
auditorium = Auditorium(
|
||||
name = unit.getString("auditorium"),
|
||||
oid = unit.getInt("auditoriumOid"),
|
||||
floor = unit.getInt("auditoriumfloor"),
|
||||
building = Building(
|
||||
name = unit.getString("building"),
|
||||
gid = unit.getInt("buildingGid"),
|
||||
oid = unit.getInt("buildingOid")
|
||||
)
|
||||
),
|
||||
date = LocalDate.parse(unit.getString("date"), DateTimeFormatter.ofPattern("yyyy.MM.dd")),
|
||||
discipline = Discipline(
|
||||
name = unit.getString("discipline"),
|
||||
oid = unit.getInt("disciplineOid"),
|
||||
type = unit.getInt("disciplinetypeload")
|
||||
),
|
||||
kindOfWork = KindOfWork(
|
||||
name = unit.getString("kindOfWork"),
|
||||
complexity = unit.getInt("kindOfWorkComplexity"),
|
||||
oid = unit.getInt("kindOfWorkOid"),
|
||||
uid = unit.getString("kindOfWorkUid")
|
||||
),
|
||||
lecturers = lecturers,
|
||||
stream = unit.getString("stream"),
|
||||
begin = LocalTime.parse(unit.getString("beginLesson"), DateTimeFormatter.ofPattern("HH:mm")),
|
||||
end = LocalTime.parse(unit.getString("endLesson"), DateTimeFormatter.ofPattern("HH:mm"))
|
||||
)
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.composes
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.sweetbread.unn.R
|
||||
import splitties.resources.appStr
|
||||
|
||||
@Composable
|
||||
fun About() {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
appStr(R.string.developer),
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text("meow")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.composes
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.toArgb
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import ru.sweetbread.unn.R
|
||||
import ru.sweetbread.unn.api.AvatarSet
|
||||
import ru.sweetbread.unn.api.Post
|
||||
import ru.sweetbread.unn.api.Type
|
||||
import ru.sweetbread.unn.api.User
|
||||
import ru.sweetbread.unn.api.getBlogposts
|
||||
import ru.sweetbread.unn.api.getUserByBitrixId
|
||||
import ru.sweetbread.unn.api.portalURL
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
|
||||
val defUser = User(
|
||||
null,
|
||||
123,
|
||||
Type.student,
|
||||
"cool.email@domain.com",
|
||||
"Джон Сигма Омегович",
|
||||
"Jon Sigma Omega",
|
||||
true,
|
||||
LocalDate.now(),
|
||||
AvatarSet(
|
||||
"https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg",
|
||||
"https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg",
|
||||
"https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@Composable
|
||||
fun Blogposts(viewModel: PostViewModel = viewModel()) {
|
||||
val posts by viewModel.posts.collectAsState()
|
||||
var extended by remember { mutableIntStateOf(0) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadPosts()
|
||||
}
|
||||
|
||||
if (posts.isNotEmpty()) {
|
||||
Log.d("Another fuck", posts.size.toString())
|
||||
LazyColumn {
|
||||
items(posts) {
|
||||
PostItem(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||
.clickable {
|
||||
extended = if (extended == it.id) 0 else it.id
|
||||
},
|
||||
post = it,
|
||||
extended == it.id
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
trackColor = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PostRepository {
|
||||
suspend fun loadPosts(): List<Post> {
|
||||
return getBlogposts()
|
||||
}
|
||||
}
|
||||
|
||||
class PostViewModel : ViewModel() {
|
||||
private val repository = PostRepository()
|
||||
private val _posts = MutableStateFlow<List<Post>>(emptyList())
|
||||
val posts: StateFlow<List<Post>> = _posts.asStateFlow()
|
||||
|
||||
suspend fun loadPosts() {
|
||||
_posts.value = repository.loadPosts()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun UserItem(modifier: Modifier = Modifier, user: User, info: String? = null) {
|
||||
Row(
|
||||
modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(50)),
|
||||
model = portalURL + user.avatar.thumbnail,
|
||||
contentDescription = user.nameEn
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(user.nameRu, fontWeight = FontWeight.Bold)
|
||||
if (!info.isNullOrBlank())
|
||||
Text(
|
||||
text = info,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun PostItem(modifier: Modifier = Modifier, post: Post, extended: Boolean = false) {
|
||||
var user: User? by remember { mutableStateOf(null) }
|
||||
var html: String by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
LaunchedEffect(post) {
|
||||
html = post.content
|
||||
user = getUserByBitrixId(post.authorId)
|
||||
}
|
||||
|
||||
Column(modifier.padding(16.dp)) {
|
||||
if (user != null)
|
||||
UserItem(user = user!!)
|
||||
else
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 38.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
trackColor = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
|
||||
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
|
||||
val linkColor = MaterialTheme.colorScheme.primary.toArgb()
|
||||
|
||||
AndroidView(
|
||||
modifier = Modifier,
|
||||
factory = {
|
||||
TextView(it).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
autoLinkMask = Linkify.WEB_URLS
|
||||
linksClickable = true
|
||||
setTextColor(textColor)
|
||||
setLinkTextColor(linkColor)
|
||||
}
|
||||
},
|
||||
update = {
|
||||
it.maxLines = if (extended) Int.MAX_VALUE else 5
|
||||
it.text = Html.fromHtml(
|
||||
html,
|
||||
Html.FROM_HTML_MODE_LEGACY,
|
||||
CoilImageGetter(it),
|
||||
null
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Text(text = post.date.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UserItemPreview() {
|
||||
UNNTheme {
|
||||
Surface {
|
||||
UserItem(
|
||||
Modifier
|
||||
.width(300.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
defUser,
|
||||
Type.student.s
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PostItemPreview() {
|
||||
val post = Post(
|
||||
id = 154923,
|
||||
authorId = 165945,
|
||||
enableComments = true,
|
||||
numComments = 0,
|
||||
date = LocalDateTime.of(2024, 3, 20, 18, 55, 20),
|
||||
content = stringResource(id = R.string.lorem)
|
||||
)
|
||||
|
||||
UNNTheme {
|
||||
Surface {
|
||||
PostItem(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.primaryContainer), post
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CoilImageGetter(
|
||||
private val textView: TextView,
|
||||
private val maxImageWidth: Int = textView.width
|
||||
) : Html.ImageGetter {
|
||||
|
||||
override fun getDrawable(source: String): Drawable {
|
||||
val urlDrawable = UrlDrawable()
|
||||
|
||||
if (maxImageWidth <= 0)
|
||||
textView.post { updateImage(source, urlDrawable, textView.width) }
|
||||
else
|
||||
updateImage(source, urlDrawable, maxImageWidth)
|
||||
|
||||
return urlDrawable
|
||||
}
|
||||
|
||||
private fun updateImage(source: String, urlDrawable: UrlDrawable, maxWidth: Int) {
|
||||
val imageLoader = ImageLoader.Builder(textView.context)
|
||||
.build()
|
||||
|
||||
val request = ImageRequest.Builder(textView.context)
|
||||
.data(source)
|
||||
.target { drawable ->
|
||||
val (scaledWidth, scaledHeight) = calculateScaledSize(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
maxWidth
|
||||
)
|
||||
|
||||
drawable.setBounds(0, 0, scaledWidth, scaledHeight)
|
||||
urlDrawable.drawable = drawable
|
||||
urlDrawable.setBounds(0, 0, scaledWidth, scaledHeight)
|
||||
|
||||
textView.text = textView.text
|
||||
}
|
||||
.build()
|
||||
|
||||
imageLoader.enqueue(request)
|
||||
}
|
||||
|
||||
private fun calculateScaledSize(
|
||||
originalWidth: Int,
|
||||
originalHeight: Int,
|
||||
maxWidth: Int
|
||||
): Pair<Int, Int> {
|
||||
if (originalWidth <= maxWidth)
|
||||
return Pair(originalWidth, originalHeight)
|
||||
|
||||
val ratio = maxWidth.toFloat() / originalWidth.toFloat()
|
||||
return Pair(
|
||||
maxWidth,
|
||||
(originalHeight * ratio).toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class UrlDrawable() : Drawable() {
|
||||
var drawable: Drawable? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun draw(canvas: android.graphics.Canvas) {
|
||||
drawable?.draw(canvas)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {}
|
||||
override fun setColorFilter(colorFilter: android.graphics.ColorFilter?) {}
|
||||
override fun getOpacity(): Int = android.graphics.PixelFormat.TRANSLUCENT
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.composes
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun GroupsNameText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
highlightColor: Color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
highlightPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp),
|
||||
highlightShape: Shape = MaterialTheme.shapes.small,
|
||||
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||
) {
|
||||
val suffix = if (text.endsWith("-В-OUP")) "-В-OUP" else null
|
||||
val mainPart = if (suffix != null) text.dropLast(6) else text
|
||||
val blocks = mainPart.split('|')
|
||||
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = horizontalArrangement,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
blocks.forEach { block ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = 2.dp)
|
||||
.background(highlightColor, highlightShape)
|
||||
.padding(highlightPadding)
|
||||
) {
|
||||
Text(text = block)
|
||||
}
|
||||
}
|
||||
|
||||
suffix?.let { Text(text = it) }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.composes
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -20,6 +25,8 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -27,64 +34,50 @@ 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.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.kizitonwose.calendar.compose.WeekCalendar
|
||||
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.sweetbread.unn.ui.Auditorium
|
||||
import ru.sweetbread.unn.ui.Building
|
||||
import ru.sweetbread.unn.ui.Discipline
|
||||
import ru.sweetbread.unn.ui.KindOfWork
|
||||
import ru.sweetbread.unn.ui.Lecturer
|
||||
import ru.sweetbread.unn.ui.LecturerRank
|
||||
import ru.sweetbread.unn.ui.ScheduleUnit
|
||||
import ru.sweetbread.unn.ui.getSchedule
|
||||
import ru.sweetbread.unn.R
|
||||
import ru.sweetbread.unn.api.Auditorium
|
||||
import ru.sweetbread.unn.api.Building
|
||||
import ru.sweetbread.unn.api.Discipline
|
||||
import ru.sweetbread.unn.api.KindOfWork
|
||||
import ru.sweetbread.unn.api.Lecturer
|
||||
import ru.sweetbread.unn.api.LecturerRank
|
||||
import ru.sweetbread.unn.api.ScheduleUnit
|
||||
import ru.sweetbread.unn.api.getScheduleDay
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import java.time.DayOfWeek
|
||||
import splitties.resources.appStr
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Calendar
|
||||
|
||||
@Composable
|
||||
fun Schedule() {
|
||||
val state = rememberWeekCalendarState(
|
||||
firstDayOfWeek = DayOfWeek.MONDAY
|
||||
)
|
||||
var selectedDate by remember { mutableStateOf(LocalDate.now()) }
|
||||
|
||||
Column {
|
||||
var curDate by remember { mutableStateOf(LocalDate.now()) }
|
||||
WeekCalendar(
|
||||
state = state,
|
||||
dayContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.aspectRatio(1f) // This is important for square sizing!
|
||||
.offset(2.dp)
|
||||
.background(if (it.date == curDate) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.secondaryContainer)
|
||||
.clickable(
|
||||
onClick = {
|
||||
curDate = it.date
|
||||
Log.d("Here bug (olClick)",
|
||||
curDate.format(DateTimeFormatter.ISO_DATE)
|
||||
)
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = it.date.dayOfMonth.toString(),
|
||||
fontWeight = if (it.date == LocalDate.now()) FontWeight.Bold else null
|
||||
)
|
||||
}
|
||||
}
|
||||
BoundedWeekPicker(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
selectedDate = selectedDate,
|
||||
onDateSelected = { selectedDate = it }
|
||||
)
|
||||
ScheduleDay(date = curDate)
|
||||
|
||||
ScheduleDay(date = selectedDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,13 +86,20 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var loadedDate by remember { mutableStateOf(LocalDate.MIN) }
|
||||
val lessons = remember { mutableListOf<ScheduleUnit>() }
|
||||
var expanded by remember { mutableIntStateOf(0) }
|
||||
|
||||
if (loadedDate == date) {
|
||||
Log.d("Loaded", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}")
|
||||
LazyColumn (modifier) {
|
||||
items(lessons) {
|
||||
ScheduleItem(unit = it)
|
||||
ScheduleItem(unit = it, modifier = Modifier.clickable {
|
||||
expanded = if (it.oid == expanded) 0
|
||||
else it.oid
|
||||
}, expanded = expanded == it.oid)
|
||||
}
|
||||
|
||||
if (lessons.isEmpty())
|
||||
item { Text(appStr(R.string.noData)) }
|
||||
}
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
@@ -112,53 +112,223 @@ fun ScheduleDay(modifier: Modifier = Modifier, date: LocalDate) {
|
||||
LaunchedEffect(date != loadedDate) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
lessons.clear()
|
||||
lessons.addAll(getSchedule(start = date, finish = date))
|
||||
lessons.addAll(getScheduleDay(date = date))
|
||||
loadedDate = date
|
||||
Log.d("Loading", "${date.format(DateTimeFormatter.ISO_DATE)} ${lessons.size}")
|
||||
Log.d("Here bug", "${loadedDate.format(DateTimeFormatter.ISO_DATE)} ${date.format(DateTimeFormatter.ISO_DATE)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit) {
|
||||
fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) {
|
||||
fun getRatio(): Float {
|
||||
val begin = LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
|
||||
val end = LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
|
||||
val now = LocalDateTime.now().atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
|
||||
if (begin > now)
|
||||
return -1f
|
||||
if (now > end)
|
||||
return 1f
|
||||
return (now - begin) / (end - begin).toFloat()
|
||||
}
|
||||
|
||||
val begin = unit.begin.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
val end = unit.end.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
var ratio by remember { mutableFloatStateOf(getRatio()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
val now = System.currentTimeMillis()
|
||||
val calendar = Calendar.getInstance().apply { timeInMillis = now }
|
||||
val seconds = calendar.get(Calendar.SECOND)
|
||||
val millisUntilNextMinute = (60 - seconds) * 1000L - calendar.get(Calendar.MILLISECOND)
|
||||
|
||||
delay(millisUntilNextMinute)
|
||||
ratio = getRatio()
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = when (ratio) {
|
||||
1f -> MaterialTheme.colorScheme.surfaceContainer
|
||||
-1f -> MaterialTheme.colorScheme.secondaryContainer
|
||||
else -> MaterialTheme.colorScheme.primaryContainer
|
||||
},
|
||||
label = "backgroundTransition"
|
||||
)
|
||||
|
||||
Row (
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.background(backgroundColor)
|
||||
.padding(8.dp)
|
||||
){
|
||||
Column (Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = unit.discipline.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.zIndex(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(text = unit.kindOfWork.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Row (Modifier) {
|
||||
Text(text = unit.auditorium.name, fontWeight = FontWeight.Bold, modifier = Modifier.padding(end = 4.dp))
|
||||
Text(text = unit.auditorium.building.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Column {
|
||||
Text(
|
||||
text = unit.discipline.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.zIndex(1f),
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility (expanded) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = unit.kindOfWork.name,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
AnimatedVisibility (expanded) {
|
||||
GroupsNameText(
|
||||
unit.stream,
|
||||
highlightColor = MaterialTheme.colorScheme.inversePrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility (expanded) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility (!expanded) {
|
||||
Row(Modifier) {
|
||||
Text(
|
||||
text = unit.auditorium.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = unit.auditorium.building.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility (expanded) {
|
||||
Column {
|
||||
Text(
|
||||
text = "${stringResource(R.string.auditorium)}: ${unit.auditorium.name}",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.building)}: ${unit.auditorium.building.name}",
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (unit.auditorium.floor != 0) {
|
||||
Text(
|
||||
text = "${stringResource(R.string.floor)}: ${unit.auditorium.floor}",
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(text = unit.lecturers[0].name, fontWeight = FontWeight.Bold)
|
||||
Text(text = stringResource(unit.lecturers[0].rank.id))
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
|
||||
Row (Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(begin.toString(), fontWeight = FontWeight.Bold)
|
||||
AnimatedVisibility (
|
||||
(0f <= ratio) and (ratio < 1f),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 2.dp)
|
||||
) {
|
||||
DividerWithMarker(
|
||||
positionPercentage = ratio,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
thickness = 3.dp,
|
||||
markerSize = 8.dp,
|
||||
markerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(end.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
val begin = unit.begin.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
val end = unit.end.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||
Text(begin.toString(), fontWeight = FontWeight.Bold)
|
||||
Text(end.toString())
|
||||
AnimatedVisibility (!expanded) {
|
||||
Column {
|
||||
Text(begin.toString(), fontWeight = FontWeight.Bold)
|
||||
Text(end.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DividerWithMarker(
|
||||
modifier: Modifier = Modifier,
|
||||
positionPercentage: Float,
|
||||
color: Color = Color.Gray,
|
||||
thickness: Dp = 1.dp,
|
||||
markerSize: Dp = 8.dp,
|
||||
markerColor: Color = Color.Red
|
||||
) {
|
||||
Canvas(modifier = modifier.height(thickness)) {
|
||||
val dividerHeight = thickness.toPx()
|
||||
val width = size.width
|
||||
val markerX = width * positionPercentage
|
||||
|
||||
drawLine(
|
||||
color = markerColor,
|
||||
start = Offset(0f, dividerHeight / 2),
|
||||
end = Offset(markerX, dividerHeight / 2),
|
||||
strokeWidth = dividerHeight
|
||||
)
|
||||
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(markerX, dividerHeight / 2),
|
||||
end = Offset(width, dividerHeight / 2),
|
||||
strokeWidth = dividerHeight / 2
|
||||
)
|
||||
|
||||
drawCircle(
|
||||
color = markerColor,
|
||||
radius = markerSize.toPx() / 2,
|
||||
center = Offset(markerX, dividerHeight / 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ScheduleItemPreview() {
|
||||
val unit = ScheduleUnit(
|
||||
oid = 1,
|
||||
Auditorium(
|
||||
name = "с/з 1(110)",
|
||||
oid = 3752,
|
||||
@@ -186,8 +356,8 @@ fun ScheduleItemPreview() {
|
||||
name = "Фамилия Имя Отчество",
|
||||
rank = LecturerRank.SLecturer,
|
||||
email = "",
|
||||
oid = 28407,
|
||||
uid = "51769"
|
||||
unnId = 28000,
|
||||
uid = "51000"
|
||||
)
|
||||
),
|
||||
stream = "3823Б1ПР1|3823Б1ПР2|3823Б1ПР3|3823Б1ПР4|3823Б1ПР5-В-OUP",
|
||||
@@ -196,9 +366,56 @@ fun ScheduleItemPreview() {
|
||||
)
|
||||
|
||||
UNNTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
ScheduleItem(unit = unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ScheduleExpandedItemPreview() {
|
||||
val unit = ScheduleUnit(
|
||||
oid = 1,
|
||||
Auditorium(
|
||||
name = "с/з 1(110)",
|
||||
oid = 3752,
|
||||
floor = 0,
|
||||
building = Building(
|
||||
name = "Корпус 6",
|
||||
gid = 30,
|
||||
oid = 155
|
||||
),
|
||||
),
|
||||
date = LocalDate.of(2024, 3, 11),
|
||||
discipline = Discipline(
|
||||
name = "Физическая культура и спорт (элективная дисциплина)",
|
||||
oid = 67895,
|
||||
type = 0
|
||||
),
|
||||
kindOfWork = KindOfWork(
|
||||
name = "Практика (семинарские занятия)",
|
||||
oid = 261,
|
||||
uid = "281474976710661",
|
||||
complexity = 1
|
||||
),
|
||||
lecturers = arrayListOf(
|
||||
Lecturer(
|
||||
name = "Фамилия Имя Отчество",
|
||||
rank = LecturerRank.SLecturer,
|
||||
email = "",
|
||||
unnId = 28000,
|
||||
uid = "51000"
|
||||
)
|
||||
),
|
||||
stream = "3823Б1ПР1|3823Б1ПР2|3823Б1ПР3|3823Б1ПР4|3823Б1ПР5-В-OUP",
|
||||
begin = LocalTime.of(10, 50),
|
||||
end = LocalTime.of(12, 20)
|
||||
)
|
||||
|
||||
UNNTheme {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
ScheduleItem(unit = unit, expanded = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
package ru.sweetbread.unn.ui.composes
|
||||
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.TextStyle
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
@Composable
|
||||
fun BoundedWeekPicker(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedDate: LocalDate,
|
||||
onDateSelected: (LocalDate) -> Unit,
|
||||
baseDate: LocalDate = LocalDate.now()
|
||||
) {
|
||||
val rangeStart = remember(baseDate) {
|
||||
baseDate
|
||||
.with(DayOfWeek.MONDAY)
|
||||
.minusWeeks(2)
|
||||
}
|
||||
|
||||
val pageCount = 2 /* prev */ + 1 /* cur */ + 5 /* next */
|
||||
|
||||
fun pageIndexForDate(date: LocalDate): Int {
|
||||
val weekStart = date.with(DayOfWeek.MONDAY)
|
||||
return ChronoUnit.WEEKS.between(rangeStart, weekStart)
|
||||
.toInt()
|
||||
.coerceIn(0, pageCount - 1)
|
||||
}
|
||||
|
||||
val initialPage = remember(selectedDate) { pageIndexForDate(selectedDate) }
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = initialPage,
|
||||
pageCount = { pageCount }
|
||||
)
|
||||
|
||||
LaunchedEffect(selectedDate) {
|
||||
val targetPage = pageIndexForDate(selectedDate)
|
||||
if (targetPage != pagerState.currentPage) {
|
||||
pagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
beyondViewportPageCount = 1,
|
||||
pageSpacing = 0.dp
|
||||
) { page ->
|
||||
val weekStart = rangeStart.plusWeeks(page.toLong())
|
||||
val days = List(6) { weekStart.plusDays(it.toLong()) }
|
||||
|
||||
WeekPage(
|
||||
days = days,
|
||||
selectedDate = selectedDate,
|
||||
onDateSelected = onDateSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekPage(
|
||||
days: List<LocalDate>,
|
||||
selectedDate: LocalDate,
|
||||
onDateSelected: (LocalDate) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
days.forEach { date ->
|
||||
DayItem(
|
||||
date = date,
|
||||
isSelected = date == selectedDate,
|
||||
onClick = { onDateSelected(date) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DayItem(
|
||||
date: LocalDate,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backgroundColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||
val contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
|
||||
val fontWeight = if (date == LocalDate.now()) FontWeight.ExtraBold else FontWeight.Normal
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = date.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault()),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = contentColor,
|
||||
fontWeight = fontWeight
|
||||
)
|
||||
Text(
|
||||
text = date.dayOfMonth.toString(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = contentColor,
|
||||
fontWeight = fontWeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.layout
|
||||
|
||||
import android.os.Bundle
|
||||
@@ -7,19 +9,20 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -36,14 +39,30 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.sweetbread.unn.R
|
||||
import ru.sweetbread.unn.ui.auth
|
||||
import ru.sweetbread.unn.api.auth
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import splitties.activities.start
|
||||
import splitties.preferences.Preferences
|
||||
|
||||
object LoginData : Preferences("loginData") {
|
||||
var login by stringPref("login", "")
|
||||
var password by stringPref("password", "")
|
||||
}
|
||||
|
||||
class LoginActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if ((LoginData.login != "") and (LoginData.password != ""))
|
||||
runBlocking {
|
||||
if (auth()) {
|
||||
start<MainActivity>()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
UNNTheme {
|
||||
Surface(
|
||||
@@ -58,20 +77,26 @@ class LoginActivity : ComponentActivity() {
|
||||
SnackbarHost(hostState = snackbarHostState)
|
||||
}
|
||||
) { innerPadding ->
|
||||
LoginPanel(Modifier.padding(innerPadding), { login, password ->
|
||||
LoginData.login = login
|
||||
LoginData.password = password
|
||||
start<MainActivity>()
|
||||
}, {
|
||||
scope.launch {
|
||||
|
||||
snackbarHostState
|
||||
.showSnackbar(
|
||||
message = "Error",
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
}
|
||||
})
|
||||
Box(Modifier.padding(innerPadding).fillMaxSize(), Alignment.Center) {
|
||||
LoginPanel(
|
||||
Modifier.imePadding(),
|
||||
{ login, password ->
|
||||
LoginData.login = login
|
||||
LoginData.password = password
|
||||
start<MainActivity>()
|
||||
finish()
|
||||
},
|
||||
{
|
||||
scope.launch {
|
||||
snackbarHostState
|
||||
.showSnackbar(
|
||||
message = "Error",
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,48 +115,52 @@ fun LoginPanel(
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Box(Modifier.fillMaxSize(), Alignment.BottomCenter) {
|
||||
Column(
|
||||
modifier
|
||||
.padding(32.dp, 0.dp)
|
||||
.clip(RoundedCornerShape(10.dp, 10.dp))
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
value = login,
|
||||
onValueChange = { login = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prompt_login)) }
|
||||
)
|
||||
Column(
|
||||
modifier
|
||||
.padding(32.dp, 0.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
value = login,
|
||||
onValueChange = { login = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prompt_login))},
|
||||
placeholder = { Text("s23380101") }
|
||||
)
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prompt_password)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringResource(R.string.prompt_password)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
|
||||
Button(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp), onClick = {
|
||||
Button(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
enabled = login.trim().isNotEmpty() and password.trim().isNotEmpty() and !loading,
|
||||
onClick = {
|
||||
loading = true
|
||||
scope.launch {
|
||||
if (auth(login, password)) {
|
||||
if (auth(login, password))
|
||||
ok(login, password)
|
||||
} else {
|
||||
else
|
||||
error()
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
}) {
|
||||
Text(stringResource(R.string.sign_in))
|
||||
}
|
||||
) {
|
||||
if (loading)
|
||||
CircularProgressIndicator()
|
||||
else
|
||||
Text(stringResource(R.string.sign_in))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,177 +1,188 @@
|
||||
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.layout
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountBox
|
||||
import androidx.compose.material.icons.filled.DateRange
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.navigation.NavType
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.kizitonwose.calendar.compose.WeekCalendar
|
||||
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpRequestRetry
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.plugins.cache.HttpCache
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.sweetbread.unn.ui.Auditorium
|
||||
import ru.sweetbread.unn.ui.Building
|
||||
import ru.sweetbread.unn.ui.Discipline
|
||||
import ru.sweetbread.unn.ui.KindOfWork
|
||||
import ru.sweetbread.unn.ui.Lecturer
|
||||
import ru.sweetbread.unn.ui.LecturerRank
|
||||
import ru.sweetbread.unn.ui.ScheduleUnit
|
||||
import ru.sweetbread.unn.ui.auth
|
||||
import ru.sweetbread.unn.ui.getSchedule
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import splitties.activities.start
|
||||
import splitties.preferences.Preferences
|
||||
import splitties.toast.toast
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import ru.sweetbread.unn.R
|
||||
import ru.sweetbread.unn.api.ME
|
||||
import ru.sweetbread.unn.api.portalURL
|
||||
import ru.sweetbread.unn.ui.composes.About
|
||||
import ru.sweetbread.unn.ui.composes.Blogposts
|
||||
import ru.sweetbread.unn.ui.composes.Schedule
|
||||
import ru.sweetbread.unn.ui.composes.ScheduleDay
|
||||
|
||||
object LoginData : Preferences("loginData") {
|
||||
var login by stringPref("login", "")
|
||||
var password by stringPref("password", "")
|
||||
}
|
||||
|
||||
val client = HttpClient {
|
||||
install(HttpCache)
|
||||
install(Logging) {
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
Log.i("Ktor", message)
|
||||
}
|
||||
}
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
socketTimeoutMillis = HttpTimeout.INFINITE_TIMEOUT_MS
|
||||
}
|
||||
install(HttpRequestRetry) {
|
||||
retryOnServerErrors(maxRetries = 3)
|
||||
exponentialDelay()
|
||||
modifyRequest { request ->
|
||||
request.headers.append("x-retry-count", retryCount.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||
import splitties.resources.appStr
|
||||
import splitties.toast.toast
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
if (LoginData.login.isEmpty() or LoginData.password.isEmpty()) start<LoginActivity>()
|
||||
runBlocking {
|
||||
if (!auth()) start<LoginActivity>()
|
||||
}
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
UNNTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
val navController = rememberNavController()
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
onClick = { toast("Not implemented") },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Home,
|
||||
contentDescription = "Home"
|
||||
)
|
||||
},
|
||||
selected = navController.currentDestination?.route?.startsWith("home") ?: false
|
||||
)
|
||||
NavigationBarItem(
|
||||
onClick = { navController.navigate("schedule/student/me/today")
|
||||
Log.d("route", navController.currentDestination?.route.toString())},
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.DateRange,
|
||||
contentDescription = "Schedule"
|
||||
)
|
||||
},
|
||||
selected = navController.currentDestination?.route?.startsWith("schedule") ?: false
|
||||
)
|
||||
NavigationBarItem(
|
||||
onClick = { toast("Not implemented") },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.AccountBox,
|
||||
contentDescription = "Account"
|
||||
)
|
||||
},
|
||||
selected = false
|
||||
)
|
||||
}
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
DrawerContent(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(.75f)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.systemBarsPadding(),
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
) {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
) {innerPadding ->
|
||||
Box(Modifier.padding(innerPadding)) {
|
||||
NavHost(navController, startDestination = "home/blogposts") {
|
||||
composable("home/blogposts") {
|
||||
Text("Not implemented")
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
when {
|
||||
currentRoute?.startsWith("portal/") == true ->
|
||||
appStr(R.string.news)
|
||||
|
||||
currentRoute?.startsWith("journal/") == true ->
|
||||
appStr(R.string.schedule)
|
||||
|
||||
else -> appStr(R.string.app_name)
|
||||
},
|
||||
Modifier.padding()
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Filled.Menu, "Меню")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
NavigationBarItem(
|
||||
onClick = {
|
||||
navController.navigate("portal/blogposts") {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.Home,
|
||||
contentDescription = "Home"
|
||||
)
|
||||
},
|
||||
selected = currentRoute?.startsWith("portal/") == true
|
||||
)
|
||||
|
||||
NavigationBarItem(
|
||||
onClick = {
|
||||
navController.navigate("journal/schedule") {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.DateRange,
|
||||
contentDescription = "Schedule"
|
||||
)
|
||||
},
|
||||
selected = currentRoute?.startsWith("journal/") == true
|
||||
)
|
||||
|
||||
NavigationBarItem(
|
||||
onClick = { toast("Not implemented") },
|
||||
icon = {
|
||||
Icon(
|
||||
Icons.Filled.AccountBox,
|
||||
contentDescription = "Account"
|
||||
)
|
||||
},
|
||||
selected = false
|
||||
)
|
||||
}
|
||||
composable("schedule/{type}/{who}/{when}",
|
||||
arguments = listOf(
|
||||
navArgument("type") { type = NavType.StringType },
|
||||
navArgument("who") { type = NavType.StringType },
|
||||
navArgument("when") { type = NavType.StringType },)
|
||||
) {
|
||||
Schedule()
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(Modifier.padding(innerPadding)) {
|
||||
NavHost(navController, startDestination = "journal/schedule") {
|
||||
composable("portal/blogposts") {
|
||||
Blogposts()
|
||||
}
|
||||
composable("journal/schedule") {
|
||||
Schedule()
|
||||
}
|
||||
composable("about") {
|
||||
About()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,4 +191,90 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) {
|
||||
Column(modifier = modifier.padding(0.dp)) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(16.dp))
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier.size(64.dp).clip(CircleShape),
|
||||
model = portalURL + ME.avatar.thumbnail,
|
||||
contentDescription = ME.nameEn
|
||||
)
|
||||
|
||||
Column (Modifier.padding(horizontal = 8.dp)) {
|
||||
Text(
|
||||
ME.nameRu.split(" ").dropLast(1).joinToString(" "),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(ME.type.s)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider()
|
||||
|
||||
TextButton(
|
||||
{},
|
||||
Modifier.fillMaxWidth(),
|
||||
shape = RectangleShape,
|
||||
enabled = false
|
||||
) {
|
||||
Text(
|
||||
appStr(R.string.record_book),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
{},
|
||||
Modifier.fillMaxWidth(),
|
||||
shape = RectangleShape,
|
||||
enabled = false
|
||||
) {
|
||||
Text(
|
||||
appStr(R.string.documents),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
{},
|
||||
Modifier.fillMaxWidth(),
|
||||
shape = RectangleShape,
|
||||
enabled = false
|
||||
) {
|
||||
Text(
|
||||
appStr(R.string.materials),
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
HorizontalDivider()
|
||||
TextButton(
|
||||
{navController.navigate("about")},
|
||||
Modifier.fillMaxWidth(),
|
||||
shape = RectangleShape
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = "Info",
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
Text(appStr(R.string.about_app))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.theme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
package ru.sweetbread.unn.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 790 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 942 B After Width: | Height: | Size: 878 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 534 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.1 KiB |
@@ -1,3 +1,5 @@
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<dimen name="activity_horizontal_margin">48dp</dimen>
|
||||
</resources>
|
||||
@@ -1,6 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<string name="app_name_reg">ННГУ</string>
|
||||
<string name="app_name_dev">ННГУ Альфа</string>
|
||||
<string name="app_name_beta">ННГУ Бета</string>
|
||||
<string name="prompt_password">Пароль</string>
|
||||
<string name="prompt_login">Логин</string>
|
||||
<string name="sign_in">Войти</string>
|
||||
<string name="assistant">Ассистент</string>
|
||||
<string name="lecturer">Преподаватель</string>
|
||||
<string name="slecturer">Старший Преподаватель</string>
|
||||
<string name="aprofessor">Доцент</string>
|
||||
<string name="auditorium">Аудитория</string>
|
||||
<string name="building">Здание</string>
|
||||
<string name="floor">Этаж</string>
|
||||
<string name="noData">Нет данных</string>
|
||||
<string name="news">Новости</string>
|
||||
<string name="schedule">Расписание</string>
|
||||
<string name="record_book">Зачётная книга</string>
|
||||
<string name="about_app">О приложении</string>
|
||||
<string name="documents">Документы</string>
|
||||
<string name="materials">Материалы</string>
|
||||
<string-array name="short_weekdays">
|
||||
<item>Пн</item>
|
||||
<item>Вт</item>
|
||||
<item>Ср</item>
|
||||
<item>Чт</item>
|
||||
<item>Пт</item>
|
||||
<item>Сб</item>
|
||||
</string-array>
|
||||
<string name="student">Студент</string>
|
||||
<string name="employee">Сотрудник</string>
|
||||
</resources>
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<dimen name="activity_horizontal_margin">200dp</dimen>
|
||||
</resources>
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<dimen name="activity_horizontal_margin">48dp</dimen>
|
||||
</resources>
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
<color name="ic_launcher_background">#1565AA</color>
|
||||
</resources>
|
||||
@@ -1,9 +1,37 @@
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">UNN</string>
|
||||
<!-- <string name="title_activity_login">LoginActivity</string>-->
|
||||
<string name="app_name_reg">UNN</string>
|
||||
<string name="app_name_dev">UNN Dev</string>
|
||||
<string name="app_name_beta">UNN Beta</string>
|
||||
<string name="prompt_email" translatable="false">Email</string>
|
||||
<string name="prompt_login">Login</string>
|
||||
<string name="prompt_password">Password</string>
|
||||
<string name="sign_in">Sign in</string>
|
||||
<!-- <string name="login_failed">"Login failed"</string>-->
|
||||
<string name="assistant">Assistant</string>
|
||||
<string name="lecturer">Lecturer</string>
|
||||
<string name="slecturer">Senior Lecturer</string>
|
||||
<string name="aprofessor">Assistant professor</string>
|
||||
<string name="auditorium">Auditorium</string>
|
||||
<string name="building">Building</string>
|
||||
<string name="floor">Floor</string>
|
||||
<string name="lorem" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel iaculis elit. Aliquam varius urna ut nisl rhoncus ullamcorper. Maecenas et nisl at dui mollis maximus nec in libero. Ut eu nulla id felis hendrerit lobortis. Maecenas vel facilisis lectus. Morbi eleifend massa a ante consequat, eu aliquam elit euismod. Aenean quis erat tincidunt, egestas ligula id, convallis tortor. Vivamus volutpat condimentum nisl sed eleifend. Aenean dapibus dolor ut orci lobortis, placerat lobortis tortor pretium. Nam eros lectus, convallis sed ultricies sit amet, lacinia sed sem. In mi odio, porta non malesuada et, cursus a metus. Morbi quis odio sed quam commodo gravida id sit amet dolor. Donec ac iaculis massa. Nulla mauris sapien, auctor consequat est in, tempus accumsan ipsum. Donec semper volutpat nisi. Quisque dignissim tellus ipsum, sed malesuada libero aliquam sed. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam eleifend pharetra orci eu scelerisque. In hac habitasse platea dictumst. Sed non neque vitae metus porttitor vestibulum ut eget felis. Aliquam venenatis a magna eu mattis. Proin rutrum, sapien id viverra finibus, nisi quam aliquam eros, et dignissim lectus sem sit amet purus. Donec et semper enim, sed pretium lacus. Nullam venenatis ullamcorper maximus. Mauris pellentesque velit non sem sollicitudin molestie. Duis hendrerit consequat enim eget euismod.</string>
|
||||
<string name="noData">No Data</string>
|
||||
<string name="news">News</string>
|
||||
<string name="schedule">Schedule</string>
|
||||
<string name="student">Student</string>
|
||||
<string name="employee">Employee</string>
|
||||
<string name="record_book">Record book</string>
|
||||
<string name="about_app">About app</string>
|
||||
<string name="documents">Documents</string>
|
||||
<string name="materials">Materials</string>
|
||||
<string name="developer">Developer</string>
|
||||
<string-array name="short_weekdays">
|
||||
<item>Mn</item>
|
||||
<item>Tu</item>
|
||||
<item>We</item>
|
||||
<item>Th</item>
|
||||
<item>Fr</item>
|
||||
<item>Sa</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.UNN" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.UNN" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||
|
||||
<!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication) apply false
|
||||
alias(libs.plugins.jetbrainsKotlinAndroid) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
id("com.google.devtools.ksp") version "2.3.2" apply false
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.secrets.gradle.plugin)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
# Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
@@ -20,4 +22,14 @@ kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.defaults.buildfeatures.resvalues=true
|
||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||
android.enableAppCompileTimeRClass=false
|
||||
android.usesSdkInManifest.disallowed=false
|
||||
android.uniquePackageNames=false
|
||||
android.dependency.useConstraints=true
|
||||
android.r8.strictFullModeForKeepRules=false
|
||||
android.r8.optimizedResourceShrinking=false
|
||||
android.builtInKotlin=false
|
||||
android.newDsl=false
|
||||
@@ -1,57 +1,70 @@
|
||||
[versions]
|
||||
agp = "8.3.0"
|
||||
datastorePreferences = "1.0.0"
|
||||
kotlin = "1.9.0"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
ktorClientCio = "2.3.9"
|
||||
ktorClientCore = "2.3.9"
|
||||
ktorClientLogging = "2.3.9"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.7.0"
|
||||
composeBom = "2023.08.00"
|
||||
appcompat = "1.6.1"
|
||||
material = "1.10.0"
|
||||
annotation = "1.6.0"
|
||||
constraintlayout = "2.1.4"
|
||||
lifecycleLivedataKtx = "2.6.1"
|
||||
lifecycleViewmodelKtx = "2.6.1"
|
||||
activity = "1.8.0"
|
||||
navigationCompose = "2.7.7"
|
||||
splittiesFunPackAndroidBaseWithViewsDsl = "3.0.0"
|
||||
agp = "9.0.1"
|
||||
coilCompose = "2.7.0"
|
||||
compose = "1.10.3"
|
||||
coreSplashscreen = "1.2.0"
|
||||
datastorePreferences = "1.2.0"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
kotlin = "2.3.10"
|
||||
coreKtx = "1.17.0"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
ktor = "3.4.0"
|
||||
lifecycle = "2.10.0"
|
||||
activityCompose = "1.12.4"
|
||||
composeBom = "2026.02.00"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.13.0"
|
||||
annotation = "1.9.1"
|
||||
constraintlayout = "2.2.1"
|
||||
activity = "1.12.4"
|
||||
navigationCompose = "2.9.7"
|
||||
roomRuntime = "2.8.4"
|
||||
secretsGradlePlugin = "2.0.1"
|
||||
sentryAndroid = "8.32.0"
|
||||
splitties = "3.0.0"
|
||||
materialIconsCoreAndroid = "1.7.8"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
|
||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" }
|
||||
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" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCio" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
|
||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorClientLogging" }
|
||||
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
|
||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
||||
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
|
||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
splitties-fun-pack-android-base-with-views-dsl = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splittiesFunPackAndroidBaseWithViewsDsl" }
|
||||
secrets-gradle-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
|
||||
sentry = { module = "io.sentry:sentry-android", version.ref = "sentryAndroid" }
|
||||
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl", version.ref = "splitties" }
|
||||
splitties-room = { module = "com.louiscad.splitties:splitties-arch-room", version.ref = "splitties" }
|
||||
androidx-material-icons-core-android = { group = "androidx.compose.material", name = "material-icons-core-android", version.ref = "materialIconsCoreAndroid" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
#Sat Mar 16 18:30:45 MSK 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
|
||||