Skip to content

Commit 6b751c2

Browse files
authored
Adding Selection to File List (#40)
- Add Multi-File Selector - Add Multi Delete, Expire and Albums - Add Albums and Expire to BottomSheet
1 parent 19367f2 commit 6b751c2

34 files changed

+1200
-231
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ dependencies {
7272
implementation(libs.media3.exoplayer.dash)
7373
implementation(libs.media3.ui)
7474
implementation(libs.media3.ui.compose)
75+
implementation(libs.androidx.swiperefreshlayout)
7576
//ksp(libs.glide.compiler)
7677
ksp(libs.room.compiler)
7778
testImplementation(libs.junit)

app/src/main/java/com/djangofiles/djangofiles/MainActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import androidx.navigation.NavOptions
3636
import androidx.navigation.fragment.NavHostFragment
3737
import androidx.navigation.ui.NavigationUI
3838
import com.djangofiles.djangofiles.databinding.ActivityMainBinding
39+
import com.djangofiles.djangofiles.db.Server
40+
import com.djangofiles.djangofiles.db.ServerDao
41+
import com.djangofiles.djangofiles.db.ServerDatabase
3942
import com.djangofiles.djangofiles.ui.home.HomeViewModel
4043
import kotlinx.coroutines.Dispatchers
4144
import kotlinx.coroutines.launch

app/src/main/java/com/djangofiles/djangofiles/ServerApi.kt

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import android.content.SharedPreferences
55
import android.util.Log
66
import android.webkit.CookieManager
77
import androidx.core.content.edit
8+
import com.djangofiles.djangofiles.db.ServerDao
9+
import com.djangofiles.djangofiles.db.ServerDatabase
810
import com.google.gson.GsonBuilder
911
import com.google.gson.annotations.SerializedName
1012
import kotlinx.coroutines.Dispatchers
@@ -157,11 +159,27 @@ class ServerApi(val context: Context, host: String) {
157159
return api.getRecent(authToken, amount, start)
158160
}
159161

162+
suspend fun albums(): Response<AlbumResponse> {
163+
Log.d("Api[albums]", "albums")
164+
val response = api.getAlbums(authToken)
165+
return response
166+
}
167+
160168
suspend fun deleteFile(fileId: Int): Response<ResponseBody> {
161-
Log.d("Api[recent]", "fileId: $fileId")
169+
Log.d("Api[deleteFile]", "fileId: $fileId")
162170
return api.fileDelete(authToken, fileId)
163171
}
164172

173+
suspend fun filesEdit(data: FilesEditRequest): Response<ResponseBody> {
174+
Log.d("Api[filesEdit]", "data: $data")
175+
return api.filesEdit(authToken, data)
176+
}
177+
178+
suspend fun filesDelete(data: FilesEditRequest): Response<ResponseBody> {
179+
Log.d("Api[filesDelete]", "data: $data")
180+
return api.filesDelete(authToken, data)
181+
}
182+
165183
interface ApiService {
166184
@FormUrlEncoded
167185
@POST("oauth/")
@@ -201,9 +219,16 @@ class ServerApi(val context: Context, host: String) {
201219
suspend fun getRecent(
202220
@Header("Authorization") token: String,
203221
@Query("amount") amount: Int,
204-
@Query("start") start: Int,
222+
@Query("start") start: Int? = null,
223+
@Query("before") before: Int? = null,
224+
@Query("after") after: Int? = null,
205225
): Response<List<FileResponse>>
206226

227+
@GET("albums/")
228+
suspend fun getAlbums(
229+
@Header("Authorization") token: String,
230+
): Response<AlbumResponse>
231+
207232
@POST("file/{id}")
208233
suspend fun fileEdit(
209234
@Header("Authorization") token: String,
@@ -217,6 +242,20 @@ class ServerApi(val context: Context, host: String) {
217242
@Path("id") fileId: Int,
218243
): Response<ResponseBody>
219244

245+
@POST("files/edit/")
246+
suspend fun filesEdit(
247+
@Header("Authorization") token: String,
248+
@Body data: FilesEditRequest
249+
): Response<ResponseBody>
250+
251+
//@DELETE("files/")
252+
//@HTTP(method = "DELETE", path = "files/", hasBody = true)
253+
@POST("files/delete/")
254+
suspend fun filesDelete(
255+
@Header("Authorization") token: String,
256+
@Body data: FilesEditRequest
257+
): Response<ResponseBody>
258+
220259
// TODO: Use VersionResponse
221260
@POST("version/")
222261
suspend fun postVersion(
@@ -274,7 +313,19 @@ class ServerApi(val context: Context, host: String) {
274313
@SerializedName("url") val url: String? = null,
275314
@SerializedName("thumb") val thumb: String? = null,
276315
@SerializedName("raw") val raw: String? = null,
277-
@SerializedName("date") val date: String? = null
316+
@SerializedName("date") val date: String? = null,
317+
@SerializedName("albums") val albums: List<Int>? = null,
318+
)
319+
320+
data class FilesEditRequest(
321+
@SerializedName("ids") val ids: List<Int>,
322+
@SerializedName("info") val info: String? = null,
323+
@SerializedName("expr") val expr: String? = null,
324+
@SerializedName("maxv") val maxv: Int? = null,
325+
@SerializedName("meta_preview") val metaPreview: Boolean? = null,
326+
@SerializedName("password") val password: String? = null,
327+
@SerializedName("private") val private: Boolean? = null,
328+
@SerializedName("albums") val albums: List<Int>? = null,
278329
)
279330

280331
data class FileResponse(
@@ -295,6 +346,27 @@ class ServerApi(val context: Context, host: String) {
295346
val thumb: String,
296347
val raw: String,
297348
val date: String,
349+
var albums: List<Int>,
350+
)
351+
352+
data class AlbumResponse(
353+
val albums: List<AlbumData>,
354+
val next: Int,
355+
val count: Int,
356+
)
357+
358+
data class AlbumData(
359+
@SerializedName("id") val id: Int,
360+
@SerializedName("user") val user: Int,
361+
@SerializedName("name") val name: String,
362+
@SerializedName("password") val password: String,
363+
@SerializedName("private") val private: Boolean,
364+
@SerializedName("info") val info: String,
365+
@SerializedName("view") val view: Int,
366+
@SerializedName("maxv") val maxv: Int,
367+
@SerializedName("expr") val expr: String,
368+
@SerializedName("date") val date: String,
369+
@SerializedName("url") val url: String,
298370
)
299371

300372
private suspend fun inputStreamToMultipart(
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.djangofiles.djangofiles.db
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.room.Dao
6+
import androidx.room.Database
7+
import androidx.room.Delete
8+
import androidx.room.Entity
9+
import androidx.room.Insert
10+
import androidx.room.OnConflictStrategy
11+
import androidx.room.PrimaryKey
12+
import androidx.room.Query
13+
import androidx.room.Room
14+
import androidx.room.RoomDatabase
15+
import androidx.room.Upsert
16+
import com.djangofiles.djangofiles.ServerApi.AlbumData
17+
import java.util.Base64
18+
19+
20+
@Dao
21+
interface AlbumDao {
22+
@Query("SELECT * FROM albumentity")
23+
fun getAll(): List<AlbumEntity>
24+
25+
@Query("SELECT * FROM albumentity WHERE name = :name LIMIT 1")
26+
fun getByName(name: String): AlbumEntity?
27+
28+
@Insert
29+
fun add(album: AlbumEntity)
30+
31+
@Upsert
32+
fun addOrUpdate(album: AlbumEntity)
33+
34+
@Delete
35+
fun delete(album: AlbumEntity)
36+
37+
@Insert(onConflict = OnConflictStrategy.REPLACE)
38+
suspend fun insertAll(albums: List<AlbumEntity>)
39+
40+
@Query("DELETE FROM AlbumEntity WHERE id NOT IN (:ids)")
41+
suspend fun deleteMissing(ids: List<Int>)
42+
43+
suspend fun syncAlbums(albumDataList: List<AlbumData>) {
44+
val entities = albumDataList.map {
45+
AlbumEntity(
46+
id = it.id,
47+
name = it.name,
48+
password = it.password,
49+
private = it.private,
50+
info = it.info,
51+
expr = it.expr,
52+
date = it.date,
53+
url = it.url
54+
)
55+
}
56+
val newIds = entities.map { it.id }
57+
insertAll(entities)
58+
deleteMissing(newIds)
59+
}
60+
}
61+
62+
63+
@Entity
64+
data class AlbumEntity(
65+
@PrimaryKey val id: Int,
66+
val name: String,
67+
val password: String,
68+
val private: Boolean,
69+
val info: String,
70+
val expr: String,
71+
val date: String,
72+
val url: String
73+
)
74+
75+
76+
@Database(entities = [AlbumEntity::class], version = 1)
77+
abstract class AlbumDatabase : RoomDatabase() {
78+
abstract fun albumDao(): AlbumDao
79+
80+
companion object {
81+
private val instances = mutableMapOf<String, AlbumDatabase>()
82+
83+
fun getInstance(context: Context, url: String): AlbumDatabase {
84+
val safeName = Base64.getUrlEncoder().withoutPadding()
85+
.encodeToString(url.toByteArray(Charsets.UTF_8))
86+
return instances[safeName] ?: synchronized(this) {
87+
instances[safeName] ?: Room.databaseBuilder(
88+
context.applicationContext,
89+
AlbumDatabase::class.java,
90+
"album-$safeName"
91+
).build().also { instances[safeName] = it }
92+
}
93+
}
94+
}
95+
}

app/src/main/java/com/djangofiles/djangofiles/RoomData.kt renamed to app/src/main/java/com/djangofiles/djangofiles/db/ServerDao.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.djangofiles.djangofiles
1+
package com.djangofiles.djangofiles.db
22

33
import android.content.Context
44
import androidx.room.Dao
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.djangofiles.djangofiles.ui.files
2+
3+
import android.app.Dialog
4+
import android.content.Context.MODE_PRIVATE
5+
import android.os.Bundle
6+
import android.util.Log
7+
import androidx.core.os.bundleOf
8+
import androidx.fragment.app.DialogFragment
9+
import androidx.fragment.app.setFragmentResult
10+
import com.djangofiles.djangofiles.R
11+
import com.djangofiles.djangofiles.ServerApi
12+
import com.djangofiles.djangofiles.ServerApi.FilesEditRequest
13+
import com.djangofiles.djangofiles.db.AlbumEntity
14+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.launch
18+
19+
class AlbumFragment : DialogFragment() {
20+
21+
private var albums: List<AlbumEntity> = emptyList()
22+
private var fileIds: List<Int> = emptyList()
23+
private var selectedIds: List<Int> = emptyList()
24+
25+
fun setAlbumData(albums: List<AlbumEntity>, fileIds: List<Int>, selectedIds: List<Int>) {
26+
this.albums = albums
27+
this.fileIds = fileIds
28+
this.selectedIds = selectedIds
29+
}
30+
31+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
32+
val sharedPreferences =
33+
requireContext().getSharedPreferences("AppPreferences", MODE_PRIVATE)
34+
val savedUrl = sharedPreferences.getString("saved_url", "").orEmpty()
35+
val api = ServerApi(requireContext(), savedUrl)
36+
37+
val selected = BooleanArray(albums.size) { i -> albums[i].id in selectedIds }
38+
Log.d("dialog[lifecycleScope]", "selected: $selected")
39+
val albumNames = albums.map { it.name }.toTypedArray()
40+
41+
val dialog = MaterialAlertDialogBuilder(requireContext(), R.style.AlertDialogTheme)
42+
.setTitle("Set Albums")
43+
.setIcon(R.drawable.md_imagesmode_24)
44+
.setNegativeButton("Cancel") { _, _ -> }
45+
if (albums.isNotEmpty()) {
46+
dialog.setMultiChoiceItems(albumNames, selected) { _, which, isChecked ->
47+
selected[which] = isChecked
48+
}
49+
dialog.setPositiveButton("Save") { _, _ ->
50+
val selectedIds = albums.mapIndexedNotNull { i, album ->
51+
if (selected[i]) album.id else null
52+
}
53+
Log.d("dialog[setButton]", "selectedIds: $selectedIds")
54+
val request = FilesEditRequest(ids = fileIds, albums = selectedIds)
55+
CoroutineScope(Dispatchers.IO).launch { api.filesEdit(request) }
56+
Log.d("dialog[setButton]", "selectedIds: $selectedIds")
57+
setFragmentResult("albums_result", bundleOf("albums" to selectedIds))
58+
dismiss()
59+
}
60+
} else {
61+
dialog.setMessage("No Albums Found.")
62+
}
63+
64+
return dialog.create()
65+
}
66+
}

0 commit comments

Comments
 (0)