feature/ios-time-entries-rework #2
@@ -1,40 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="48 48 416 416" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#818CF8" />
|
||||
<stop offset="100%" stop-color="#4F46E5" />
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 4.0 -->
|
||||
<svg width="416" height="416" viewBox="0 0 416 416" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id="linearGradient1" x1="0" y1="0" x2="416" y2="416" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="1e-05" stop-color="#818cf8" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#4f46e5" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- App Icon Background -->
|
||||
<rect x="48" y="48" width="416" height="416" rx="96" fill="url(#bg)" />
|
||||
|
||||
<!-- Inner Icon Group -->
|
||||
<g stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Stopwatch Top Button -->
|
||||
<path d="M256 96 v44" stroke-width="28" />
|
||||
<path d="M224 88 h64" stroke-width="24" />
|
||||
|
||||
<!-- Stopwatch Side Button -->
|
||||
<path d="M352 176 l 24 -24" stroke-width="24" />
|
||||
<!-- Cap for side button -->
|
||||
<path d="M362 138 l 28 28" stroke-width="24" />
|
||||
|
||||
<!-- Outer Ring -->
|
||||
<circle cx="256" cy="272" r="132" stroke-width="28" />
|
||||
|
||||
<!-- Clock Hands -->
|
||||
<!-- Minute Hand -->
|
||||
<path d="M256 184 v 88" stroke-width="24" />
|
||||
<!-- Hour Hand -->
|
||||
<path d="M256 272 l 48 32" stroke-width="24" />
|
||||
|
||||
<!-- Dial Tick Marks -->
|
||||
<g stroke-width="12" opacity="0.6">
|
||||
<line x1="256" y1="172" x2="256" y2="188" />
|
||||
<line x1="256" y1="356" x2="256" y2="372" />
|
||||
<line x1="172" y1="272" x2="188" y2="272" />
|
||||
<line x1="340" y1="272" x2="324" y2="272" />
|
||||
<path id="Path" fill="url(#linearGradient1)" stroke="none" d="M 96 0 L 320 0 C 373.019348 0 416 42.980652 416 96 L 416 320 C 416 373.019348 373.019348 416 320 416 L 96 416 C 42.980667 416 0 373.019348 0 320 L 0 96 C 0 42.980652 42.980667 0 96 0 Z"/>
|
||||
<g id="Group">
|
||||
<path id="path1" fill="none" stroke="#ffffff" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" d="M 208 48 L 208 92"/>
|
||||
<path id="path2" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 176 40 L 240 40"/>
|
||||
<path id="path3" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 304 128 L 328 104"/>
|
||||
<path id="path4" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 314 90 L 342 118"/>
|
||||
<path id="path5" fill="none" stroke="#ffffff" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" d="M 340 224 C 340 296.901581 280.901581 356 208 356 C 135.098419 356 76 296.901581 76 224 C 76 151.098419 135.098419 92 208 92 C 280.901581 92 340 151.098419 340 224 Z"/>
|
||||
<path id="path6" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 208 136 L 208 224"/>
|
||||
<path id="path7" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 208 224 L 256 256"/>
|
||||
<g id="g1" opacity="0.6">
|
||||
<path id="path8" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M 208 308 L 208 324"/>
|
||||
<path id="path9" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M 124 224 L 140 224"/>
|
||||
<path id="path10" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M 292 224 L 276 224"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -1,43 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#818CF8" />
|
||||
<stop offset="100%" stop-color="#4F46E5" />
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 4.0 -->
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id="linearGradient1" x1="48" y1="48" x2="464" y2="464" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="1e-05" stop-color="#818cf8" stop-opacity="1"/>
|
||||
<stop offset="1" stop-color="#4f46e5" stop-opacity="1"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#4F46E5" flood-opacity="0.4" />
|
||||
<filter id="filter1" x="0" y="0" width="512" height="512" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="16"/>
|
||||
<feOffset dx="0" dy="12" result="offsetblur"/>
|
||||
<feFlood flood-color="#4f46e5" flood-opacity="0.4"/>
|
||||
<feComposite in2="offsetblur" operator="in"/>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- App Icon Background -->
|
||||
<rect x="48" y="48" width="416" height="416" rx="96" fill="url(#bg)" filter="url(#shadow)" />
|
||||
|
||||
<!-- Inner Icon Group -->
|
||||
<g stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Stopwatch Top Button -->
|
||||
<path d="M256 96 v44" stroke-width="28" />
|
||||
<path d="M224 88 h64" stroke-width="24" />
|
||||
|
||||
<!-- Stopwatch Side Button -->
|
||||
<path d="M352 176 l 24 -24" stroke-width="24" />
|
||||
<!-- Cap for side button -->
|
||||
<path d="M362 138 l 28 28" stroke-width="24" />
|
||||
|
||||
<!-- Outer Ring -->
|
||||
<circle cx="256" cy="272" r="132" stroke-width="28" />
|
||||
|
||||
<!-- Clock Hands -->
|
||||
<!-- Minute Hand -->
|
||||
<path d="M256 184 v 88" stroke-width="24" />
|
||||
<!-- Hour Hand -->
|
||||
<path d="M256 272 l 48 32" stroke-width="24" />
|
||||
|
||||
<!-- Dial Tick Marks -->
|
||||
<g stroke-width="12" opacity="0.6">
|
||||
<line x1="256" y1="172" x2="256" y2="188" />
|
||||
<line x1="256" y1="356" x2="256" y2="372" />
|
||||
<line x1="172" y1="272" x2="188" y2="272" />
|
||||
<line x1="340" y1="272" x2="324" y2="272" />
|
||||
<path id="Path" fill="url(#linearGradient1)" stroke="none" filter="url(#filter1)" d="M 144 48 L 368 48 C 421.019348 48 464 90.980652 464 144 L 464 368 C 464 421.019348 421.019348 464 368 464 L 144 464 C 90.980667 464 48 421.019348 48 368 L 48 144 C 48 90.980652 90.980667 48 144 48 Z"/>
|
||||
<g id="Group">
|
||||
<path id="path1" fill="none" stroke="#ffffff" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" d="M 256 96 L 256 140"/>
|
||||
<path id="path2" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 224 88 L 288 88"/>
|
||||
<path id="path3" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 352 176 L 376 152"/>
|
||||
<path id="path4" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 362 138 L 390 166"/>
|
||||
<path id="path5" fill="none" stroke="#ffffff" stroke-width="28" stroke-linecap="round" stroke-linejoin="round" d="M 388 272 C 388 344.901581 328.901581 404 256 404 C 183.098419 404 124 344.901581 124 272 C 124 199.098419 183.098419 140 256 140 C 328.901581 140 388 199.098419 388 272 Z"/>
|
||||
<path id="path6" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 256 184 L 256 272"/>
|
||||
<path id="path7" fill="none" stroke="#ffffff" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" d="M 256 272 L 304 304"/>
|
||||
<g id="g1" opacity="0.6">
|
||||
<path id="path8" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M 256 356 L 256 372"/>
|
||||
<path id="path9" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M 172 272 L 188 272"/>
|
||||
<path id="path10" fill="none" stroke="#ffffff" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" d="M 340 272 L 324 272"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -2,27 +2,19 @@ import SwiftUI
|
||||
|
||||
struct TimeEntriesView: View {
|
||||
@StateObject private var viewModel = TimeEntriesViewModel()
|
||||
@State private var selectedDay: Date? = Calendar.current.startOfDay(for: Date())
|
||||
@State private var visibleWeekStart: Date = Self.mondayOfWeek(containing: Date())
|
||||
|
||||
// dayOffset is the source of truth: 0 = today, -1 = yesterday, etc.
|
||||
@State private var dayOffset: Int = 0
|
||||
// tabSelection is always snapped back to 1 (middle) after each swipe.
|
||||
// Pages are: 0 = dayOffset-1, 1 = dayOffset, 2 = dayOffset+1
|
||||
@State private var tabSelection: Int = 1
|
||||
|
||||
@State private var showFilterSheet = false
|
||||
@State private var showAddEntry = false
|
||||
@State private var entryToEdit: TimeEntry?
|
||||
@State private var entryToDelete: TimeEntry?
|
||||
@State private var showDeleteConfirmation = false
|
||||
|
||||
private static func mondayOfWeek(containing date: Date) -> Date {
|
||||
var cal = Calendar.current
|
||||
cal.firstWeekday = 2 // Monday
|
||||
let comps = cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date)
|
||||
return cal.date(from: comps) ?? Calendar.current.startOfDay(for: date)
|
||||
}
|
||||
|
||||
private var visibleWeekDays: [Date] {
|
||||
(0..<7).compactMap {
|
||||
Calendar.current.date(byAdding: .day, value: $0, to: visibleWeekStart)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
@@ -37,6 +29,7 @@ struct TimeEntriesView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle("Entries")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbarContent }
|
||||
.task { await viewModel.loadEntries() }
|
||||
.refreshable { await viewModel.loadEntries() }
|
||||
@@ -81,32 +74,64 @@ struct TimeEntriesView: View {
|
||||
Image(systemName: viewModel.activeFilters.isEmpty ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
|
||||
}
|
||||
}
|
||||
|
||||
// Place the DatePicker in the principal placement (center of nav bar)
|
||||
ToolbarItem(placement: .principal) {
|
||||
DatePicker(
|
||||
"",
|
||||
selection: Binding(
|
||||
get: {
|
||||
Calendar.current.date(byAdding: .day, value: dayOffset, to: Calendar.current.startOfDay(for: Date())) ?? Date()
|
||||
},
|
||||
set: { newDate in
|
||||
let today = Calendar.current.startOfDay(for: Date())
|
||||
let normalizedNewDate = Calendar.current.startOfDay(for: newDate)
|
||||
let components = Calendar.current.dateComponents([.day], from: today, to: normalizedNewDate)
|
||||
if let dayDifference = components.day {
|
||||
dayOffset = dayDifference
|
||||
}
|
||||
}
|
||||
),
|
||||
displayedComponents: .date
|
||||
)
|
||||
.datePickerStyle(.compact)
|
||||
.labelsHidden()
|
||||
.environment(\.locale, Locale.current) // Ensure correct start of week
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Main content
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(spacing: 0) {
|
||||
WeekStripView(
|
||||
weekDays: visibleWeekDays,
|
||||
selectedDay: $selectedDay,
|
||||
daysWithEntries: viewModel.daysWithEntries,
|
||||
onSwipeLeft: {
|
||||
visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: visibleWeekStart) ?? visibleWeekStart
|
||||
},
|
||||
onSwipeRight: {
|
||||
visibleWeekStart = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: visibleWeekStart) ?? visibleWeekStart
|
||||
}
|
||||
)
|
||||
|
||||
Divider()
|
||||
// Only 3 pages exist at any time: previous, current, next.
|
||||
// After each swipe settles, we reset tabSelection to 1 and shift
|
||||
// dayOffset, so the carousel appears infinite while staying cheap.
|
||||
TabView(selection: $tabSelection) {
|
||||
ForEach(0..<3, id: \.self) { page in
|
||||
let offset = dayOffset + (page - 1)
|
||||
let day = Calendar.current.date(
|
||||
byAdding: .day,
|
||||
value: offset,
|
||||
to: Calendar.current.startOfDay(for: Date())
|
||||
) ?? Date()
|
||||
|
||||
ScrollView {
|
||||
if let day = selectedDay {
|
||||
dayEntriesSection(for: day)
|
||||
} else {
|
||||
allEntriesSection
|
||||
}
|
||||
.tag(page)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.onChange(of: tabSelection) { _, newPage in
|
||||
guard newPage != 1 else { return }
|
||||
// Shift the logical day offset by how many pages we moved.
|
||||
dayOffset += newPage - 1
|
||||
// Snap back to the middle page without animation so the
|
||||
// surrounding pages are refreshed invisibly.
|
||||
var tx = Transaction()
|
||||
tx.disablesAnimations = true
|
||||
withTransaction(tx) {
|
||||
tabSelection = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +141,8 @@ struct TimeEntriesView: View {
|
||||
private func dayEntriesSection(for day: Date) -> some View {
|
||||
let dayEntries = viewModel.entries(for: day)
|
||||
return VStack(alignment: .leading, spacing: 0) {
|
||||
// Section header
|
||||
|
||||
// Optional: A small summary header for the day
|
||||
HStack {
|
||||
Text(dayTitle(day))
|
||||
.font(.subheadline)
|
||||
@@ -155,41 +181,7 @@ struct TimeEntriesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - All entries (no day selected) — grouped by day
|
||||
|
||||
private var allEntriesSection: some View {
|
||||
LazyVStack(alignment: .leading, pinnedViews: .sectionHeaders) {
|
||||
ForEach(viewModel.entriesByDay, id: \.date) { group in
|
||||
Section {
|
||||
ForEach(Array(group.entries.enumerated()), id: \.element.id) { index, entry in
|
||||
EntryRow(entry: entry)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { entryToEdit = entry }
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
entryToDelete = entry
|
||||
showDeleteConfirmation = true
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
if index < group.entries.count - 1 {
|
||||
Divider().padding(.leading, 56)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(dayTitle(group.date))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 40) // Give some breathing room at the bottom of the scroll
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
@@ -198,6 +190,7 @@ struct TimeEntriesView: View {
|
||||
let cal = Calendar.current
|
||||
if cal.isDateInToday(date) { return "Today" }
|
||||
if cal.isDateInYesterday(date) { return "Yesterday" }
|
||||
if cal.isDateInTomorrow(date) { return "Tomorrow" }
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, MMM d"
|
||||
return formatter.string(from: date)
|
||||
@@ -259,139 +252,6 @@ struct EntryRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Week Strip View
|
||||
|
||||
struct WeekStripView: View {
|
||||
let weekDays: [Date]
|
||||
@Binding var selectedDay: Date?
|
||||
let daysWithEntries: Set<Date>
|
||||
let onSwipeLeft: () -> Void
|
||||
let onSwipeRight: () -> Void
|
||||
|
||||
@GestureState private var dragOffset: CGFloat = 0
|
||||
|
||||
private let cal = Calendar.current
|
||||
|
||||
private var monthYearLabel: String {
|
||||
// Show the month/year of the majority of days in the strip
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMMM yyyy"
|
||||
let midWeek = weekDays.count >= 4 ? weekDays[3] : (weekDays.first ?? Date())
|
||||
return formatter.string(from: midWeek)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
// Month / year header with navigation arrows
|
||||
HStack {
|
||||
Button { onSwipeRight() } label: {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.footnote.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
Spacer()
|
||||
Text(monthYearLabel)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.primary)
|
||||
Spacer()
|
||||
Button { onSwipeLeft() } label: {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.footnote.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, 6)
|
||||
|
||||
// Day cells
|
||||
HStack(spacing: 0) {
|
||||
ForEach(weekDays, id: \.self) { day in
|
||||
DayCell(
|
||||
day: day,
|
||||
isSelected: selectedDay.map { cal.isDate($0, inSameDayAs: day) } ?? false,
|
||||
isToday: cal.isDateInToday(day),
|
||||
hasDot: daysWithEntries.contains(cal.startOfDay(for: day))
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
let normalized = cal.startOfDay(for: day)
|
||||
if let current = selectedDay, cal.isDate(current, inSameDayAs: normalized) {
|
||||
selectedDay = nil
|
||||
} else {
|
||||
selectedDay = normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 40, coordinateSpace: .local)
|
||||
.onEnded { value in
|
||||
if value.translation.width < -40 {
|
||||
onSwipeLeft()
|
||||
} else if value.translation.width > 40 {
|
||||
onSwipeRight()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Day Cell
|
||||
|
||||
private struct DayCell: View {
|
||||
let day: Date
|
||||
let isSelected: Bool
|
||||
let isToday: Bool
|
||||
let hasDot: Bool
|
||||
|
||||
private static let weekdayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEEEE" // Single letter: M T W T F S S
|
||||
return f
|
||||
}()
|
||||
|
||||
private static let dayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d"
|
||||
return f
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 3) {
|
||||
Text(Self.weekdayFormatter.string(from: day))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
ZStack {
|
||||
if isSelected {
|
||||
Circle()
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: 32, height: 32)
|
||||
} else if isToday {
|
||||
Circle()
|
||||
.strokeBorder(Color.accentColor, lineWidth: 1.5)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
|
||||
Text(Self.dayFormatter.string(from: day))
|
||||
.font(.callout.weight(isToday || isSelected ? .semibold : .regular))
|
||||
.foregroundStyle(isSelected ? .white : (isToday ? Color.accentColor : .primary))
|
||||
}
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
// Dot indicator
|
||||
Circle()
|
||||
.fill(hasDot ? Color.accentColor.opacity(isSelected ? 0 : 0.7) : Color.clear)
|
||||
.frame(width: 4, height: 4)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Filter Sheet
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app_icon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 918 KiB |
Reference in New Issue
Block a user