Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ jobs:
run: chmod +x ./gradlew

- name: Build with Gradle
run: ./gradlew detekt assembleRelease
run: ./gradlew assembleRelease

- name: Upload the universal artifact
uses: actions/upload-artifact@v4
with:
path: ./composeApp/build/outputs/apk/release/composeApp-release-unsigned.apk
name: progres

- name: Check linting with Detekt
run: ./gradlew detekt

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ captures
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings

# Detekt CLI jars
detekt-*.jar
7 changes: 5 additions & 2 deletions composeApp/src/commonMain/kotlin/di/ApplicationModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package di
import org.koin.core.module.Module
import org.koin.dsl.module
import utils.CredentialManager
import utils.FileStorageManager
import utils.PlatformUtils

val ApplicationModule: (
credentialManger: CredentialManager,
platformUtils: PlatformUtils
) -> Module = { credentialManager, platformUtils ->
platformUtils: PlatformUtils,
dataPath: String
) -> Module = { credentialManager, platformUtils, dataPath ->
module {
single { platformUtils }
single { credentialManager }
single { FileStorageManager(dataPath) }
}
}
2 changes: 1 addition & 1 deletion composeApp/src/commonMain/kotlin/di/InitKoin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fun initKoin(
PreferencesModule(datastorePath),
DomainModule,
ScreenModelsModule,
ApplicationModule(credentialManager, platformUtils),
ApplicationModule(credentialManager, platformUtils, datastorePath),
UpdateCheckerModule,
)
}
153 changes: 153 additions & 0 deletions composeApp/src/commonMain/kotlin/utils/FileStorageManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package utils

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okio.FileSystem
import okio.Path.Companion.toPath

