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
25 changes: 25 additions & 0 deletions OpenAppLock/Assets.xcassets/AppLogo.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "applogo-light.svg",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "applogo-dark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
62 changes: 62 additions & 0 deletions OpenAppLock/Assets.xcassets/AppLogo.imageset/applogo-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions OpenAppLock/Assets.xcassets/AppLogo.imageset/applogo-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
146 changes: 92 additions & 54 deletions OpenAppLock/Views/AppLists/AppListLibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,48 @@ struct AppListLibraryView: View {
}

var body: some View {
List {
Section {
if lists.isEmpty {
Text("No app lists yet. Create one to choose which apps a rule affects.")
.foregroundStyle(.secondary)
Group {
if lists.isEmpty {
ContentUnavailableView {
Label("No App Lists", systemImage: "square.stack.3d.up")
} description: {
// Identifier on the description so it stays a distinct
// element instead of collapsing onto the action button.
Text("Create one to choose which apps a rule affects.")
.accessibilityIdentifier("emptyAppListsLabel")
} else {
ForEach(lists) { list in
listRow(list)
} actions: {
Button("New List") {
creatingList = true
}
.accessibilityIdentifier("newAppListButton")
}
} header: {
Text("Your App Lists").textCase(nil)
} footer: {
if listsLocked {
Label(
"Hard Mode is on — app lists are locked until the block ends.",
systemImage: "lock.fill"
)
.accessibilityElement(children: .combine)
.accessibilityIdentifier("appListsLockedNotice")
}
}
Section {
Button {
creatingList = true
} label: {
Label("New List", systemImage: "plus")
} else {
List {
Section {
ForEach(lists) { list in
listRow(list)
}
} header: {
Text("Your App Lists").textCase(nil)
} footer: {
if listsLocked {
Label(
"Hard Mode is on — app lists are locked until the block ends.",
systemImage: "lock.fill"
)
.accessibilityElement(children: .combine)
.accessibilityIdentifier("appListsLockedNotice")
}
}
Section {
Button {
creatingList = true
} label: {
Label("New List", systemImage: "plus")
}
.accessibilityIdentifier("newAppListButton")
}
}
.accessibilityIdentifier("newAppListButton")
}
}
.navigationDestination(isPresented: $creatingList) {
Expand All @@ -90,49 +102,75 @@ struct AppListLibraryView: View {
}
}

@ViewBuilder
private func listRow(_ list: AppList) -> some View {
HStack {
Button {
if isPicking {
if isPicking {
// Picker mode: tapping the row selects the list, so it keeps a
// distinct trailing Edit affordance to open the list for editing.
HStack {
Button {
selection?.wrappedValue = list
onPick?()
} else if !listsLocked {
editingList = list
}
} label: {
HStack {
if isPicking {
} label: {
HStack {
Image(systemName: isSelected(list) ? "checkmark.circle.fill" : "circle")
.foregroundStyle(
isSelected(list) ? AnyShapeStyle(.tint) : AnyShapeStyle(Color.secondary)
)
.frame(width: 28)
rowText(list)
}
VStack(alignment: .leading, spacing: 2) {
Text(list.name)
.foregroundStyle(Color.primary)
Text(list.appCountLabel)
.font(.caption)
.foregroundStyle(Color.secondary)
}
.accessibilityIdentifier("appListRow-\(list.name)")
Spacer()
if !listsLocked {
Button("Edit") {
editingList = list
}
.font(.subheadline)
.accessibilityIdentifier("editAppListButton-\(list.name)")
}
}
.accessibilityIdentifier("appListRow-\(list.name)")
Spacer()
if !listsLocked {
Button("Edit") {
editingList = list
.buttonStyle(.borderless)
.swipeActions { deleteAction(list) }
} else {
// Management mode: the whole row taps to edit (a full-width target),
// with a disclosure chevron instead of a redundant Edit button.
Button {
if !listsLocked { editingList = list }
} label: {
HStack {
rowText(list)
Spacer()
if !listsLocked {
Image(systemName: "chevron.right")
.font(.caption.weight(.semibold))
.foregroundStyle(Color(.tertiaryLabel))
}
}
.font(.subheadline)
.accessibilityIdentifier("editAppListButton-\(list.name)")
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityIdentifier("appListRow-\(list.name)")
.swipeActions { deleteAction(list) }
}
.buttonStyle(.borderless)
.swipeActions {
if !listsLocked {
Button("Delete", role: .destructive) {
delete(list)
}
}

private func rowText(_ list: AppList) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(list.name)
.foregroundStyle(Color.primary)
Text(list.appCountLabel)
.font(.caption)
.foregroundStyle(Color.secondary)
}
}

@ViewBuilder
private func deleteAction(_ list: AppList) -> some View {
if !listsLocked {
Button("Delete", role: .destructive) {
delete(list)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions OpenAppLock/Views/Components/DayOfWeekPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct DayOfWeekPicker: View {
} label: {
Text(day.shortLabel)
.font(.subheadline.weight(.semibold))
// Keep the circle a fixed size so all seven always fit one row;
// let the letter shrink to fit instead of clipping at large
// Dynamic Type sizes.
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundStyle(isOn ? Color.white : Color.secondary)
.frame(width: 38, height: 38)
.background(
Expand Down
28 changes: 21 additions & 7 deletions OpenAppLock/Views/Onboarding/OnboardingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,40 @@ struct OnboardingView: View {
.padding()
}

/// Shared vertical rhythm so both onboarding steps line up.
private var stepSpacing: CGFloat { 20 }

private var welcome: some View {
VStack(spacing: 16) {
Image(systemName: "scissors")
.font(.system(size: 56))
.foregroundStyle(.tint)
VStack(spacing: stepSpacing) {
appLogo
Text("OpenAppLock")
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
Text("Block your most distracting apps with rules that keep you honest — on a schedule, with no way out when you choose Hard Mode.")
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}

/// The real app icon, shown as a rounded tile so the welcome screen leads
/// with the app's own identity rather than a generic symbol.
private var appLogo: some View {
Image("AppLogo")
.resizable()
.scaledToFit()
.frame(width: 96, height: 96)
.clipShape(RoundedRectangle(cornerRadius: 21, style: .continuous))
.shadow(color: .black.opacity(0.15), radius: 10, y: 4)
.accessibilityHidden(true)
}

private var permission: some View {
VStack(spacing: 24) {
VStack(spacing: stepSpacing) {
Image(systemName: "hourglass")
.font(.system(size: 48))
.font(.system(size: 52))
.foregroundStyle(.tint)
Text("Allow Screen Time Access")
.font(.title.bold())
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 14) {
bullet("shield.fill", "OpenAppLock uses Apple's Screen Time framework to block the apps you choose.")
Expand Down
Loading
Loading