Skip to content
Closed
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: 7 additions & 2 deletions CalenTask/Features/Settings/SettingsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ import Observation
/// Nuovo modulo: `case` in `AppModule` (+ le sue `AppFeature`) e le sue
/// opzioni in `ModuleOptions.swift`. Nient'altro da toccare qui.
enum SettingsPage: Hashable, Identifiable {
case general, appearance, notifications, sync, developer, account
case general, appearance, notifications, sync, trash, developer, account
case modules
case module(AppModule)

/// Le pagine generali nella barra laterale (l'account ha la sua testata).
static let generalPages: [SettingsPage] = [.general, .appearance, .notifications, .sync, .developer]
static let generalPages: [SettingsPage] = [.general, .appearance, .notifications, .sync, .trash, .developer]

var id: String {
switch self {
case .general: "general"
case .appearance: "appearance"
case .notifications: "notifications"
case .sync: "sync"
case .trash: "trash"
case .developer: "developer"
case .account: "account"
case .modules: "modules"
Expand All @@ -37,6 +38,7 @@ enum SettingsPage: Hashable, Identifiable {
case .appearance: "Aspetto"
case .notifications: "Notifiche"
case .sync: "Sincronizzazione"
case .trash: "Cestino"
case .developer: "Sviluppatore"
case .account: "Account e spazi"
case .modules: "Panoramica moduli"
Expand All @@ -50,6 +52,7 @@ enum SettingsPage: Hashable, Identifiable {
case .appearance: "Tema e contenuto della barra laterale."
case .notifications: "Permesso di sistema e riepilogo del mattino."
case .sync: "iCloud tra i tuoi dispositivi e calendari di sistema."
case .trash: "Attività eliminate: ripristinale o svuota. Dopo 30 giorni spariscono da sole."
case .developer: "Aggiornamenti da GitHub, pre-release e diagnostica."
case .account: "Il tuo profilo e gli spazi di lavoro."
case .modules: "Accendi solo ciò che ti serve: spegnere un modulo nasconde i suoi strumenti ma conserva dati e preferenze."
Expand All @@ -63,6 +66,7 @@ enum SettingsPage: Hashable, Identifiable {
case .appearance: "paintbrush"
case .notifications: "bell.badge"
case .sync: "arrow.triangle.2.circlepath.icloud"
case .trash: "trash"
case .developer: "hammer"
case .account: "person.crop.circle"
case .modules: "square.grid.2x2"
Expand All @@ -76,6 +80,7 @@ enum SettingsPage: Hashable, Identifiable {
case .appearance: Color(hex: "#5B5BD6")
case .notifications: Color(hex: "#E54666")
case .sync: Color(hex: "#0091FF")
case .trash: Color(hex: "#8B8D98")
case .developer: Color(hex: "#F76B15")
case .account: Color(hex: "#30A46C")
case .modules: Color(hex: "#8E4EC6")
Expand Down
1 change: 1 addition & 0 deletions CalenTask/Features/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ struct SettingsPageView: View {
case .appearance: AppearanceSettingsView()
case .notifications: NotificationsSettingsView()
case .sync: SyncSettingsView()
case .trash: TrashSettingsView()
case .developer: DeveloperSettingsView()
case .account: AccountSettingsView()
case .modules: ModulesOverviewView()
Expand Down
59 changes: 59 additions & 0 deletions CalenTask/Features/Settings/TrashSettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation
import SwiftData
import SwiftUI

/// Impostazioni › Cestino (#22): le attività eliminate si possono ripristinare
/// (con le sotto-attività) o cancellare per sempre; dopo 30 giorni le elimina
/// `TrashService.purgeExpired`.
struct TrashSettingsView: View {
@Environment(\.modelContext) private var modelContext
@Query(
filter: #Predicate<TodoTask> { $0.deletedAt != nil },
sort: \TodoTask.deletedAt, order: .reverse
) private var deleted: [TodoTask]
@State private var confirmEmpty = false

var body: some View {
Form {
Section { SettingsPageHeader(page: .trash) }

if deleted.isEmpty {
Section {
Label("Il cestino è vuoto.", systemImage: "trash")
.foregroundStyle(.secondary)
}
} else {
Section {
ForEach(deleted) { task in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(task.title.isEmpty ? "Senza titolo" : task.title)
if let when = task.deletedAt {
Text("Eliminata \(when.formatted(.relative(presentation: .named)))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
Spacer()
Button("Ripristina") { task.restoreSubtree() }
.buttonStyle(.borderless)
}
}
}
Section {
Button("Svuota cestino", role: .destructive) { confirmEmpty = true }
}
}
}
.formStyle(.grouped)
.navigationTitle("Cestino")
.confirmationDialog(
"Eliminare per sempre \(deleted.count) attività?",
isPresented: $confirmEmpty, titleVisibility: .visible
) {
Button("Elimina per sempre", role: .destructive) {
TrashService.deleteForever(deleted, in: modelContext)
}
}
}
}
11 changes: 11 additions & 0 deletions CalenTask/Models/TodoTask+Mutations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ extension TodoTask {
touch()
}

/// Ripristina dal cestino la task e le sotto-attività eliminate con lei.
@MainActor
func restoreSubtree() {
guard deletedAt != nil else { return }
for child in subtasks where child.deletedAt != nil {
child.restoreSubtree()
}
deletedAt = nil
touch()
}

/// Soft-deletes the whole subtree. Deleting a phase takes its children
/// with it (coherent with the unified tree — see Progettazione/04).
@MainActor
Expand Down
8 changes: 8 additions & 0 deletions CalenTask/Services/TrashService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ enum TrashService {
context.saveOrLog()
return purgeable.count
}

/// Elimina subito e per sempre le task indicate (svuota cestino).
@MainActor
static func deleteForever(_ tasks: [TodoTask], in context: ModelContext) {
guard !tasks.isEmpty else { return }
tasks.forEach { context.delete($0) }
context.saveOrLog()
}
}
15 changes: 15 additions & 0 deletions CalenTaskTests/TrashServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,19 @@ struct TrashServiceTests {
let titles = try context.fetch(FetchDescriptor<TodoTask>()).map(\.title).sorted()
#expect(titles == ["Recente", "Viva"])
}

@Test func restoreSubtreeBringsBackChildrenToo() throws {
let context = try makeContext()
let author = UUID(), ws = UUID()
let parent = TodoTask(workspaceID: ws, title: "Padre", createdByID: author)
let child = TodoTask(workspaceID: ws, title: "Figlio", createdByID: author)
context.insert(parent)
context.insert(child)
child.parentTask = parent
parent.softDeleteSubtree()
#expect(parent.deletedAt != nil && child.deletedAt != nil)

parent.restoreSubtree()
#expect(parent.deletedAt == nil && child.deletedAt == nil)
}
}
Loading