Skip to content
Open
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
13 changes: 13 additions & 0 deletions TodoFlow/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "TodoFlow",
platforms: [.macOS(.v13)],
targets: [
.executableTarget(
name: "TodoFlow",
path: "Sources/TodoFlow"
)
]
)
193 changes: 193 additions & 0 deletions TodoFlow/Sources/TodoFlow/AppStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import Foundation
import SwiftUI

// MARK: - AppStore

@MainActor
final class AppStore: ObservableObject {

@Published var courses: [Course] = []
@Published var activities: [Activity] = []

// MARK: Init

init() {
load()
}

// MARK: - Persistence

private var saveURL: URL {
let dir = FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("TodoFlow", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("data.json")
}

private struct Snapshot: Codable {
var courses: [Course]
var activities: [Activity]
}

func save() {
let snap = Snapshot(courses: courses, activities: activities)
if let data = try? JSONEncoder().encode(snap) {
try? data.write(to: saveURL, options: .atomic)
}
}

func load() {
guard
let data = try? Data(contentsOf: saveURL),
let snap = try? JSONDecoder().decode(Snapshot.self, from: data)
else {
insertSampleData()
return
}
courses = snap.courses
activities = snap.activities
}

// MARK: - Course CRUD

func addCourse(_ c: Course) { courses.append(c); save() }
func updateCourse(_ c: Course) { if let i = courses.firstIndex(where: { $0.id == c.id }) { courses[i] = c; save() } }
func deleteCourse(_ c: Course) { courses.removeAll { $0.id == c.id }; save() }

// MARK: - Assignment CRUD

func addAssignment(_ a: Assignment) {
guard let i = courses.firstIndex(where: { $0.id == a.courseId }) else { return }
courses[i].assignments.append(a)
save()
}

func toggleAssignment(_ a: Assignment) {
for i in courses.indices {
if let j = courses[i].assignments.firstIndex(where: { $0.id == a.id }) {
courses[i].assignments[j].isCompleted.toggle()
save()
return
}
}
}

func updateAssignment(_ a: Assignment) {
for i in courses.indices {
if let j = courses[i].assignments.firstIndex(where: { $0.id == a.id }) {
courses[i].assignments[j] = a
save()
return
}
}
}

func deleteAssignment(_ a: Assignment) {
for i in courses.indices { courses[i].assignments.removeAll { $0.id == a.id } }
save()
}

// MARK: - Activity CRUD

func addActivity(_ a: Activity) { activities.append(a); save() }
func updateActivity(_ a: Activity) { if let i = activities.firstIndex(where: { $0.id == a.id }) { activities[i] = a; save() } }
func deleteActivity(_ a: Activity) { activities.removeAll { $0.id == a.id }; save() }

// MARK: - Sticky Note CRUD

func addStickyNote(_ note: StickyNote, to activityId: UUID) {
guard let i = activities.firstIndex(where: { $0.id == activityId }) else { return }
activities[i].stickyNotes.append(note)
save()
}

func updateStickyNote(_ note: StickyNote, in activityId: UUID) {
guard let i = activities.firstIndex(where: { $0.id == activityId }),
let j = activities[i].stickyNotes.firstIndex(where: { $0.id == note.id }) else { return }
activities[i].stickyNotes[j] = note
save()
}

func deleteStickyNote(_ noteId: UUID, from activityId: UUID) {
guard let i = activities.firstIndex(where: { $0.id == activityId }) else { return }
activities[i].stickyNotes.removeAll { $0.id == noteId }
save()
}

// MARK: - Calendar feed

var calendarEvents: [CalendarEvent] {
var events: [CalendarEvent] = []
for course in courses {
for a in course.assignments where !a.isCompleted {
events.append(CalendarEvent(
title: a.title,
date: a.dueDate,
courseId: course.id,
colorHex: course.colorHex
))
}
}
for act in activities {
events.append(CalendarEvent(
title: act.title,
date: act.startDate,
courseId: nil,
colorHex: Activity.colorHex(for: act.category)
))
}
return events.sorted { $0.date < $1.date }
}

var upcomingCount: Int {
let horizon = Calendar.current.date(byAdding: .day, value: 7, to: Date())!
return courses.reduce(0) { sum, c in
sum + c.assignments.filter { !$0.isCompleted && $0.dueDate >= Date() && $0.dueDate <= horizon }.count
}
}

// MARK: - Sample data

private func insertSampleData() {
let cal = Calendar.current
let now = Date()
func days(_ n: Int) -> Date { cal.date(byAdding: .day, value: n, to: now)! }

var cs = Course(name: "数据结构", code: "CS101", professor: "张教授", colorHex: "3B82F6")
cs.assignments = [
Assignment(title: "作业一:链表实现", dueDate: days(3), courseId: cs.id),
Assignment(title: "期中项目提交", dueDate: days(14), courseId: cs.id),
]

var eng = Course(name: "学术英语写作", code: "ENG201", professor: "Smith 教授", colorHex: "10B981")
eng.assignments = [
Assignment(title: "Essay Draft 1", dueDate: days(5), courseId: eng.id),
Assignment(title: "Final Essay", dueDate: days(21), courseId: eng.id),
]

var econ = Course(name: "微观经济学", code: "ECON101", professor: "李教授", colorHex: "F59E0B")
econ.assignments = [
Assignment(title: "第三章习题集", dueDate: days(7), courseId: econ.id),
]

courses = [cs, eng, econ]

activities = [
Activity(
title: "学生会策划会议",
startDate: days(2),
description: "讨论五四晚会安排",
category: "会议"
),
Activity(
title: "NLP 科研训练项目",
startDate: days(-7),
endDate: days(60),
description: "基于大模型的情感分析研究",
category: "研究"
),
]
save()
}
}
140 changes: 140 additions & 0 deletions TodoFlow/Sources/TodoFlow/Models.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Foundation

// MARK: - Course

struct Course: Identifiable, Codable, Hashable {
var id: UUID = UUID()
var name: String
var code: String
var professor: String
var colorHex: String
var folderPath: String? // override; default = ~/Desktop/<name>
var eLearningCourseId: Int? // Moodle course ID
var assignments: [Assignment] = []
var createdAt: Date = Date()
}

// MARK: - Assignment

struct Assignment: Identifiable, Codable, Hashable {
var id: UUID = UUID()
var title: String
var dueDate: Date
var courseId: UUID
var isCompleted: Bool = false
var notes: String = ""
var source: Source = .manual

enum Source: String, Codable {
case manual
case elearning
}
}

// MARK: - Activity

struct Activity: Identifiable, Codable {
var id: UUID = UUID()
var title: String
var startDate: Date
var endDate: Date?
var description: String = ""
var category: String = "项目" // free-form; was enum, kept as String for Codable compat
var stickyNotes: [StickyNote] = []
var createdAt: Date = Date()

// Default suggestions shown in the add sheet
static let suggestedCategories = ["项目", "会议", "活动", "研究", "比赛", "实习", "社团", "其他"]

static func icon(for category: String) -> String {
switch category {
case "项目": return "target"
case "会议": return "person.2"
case "活动": return "calendar.badge.plus"
case "研究": return "magnifyingglass"
case "比赛": return "trophy"
case "实习": return "briefcase"
case "社团": return "person.3"
default: return "star"
}
}

// Deterministic color per category name (same name → same color)
static func colorHex(for category: String) -> String {
let palette = ["3B82F6", "8B5CF6", "F59E0B", "10B981", "EF4444", "EC4899", "06B6D4", "84CC16"]
let hash = category.unicodeScalars.reduce(0) { $0 &+ Int($1.value) }
return palette[abs(hash) % palette.count]
}
}

// MARK: - StickyNote

struct StickyNote: Identifiable, Codable {
var id: UUID = UUID()
var content: String
var colorName: StickyColor = .yellow
var createdAt: Date = Date()
var activityId: UUID

enum StickyColor: String, Codable, CaseIterable {
case yellow = "yellow"
case pink = "pink"
case blue = "blue"
case green = "green"
case purple = "purple"
}
}

// MARK: - Local File (not Codable – rebuilt at runtime)

struct LocalFile: Identifiable {
var id: String { path }
var name: String
var path: String
var category: FileCategory
var dateModified: Date
var fileExtension: String

enum FileCategory: String, CaseIterable {
case courseware = "课件"
case reading = "阅读材料"
case lecture = "讲解视频"
case submission = "作业提交"
case other = "其他"
}
}

// MARK: - eLearning Resource

struct ELearningResource: Identifiable, Codable {
var id: String
var courseId: Int
var courseName: String
var name: String
var modType: String // assign / resource / url / folder / quiz …
var url: String
var addedTimestamp: Date
var isNew: Bool = false

var typeIcon: String {
switch modType {
case "resource": return "doc.fill"
case "url": return "link"
case "assign": return "pencil.and.list.clipboard"
case "forum": return "bubble.left.and.bubble.right"
case "quiz": return "questionmark.circle.fill"
case "folder": return "folder.fill"
default: return "doc"
}
}
}

// MARK: - Calendar Event (computed, not persisted)

struct CalendarEvent: Identifiable {
var id: String { "\(title)-\(date.timeIntervalSince1970)-\(courseId?.uuidString ?? "act")" }
var title: String
var date: Date
var courseId: UUID?
var colorHex: String
}
Loading