remove TallyFlow Project
This commit is contained in:
parent
b209199c2d
commit
1fe3386d55
@ -1,63 +0,0 @@
|
|||||||
# TallyFlow
|
|
||||||
|
|
||||||
## Product summary
|
|
||||||
TallyFlow is an iOS productivity tracker for measuring tasks by either time or count.
|
|
||||||
It supports hierarchical tags (super tag / sub tag), flexible goals, historical statistics, widgets, Live Activities, and a paired watchOS experience.
|
|
||||||
|
|
||||||
## Source root
|
|
||||||
- iOS source root folder: `IOS/`
|
|
||||||
- Do not assume the default inner app folder name is `TallyFlow`; use `IOS/` as the app source root.
|
|
||||||
|
|
||||||
## Non-negotiable constraints
|
|
||||||
- Use SwiftUI.
|
|
||||||
- Use SwiftData as the local persistence layer.
|
|
||||||
- Keep the project buildable after every milestone.
|
|
||||||
- Do not scan or refactor the entire project.
|
|
||||||
- Read only the files explicitly listed in each prompt.
|
|
||||||
- Edit only the files explicitly listed in each prompt.
|
|
||||||
- Do not mention competitor app names in code comments, docs, or commit messages.
|
|
||||||
- Korean is the default app language; English must be supported.
|
|
||||||
- Theme must support Light and Dark modes with custom green/yellow palettes, not generic system default vibes.
|
|
||||||
- All date aggregation must respect configurable week start day and configurable custom day start time.
|
|
||||||
- Time tracking must support multiple concurrent running timer tasks.
|
|
||||||
- A task can have multiple tags.
|
|
||||||
- Selecting a sub tag implies its parent super tag is also associated logically.
|
|
||||||
|
|
||||||
## Architecture rules
|
|
||||||
- Separate app shell, domain models, services, and feature views.
|
|
||||||
- Create a single source of truth for:
|
|
||||||
1. task definitions,
|
|
||||||
2. tag hierarchy,
|
|
||||||
3. goals,
|
|
||||||
4. tracking sessions / count logs,
|
|
||||||
5. calendar boundary calculations.
|
|
||||||
- Avoid business logic inside SwiftUI view bodies.
|
|
||||||
- Put theme colors/tokens in dedicated files.
|
|
||||||
- Keep milestone 1 free of widget/watch/live activity target work; only prepare the architecture so those can be added later.
|
|
||||||
|
|
||||||
## UX rules
|
|
||||||
- Tab order must be:
|
|
||||||
1. Main
|
|
||||||
2. Tasks
|
|
||||||
3. Tags
|
|
||||||
4. Goals
|
|
||||||
5. Stats
|
|
||||||
6. Settings
|
|
||||||
- Add a splash/loading screen that shows the app name.
|
|
||||||
- Main tab should be scaffolded for:
|
|
||||||
- today / week / month summary cards,
|
|
||||||
- task action buttons,
|
|
||||||
- future layout-edit mode.
|
|
||||||
- Settings must be scaffolded for:
|
|
||||||
- theme mode,
|
|
||||||
- language,
|
|
||||||
- week start day,
|
|
||||||
- custom day start time,
|
|
||||||
- premium section entry.
|
|
||||||
|
|
||||||
## Coding style
|
|
||||||
- Prefer small SwiftUI views.
|
|
||||||
- Prefer explicit model names over vague names.
|
|
||||||
- Add TODO markers only where the next milestone will extend logic.
|
|
||||||
- Do not add placeholder lorem ipsum text.
|
|
||||||
- Do not introduce third-party dependencies in milestone 1.
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import SwiftData
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct RootTabView: View {
|
|
||||||
var body: some View {
|
|
||||||
TabView {
|
|
||||||
Tab(String(localized: "tab.main"), systemImage: "house.fill") {
|
|
||||||
MainTabView()
|
|
||||||
}
|
|
||||||
Tab(String(localized: "tab.tasks"), systemImage: "checkmark.circle.fill") {
|
|
||||||
TasksTabView()
|
|
||||||
}
|
|
||||||
Tab(String(localized: "tab.tags"), systemImage: "tag.fill") {
|
|
||||||
TagsTabView()
|
|
||||||
}
|
|
||||||
Tab(String(localized: "tab.goals"), systemImage: "target") {
|
|
||||||
GoalsTabView()
|
|
||||||
}
|
|
||||||
Tab(String(localized: "tab.stats"), systemImage: "chart.bar.fill") {
|
|
||||||
StatsTabView()
|
|
||||||
}
|
|
||||||
Tab(String(localized: "tab.settings"), systemImage: "gearshape.fill") {
|
|
||||||
SettingsTabView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tint(Color.tfPrimary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
RootTabView()
|
|
||||||
.modelContainer(
|
|
||||||
for: [TaskItem.self, TagEntity.self, GoalEntity.self, TrackingRecord.self],
|
|
||||||
inMemory: true
|
|
||||||
)
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct SplashView: View {
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
Color.tfBackground.ignoresSafeArea()
|
|
||||||
VStack(spacing: 14) {
|
|
||||||
Image(systemName: "chart.bar.fill")
|
|
||||||
.font(.system(size: 60, weight: .semibold))
|
|
||||||
.foregroundStyle(Color.tfPrimary)
|
|
||||||
Text("TallyFlow")
|
|
||||||
.font(.system(size: 38, weight: .bold, design: .rounded))
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
Text(String(localized: "splash.tagline"))
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
SplashView()
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// TallyFlow
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/26/26.
|
|
||||||
//
|
|
||||||
import SwiftData
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@Environment(AppPreferences.self) private var preferences
|
|
||||||
@State private var isShowingSplash = true
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
if isShowingSplash {
|
|
||||||
SplashView()
|
|
||||||
.transition(.opacity)
|
|
||||||
.onAppear {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.8) {
|
|
||||||
withAnimation(.easeOut(duration: 0.4)) {
|
|
||||||
isShowingSplash = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RootTabView()
|
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preferredColorScheme(preferences.themeMode.colorScheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
.modelContainer(
|
|
||||||
for: [TaskItem.self, TagEntity.self, GoalEntity.self, TrackingRecord.self],
|
|
||||||
inMemory: true
|
|
||||||
)
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
enum GoalUnit: String, Codable, CaseIterable {
|
|
||||||
case seconds
|
|
||||||
case count
|
|
||||||
}
|
|
||||||
|
|
||||||
enum GoalPeriod: String, Codable, CaseIterable {
|
|
||||||
case daily
|
|
||||||
case weekly
|
|
||||||
case monthly
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cannot be stored directly in SwiftData; use targetType computed property on GoalEntity.
|
|
||||||
enum TargetType {
|
|
||||||
case duration(min: Double, max: Double?)
|
|
||||||
case count(min: Int, max: Int?)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class GoalEntity {
|
|
||||||
var id: UUID
|
|
||||||
var title: String
|
|
||||||
var targetValue: Double
|
|
||||||
var targetMax: Double?
|
|
||||||
var unit: GoalUnit
|
|
||||||
var period: GoalPeriod
|
|
||||||
var createdAt: Date
|
|
||||||
var isActive: Bool
|
|
||||||
|
|
||||||
var task: TaskItem?
|
|
||||||
var tag: TagEntity?
|
|
||||||
|
|
||||||
var targetType: TargetType {
|
|
||||||
switch unit {
|
|
||||||
case .seconds:
|
|
||||||
return .duration(min: targetValue, max: targetMax)
|
|
||||||
case .count:
|
|
||||||
return .count(min: Int(targetValue), max: targetMax.map(Int.init))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
|
||||||
id: UUID = UUID(),
|
|
||||||
title: String,
|
|
||||||
targetValue: Double,
|
|
||||||
targetMax: Double? = nil,
|
|
||||||
unit: GoalUnit = .seconds,
|
|
||||||
period: GoalPeriod = .daily,
|
|
||||||
createdAt: Date = .now,
|
|
||||||
isActive: Bool = true
|
|
||||||
) {
|
|
||||||
self.id = id
|
|
||||||
self.title = title
|
|
||||||
self.targetValue = targetValue
|
|
||||||
self.targetMax = targetMax
|
|
||||||
self.unit = unit
|
|
||||||
self.period = period
|
|
||||||
self.createdAt = createdAt
|
|
||||||
self.isActive = isActive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GoalPeriod {
|
|
||||||
var localizedName: String {
|
|
||||||
switch self {
|
|
||||||
case .daily: return String(localized: "goal.period.daily")
|
|
||||||
case .weekly: return String(localized: "goal.period.weekly")
|
|
||||||
case .monthly: return String(localized: "goal.period.monthly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class TagEntity {
|
|
||||||
var id: UUID
|
|
||||||
var name: String
|
|
||||||
var colorHex: String
|
|
||||||
var createdAt: Date
|
|
||||||
var sortOrder: Int
|
|
||||||
|
|
||||||
var parent: TagEntity?
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .cascade, inverse: \TagEntity.parent)
|
|
||||||
var subTags: [TagEntity] = []
|
|
||||||
|
|
||||||
// Inverse is declared on TaskItem.tags
|
|
||||||
var tasks: [TaskItem] = []
|
|
||||||
|
|
||||||
var isSubTag: Bool { parent != nil }
|
|
||||||
|
|
||||||
init(
|
|
||||||
id: UUID = UUID(),
|
|
||||||
name: String,
|
|
||||||
colorHex: String = "#52A878",
|
|
||||||
createdAt: Date = .now,
|
|
||||||
sortOrder: Int = 0
|
|
||||||
) {
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.colorHex = colorHex
|
|
||||||
self.createdAt = createdAt
|
|
||||||
self.sortOrder = sortOrder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
enum TaskType: String, Codable, CaseIterable {
|
|
||||||
case timer
|
|
||||||
case counter
|
|
||||||
}
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class TaskItem {
|
|
||||||
var id: UUID
|
|
||||||
var name: String
|
|
||||||
var icon: String
|
|
||||||
var taskType: TaskType
|
|
||||||
var createdAt: Date
|
|
||||||
var isArchived: Bool
|
|
||||||
var sortOrder: Int
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .nullify, inverse: \TagEntity.tasks)
|
|
||||||
var tags: [TagEntity] = []
|
|
||||||
|
|
||||||
@Relationship(deleteRule: .cascade, inverse: \TrackingRecord.task)
|
|
||||||
var trackingRecords: [TrackingRecord] = []
|
|
||||||
|
|
||||||
init(
|
|
||||||
id: UUID = UUID(),
|
|
||||||
name: String,
|
|
||||||
icon: String = "star",
|
|
||||||
taskType: TaskType = .timer,
|
|
||||||
createdAt: Date = .now,
|
|
||||||
isArchived: Bool = false,
|
|
||||||
sortOrder: Int = 0
|
|
||||||
) {
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.icon = icon
|
|
||||||
self.taskType = taskType
|
|
||||||
self.createdAt = createdAt
|
|
||||||
self.isArchived = isArchived
|
|
||||||
self.sortOrder = sortOrder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
enum TrackingRecordType: String, Codable {
|
|
||||||
case timerSession
|
|
||||||
case countLog
|
|
||||||
}
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class TrackingRecord {
|
|
||||||
var id: UUID
|
|
||||||
var recordType: TrackingRecordType
|
|
||||||
var startTime: Date?
|
|
||||||
var endTime: Date?
|
|
||||||
var timestamp: Date?
|
|
||||||
var duration: TimeInterval
|
|
||||||
var createdAt: Date
|
|
||||||
|
|
||||||
// Inverse is declared on TaskItem.trackingRecords
|
|
||||||
var task: TaskItem?
|
|
||||||
|
|
||||||
var isRunning: Bool { recordType == .timerSession && startTime != nil && endTime == nil }
|
|
||||||
|
|
||||||
init(
|
|
||||||
id: UUID = UUID(),
|
|
||||||
task: TaskItem? = nil,
|
|
||||||
recordType: TrackingRecordType,
|
|
||||||
startTime: Date? = nil,
|
|
||||||
endTime: Date? = nil,
|
|
||||||
timestamp: Date? = nil,
|
|
||||||
duration: TimeInterval = 0,
|
|
||||||
createdAt: Date = .now
|
|
||||||
) {
|
|
||||||
self.id = id
|
|
||||||
self.task = task
|
|
||||||
self.recordType = recordType
|
|
||||||
self.startTime = startTime
|
|
||||||
self.endTime = endTime
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.duration = duration
|
|
||||||
self.createdAt = createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
final class CalendarBoundaryService {
|
|
||||||
private let preferences: AppPreferences
|
|
||||||
|
|
||||||
init(preferences: AppPreferences) {
|
|
||||||
self.preferences = preferences
|
|
||||||
}
|
|
||||||
|
|
||||||
private var calendar: Calendar {
|
|
||||||
var cal = Calendar.current
|
|
||||||
cal.firstWeekday = preferences.weekStartDay
|
|
||||||
return cal
|
|
||||||
}
|
|
||||||
|
|
||||||
func customDayStart(for date: Date) -> Date {
|
|
||||||
// TODO: apply dayStartHour / dayStartMinute boundary shift in milestone 2
|
|
||||||
var comps = calendar.dateComponents([.year, .month, .day], from: date)
|
|
||||||
comps.hour = preferences.dayStartHour
|
|
||||||
comps.minute = preferences.dayStartMinute
|
|
||||||
comps.second = 0
|
|
||||||
return calendar.date(from: comps) ?? date
|
|
||||||
}
|
|
||||||
|
|
||||||
func dayRange(for date: Date) -> DateInterval {
|
|
||||||
let start = customDayStart(for: date)
|
|
||||||
let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start
|
|
||||||
return DateInterval(start: start, end: end)
|
|
||||||
}
|
|
||||||
|
|
||||||
func weekRange(for date: Date) -> DateInterval {
|
|
||||||
// TODO: align to custom day-start boundary in milestone 2
|
|
||||||
guard let interval = calendar.dateInterval(of: .weekOfYear, for: date) else {
|
|
||||||
return DateInterval(start: date, duration: 7 * 86_400)
|
|
||||||
}
|
|
||||||
return interval
|
|
||||||
}
|
|
||||||
|
|
||||||
func monthRange(for date: Date) -> DateInterval {
|
|
||||||
// TODO: align to custom day-start boundary in milestone 2
|
|
||||||
guard let interval = calendar.dateInterval(of: .month, for: date) else {
|
|
||||||
return DateInterval(start: date, duration: 30 * 86_400)
|
|
||||||
}
|
|
||||||
return interval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
@Observable
|
|
||||||
final class TrackingEngine {
|
|
||||||
private(set) var activeSessions: [UUID: Date] = [:]
|
|
||||||
private(set) var elapsedTimes: [UUID: TimeInterval] = [:]
|
|
||||||
|
|
||||||
private var tickTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
tickTask = Task {
|
|
||||||
while !Task.isCancelled {
|
|
||||||
try? await Task.sleep(for: .seconds(1))
|
|
||||||
self.tick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func tick() {
|
|
||||||
let now = Date.now
|
|
||||||
for (id, start) in activeSessions {
|
|
||||||
elapsedTimes[id] = now.timeIntervalSince(start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRunning(_ task: TaskItem) -> Bool {
|
|
||||||
activeSessions[task.id] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func elapsed(for task: TaskItem) -> TimeInterval {
|
|
||||||
elapsedTimes[task.id] ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func startTimer(for task: TaskItem, context: ModelContext) {
|
|
||||||
guard !isRunning(task) else { return }
|
|
||||||
let now = Date.now
|
|
||||||
let record = TrackingRecord(task: task, recordType: .timerSession, startTime: now)
|
|
||||||
context.insert(record)
|
|
||||||
task.trackingRecords.append(record)
|
|
||||||
activeSessions[task.id] = now
|
|
||||||
elapsedTimes[task.id] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopTimer(for task: TaskItem, context: ModelContext) {
|
|
||||||
guard let startTime = activeSessions[task.id] else { return }
|
|
||||||
let now = Date.now
|
|
||||||
if let record = task.trackingRecords.first(where: { $0.isRunning }) {
|
|
||||||
record.endTime = now
|
|
||||||
record.duration = now.timeIntervalSince(startTime)
|
|
||||||
}
|
|
||||||
activeSessions.removeValue(forKey: task.id)
|
|
||||||
elapsedTimes.removeValue(forKey: task.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func incrementCounter(for task: TaskItem, context: ModelContext) {
|
|
||||||
let record = TrackingRecord(task: task, recordType: .countLog, timestamp: .now)
|
|
||||||
context.insert(record)
|
|
||||||
task.trackingRecords.append(record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct GoalsTabView: View {
|
|
||||||
@Query(sort: \GoalEntity.createdAt) private var goals: [GoalEntity]
|
|
||||||
@State private var showAddSheet = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Group {
|
|
||||||
if goals.isEmpty {
|
|
||||||
EmptyStateView(
|
|
||||||
icon: "target",
|
|
||||||
message: String(localized: "goals.empty")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
List(goals) { goal in
|
|
||||||
GoalRowView(goal: goal)
|
|
||||||
}
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
.navigationTitle(String(localized: "tab.goals"))
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .primaryAction) {
|
|
||||||
Button { showAddSheet = true } label: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
.tint(Color.tfPrimary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showAddSheet) {
|
|
||||||
AddGoalSheet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Goal row
|
|
||||||
|
|
||||||
private struct GoalRowView: View {
|
|
||||||
let goal: GoalEntity
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(goal.title)
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text(goal.period.localizedName)
|
|
||||||
Text("·")
|
|
||||||
Text(targetDescription)
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Text(goal.isActive
|
|
||||||
? String(localized: "goal.active")
|
|
||||||
: String(localized: "goal.inactive"))
|
|
||||||
.font(.caption)
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.vertical, 3)
|
|
||||||
.background(
|
|
||||||
goal.isActive
|
|
||||||
? Color.tfPrimary.opacity(0.15)
|
|
||||||
: Color.tfMuted.opacity(0.15)
|
|
||||||
)
|
|
||||||
.foregroundStyle(goal.isActive ? Color.tfPrimary : Color.tfMuted)
|
|
||||||
.clipShape(Capsule())
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var targetDescription: String {
|
|
||||||
switch goal.targetType {
|
|
||||||
case .duration(let min, let max):
|
|
||||||
if let max {
|
|
||||||
return "\(min.hhmmss) – \(max.hhmmss)"
|
|
||||||
}
|
|
||||||
return min.hhmmss
|
|
||||||
case .count(let min, let max):
|
|
||||||
if let max { return "\(min) – \(max)" }
|
|
||||||
return "\(min)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Add goal sheet
|
|
||||||
|
|
||||||
private struct AddGoalSheet: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
@Query(sort: \TaskItem.sortOrder) private var allTasks: [TaskItem]
|
|
||||||
@Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity]
|
|
||||||
|
|
||||||
@State private var title = ""
|
|
||||||
@State private var selectedTaskID: UUID? = nil
|
|
||||||
@State private var selectedTagID: UUID? = nil
|
|
||||||
@State private var period: GoalPeriod = .daily
|
|
||||||
@State private var unit: GoalUnit = .seconds
|
|
||||||
@State private var targetMin: Double = 1800
|
|
||||||
@State private var hasMax = false
|
|
||||||
@State private var targetMaxValue: Double = 3600
|
|
||||||
|
|
||||||
private var activeTasks: [TaskItem] { allTasks.filter { !$0.isArchived } }
|
|
||||||
private var canSave: Bool { !title.trimmingCharacters(in: .whitespaces).isEmpty }
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section(String(localized: "goal.form.section.title")) {
|
|
||||||
TextField(String(localized: "goal.form.title.placeholder"), text: $title)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(String(localized: "goal.form.section.scope")) {
|
|
||||||
Picker(String(localized: "goal.form.task"), selection: $selectedTaskID) {
|
|
||||||
Text(String(localized: "goal.form.none")).tag(Optional<UUID>.none)
|
|
||||||
ForEach(activeTasks) { task in
|
|
||||||
Label(task.name, systemImage: task.icon).tag(Optional(task.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Picker(String(localized: "goal.form.tag"), selection: $selectedTagID) {
|
|
||||||
Text(String(localized: "goal.form.none")).tag(Optional<UUID>.none)
|
|
||||||
ForEach(allTags) { tag in
|
|
||||||
Text(tag.name).tag(Optional(tag.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(String(localized: "goal.form.section.period")) {
|
|
||||||
Picker("", selection: $period) {
|
|
||||||
ForEach(GoalPeriod.allCases, id: \.self) { p in
|
|
||||||
Text(p.localizedName).tag(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(String(localized: "goal.form.section.target")) {
|
|
||||||
Picker("", selection: $unit) {
|
|
||||||
Text(String(localized: "goal.unit.duration")).tag(GoalUnit.seconds)
|
|
||||||
Text(String(localized: "goal.unit.count")).tag(GoalUnit.count)
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.labelsHidden()
|
|
||||||
.onChange(of: unit) { _, newUnit in
|
|
||||||
targetMin = newUnit == .seconds ? 1800 : 1
|
|
||||||
targetMaxValue = newUnit == .seconds ? 3600 : 5
|
|
||||||
}
|
|
||||||
|
|
||||||
minValueStepper
|
|
||||||
|
|
||||||
Toggle(String(localized: "goal.form.has_max"), isOn: $hasMax)
|
|
||||||
|
|
||||||
if hasMax {
|
|
||||||
maxValueStepper
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Toggle(String(localized: "goal.active"), isOn: .constant(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle(String(localized: "goal.add.title"))
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button(String(localized: "action.cancel")) { dismiss() }
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
Button(String(localized: "action.save")) { save() }
|
|
||||||
.disabled(!canSave)
|
|
||||||
.tint(Color.tfPrimary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var minValueStepper: some View {
|
|
||||||
if unit == .seconds {
|
|
||||||
Stepper(
|
|
||||||
value: $targetMin,
|
|
||||||
in: 60...86400,
|
|
||||||
step: 300
|
|
||||||
) {
|
|
||||||
Text("\(String(localized: "goal.form.min")): \(targetMin.hhmmss)")
|
|
||||||
.font(.subheadline.monospacedDigit())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Stepper(
|
|
||||||
value: $targetMin,
|
|
||||||
in: 1...9999,
|
|
||||||
step: 1
|
|
||||||
) {
|
|
||||||
Text("\(String(localized: "goal.form.min")): \(Int(targetMin))")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var maxValueStepper: some View {
|
|
||||||
if unit == .seconds {
|
|
||||||
Stepper(
|
|
||||||
value: $targetMaxValue,
|
|
||||||
in: targetMin...86400 * 7,
|
|
||||||
step: 300
|
|
||||||
) {
|
|
||||||
Text("\(String(localized: "goal.form.max")): \(targetMaxValue.hhmmss)")
|
|
||||||
.font(.subheadline.monospacedDigit())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Stepper(
|
|
||||||
value: $targetMaxValue,
|
|
||||||
in: targetMin...9999,
|
|
||||||
step: 1
|
|
||||||
) {
|
|
||||||
Text("\(String(localized: "goal.form.max")): \(Int(targetMaxValue))")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func save() {
|
|
||||||
let goal = GoalEntity(
|
|
||||||
title: title.trimmingCharacters(in: .whitespaces),
|
|
||||||
targetValue: targetMin,
|
|
||||||
targetMax: hasMax ? targetMaxValue : nil,
|
|
||||||
unit: unit,
|
|
||||||
period: period
|
|
||||||
)
|
|
||||||
goal.task = activeTasks.first { $0.id == selectedTaskID }
|
|
||||||
goal.tag = allTags.first { $0.id == selectedTagID }
|
|
||||||
context.insert(goal)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
GoalsTabView()
|
|
||||||
.modelContainer(for: [GoalEntity.self, TaskItem.self, TagEntity.self], inMemory: true)
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
// Module-level so GoalsTabView (same module) can reuse this formatter.
|
|
||||||
extension TimeInterval {
|
|
||||||
var hhmmss: String {
|
|
||||||
let s = Int(self)
|
|
||||||
return String(format: "%02d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MainTabView: View {
|
|
||||||
@Query(sort: \TaskItem.sortOrder) private var tasks: [TaskItem]
|
|
||||||
@State private var engine = TrackingEngine()
|
|
||||||
|
|
||||||
private var activeTasks: [TaskItem] { tasks.filter { !$0.isArchived } }
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
ScrollView {
|
|
||||||
VStack(spacing: 20) {
|
|
||||||
SummaryCardsSection()
|
|
||||||
if !activeTasks.isEmpty {
|
|
||||||
TaskListSection(tasks: activeTasks)
|
|
||||||
}
|
|
||||||
LayoutEditEntryBanner()
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
.navigationTitle(String(localized: "tab.main"))
|
|
||||||
}
|
|
||||||
.environment(engine)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Summary cards
|
|
||||||
|
|
||||||
private struct SummaryCardsSection: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 12) {
|
|
||||||
SummaryCard(period: String(localized: "main.period.today"), icon: "sun.max.fill")
|
|
||||||
SummaryCard(period: String(localized: "main.period.week"), icon: "calendar.badge.clock")
|
|
||||||
SummaryCard(period: String(localized: "main.period.month"), icon: "calendar")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct SummaryCard: View {
|
|
||||||
let period: String
|
|
||||||
let icon: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(Color.tfPrimary)
|
|
||||||
.frame(width: 28)
|
|
||||||
Text(period)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
Spacer()
|
|
||||||
Text("—")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.tfSurface)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
||||||
.shadow(color: Color.tfSeparator, radius: 2, x: 0, y: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Task list
|
|
||||||
|
|
||||||
private struct TaskListSection: View {
|
|
||||||
let tasks: [TaskItem]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
Text("작업")
|
|
||||||
.font(.footnote.weight(.semibold))
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
.textCase(.uppercase)
|
|
||||||
ForEach(tasks) { task in
|
|
||||||
TaskActionCard(task: task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TaskActionCard: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
@Environment(TrackingEngine.self) private var engine
|
|
||||||
let task: TaskItem
|
|
||||||
|
|
||||||
@State private var counterPulse = false
|
|
||||||
|
|
||||||
private var primaryColor: Color {
|
|
||||||
task.tags.first.map { Color(hex: $0.colorHex) } ?? Color.tfPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: task.icon)
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(primaryColor)
|
|
||||||
.frame(width: 28)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(task.name)
|
|
||||||
.font(.subheadline.weight(.semibold))
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
Text(statusLabel)
|
|
||||||
.font(.caption.monospacedDigit())
|
|
||||||
.foregroundStyle(engine.isRunning(task) ? primaryColor : Color.tfMuted)
|
|
||||||
.contentTransition(.numericText())
|
|
||||||
.animation(.linear(duration: 0.2), value: statusLabel)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
actionButton
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.tfSurface)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
||||||
.shadow(color: Color.tfSeparator, radius: 2, x: 0, y: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var statusLabel: String {
|
|
||||||
switch task.taskType {
|
|
||||||
case .timer:
|
|
||||||
return engine.isRunning(task)
|
|
||||||
? engine.elapsed(for: task).hhmmss
|
|
||||||
: String(localized: "task.type.timer")
|
|
||||||
case .counter:
|
|
||||||
return String(localized: "task.type.counter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var actionButton: some View {
|
|
||||||
switch task.taskType {
|
|
||||||
case .timer:
|
|
||||||
let running = engine.isRunning(task)
|
|
||||||
Button { toggleTimer() } label: {
|
|
||||||
Image(systemName: running ? "stop.fill" : "play.fill")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
.background(running ? Color.red : primaryColor)
|
|
||||||
.clipShape(Circle())
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
case .counter:
|
|
||||||
Button { logCount() } label: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
.font(.title3)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
.background(primaryColor)
|
|
||||||
.clipShape(Circle())
|
|
||||||
.scaleEffect(counterPulse ? 1.3 : 1.0)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleTimer() {
|
|
||||||
if engine.isRunning(task) {
|
|
||||||
engine.stopTimer(for: task, context: context)
|
|
||||||
} else {
|
|
||||||
engine.startTimer(for: task, context: context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func logCount() {
|
|
||||||
engine.incrementCounter(for: task, context: context)
|
|
||||||
withAnimation(.spring(duration: 0.15, bounce: 0.6)) { counterPulse = true }
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(for: .milliseconds(350))
|
|
||||||
withAnimation(.spring(duration: 0.2)) { counterPulse = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Layout-edit mode entry
|
|
||||||
|
|
||||||
private struct LayoutEditEntryBanner: View {
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
Image(systemName: "square.grid.2x2")
|
|
||||||
.foregroundStyle(Color.tfAccent)
|
|
||||||
Text(String(localized: "main.layout_edit.placeholder"))
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.footnote.weight(.semibold))
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(Color.tfSurface)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.stroke(Color.tfSeparator, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
MainTabView()
|
|
||||||
.modelContainer(for: [TaskItem.self, TagEntity.self, TrackingRecord.self], inMemory: true)
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct SettingsTabView: View {
|
|
||||||
@Environment(AppPreferences.self) private var preferences
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
@Bindable var prefs = preferences
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section(String(localized: "settings.section.appearance")) {
|
|
||||||
Picker(String(localized: "settings.theme"), selection: $prefs.themeMode) {
|
|
||||||
ForEach(ThemeMode.allCases) { mode in
|
|
||||||
Text(mode.localizedName).tag(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(String(localized: "settings.section.language")) {
|
|
||||||
Picker(String(localized: "settings.language"), selection: $prefs.languageCode) {
|
|
||||||
Text("한국어").tag("ko")
|
|
||||||
Text("English").tag("en")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(String(localized: "settings.section.calendar")) {
|
|
||||||
Picker(String(localized: "settings.week_start"), selection: $prefs.weekStartDay) {
|
|
||||||
Text(String(localized: "weekday.sunday")).tag(1)
|
|
||||||
Text(String(localized: "weekday.monday")).tag(2)
|
|
||||||
Text(String(localized: "weekday.saturday")).tag(7)
|
|
||||||
}
|
|
||||||
Stepper(value: $prefs.dayStartHour, in: 0...23) {
|
|
||||||
HStack {
|
|
||||||
Text(String(localized: "settings.day_start"))
|
|
||||||
Spacer()
|
|
||||||
Text(String(format: "%02d:00", prefs.dayStartHour))
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
NavigationLink {
|
|
||||||
// TODO: premium paywall in milestone 3+
|
|
||||||
Text(String(localized: "premium.coming_soon"))
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
} label: {
|
|
||||||
Label(String(localized: "settings.premium"), systemImage: "star.fill")
|
|
||||||
.foregroundStyle(Color.tfAccent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
.navigationTitle(String(localized: "tab.settings"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
SettingsTabView()
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct StatsTabView: View {
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
EmptyStateView(
|
|
||||||
icon: "chart.bar",
|
|
||||||
message: String(localized: "stats.empty")
|
|
||||||
)
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
.navigationTitle(String(localized: "tab.stats"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
StatsTabView()
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,211 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct TagsTabView: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
@Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity]
|
|
||||||
@State private var showAddSheet = false
|
|
||||||
@State private var editTarget: TagEntity?
|
|
||||||
|
|
||||||
private var superTags: [TagEntity] { allTags.filter { $0.parent == nil } }
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Group {
|
|
||||||
if superTags.isEmpty {
|
|
||||||
EmptyStateView(
|
|
||||||
icon: "tag",
|
|
||||||
message: String(localized: "tags.empty")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
List {
|
|
||||||
ForEach(superTags) { tag in
|
|
||||||
TagRowView(tag: tag, onEdit: { editTarget = $0 })
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
context.delete(tag)
|
|
||||||
} label: {
|
|
||||||
Label("삭제", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
editTarget = tag
|
|
||||||
} label: {
|
|
||||||
Label("편집", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
.navigationTitle(String(localized: "tab.tags"))
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .primaryAction) {
|
|
||||||
Button { showAddSheet = true } label: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
.tint(Color.tfPrimary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showAddSheet) {
|
|
||||||
TagFormView(superTags: superTags)
|
|
||||||
}
|
|
||||||
.sheet(item: $editTarget) { tag in
|
|
||||||
TagFormView(existing: tag, superTags: superTags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Row
|
|
||||||
|
|
||||||
private struct TagRowView: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
let tag: TagEntity
|
|
||||||
let onEdit: (TagEntity) -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Circle()
|
|
||||||
.fill(Color(hex: tag.colorHex))
|
|
||||||
.frame(width: 10, height: 10)
|
|
||||||
Text(tag.name)
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
Spacer()
|
|
||||||
if !tag.subTags.isEmpty {
|
|
||||||
Text(String(format: String(localized: "tags.subtag_count"), tag.subTags.count))
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ForEach(tag.subTags) { sub in
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Spacer().frame(width: 14)
|
|
||||||
Circle()
|
|
||||||
.fill(Color(hex: sub.colorHex))
|
|
||||||
.frame(width: 8, height: 8)
|
|
||||||
Text(sub.name)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.contextMenu {
|
|
||||||
Button("편집") { onEdit(sub) }
|
|
||||||
Button("삭제", role: .destructive) { context.delete(sub) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Form
|
|
||||||
|
|
||||||
struct TagFormView: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var existing: TagEntity? = nil
|
|
||||||
var superTags: [TagEntity]
|
|
||||||
|
|
||||||
@State private var name = ""
|
|
||||||
@State private var selectedColor = "#52A878"
|
|
||||||
@State private var selectedParent: TagEntity? = nil
|
|
||||||
|
|
||||||
private let colorPresets = [
|
|
||||||
"#52A878", "#1E5C40", "#4CBF80",
|
|
||||||
"#C99728", "#E8B84B", "#D64545",
|
|
||||||
"#4A90D9", "#9B59B6", "#E67E22",
|
|
||||||
"#1ABC9C"
|
|
||||||
]
|
|
||||||
|
|
||||||
private var isEditing: Bool { existing != nil }
|
|
||||||
private var isSaveDisabled: Bool { name.trimmingCharacters(in: .whitespaces).isEmpty }
|
|
||||||
private var availableParents: [TagEntity] {
|
|
||||||
superTags.filter { $0.id != existing?.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section("이름") {
|
|
||||||
TextField("태그 이름", text: $name)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("색상") {
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 12) {
|
|
||||||
ForEach(colorPresets, id: \.self) { hex in
|
|
||||||
Circle()
|
|
||||||
.fill(Color(hex: hex))
|
|
||||||
.frame(width: 36, height: 36)
|
|
||||||
.overlay {
|
|
||||||
if hex == selectedColor {
|
|
||||||
Image(systemName: "checkmark")
|
|
||||||
.font(.system(size: 14, weight: .bold))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onTapGesture { selectedColor = hex }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("상위 태그") {
|
|
||||||
Picker("상위 태그", selection: $selectedParent) {
|
|
||||||
Text("없음 (슈퍼 태그)").tag(nil as TagEntity?)
|
|
||||||
ForEach(availableParents) { tag in
|
|
||||||
Text(tag.name).tag(tag as TagEntity?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle(isEditing ? "태그 편집" : "태그 추가")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("취소") { dismiss() }
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
Button("저장") { save() }
|
|
||||||
.disabled(isSaveDisabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear { loadExisting() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadExisting() {
|
|
||||||
guard let tag = existing else { return }
|
|
||||||
name = tag.name
|
|
||||||
selectedColor = tag.colorHex
|
|
||||||
selectedParent = tag.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
private func save() {
|
|
||||||
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
|
||||||
guard !trimmed.isEmpty else { return }
|
|
||||||
|
|
||||||
if let tag = existing {
|
|
||||||
tag.name = trimmed
|
|
||||||
tag.colorHex = selectedColor
|
|
||||||
tag.parent = selectedParent
|
|
||||||
} else {
|
|
||||||
let newTag = TagEntity(name: trimmed, colorHex: selectedColor)
|
|
||||||
newTag.parent = selectedParent
|
|
||||||
context.insert(newTag)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
TagsTabView()
|
|
||||||
.modelContainer(for: [TagEntity.self, TaskItem.self], inMemory: true)
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,260 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct TasksTabView: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
@Query(sort: \TaskItem.sortOrder) private var tasks: [TaskItem]
|
|
||||||
@State private var showAddSheet = false
|
|
||||||
@State private var editTarget: TaskItem?
|
|
||||||
|
|
||||||
private var activeTasks: [TaskItem] { tasks.filter { !$0.isArchived } }
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Group {
|
|
||||||
if activeTasks.isEmpty {
|
|
||||||
EmptyStateView(
|
|
||||||
icon: "checkmark.circle",
|
|
||||||
message: String(localized: "tasks.empty")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
List {
|
|
||||||
ForEach(activeTasks) { task in
|
|
||||||
TaskRowView(task: task)
|
|
||||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
||||||
Button(role: .destructive) {
|
|
||||||
context.delete(task)
|
|
||||||
} label: {
|
|
||||||
Label("삭제", systemImage: "trash")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
editTarget = task
|
|
||||||
} label: {
|
|
||||||
Label("편집", systemImage: "pencil")
|
|
||||||
}
|
|
||||||
.tint(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
.navigationTitle(String(localized: "tab.tasks"))
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .primaryAction) {
|
|
||||||
Button { showAddSheet = true } label: {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
.tint(Color.tfPrimary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showAddSheet) {
|
|
||||||
TaskFormView()
|
|
||||||
}
|
|
||||||
.sheet(item: $editTarget) { task in
|
|
||||||
TaskFormView(existing: task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Row
|
|
||||||
|
|
||||||
private struct TaskRowView: View {
|
|
||||||
let task: TaskItem
|
|
||||||
|
|
||||||
private var primaryColor: Color {
|
|
||||||
task.tags.first.map { Color(hex: $0.colorHex) } ?? Color.tfPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: task.icon)
|
|
||||||
.foregroundStyle(primaryColor)
|
|
||||||
.frame(width: 28)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(task.name)
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Text(task.taskType == .timer ? "타이머" : "카운터")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
if let firstTag = task.tags.first {
|
|
||||||
Text("·")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
Text(firstTag.name)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(Color(hex: firstTag.colorHex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: task.taskType == .timer ? "timer" : "number")
|
|
||||||
.foregroundStyle(primaryColor)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Form
|
|
||||||
|
|
||||||
struct TaskFormView: View {
|
|
||||||
@Environment(\.modelContext) private var context
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Query(sort: \TagEntity.sortOrder) private var allTags: [TagEntity]
|
|
||||||
|
|
||||||
var existing: TaskItem? = nil
|
|
||||||
|
|
||||||
@State private var name = ""
|
|
||||||
@State private var icon = "star"
|
|
||||||
@State private var taskType: TaskType = .timer
|
|
||||||
@State private var selectedTagIDs: Set<UUID> = []
|
|
||||||
|
|
||||||
private let iconPresets = [
|
|
||||||
"star", "heart", "bolt", "flame",
|
|
||||||
"book.fill", "pencil", "laptopcomputer", "dumbbell.fill",
|
|
||||||
"music.note", "camera.fill", "gamecontroller.fill", "fork.knife",
|
|
||||||
"figure.walk", "bicycle", "car.fill", "airplane",
|
|
||||||
"moon.stars.fill", "sun.max.fill", "drop.fill", "leaf.fill"
|
|
||||||
]
|
|
||||||
|
|
||||||
private var isEditing: Bool { existing != nil }
|
|
||||||
private var isSaveDisabled: Bool { name.trimmingCharacters(in: .whitespaces).isEmpty }
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
Form {
|
|
||||||
Section("이름") {
|
|
||||||
TextField("작업 이름", text: $name)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("아이콘") {
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 12) {
|
|
||||||
ForEach(iconPresets, id: \.self) { sym in
|
|
||||||
Image(systemName: sym)
|
|
||||||
.font(.title2)
|
|
||||||
.foregroundStyle(sym == icon ? Color.tfPrimary : Color.tfMuted)
|
|
||||||
.frame(width: 44, height: 44)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(sym == icon ? Color.tfPrimary.opacity(0.15) : Color.clear)
|
|
||||||
)
|
|
||||||
.onTapGesture { icon = sym }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section("유형") {
|
|
||||||
Picker("유형", selection: $taskType) {
|
|
||||||
Text("타이머").tag(TaskType.timer)
|
|
||||||
Text("카운터").tag(TaskType.counter)
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !allTags.isEmpty {
|
|
||||||
Section("태그") {
|
|
||||||
ForEach(allTags) { tag in
|
|
||||||
TagSelectionRow(
|
|
||||||
tag: tag,
|
|
||||||
isSelected: selectedTagIDs.contains(tag.id),
|
|
||||||
onToggle: { toggleTag(tag) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationTitle(isEditing ? "작업 편집" : "작업 추가")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("취소") { dismiss() }
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
Button("저장") { save() }
|
|
||||||
.disabled(isSaveDisabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear { loadExisting() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadExisting() {
|
|
||||||
guard let task = existing else { return }
|
|
||||||
name = task.name
|
|
||||||
icon = task.icon
|
|
||||||
taskType = task.taskType
|
|
||||||
selectedTagIDs = Set(task.tags.map { $0.id })
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleTag(_ tag: TagEntity) {
|
|
||||||
if selectedTagIDs.contains(tag.id) {
|
|
||||||
selectedTagIDs.remove(tag.id)
|
|
||||||
} else {
|
|
||||||
selectedTagIDs.insert(tag.id)
|
|
||||||
// Selecting a sub tag implicitly includes its parent super tag
|
|
||||||
if let parent = tag.parent {
|
|
||||||
selectedTagIDs.insert(parent.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func save() {
|
|
||||||
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
|
||||||
guard !trimmed.isEmpty else { return }
|
|
||||||
|
|
||||||
let selectedTags = allTags.filter { selectedTagIDs.contains($0.id) }
|
|
||||||
|
|
||||||
if let task = existing {
|
|
||||||
task.name = trimmed
|
|
||||||
task.icon = icon
|
|
||||||
task.taskType = taskType
|
|
||||||
task.tags = selectedTags
|
|
||||||
} else {
|
|
||||||
let newTask = TaskItem(name: trimmed, icon: icon, taskType: taskType)
|
|
||||||
newTask.tags = selectedTags
|
|
||||||
context.insert(newTask)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct TagSelectionRow: View {
|
|
||||||
let tag: TagEntity
|
|
||||||
let isSelected: Bool
|
|
||||||
let onToggle: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
if tag.isSubTag {
|
|
||||||
Spacer().frame(width: 14)
|
|
||||||
}
|
|
||||||
Circle()
|
|
||||||
.fill(Color(hex: tag.colorHex))
|
|
||||||
.frame(width: 10, height: 10)
|
|
||||||
Text(tag.name)
|
|
||||||
.foregroundStyle(Color.tfOnBackground)
|
|
||||||
if tag.isSubTag, let parent = tag.parent {
|
|
||||||
Text("(\(parent.name))")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if isSelected {
|
|
||||||
Image(systemName: "checkmark")
|
|
||||||
.foregroundStyle(Color.tfPrimary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture { onToggle() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
TasksTabView()
|
|
||||||
.modelContainer(for: [TaskItem.self, TagEntity.self, TrackingRecord.self], inMemory: true)
|
|
||||||
.environment(AppPreferences())
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
//
|
|
||||||
// Item.swift
|
|
||||||
// TallyFlow
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/26/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class Item {
|
|
||||||
var timestamp: Date
|
|
||||||
|
|
||||||
init(timestamp: Date) {
|
|
||||||
self.timestamp = timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,870 +0,0 @@
|
|||||||
{
|
|
||||||
"sourceLanguage" : "ko",
|
|
||||||
"strings" : {
|
|
||||||
"" : {
|
|
||||||
|
|
||||||
},
|
|
||||||
"—" : {
|
|
||||||
"comment" : "A separator between the summary card and the task action section.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"·" : {
|
|
||||||
"comment" : "A period used to separate two items.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"(%@)" : {
|
|
||||||
"comment" : "A tag that shows the name of the parent tag in parentheses.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"%@: %@" : {
|
|
||||||
"comment" : "A stepper that lets the user select a minimum value for a goal. The value is displayed in a formatted time string. The argument is the string “goal.form.min”.",
|
|
||||||
"isCommentAutoGenerated" : true,
|
|
||||||
"localizations" : {
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "%1$@: %2$@"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"%@: %lld" : {
|
|
||||||
"comment" : "A label displaying the minimum value for a goal. The argument is the string “goal.form.min”.",
|
|
||||||
"isCommentAutoGenerated" : true,
|
|
||||||
"localizations" : {
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "new",
|
|
||||||
"value" : "%1$@: %2$lld"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"action.cancel" : {
|
|
||||||
"comment" : "The label of a button that cancels an action.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"action.save" : {
|
|
||||||
"comment" : "The label of a button that saves the goal.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"English" : {
|
|
||||||
"comment" : "A language option in the settings.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.active" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Active"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "활성"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"goal.add.title" : {
|
|
||||||
"comment" : "The title of the add goal sheet.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.has_max" : {
|
|
||||||
"comment" : "A toggle that allows users to specify a maximum value for a goal.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.max" : {
|
|
||||||
"comment" : "A stepper that lets the user select a maximum value for a goal. The value is displayed in a formatted time string.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.min" : {
|
|
||||||
"comment" : "A label displaying the minimum value of a goal. The value is formatted as a time.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.none" : {
|
|
||||||
"comment" : "A placeholder for a picker that allows the user to select a task.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.section.period" : {
|
|
||||||
"comment" : "A section in the goal form where the user can select the period for the goal.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.section.scope" : {
|
|
||||||
"comment" : "A section in the goal form that allows the user to select a task and a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.section.target" : {
|
|
||||||
"comment" : "A section in the goal form that allows the user to set the target value.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.section.title" : {
|
|
||||||
"comment" : "A section in the goal form.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.tag" : {
|
|
||||||
"comment" : "A label for the tag picker in the goal form.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.task" : {
|
|
||||||
"comment" : "A label for the task picker in the goal form.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.form.title.placeholder" : {
|
|
||||||
"comment" : "A placeholder text for the goal's title.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.inactive" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Inactive"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "비활성"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"goal.period.daily" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Daily"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "매일"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"goal.period.monthly" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Monthly"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "매월"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"goal.period.weekly" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Weekly"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "매주"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"goal.unit.count" : {
|
|
||||||
"comment" : "A unit of measurement for a goal.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goal.unit.duration" : {
|
|
||||||
"comment" : "A unit of time for a goal.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"goals.empty" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "No goals yet"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "목표가 없습니다"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.actions.log_count" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Log Count"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "횟수 기록"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.actions.start_timer" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Start Timer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "타이머 시작"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.actions.title" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Quick Actions"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "빠른 실행"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.layout_edit.placeholder" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Edit Layout (coming soon)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "레이아웃 편집 (준비 중)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.period.month" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "This Month"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "이번 달"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.period.today" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Today"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "오늘"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main.period.week" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "This Week"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "이번 주"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"premium.coming_soon" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Coming Soon"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "준비 중입니다"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.day_start" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Day Start Time"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "하루 시작 시간"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.language" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Language"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "언어"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.premium" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Premium"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "프리미엄"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.section.appearance" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Appearance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "외관"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.section.calendar" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Calendar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "캘린더"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.section.language" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Language"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "언어"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.theme" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Theme"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "테마"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.week_start" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Week Start Day"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "주 시작일"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"splash.tagline" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Track. Grow."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "기록하고, 성장하세요"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stats.empty" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "No statistics yet"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "아직 통계가 없습니다"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tab.goals" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Goals"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "목표"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tab.main" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Main"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "메인"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tab.settings" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Settings"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "설정"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tab.stats" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Stats"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "통계"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tab.tags" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Tags"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "태그"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tab.tasks" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Tasks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "할일"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags.empty" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "No tags yet"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "태그가 없습니다"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags.subtag_count" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "%d subtags"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "하위 태그 %d개"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TallyFlow" : {
|
|
||||||
"comment" : "The name of the app.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"task.type.count" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Count"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "횟수"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"task.type.counter" : {
|
|
||||||
"comment" : "A label for a task with a counter.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"task.type.time" : {
|
|
||||||
"extractionState" : "stale",
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Time"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "시간"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"task.type.timer" : {
|
|
||||||
"comment" : "A label for a timer task.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"tasks.empty" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "No tasks yet"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "작업이 없습니다"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"theme.dark" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Dark"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "다크"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"theme.light" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Light"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "라이트"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"theme.system" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "System"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "시스템"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"weekday.monday" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Monday"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "월요일"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"weekday.saturday" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Saturday"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "토요일"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"weekday.sunday" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Sunday"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ko" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "일요일"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"삭제" : {
|
|
||||||
"comment" : "A button that deletes a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"상위 태그" : {
|
|
||||||
"comment" : "A section for selecting a parent tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"색상" : {
|
|
||||||
"comment" : "A section for selecting a color for a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"아이콘" : {
|
|
||||||
"comment" : "A section for selecting an icon for the task.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"없음 (슈퍼 태그)" : {
|
|
||||||
"comment" : "A placeholder for a tag that has no parent.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"유형" : {
|
|
||||||
"comment" : "A section for selecting the type of task.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"이름" : {
|
|
||||||
"comment" : "A label for the tag's name.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"작업" : {
|
|
||||||
"comment" : "A heading for a list of tasks.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"작업 이름" : {
|
|
||||||
"comment" : "A label for the task name field.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"작업 추가" : {
|
|
||||||
"comment" : "The title of the task creation screen.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"작업 편집" : {
|
|
||||||
"comment" : "The title of the screen for editing a task.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"저장" : {
|
|
||||||
"comment" : "A button that saves the current tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"취소" : {
|
|
||||||
"comment" : "A button that cancels the current action.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"카운터" : {
|
|
||||||
"comment" : "A label describing a task type",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"타이머" : {
|
|
||||||
"comment" : "A label for a task type that is a timer.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"태그" : {
|
|
||||||
"comment" : "A section for selecting tags for a task.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"태그 이름" : {
|
|
||||||
"comment" : "A label for the name of a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"태그 추가" : {
|
|
||||||
"comment" : "A title for a view that lets the user add a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"태그 편집" : {
|
|
||||||
"comment" : "The title of a screen for editing a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"편집" : {
|
|
||||||
"comment" : "A button that opens a sheet for editing a tag.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"한국어" : {
|
|
||||||
"comment" : "Korean for \"한국어\".",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version" : "1.1"
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Observation
|
|
||||||
|
|
||||||
@Observable
|
|
||||||
final class AppPreferences {
|
|
||||||
// Calendar.weekday constants: 1 = Sunday, 2 = Monday … 7 = Saturday
|
|
||||||
var themeMode: ThemeMode = ThemeMode(rawValue: UserDefaults.standard.string(forKey: "pref.themeMode") ?? "system") ?? .system {
|
|
||||||
didSet { UserDefaults.standard.set(themeMode.rawValue, forKey: "pref.themeMode") }
|
|
||||||
}
|
|
||||||
|
|
||||||
var languageCode: String = UserDefaults.standard.string(forKey: "pref.languageCode") ?? "ko" {
|
|
||||||
didSet { UserDefaults.standard.set(languageCode, forKey: "pref.languageCode") }
|
|
||||||
}
|
|
||||||
|
|
||||||
var weekStartDay: Int = (UserDefaults.standard.object(forKey: "pref.weekStartDay") as? Int) ?? 2 {
|
|
||||||
didSet { UserDefaults.standard.set(weekStartDay, forKey: "pref.weekStartDay") }
|
|
||||||
}
|
|
||||||
|
|
||||||
var dayStartHour: Int = (UserDefaults.standard.object(forKey: "pref.dayStartHour") as? Int) ?? 0 {
|
|
||||||
didSet { UserDefaults.standard.set(dayStartHour, forKey: "pref.dayStartHour") }
|
|
||||||
}
|
|
||||||
|
|
||||||
var dayStartMinute: Int = (UserDefaults.standard.object(forKey: "pref.dayStartMinute") as? Int) ?? 0 {
|
|
||||||
didSet { UserDefaults.standard.set(dayStartMinute, forKey: "pref.dayStartMinute") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// TallyFlowApp.swift
|
|
||||||
// TallyFlow
|
|
||||||
//
|
|
||||||
// Created by 송예찬 on 6/26/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
@main
|
|
||||||
struct TallyFlowApp: App {
|
|
||||||
private let preferences = AppPreferences()
|
|
||||||
|
|
||||||
var sharedModelContainer: ModelContainer = {
|
|
||||||
let schema = Schema([
|
|
||||||
TaskItem.self,
|
|
||||||
TagEntity.self,
|
|
||||||
GoalEntity.self,
|
|
||||||
TrackingRecord.self,
|
|
||||||
])
|
|
||||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
|
||||||
do {
|
|
||||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
|
||||||
} catch {
|
|
||||||
fatalError("Could not create ModelContainer: \(error)")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var body: some Scene {
|
|
||||||
WindowGroup {
|
|
||||||
ContentView()
|
|
||||||
.environment(preferences)
|
|
||||||
}
|
|
||||||
.modelContainer(sharedModelContainer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
// MARK: – Semantic adaptive color tokens
|
|
||||||
|
|
||||||
extension Color {
|
|
||||||
// Brand Greens
|
|
||||||
static let tfPrimary = Color(light: Color(hex: "#1E5C40"), dark: Color(hex: "#4CBF80"))
|
|
||||||
static let tfSecondary = Color(light: Color(hex: "#52A878"), dark: Color(hex: "#2D7050"))
|
|
||||||
|
|
||||||
// Brand Accent – Amber Gold
|
|
||||||
static let tfAccent = Color(light: Color(hex: "#C99728"), dark: Color(hex: "#E8B84B"))
|
|
||||||
|
|
||||||
// Surfaces
|
|
||||||
static let tfBackground = Color(light: Color(hex: "#F5FAF7"), dark: Color(hex: "#0C100D"))
|
|
||||||
static let tfSurface = Color(light: Color(hex: "#FFFFFF"), dark: Color(hex: "#141D16"))
|
|
||||||
|
|
||||||
// Text / Content
|
|
||||||
static let tfOnBackground = Color(light: Color(hex: "#112318"), dark: Color(hex: "#E8F0EB"))
|
|
||||||
static let tfMuted = Color(light: Color(hex: "#5E7A69"), dark: Color(hex: "#5A7B66"))
|
|
||||||
static let tfSeparator = Color(light: Color(hex: "#DCE8E1"), dark: Color(hex: "#1E2B22"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Color helpers
|
|
||||||
|
|
||||||
extension Color {
|
|
||||||
init(light: Color, dark: Color) {
|
|
||||||
self.init(uiColor: UIColor { traits in
|
|
||||||
traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
init(hex: String) {
|
|
||||||
let cleaned = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
|
|
||||||
var value: UInt64 = 0
|
|
||||||
Scanner(string: cleaned).scanHexInt64(&value)
|
|
||||||
self.init(
|
|
||||||
red: Double((value >> 16) & 0xFF) / 255,
|
|
||||||
green: Double((value >> 8) & 0xFF) / 255,
|
|
||||||
blue: Double( value & 0xFF) / 255
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: – ThemeMode → ColorScheme
|
|
||||||
|
|
||||||
extension ThemeMode {
|
|
||||||
var colorScheme: ColorScheme? {
|
|
||||||
switch self {
|
|
||||||
case .system: return nil
|
|
||||||
case .light: return .light
|
|
||||||
case .dark: return .dark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: – Shared reusable views
|
|
||||||
|
|
||||||
struct EmptyStateView: View {
|
|
||||||
let icon: String
|
|
||||||
let message: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.font(.system(size: 48))
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
Text(message)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(Color.tfMuted)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview("EmptyStateView") {
|
|
||||||
EmptyStateView(icon: "tag", message: "태그가 없습니다")
|
|
||||||
.background(Color.tfBackground)
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
enum ThemeMode: String, CaseIterable, Identifiable {
|
|
||||||
case system
|
|
||||||
case light
|
|
||||||
case dark
|
|
||||||
|
|
||||||
var id: String { rawValue }
|
|
||||||
|
|
||||||
var localizedName: String {
|
|
||||||
switch self {
|
|
||||||
case .system: return String(localized: "theme.system")
|
|
||||||
case .light: return String(localized: "theme.light")
|
|
||||||
case .dark: return String(localized: "theme.dark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,333 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 77;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
|
||||||
72C598532FED9ACF00D61864 /* TallyFlow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TallyFlow.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
/* End PBXFileReference section */
|
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
72C598552FED9ACF00D61864 /* IOS */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
path = IOS;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
|
||||||
72C598502FED9ACF00D61864 /* Frameworks */ = {
|
|
||||||
isa = PBXFrameworksBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXFrameworksBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
|
||||||
72C5984A2FED9ACF00D61864 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
72C598552FED9ACF00D61864 /* IOS */,
|
|
||||||
72C598542FED9ACF00D61864 /* Products */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
72C598542FED9ACF00D61864 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
72C598532FED9ACF00D61864 /* TallyFlow.app */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXGroup section */
|
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
|
||||||
72C598522FED9ACF00D61864 /* TallyFlow */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = 72C598602FED9AD100D61864 /* Build configuration list for PBXNativeTarget "TallyFlow" */;
|
|
||||||
buildPhases = (
|
|
||||||
72C5984F2FED9ACF00D61864 /* Sources */,
|
|
||||||
72C598502FED9ACF00D61864 /* Frameworks */,
|
|
||||||
72C598512FED9ACF00D61864 /* Resources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
dependencies = (
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
|
||||||
72C598552FED9ACF00D61864 /* IOS */,
|
|
||||||
);
|
|
||||||
name = TallyFlow;
|
|
||||||
packageProductDependencies = (
|
|
||||||
);
|
|
||||||
productName = TallyFlow;
|
|
||||||
productReference = 72C598532FED9ACF00D61864 /* TallyFlow.app */;
|
|
||||||
productType = "com.apple.product-type.application";
|
|
||||||
};
|
|
||||||
/* End PBXNativeTarget section */
|
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
|
||||||
72C5984B2FED9ACF00D61864 /* Project object */ = {
|
|
||||||
isa = PBXProject;
|
|
||||||
attributes = {
|
|
||||||
BuildIndependentTargetsInParallel = 1;
|
|
||||||
LastSwiftUpdateCheck = 2650;
|
|
||||||
LastUpgradeCheck = 2650;
|
|
||||||
TargetAttributes = {
|
|
||||||
72C598522FED9ACF00D61864 = {
|
|
||||||
CreatedOnToolsVersion = 26.5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
buildConfigurationList = 72C5984E2FED9ACF00D61864 /* Build configuration list for PBXProject "TallyFlow" */;
|
|
||||||
developmentRegion = en;
|
|
||||||
hasScannedForEncodings = 0;
|
|
||||||
knownRegions = (
|
|
||||||
en,
|
|
||||||
Base,
|
|
||||||
);
|
|
||||||
mainGroup = 72C5984A2FED9ACF00D61864;
|
|
||||||
minimizedProjectReferenceProxies = 1;
|
|
||||||
preferredProjectObjectVersion = 77;
|
|
||||||
productRefGroup = 72C598542FED9ACF00D61864 /* Products */;
|
|
||||||
projectDirPath = "";
|
|
||||||
projectRoot = "";
|
|
||||||
targets = (
|
|
||||||
72C598522FED9ACF00D61864 /* TallyFlow */,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* End PBXProject section */
|
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
|
||||||
72C598512FED9ACF00D61864 /* Resources */ = {
|
|
||||||
isa = PBXResourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXResourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
|
||||||
72C5984F2FED9ACF00D61864 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXSourcesBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
|
||||||
72C5985E2FED9AD100D61864 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_TESTABILITY = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_OPTIMIZATION_LEVEL = 0;
|
|
||||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
||||||
"DEBUG=1",
|
|
||||||
"$(inherited)",
|
|
||||||
);
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
72C5985F2FED9AD100D61864 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
||||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
||||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_COMMA = YES;
|
|
||||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
||||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
||||||
CLANG_WARN_EMPTY_BODY = YES;
|
|
||||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
||||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
||||||
CLANG_WARN_INT_CONVERSION = YES;
|
|
||||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
||||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
||||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
||||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
||||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
||||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
||||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
||||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
|
||||||
MTL_FAST_MATH = YES;
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
|
||||||
VALIDATE_PRODUCT = YES;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
72C598612FED9AD100D61864 /* Debug */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.TallyFlow;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
72C598622FED9AD100D61864 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = 1.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.TallyFlow;
|
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
||||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
|
||||||
72C5984E2FED9ACF00D61864 /* Build configuration list for PBXProject "TallyFlow" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
72C5985E2FED9AD100D61864 /* Debug */,
|
|
||||||
72C5985F2FED9AD100D61864 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
72C598602FED9AD100D61864 /* Build configuration list for PBXNativeTarget "TallyFlow" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
72C598612FED9AD100D61864 /* Debug */,
|
|
||||||
72C598622FED9AD100D61864 /* Release */,
|
|
||||||
);
|
|
||||||
defaultConfigurationIsVisible = 0;
|
|
||||||
defaultConfigurationName = Release;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
|
||||||
};
|
|
||||||
rootObject = 72C5984B2FED9ACF00D61864 /* Project object */;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
Binary file not shown.
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>TallyFlow.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user