Skip to content
Merged
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
9 changes: 9 additions & 0 deletions app/src/main/java/com/gatecontrol/android/drive/DriveApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ class DriveApi @Inject constructor(
suspend fun insightApply(id: Long, name: Boolean, folder: Boolean): DResult<DInsightApplied> =
post("nodes/$id/insight/apply", DriveJson.applyBody(name, folder), DriveJson::insightApplied)

/**
* Drive 1.18 (`overview`): a file opened from this phone's cache, without `content` (for "Zuletzt geöffnet";
* `POST nodes/<id>/opened` → 204).
*/
suspend fun opened(id: Long): DResult<Unit> = post("nodes/$id/opened", JsonObject(), unit)

/** Drive 1.18 (`overview`): everything the overview shows, in one call. */
suspend fun overview(): DResult<DOverview> = get("overview", parse = DriveOverviewJson::overview)

/** The deadlines of the files I read, [from]..[to] (`YYYY-MM-DD`; default today to a year on). */
suspend fun deadlines(from: String? = null, to: String? = null): DResult<List<DDeadline>> {
val q = buildMap {
Expand Down
26 changes: 15 additions & 11 deletions app/src/main/java/com/gatecontrol/android/drive/DriveLogic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -308,22 +308,26 @@ internal object DriveLogic {
/** The doc kind of a file in a list (a small symbol): only when the AI is done. */
fun docKind(node: DNode): String? = node.insight?.takeIf { it.status == "ready" && !node.isDir }?.kind

// ─── the Files and Photos tabs ───────────────────────────────────
// ─── the pages of the Drive tab ──────────────────────────────────

/** The Drive tab's pages: the files, then (feature `photos`) the photos; swiping goes from one to the other. */
const val TAB_FILES = 0
const val TAB_PHOTOS = 1
/**
* The Drive tab's pages (Drive 1.18): the overview, the files, then (feature `photos`) the photos. Drive always
* opens on the overview (the page is not kept); a folder or a photo opened from elsewhere has a page of its own.
*/
enum class Tab { OVERVIEW, FILES, PHOTOS }

fun tabs(hasPhotos: Boolean): List<Tab> = if (hasPhotos) listOf(Tab.OVERVIEW, Tab.FILES, Tab.PHOTOS) else listOf(Tab.OVERVIEW, Tab.FILES)

fun tabCount(hasPhotos: Boolean): Int = if (hasPhotos) 2 else 1
fun tabCount(hasPhotos: Boolean): Int = tabs(hasPhotos).size

/** The page to show for the saved choice "photos" ([photos]); without the Photos tab always the files. */
fun tabOf(photos: Boolean, hasPhotos: Boolean): Int = if (photos && hasPhotos) TAB_PHOTOS else TAB_FILES
/** The page of [tab]; the photos without the Photos tab: the files. */
fun pageOf(tab: Tab, hasPhotos: Boolean): Int = tabs(hasPhotos).indexOf(tab).takeIf { it >= 0 } ?: tabs(hasPhotos).indexOf(Tab.FILES)

/** Whether [page] is the photos (what is saved as the tab's choice). */
fun isPhotosTab(page: Int, hasPhotos: Boolean): Boolean = hasPhotos && page == TAB_PHOTOS
/** The tab at [page] (a page beyond the last: the last). */
fun tabAt(page: Int, hasPhotos: Boolean): Tab = tabs(hasPhotos).let { it[page.coerceIn(0, it.size - 1)] }

/** Swiping between the pages: only when there are two, not while photos are ticked (that bar belongs to the page). */
fun tabSwipe(hasPhotos: Boolean, selecting: Boolean): Boolean = hasPhotos && !selecting
/** Swiping between the pages: not while items are ticked (that bar belongs to the page). */
fun tabSwipe(selecting: Boolean): Boolean = !selecting

// ─── sorting a folder ───────────────────────────────────────────

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ data class DMe(
const val FEATURE_PHOTO_PEOPLE = "photoPeople"
/** Drive 1.17: deadlines into a calendar of the mail plugin too (`deadlines/calendars`, `deadlines/targets`). */
const val FEATURE_DEADLINE_MAIL_CALENDAR = "deadlineMailCalendar"
/** Drive 1.18: the overview of Drive in one call (`GET overview`); without it the app builds it from the rest. */
const val FEATURE_OVERVIEW = "overview"
}
}

Expand Down
357 changes: 357 additions & 0 deletions app/src/main/java/com/gatecontrol/android/drive/DriveOverview.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,8 @@ class DriveTransfers @Inject constructor(
val target = File(dir, name)
if (target.isFile && (versionId != null || target.length() == node.size)) {
target.setLastModified(System.currentTimeMillis())
// Drive 1.18: "Zuletzt geöffnet" — the copy here loaded nothing from Drive, so it is told (errors stay quiet)
if (DriveOverviewLogic.reportOpen(live.me.value, node, versionId)) scope.launch { api.opened(node.id) }
return@withContext DResult.Ok(target)
}
_downloading.update { it + (node.id to 0f) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,9 @@ private fun DriveHome(navController: NavHostController, onShown: (Long?) -> Unit
vault = nav.vault,
settings = { nav.go(Screen.DriveSettings.route) },
cleanup = { nav.go(Screen.DriveCleanup.route) },
backup = { nav.go(Screen.DriveBackup.route) },
// Mail › "Zum Ablegen" (only where Mail's routes are there)
mailFiling = { runCatching { navController.navigate(Screen.MailFiling.route) } },
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,21 +577,13 @@ internal fun UploadsSheet(
internal object DriveViewPref {
private const val PREFS = "drive_ui"
private const val KEY = "grid"
private const val KEY_PHOTOS = "photos"

fun grid(context: Context): Boolean = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(KEY, false)

fun setGrid(context: Context, grid: Boolean) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(KEY, grid).apply()
}

/** The Drive tab shows the photos (Drive 1.7), not the files. */
fun photos(context: Context): Boolean = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(KEY_PHOTOS, false)

fun setPhotos(context: Context, photos: Boolean) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(KEY_PHOTOS, photos).apply()
}

private const val KEY_CAMERA = "photos_camera"

/** The Photos tab shows only the camera uploads (Drive 1.10, feature `photosFolder`). */
Expand Down
114 changes: 79 additions & 35 deletions app/src/main/java/com/gatecontrol/android/ui/drive/DriveHomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,15 @@ data class DriveNav(
val cleanup: () -> Unit = {},
/** The folder the Files page shows now (null: none, or the photos) — a folder page's path goes back to it. */
val shown: (Long?) -> Unit = {},
/** The photo backup (the overview's "Fotos gesichert", Drive 1.18). */
val backup: () -> Unit = {},
/** Mail › "Zum Ablegen" (the overview's attachments to file); null: no mail here. */
val mailFiling: (() -> Unit)? = null,
)

/**
* The Drive tab: the chosen area's files, the areas and the trash in the menu top left. The
* photo backup is in the settings.
* The Drive tab (Drive 1.18): "Übersicht" | "Dateien" | "Fotos", always opening on the overview; the areas and the
* trash in the menu top left. The photo backup is in the settings.
*/
@Composable
fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel()) {
Expand All @@ -131,6 +135,7 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
}
val folderViewModel: DriveFolderViewModel = hiltViewModel(key = "drive-tab")
val photosViewModel: DrivePhotosViewModel = hiltViewModel(key = "drive-photos")
val overviewViewModel: DriveOverviewViewModel = hiltViewModel(key = "drive-overview")
val context = LocalContext.current
val scope = rememberCoroutineScope()
val drawer = rememberDrawerState(DrawerValue.Closed)
Expand Down Expand Up @@ -168,37 +173,29 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
}
}
val openMenu: () -> Unit = { scope.launch { drawer.open() } }
// Drive 1.7 (feature `photos`): the tab has two pages, the files and the photos; a swipe goes from one to the other.
// Drive 1.18: Übersicht | Dateien | Fotos (feature `photos`); a swipe goes from one to the next. Drive opens on the
// overview: the page is not kept (only while the tab is on the back stack, e.g. back from a folder)
val hasPhotos = me?.has(DMe.FEATURE_PHOTOS) == true
val tabs = DriveLogic.tabs(hasPhotos)
val ticked by photosViewModel.selected.collectAsStateWithLifecycle()
val folderTicked by folderViewModel.bulk.selected.collectAsStateWithLifecycle()
/** Items are ticked on one of the pages: its own bar shows instead of the tab's, swiping stays off. */
val selecting = ticked.isNotEmpty() || folderTicked.isNotEmpty()
val pager = rememberPagerState(initialPage = DriveLogic.tabOf(DriveViewPref.photos(context), hasPhotos)) { DriveLogic.tabCount(hasPhotos) }
val onPhotosPage = DriveLogic.isPhotosTab(pager.currentPage, hasPhotos)
val showPage: (Boolean) -> Unit = { photos -> scope.launch { pager.animateScrollToPage(DriveLogic.tabOf(photos, hasPhotos)) } }
val pager = rememberPagerState(initialPage = DriveLogic.pageOf(DriveLogic.Tab.OVERVIEW, hasPhotos)) { DriveLogic.tabCount(hasPhotos) }
val tab = DriveLogic.tabAt(pager.currentPage, hasPhotos)
val onPhotosPage = tab == DriveLogic.Tab.PHOTOS
val onFilesPage = tab == DriveLogic.Tab.FILES
val showTab: (DriveLogic.Tab) -> Unit = { t -> scope.launch { pager.animateScrollToPage(DriveLogic.pageOf(t, hasPhotos)) } }
// the folder on the Files page: the path of a folder page goes back here when it names this one
val shownId = shown?.id?.takeIf { !onPhotosPage }
val shownId = shown?.id?.takeIf { onFilesPage }
// Drive 1.14 (`photoSearch`): photos without words get them now (in the background, under the backup's conditions)
val photoSearch = me?.has(DMe.FEATURE_PHOTO_SEARCH) == true
LaunchedEffect(onPhotosPage, photoSearch) {
if (onPhotosPage && photoSearch) com.gatecontrol.android.drive.backup.DrivePhotoLabelsWork.request(context)
}
LaunchedEffect(shownId) { nav.shown(shownId) }
// the page the person ends on is kept, and shown again when the person comes back to the tab
var synced by remember { mutableStateOf(false) }
LaunchedEffect(hasPhotos) {
if (!hasPhotos) return@LaunchedEffect
val want = DriveLogic.tabOf(DriveViewPref.photos(context), true)
if (pager.currentPage != want) pager.scrollToPage(want)
synced = true
}
LaunchedEffect(pager.settledPage, synced) { if (synced && hasPhotos) DriveViewPref.setPhotos(context, DriveLogic.isPhotosTab(pager.settledPage, true)) }
LifecycleResumeEffect(pager, hasPhotos, synced) {
val want = DriveLogic.tabOf(DriveViewPref.photos(context), hasPhotos)
if (synced && pager.currentPage != want && !pager.isScrollInProgress) scope.launch { pager.scrollToPage(want) }
onPauseOrDispose { }
}
/** "Wer ist das?" of the overview: the person being named. */
var naming by remember { mutableStateOf<Long?>(null) }
val vaults by viewModel.vaults.collectAsStateWithLifecycle()
val expanded by viewModel.expanded.collectAsStateWithLifecycle()
val children by viewModel.children.collectAsStateWithLifecycle()
Expand All @@ -210,7 +207,7 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
}
val grid = rememberDriveGrid()
// a folder below the area's top (opened from the menu): back goes up a level
BackHandler(enabled = !drawer.isOpen && !selecting && !onPhotosPage && shown != null && shown.parentId != null && shown.areaId == current?.id) {
BackHandler(enabled = !drawer.isOpen && !selecting && onFilesPage && shown != null && shown.parentId != null && shown.areaId == current?.id) {
shown?.parentId?.let(folderViewModel::open)
}
BackHandler(enabled = drawer.isOpen) { scope.launch { drawer.close() } }
Expand All @@ -232,7 +229,7 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
areas = areas,
personal = personal,
shared = shared,
selected = shown?.id?.takeIf { !onPhotosPage },
selected = shown?.id?.takeIf { onFilesPage },
onToggle = viewModel::toggle,
onOpen = { row ->
val area = areas.firstOrNull { it.id == row.areaId }
Expand All @@ -244,7 +241,7 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
folderViewModel.open(row.id)
}
}
if (onPhotosPage) showPage(false)
if (!onFilesPage) showTab(DriveLogic.Tab.FILES)
close()
},
// Drive 1.9 (`vault`): the vaults are opened from here only (Drive 1.11)
Expand Down Expand Up @@ -285,40 +282,79 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
bar = {
TabBar(
when {
tab == DriveLogic.Tab.OVERVIEW -> stringResource(R.string.drive_title)
onPhotosPage -> stringResource(R.string.drive_mode_photos)
shown != null && shown.parentId != null && shown.areaId == current.id -> shown.name
else -> current.label
},
onMenu = openMenu,
onSearch = if (onPhotosPage && DriveLogic.photoSearchable(me)) nav.photoSearch else nav.search,
// the overview has its own search field
onSearch = when {
tab == DriveLogic.Tab.OVERVIEW -> null
onPhotosPage && DriveLogic.photoSearchable(me) -> nav.photoSearch
else -> nav.search
},
actions = {
if (!onPhotosPage) {
FolderViewActions(folderViewModel, grid)
} else if (me?.has(DMe.FEATURE_PHOTO_PLACES) == true) {
// Drive 1.16: the photos on the map
GcIconButton(GcIcons.Map, contentDescription = stringResource(R.string.drive_pp_map), onClick = nav.photoMap, tint = MaterialTheme.colorScheme.onBackground)
when (tab) {
DriveLogic.Tab.OVERVIEW ->
GcIconButton(GcIcons.Settings, contentDescription = stringResource(R.string.drive_settings_title), onClick = nav.settings, tint = MaterialTheme.colorScheme.onBackground)
DriveLogic.Tab.FILES -> FolderViewActions(folderViewModel, grid)
DriveLogic.Tab.PHOTOS -> {
// Drive 1.16: the photos on the map, the people (Drive 1.18: the symbols of the page's head)
if (me?.has(DMe.FEATURE_PHOTO_PLACES) == true) {
GcIconButton(GcIcons.Map, contentDescription = stringResource(R.string.drive_pp_map), onClick = nav.photoMap, tint = MaterialTheme.colorScheme.onBackground)
}
if (me?.has(DMe.FEATURE_PHOTO_PEOPLE) == true) {
GcIconButton(GcIcons.Family, contentDescription = stringResource(R.string.drive_pp_people), onClick = nav.people, tint = MaterialTheme.colorScheme.onBackground)
}
}
}
},
)
},
)
if (hasPhotos) Box(Modifier.inert(selecting)) { DriveModeSwitch(photos = onPhotosPage, onPhotos = showPage) }
Box(Modifier.inert(selecting)) { DriveModeSwitch(tabs = tabs, tab = tab, onTab = showTab) }
HorizontalPager(
state = pager,
modifier = Modifier.weight(1f),
// ticked items keep their page (the selection's bars belong to it)
// As a page beside Start, swiping changes the page: Dateien and Fotos by tapping only.
userScrollEnabled = DriveLogic.tabSwipe(hasPhotos, selecting) && !com.gatecontrol.android.ui.start.LocalInStartPager.current,
key = { it },
userScrollEnabled = DriveLogic.tabSwipe(selecting) && !com.gatecontrol.android.ui.start.LocalInStartPager.current,
key = { DriveLogic.tabAt(it, hasPhotos) },
) { page ->
if (DriveLogic.isPhotosTab(page, hasPhotos)) {
val pageTab = DriveLogic.tabAt(page, hasPhotos)
if (pageTab == DriveLogic.Tab.OVERVIEW) {
DriveOverviewContent(
viewModel = overviewViewModel,
areas = home.areas,
naming = { naming = it },
actions = OverviewActions(
search = nav.search,
backup = nav.backup,
storage = if (me?.has(DMe.FEATURE_STORAGE) == true) nav.storage else null,
deadline = { d -> d.node?.id?.let(nav.file) ?: nav.deadlines() },
deadlines = nav.deadlines,
review = { photosViewModel.setClass("review"); showTab(DriveLogic.Tab.PHOTOS) },
toFile = nav.mailFiling,
file = nav.file,
folder = nav.folder,
files = { showTab(DriveLogic.Tab.FILES) },
photos = { showTab(DriveLogic.Tab.PHOTOS) },
memory = {},
people = nav.people,
person = nav.person,
who = {},
map = nav.photoMap,
place = nav.place,
),
)
} else if (pageTab == DriveLogic.Tab.PHOTOS) {
DrivePhotosContent(
viewModel = photosViewModel,
topBar = {},
onAlbum = nav.album,
onCleanup = nav.cleanup,
onFile = nav.file,
places = PhotosPlacesNav(search = nav.photoSearch, map = nav.photoMap, place = nav.place, people = nav.people, person = nav.person),
ownHead = false,
)
} else {
Expand Down Expand Up @@ -375,6 +411,14 @@ fun DriveHomeScreen(nav: DriveNav, viewModel: DriveHomeViewModel = hiltViewModel
}
}
}
naming?.let { id ->
NamePersonSheet(
personId = id,
onDismiss = { naming = null; overviewViewModel.load() },
// "Überspringen" and "Nächste Person" (each person at most once, then "Alle durchgesehen")
round = true,
)
}
guardAsk?.let { (incident, undo) ->
GuardConfirm(
incident = incident,
Expand Down
Loading
Loading