iOS drawer: flat reorderable calendars, long-press menu, left-swipe close, header actions
Testing feedback: - calendars are now one flat, drag-reorderable list (no per-source grouping); order is device-local (CalendarStore.calendarOrder, mirrors the web cal_order). Reordering is done via a 'Sort' toggle (native edit mode with drag handles). - banish + reminder-mute moved from row swipe actions to a long-press context menu, which frees the horizontal swipe: swiping the drawer left closes it. - the hamburger moved to the LEFT (drawer opens left); new device-local setting 'Hide menu button' (open via edge-swipe only), not synced. - the settings entry moved from the footer into the header as an icon, next to a restored manual-sync icon and the close button; the footer button is gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ struct CalendarDrawer: View {
|
||||
let onSwitchGroup: (CalGroup?) -> Void
|
||||
let onSelectView: (CalViewType) -> Void
|
||||
let onOpenMenu: () -> Void
|
||||
let onSync: () -> Void
|
||||
let onClose: () -> Void
|
||||
|
||||
@Environment(AppState.self) private var appState
|
||||
@@ -25,8 +26,6 @@ struct CalendarDrawer: View {
|
||||
}
|
||||
Divider()
|
||||
CalendarFilterContent(api: api, store: store)
|
||||
Divider()
|
||||
navFooter
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.background(Color(.systemBackground))
|
||||
@@ -51,10 +50,22 @@ struct CalendarDrawer: View {
|
||||
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||
}
|
||||
Spacer()
|
||||
Button { onClose() } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 15, weight: .semibold))
|
||||
HStack(spacing: 18) {
|
||||
Button { onSync() } label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath").font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
.buttonStyle(.plain).foregroundStyle(Color.accentColor)
|
||||
.accessibilityLabel(L10n.t("menu.sync", appLang))
|
||||
Button { onOpenMenu() } label: {
|
||||
Image(systemName: "gearshape").font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
.buttonStyle(.plain).foregroundStyle(Color.accentColor)
|
||||
.accessibilityLabel(L10n.t("menu.section.settings", appLang))
|
||||
Button { onClose() } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 15, weight: .semibold))
|
||||
}
|
||||
.buttonStyle(.plain).foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain).foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 10)
|
||||
}
|
||||
@@ -111,20 +122,4 @@ struct CalendarDrawer: View {
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: – Nav footer (single entry into the full menu)
|
||||
|
||||
private var navFooter: some View {
|
||||
Button { onOpenMenu() } label: {
|
||||
Label(L10n.t("menu.section.settings", appLang), systemImage: "gearshape")
|
||||
.font(.body.weight(.medium))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.padding(.horizontal, 16)
|
||||
// Extra bottom inset so the button clears the home indicator and the
|
||||
// screen's rounded corners.
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ struct CalendarHostView: View {
|
||||
@AppStorage("defaultView") private var defaultView = "month"
|
||||
// Opt-in: empty keeps the translucent `.bar` material; a hex tints the top bar.
|
||||
@AppStorage("surfaceColor") private var surfaceHex = ""
|
||||
// Device-local: hide the hamburger (drawer then opens only via edge-swipe).
|
||||
@AppStorage("hideMenuButton") private var hideMenuButton = false
|
||||
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
@@ -63,6 +65,7 @@ struct CalendarHostView: View {
|
||||
onSwitchGroup: { g in closeDrawer(); switchGroup(g) },
|
||||
onSelectView: { vt in store.viewType = vt; closeDrawer() },
|
||||
onOpenMenu: { closeDrawer(); showMenu = true },
|
||||
onSync: { closeDrawer(); Task { await syncFromServer(force: true) } },
|
||||
onClose: { closeDrawer() }
|
||||
)
|
||||
.frame(width: drawerWidth)
|
||||
@@ -70,6 +73,16 @@ struct CalendarHostView: View {
|
||||
.shadow(color: .black.opacity(showDrawer ? 0.25 : 0), radius: 12, x: 4)
|
||||
.offset(x: showDrawer ? 0 : -(drawerWidth + 60))
|
||||
.animation(.easeInOut(duration: 0.25), value: showDrawer)
|
||||
// Swipe the drawer toward the left edge to close it.
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 20)
|
||||
.onEnded { v in
|
||||
if showDrawer, v.translation.width < -45,
|
||||
abs(v.translation.width) > abs(v.translation.height) {
|
||||
closeDrawer()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// A narrow leading strip opens the drawer via edge-swipe (kept off the
|
||||
// content area so month paging / week swipe stay free).
|
||||
@@ -169,15 +182,13 @@ struct CalendarHostView: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
HStack(spacing: 2) {
|
||||
if !hideMenuButton { menuButton }
|
||||
Button { store.navigatePrev() } label: { Image(systemName: "chevron.left") }
|
||||
Button { store.navigateNext() } label: { Image(systemName: "chevron.right") }
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(spacing: 8) {
|
||||
Button(L10n.t("nav.today", appLang)) { store.moveToToday() }.font(.callout)
|
||||
menuButton
|
||||
}
|
||||
Button(L10n.t("nav.today", appLang)) { store.moveToToday() }.font(.callout)
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
@@ -223,6 +234,8 @@ struct CalendarHostView: View {
|
||||
/// updates reliably on month change — is identical in both modes.
|
||||
@ViewBuilder private var barContents: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Menu (hamburger) on the left — the drawer opens from the left.
|
||||
if !hideMenuButton { menuButton.padding(.leading, 2) }
|
||||
HStack(spacing: 2) {
|
||||
Button { store.navigatePrev() } label: {
|
||||
Image(systemName: "chevron.left")
|
||||
@@ -235,7 +248,7 @@ struct CalendarHostView: View {
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.padding(.leading, hideMenuButton ? 6 : 0)
|
||||
Spacer(minLength: 6)
|
||||
Text(titleString)
|
||||
.font(.headline)
|
||||
@@ -246,8 +259,7 @@ struct CalendarHostView: View {
|
||||
Button(L10n.t("nav.today", appLang)) { store.moveToToday() }
|
||||
.font(.callout).padding(.horizontal, 6)
|
||||
.lineLimit(1).fixedSize()
|
||||
menuButton
|
||||
.padding(.trailing, 2)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
.frame(height: 48)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user