Skip to content

Commit fcaa9cb

Browse files
authored
Add Upload Options (#41)
- Add Albums to Upload - Update Upload Layouts - Update Upload Adapter
1 parent 6b751c2 commit fcaa9cb

18 files changed

+462
-259
lines changed

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ The URL to the file is automatically copied to the clipboard and the preview is
3030

3131
- Supports Android 8 (API 26) 2017 or Newer.
3232

33+
Screenshots can be found on the website: https://django-files.github.io/android/
34+
3335
| Django Files | Link |
3436
| ----------------- | :-------------------------------------------- |
3537
| Website | https://django-files.github.io/ |
@@ -88,21 +90,17 @@ Additionally, the URL is copied to the clipboard and the preview show in the app
8890

8991
- Share or Open any files and automatically copy the URL to the clipboard.
9092
- Upload Previews for Media with File Selector when uploading multiple files.
91-
- Native File List with Options, Infinite Scroll, and Live Previews for Media.
93+
- Native File List with Options, Infinite Scroll, and Multi-Select with options.
9294
- Supports Native Local Login, GitHub OAuth, Google OAuth, and Discord OAuth.
9395
- Ability to add multiple servers and switch on the fly from the Server List menu.
9496

9597
### Planned
9698

97-
- Add Default Upload Options.
98-
- Option to Disable Preview Page.
99-
- Ability to Auto Authenticate when your session expires.
100-
- Upload Preview
101-
- Upload Options
99+
- File Upload
100+
- Add More Upload Options
102101
- File List
103-
- Multi-Select with File Options
104102
- Response Caching for Infinite Scroll
105-
- File Preview
103+
- File List Preview
106104
- File Options
107105
- PDF Previews
108106

TODO.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
## Authentication
77

8+
> [!NOTE]
9+
> We have set mobile session length to 6 months user configurable and added session management.
10+
811
Options for persistent authentication are:
912

1013
1. Implement secure scoped token authentication.
@@ -17,7 +20,6 @@ Options for persistent authentication are:
1720

1821
## File List
1922

20-
- Add Multi-Select
2123
- Add Swiper to Preview
2224

2325
## File Preview
@@ -37,7 +39,6 @@ Options for persistent authentication are:
3739

3840
## Room
3941

40-
- Move Room to Directory
4142
- Implement Room Active Server
4243

4344
---
@@ -51,9 +52,12 @@ Options for persistent authentication are:
5152

5253
## Uploads
5354

55+
- Album Selector
56+
5457
### File
5558

5659
- File Previews
60+
- File Name
5761

5862
### Multiple Files
5963

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

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,31 @@ class ServerApi(val context: Context, host: String) {
120120
return api.getMethods()
121121
}
122122

123-
suspend fun upload(fileName: String, inputStream: InputStream): Response<UploadResponse> {
124-
Log.d("Api[upload]", "fileName: $fileName")
123+
suspend fun upload(
124+
fileName: String,
125+
inputStream: InputStream,
126+
editRequest: FileEditRequest,
127+
): Response<UploadResponse> {
128+
Log.d("Api[upload]", "fileName: $fileName - $editRequest")
125129
val multiPart: MultipartBody.Part = inputStreamToMultipart(inputStream, fileName)
126-
var response = api.postUpload(authToken, multiPart)
130+
var response = api.postUpload(
131+
authToken, multiPart,
132+
password = editRequest.password,
133+
private = editRequest.private,
134+
albums = editRequest.albums,
135+
)
127136
// TODO: Determine how to make this block a reusable function...
128137
Log.d("Api[upload]", "response.code: ${response.code()}")
129138
if (response.code() == 401) {
130139
val token = reAuthenticate()
131140
Log.d("Api[upload]", "token: $token")
132141
if (token != null) {
133-
response = api.postUpload(token, multiPart)
142+
response = api.postUpload(
143+
token, multiPart,
144+
password = editRequest.password,
145+
private = editRequest.private,
146+
albums = editRequest.albums,
147+
)
134148
}
135149
}
136150
return response
@@ -201,10 +215,11 @@ class ServerApi(val context: Context, host: String) {
201215
@Part file: MultipartBody.Part,
202216
@Header("Format") format: String? = null,
203217
@Header("Expires-At") expiresAt: String? = null,
204-
@Header("Strip-GPS") stripGps: String? = null,
205-
@Header("Strip-EXIF") stripExif: String? = null,
206-
@Header("Private") private: String? = null,
218+
@Header("Strip-GPS") stripGps: Boolean? = null,
219+
@Header("Strip-EXIF") stripExif: Boolean? = null,
220+
@Header("Private") private: Boolean? = null,
207221
@Header("Password") password: String? = null,
222+
@Header("Albums") albums: List<Int>? = null,
208223
): Response<UploadResponse>
209224

210225
@POST("shorten/")
@@ -298,27 +313,19 @@ class ServerApi(val context: Context, host: String) {
298313

299314
data class FileEditRequest(
300315
@SerializedName("id") val id: Int? = null,
301-
@SerializedName("user") val user: Int? = null,
302-
@SerializedName("size") val size: Int? = null,
303-
@SerializedName("mime") val mime: String? = null,
304-
@SerializedName("name") val name: String? = null,
305-
@SerializedName("info") val info: String? = null,
306-
@SerializedName("expr") val expr: String? = null,
307-
@SerializedName("view") val view: Int? = null,
308-
@SerializedName("maxv") val maxv: Int? = null,
309-
@SerializedName("meta_preview") val metaPreview: Boolean? = null,
310-
@SerializedName("password") val password: String? = null,
311-
@SerializedName("private") val private: Boolean? = null,
312-
@SerializedName("avatar") val avatar: Boolean? = null,
313-
@SerializedName("url") val url: String? = null,
314-
@SerializedName("thumb") val thumb: String? = null,
315-
@SerializedName("raw") val raw: String? = null,
316-
@SerializedName("date") val date: String? = null,
317-
@SerializedName("albums") val albums: List<Int>? = null,
316+
@SerializedName("name") var name: String? = null,
317+
@SerializedName("info") var info: String? = null,
318+
@SerializedName("expr") var expr: String? = null,
319+
@SerializedName("maxv") var maxv: Int? = null,
320+
@SerializedName("meta_preview") var metaPreview: Boolean? = null,
321+
@SerializedName("password") var password: String? = null,
322+
@SerializedName("private") var private: Boolean? = null,
323+
@SerializedName("albums") var albums: List<Int>? = null,
318324
)
319325

320326
data class FilesEditRequest(
321327
@SerializedName("ids") val ids: List<Int>,
328+
@SerializedName("name") val name: String? = null,
322329
@SerializedName("info") val info: String? = null,
323330
@SerializedName("expr") val expr: String? = null,
324331
@SerializedName("maxv") val maxv: Int? = null,
@@ -384,10 +391,23 @@ class ServerApi(val context: Context, host: String) {
384391
private fun createRetrofit(): Retrofit {
385392
val baseUrl = "${hostname}/api/"
386393
Log.d("createRetrofit", "baseUrl: $baseUrl")
394+
395+
val versionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName
396+
Log.d("Home[onViewCreated]", "versionName: $versionName")
397+
val userAgent = "DjangoFiles Android/${versionName}"
398+
Log.d("createRetrofit", "userAgent: $userAgent")
399+
387400
cookieJar = SimpleCookieJar()
388401
client = OkHttpClient.Builder()
389402
.cookieJar(cookieJar)
403+
.addInterceptor { chain ->
404+
val request = chain.request().newBuilder()
405+
.header("User-Agent", userAgent)
406+
.build()
407+
chain.proceed(request)
408+
}
390409
.build()
410+
391411
val gson = GsonBuilder().create()
392412
return Retrofit.Builder()
393413
.baseUrl(baseUrl)

app/src/main/java/com/djangofiles/djangofiles/db/AlbumDao.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.djangofiles.djangofiles.db
22

33
import android.content.Context
4-
import android.util.Log
54
import androidx.room.Dao
65
import androidx.room.Database
76
import androidx.room.Delete

app/src/main/java/com/djangofiles/djangofiles/ui/files/AlbumFragment.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ class AlbumFragment : DialogFragment() {
2222
private var fileIds: List<Int> = emptyList()
2323
private var selectedIds: List<Int> = emptyList()
2424

25-
fun setAlbumData(albums: List<AlbumEntity>, fileIds: List<Int>, selectedIds: List<Int>) {
25+
fun setAlbumData(albums: List<AlbumEntity>, fileIds: List<Int>, selectedIds: List<Int>?) {
2626
this.albums = albums
2727
this.fileIds = fileIds
28-
this.selectedIds = selectedIds
28+
this.selectedIds = selectedIds ?: emptyList()
2929
}
3030

3131
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -51,8 +51,10 @@ class AlbumFragment : DialogFragment() {
5151
if (selected[i]) album.id else null
5252
}
5353
Log.d("dialog[setButton]", "selectedIds: $selectedIds")
54-
val request = FilesEditRequest(ids = fileIds, albums = selectedIds)
55-
CoroutineScope(Dispatchers.IO).launch { api.filesEdit(request) }
54+
if (!fileIds.isEmpty()) {
55+
val request = FilesEditRequest(ids = fileIds, albums = selectedIds)
56+
CoroutineScope(Dispatchers.IO).launch { api.filesEdit(request) }
57+
}
5658
Log.d("dialog[setButton]", "selectedIds: $selectedIds")
5759
setFragmentResult("albums_result", bundleOf("albums" to selectedIds))
5860
dismiss()

app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesFragment.kt

Lines changed: 62 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ class FilesFragment : Fragment() {
193193
"viewModel.selectedUris.value: ${viewModel.selected.value}"
194194
)
195195
binding.filesSelectedHeader.visibility = View.VISIBLE
196-
Log.w("updateCheckButton", "DEBUG 1")
196+
Log.i("updateCheckButton", "DEBUG 1 - updateCheckButton")
197197
updateCheckButton()
198198
}
199199

@@ -217,8 +217,11 @@ class FilesFragment : Fragment() {
217217
}
218218

219219
viewModel.filesData.observe(viewLifecycleOwner) { list ->
220-
Log.d("filesData[observe]", "list: ${list?.size}")
221-
updateCheckButton()
220+
if (list != null) {
221+
Log.d("filesData[observe]", "list: ${list.size}")
222+
Log.i("updateCheckButton", "DEBUG 2 - updateCheckButton")
223+
updateCheckButton()
224+
}
222225
}
223226
viewModel.selected.observe(viewLifecycleOwner) { selected ->
224227
Log.d("selected[observe]", "selected.size: ${selected?.size}")
@@ -229,7 +232,7 @@ class FilesFragment : Fragment() {
229232
binding.filesSelectedHeader.visibility = View.GONE
230233
}
231234
//binding.filesSelectedText.text = getString(R.string.files_selected, selected.size)
232-
Log.w("updateCheckButton", "DEBUG 3")
235+
Log.i("updateCheckButton", "DEBUG 3 - updateCheckButton")
233236
updateCheckButton()
234237
}
235238
viewModel.atEnd.observe(viewLifecycleOwner) {
@@ -329,7 +332,7 @@ class FilesFragment : Fragment() {
329332
Log.d("File[filesSelectAll]", "size: ${viewModel.selected.value?.size}")
330333
//binding.filesSelectedText.text =
331334
// getString(R.string.files_selected, viewModel.selected.value?.size)
332-
Log.w("updateCheckButton", "DEBUG 4")
335+
Log.i("updateCheckButton", "DEBUG 4 - updateCheckButton")
333336
updateCheckButton()
334337

335338
if (positionIds.isNotEmpty()) {
@@ -576,60 +579,6 @@ class FilesFragment : Fragment() {
576579
}
577580
}
578581

579-
fun Context.showExpireDialog(
580-
fileIds: List<Int>,
581-
callback: (newExpr: String) -> Unit,
582-
currentValue: String? = null,
583-
) {
584-
// TODO: Refactor this function to not use a callback or not exist at all...
585-
Log.d("showExpireDialog", "$fileIds: $fileIds")
586-
587-
val layout = LinearLayout(this)
588-
layout.orientation = LinearLayout.VERTICAL
589-
layout.setPadding(10, 0, 10, 40)
590-
591-
val input = EditText(this)
592-
input.inputType = android.text.InputType.TYPE_CLASS_TEXT
593-
input.maxLines = 1
594-
input.hint = "6mo"
595-
596-
if (currentValue != null) {
597-
Log.d("showExpireDialog", "input.setText: currentValue: $currentValue")
598-
input.setText(currentValue)
599-
input.setSelection(0, currentValue.length)
600-
}
601-
input.requestFocus()
602-
layout.addView(input)
603-
604-
val savedUrl =
605-
this.getSharedPreferences("AppPreferences", MODE_PRIVATE).getString("saved_url", "")
606-
.toString()
607-
MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
608-
.setView(layout)
609-
.setTitle("Set Expiration")
610-
.setIcon(R.drawable.md_timer_24)
611-
.setMessage("Leave Blank for None")
612-
.setNegativeButton("Cancel", null)
613-
.setPositiveButton("Save") { _, _ ->
614-
val newExpire = input.text.toString().trim()
615-
Log.d("showExpireDialog", "newExpire: $newExpire")
616-
val api = ServerApi(this, savedUrl)
617-
CoroutineScope(Dispatchers.IO).launch {
618-
val response =
619-
api.filesEdit(FilesEditRequest(ids = fileIds, expr = newExpire))
620-
Log.d("showExpireDialog", "response: $response")
621-
if (response.isSuccessful) {
622-
withContext(Dispatchers.Main) {
623-
callback(newExpire)
624-
}
625-
} else {
626-
Log.w("showExpireDialog", "RESPONSE FAILURE")
627-
}
628-
}
629-
}
630-
.show()
631-
}
632-
633582
private fun Context.deleteConfirmDialog(
634583
fileIds: List<Int>,
635584
selectedPositions: List<Int>,
@@ -694,6 +643,60 @@ suspend fun Context.getAlbums(savedUrl: String) {
694643
}
695644
}
696645

646+
fun Context.showExpireDialog(
647+
fileIds: List<Int>,
648+
callback: (newExpr: String) -> Unit,
649+
currentValue: String? = null,
650+
) {
651+
// TODO: Refactor this function to not use a callback or not exist at all...
652+
Log.d("showExpireDialog", "$fileIds: $fileIds")
653+
654+
val layout = LinearLayout(this)
655+
layout.orientation = LinearLayout.VERTICAL
656+
layout.setPadding(10, 0, 10, 40)
657+
658+
val input = EditText(this)
659+
input.inputType = android.text.InputType.TYPE_CLASS_TEXT
660+
input.maxLines = 1
661+
input.hint = "6mo"
662+
663+
if (currentValue != null) {
664+
Log.d("showExpireDialog", "input.setText: currentValue: $currentValue")
665+
input.setText(currentValue)
666+
input.setSelection(0, currentValue.length)
667+
}
668+
input.requestFocus()
669+
layout.addView(input)
670+
671+
val savedUrl =
672+
this.getSharedPreferences("AppPreferences", MODE_PRIVATE).getString("saved_url", "")
673+
.toString()
674+
MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
675+
.setView(layout)
676+
.setTitle("Set Expiration")
677+
.setIcon(R.drawable.md_timer_24)
678+
.setMessage("Leave Blank for None")
679+
.setNegativeButton("Cancel", null)
680+
.setPositiveButton("Save") { _, _ ->
681+
val newExpire = input.text.toString().trim()
682+
Log.d("showExpireDialog", "newExpire: $newExpire")
683+
val api = ServerApi(this, savedUrl)
684+
CoroutineScope(Dispatchers.IO).launch {
685+
val response =
686+
api.filesEdit(FilesEditRequest(ids = fileIds, expr = newExpire))
687+
Log.d("showExpireDialog", "response: $response")
688+
if (response.isSuccessful) {
689+
withContext(Dispatchers.Main) {
690+
callback(newExpire)
691+
}
692+
} else {
693+
Log.w("showExpireDialog", "RESPONSE FAILURE")
694+
}
695+
}
696+
}
697+
.show()
698+
}
699+
697700
fun Context.openUrl(url: String) {
698701
val openIntent = Intent(Intent.ACTION_VIEW).apply {
699702
setDataAndType(url.toUri(), "text/plain")

app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesViewAdapter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,10 @@ class FilesViewAdapter(
322322
if (index != -1) {
323323
val file = dataSet[index]
324324
if (request.private != null) {
325-
file.private = request.private
325+
file.private = request.private!!
326326
}
327327
if (request.password != null) {
328-
file.password = request.password
328+
file.password = request.password!!
329329
}
330330
notifyItemChanged(index)
331331
}

0 commit comments

Comments
 (0)