/**
* Manages file storage operations for profile pictures and university logos
*/
class FileStorageManager(
private val baseDataPath: String
) {
private val fileSystem = FileSystem.SYSTEM
private val imagesDir = "$baseDataPath/images".toPath()
private val profilePicturesDir = imagesDir / "profiles"
private val universityLogosDir = imagesDir / "logos"

init {
// Ensure directories exist
try {
fileSystem.createDirectories(profilePicturesDir)
fileSystem.createDirectories(universityLogosDir)
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* Saves a profile picture and returns the file path
*/
suspend fun saveProfilePicture(studentId: String, imageData: ByteArray): String? = withContext(Dispatchers.IO) {
try {
val fileName = "profile_${studentId.replace(Regex("[^a-zA-Z0-9]"), "_")}.jpg"
val filePath = profilePicturesDir / fileName

fileSystem.write(filePath) {
write(imageData)
}

filePath.toString()
} catch (e: Exception) {
e.printStackTrace()
null
}
}

/**
* Saves a university logo and returns the file path
*/
suspend fun saveUniversityLogo(establishmentId: String, imageData: ByteArray): String? = withContext(
Dispatchers.IO
) {
try {
val fileName = "logo_${establishmentId.replace(Regex("[^a-zA-Z0-9]"), "_")}.jpg"
val filePath = universityLogosDir / fileName

fileSystem.write(filePath) {
write(imageData)
}

filePath.toString()
} catch (e: Exception) {
e.printStackTrace()
null
}
}

/**
* Loads image data from a file path
*/
suspend fun loadImage(filePath: String): ByteArray? = withContext(Dispatchers.IO) {
try {
val path = filePath.toPath()
if (fileSystem.exists(path)) {
fileSystem.read(path) {
readByteArray()
}
} else {
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}

/**
* Deletes an image file
*/
suspend fun deleteImage(filePath: String): Boolean = withContext(Dispatchers.IO) {
try {
val path = filePath.toPath()
if (fileSystem.exists(path)) {
fileSystem.delete(path)
true
} else {
false
}
} catch (e: Exception) {
e.printStackTrace()
false
}
}

/**
* Deletes all profile pictures
*/
suspend fun deleteAllProfilePictures(): Boolean = withContext(Dispatchers.IO) {
try {
if (fileSystem.exists(profilePicturesDir)) {
fileSystem.deleteRecursively(profilePicturesDir)
fileSystem.createDirectories(profilePicturesDir)
true
} else {
true
}
} catch (e: Exception) {
e.printStackTrace()
false
}
}

/**
* Deletes all university logos
*/
suspend fun deleteAllUniversityLogos(): Boolean = withContext(Dispatchers.IO) {
try {
if (fileSystem.exists(universityLogosDir)) {
fileSystem.deleteRecursively(universityLogosDir)
fileSystem.createDirectories(universityLogosDir)
true
} else {
true
}
} catch (e: Exception) {
e.printStackTrace()
false
}
}

/**
* Checks if an image file exists
*/
fun imageExists(filePath: String): Boolean {
return try {
fileSystem.exists(filePath.toPath())
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
2 changes: 2 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ style:
UnusedPrivateMember:
ignoreAnnotated:
- "Preview"
PrintStackTrace:
active: false

exceptions:
SwallowedException:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ actual val ProgresDatabaseModule = module {
override fun onOpen(db: SupportSQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
}

override fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
// Handle migration from BLOB to file storage
// Clear affected tables to force fresh sync with new schema
if (oldVersion < newVersion) {
try {
db.execSQL("DROP TABLE IF EXISTS IndividualInfoTable")
db.execSQL("DROP TABLE IF EXISTS StudentCardTable")
// Recreate tables with new schema
onCreate(db)
} catch (e: Exception) {
// If migration fails, recreate database
super.onUpgrade(db, oldVersion, newVersion)
}
}
}
}
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package mehiz.abdallah.progres.data.daos

import mehiz.abdallah.progres.data.db.IndividualInfoTable
import mehiz.abdallah.progres.data.db.ProgresDB
import utils.FileStorageManager

class IndividualInfoDao(
db: ProgresDB
db: ProgresDB,
private val fileStorageManager: FileStorageManager
) {

private val queries = db.individualInfoTableQueries
Expand All @@ -18,7 +20,7 @@ class IndividualInfoDao(
firstNameLatin = firstNameLatin,
lastNameLatin = lastNameLatin,
lastNameArabic = lastNameArabic,
photo = photo,
photoPath = photoPath,
dateOfBirth = dateOfBirth,
placeOfBirthLatin = placeOfBirthLatin,
placeOfBirthArabic = placeOfBirthArabic,
Expand All @@ -30,11 +32,18 @@ class IndividualInfoDao(
return queries.getById(id).executeAsOneOrNull()
}

fun getIndividualPhotoById(id: Long): ByteArray? {
return queries.getStudentPhotoById(id).executeAsOne().photo
fun getIndividualPhotoPathById(id: Long): String? {
return queries.getStudentPhotoPathById(id).executeAsOneOrNull()
}

fun deleteAllIndividualInfo() {
suspend fun getIndividualPhotoById(id: Long): ByteArray? {
val photoPath = getIndividualPhotoPathById(id)
return photoPath?.let { fileStorageManager.loadImage(it) }
}

suspend fun deleteAllIndividualInfo() {
// Delete all profile pictures before clearing database
fileStorageManager.deleteAllProfilePictures()
queries.delete()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package mehiz.abdallah.progres.data.daos

import mehiz.abdallah.progres.data.db.ProgresDB
import mehiz.abdallah.progres.data.db.StudentCardTable
import utils.FileStorageManager

@Suppress("TooManyFunctions")
class StudentCardDao(
db: ProgresDB
db: ProgresDB,
private val fileStorageManager: FileStorageManager
) {
private val queries = db.studentCardTableQueries

Expand Down Expand Up @@ -37,7 +39,7 @@ class StudentCardDao(
levelId = levelId,
establishmentStringLatin = establishmentStringLatin,
establishmentStringArabic = establishmentStringArabic,
establishmentLogo = establishmentLogo,
establishmentLogoPath = establishmentLogoPath,
cycleStringLatin = cycleStringLatin,
cycleStringArabic = cycleStringArabic
)
Expand All @@ -60,11 +62,18 @@ class StudentCardDao(
return queries.getCardByAcademicYear(id).executeAsOne()
}

fun deleteCard(id: Long) {
suspend fun deleteCard(id: Long) {
// Get the establishment logo path before deleting
val card = queries.getCard(id).executeAsOneOrNull()
card?.establishmentLogoPath?.let { logoPath ->
fileStorageManager.deleteImage(logoPath)
}
queries.deleteCardWithId(id)
}

fun deleteAllCards() {
suspend fun deleteAllCards() {
// Delete all university logos before clearing database
fileStorageManager.deleteAllUniversityLogos()
queries.deleteAllCards()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE IndividualInfoTable(
CREATE TABLE IF NOT EXISTS IndividualInfoTable(
id INTEGER NOT NULL PRIMARY KEY,
firstNameArabic TEXT NOT NULL,
firstNameLatin TEXT NOT NULL,
Expand All @@ -7,7 +7,7 @@ lastNameLatin TEXT NOT NULL,
dateOfBirth TEXT NOT NULL,
placeOfBirthArabic TEXT,
placeOfBirthLatin TEXT,
photo BLOB,
photoPath TEXT,
uuid TEXT NOT NULL
);

Expand All @@ -21,7 +21,7 @@ lastNameLatin,
dateOfBirth,
placeOfBirthArabic,
placeOfBirthLatin,
photo,
photoPath,
uuid
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

Expand All @@ -31,8 +31,8 @@ SELECT * FROM IndividualInfoTable LIMIT 1;
getById:
SELECT * FROM IndividualInfoTable WHERE id = :id;

getStudentPhotoById:
SELECT photo FROM IndividualInfoTable WHERE id = :id;
getStudentPhotoPathById:
SELECT photoPath FROM IndividualInfoTable WHERE id = :id;

delete:
DELETE FROM IndividualInfoTable;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ individualPlaceOfBirthArabic TEXT,
individualPlaceOfBirthLatin TEXT,
establishmentStringArabic TEXT NOT NULL,
establishmentStringLatin TEXT NOT NULL,
establishmentLogo BLOB,
establishmentLogoPath TEXT,
levelId INTEGER NOT NULL,
levelStringLongArabic TEXT NOT NULL,
levelStringLongLatin TEXT NOT NULL,
Expand Down Expand Up @@ -44,7 +44,7 @@ individualPlaceOfBirthLatin,
establishmentStringArabic,
establishmentStringLatin,
levelId,
establishmentLogo,
establishmentLogoPath,
levelStringLongArabic,
levelStringLongLatin,
registrationNumber,
Expand Down
Loading
Loading