diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index 502dc89..9bec8be 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -80,7 +80,7 @@ 0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = ""; }; 0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = ""; }; - 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = ""; }; 208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = ""; }; @@ -104,7 +104,7 @@ 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = ""; }; 92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = ""; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; - 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -289,7 +289,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 2620; TargetAttributes = { 28B43516AD88946E21D9BFE0 = { DevelopmentTeam = K8BLMMR883; @@ -423,6 +423,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 2; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -469,6 +470,7 @@ DEVELOPMENT_TEAM = K8BLMMR883; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -483,6 +485,7 @@ MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; @@ -498,7 +501,12 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = KisaniCalWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -545,6 +553,7 @@ DEVELOPMENT_TEAM = K8BLMMR883; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -566,6 +575,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -581,7 +591,12 @@ GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = KisaniCalWidgets/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; @@ -615,6 +630,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 2; PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal; SDKROOT = iphoneos; SWIFT_VERSION = 5.9; diff --git a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate index bcd6309..a7eebe0 100644 Binary files a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate and b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme index 9db359c..675309e 100644 --- a/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme +++ b/KisaniCal.xcodeproj/xcshareddata/xcschemes/KisaniCal.xcscheme @@ -1,11 +1,10 @@ + LastUpgradeVersion = "2620" + version = "1.3"> + buildImplicitDependencies = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - diff --git a/KisaniCal/Models/ExerciseModels.swift b/KisaniCal/Models/ExerciseModels.swift index 9bdd888..51603d1 100644 --- a/KisaniCal/Models/ExerciseModels.swift +++ b/KisaniCal/Models/ExerciseModels.swift @@ -313,10 +313,32 @@ class WorkoutViewModel: ObservableObject { let today = iso.string(from: Date()) let last = UserDefaults.kisani.string(forKey: kLastActiveDay) guard last != today else { return } - if last != nil { clearAllDoneSets() } // skip the wipe on the very first launch + if last != nil { + clearAllDoneSets() // skip the wipe on the very first launch + applyScheduledProgramForToday() // follow the weekly schedule on a new day + } UserDefaults.kisani.set(today, forKey: kLastActiveDay) } + /// The program assigned to today in the weekly schedule (nil = rest day). + var todaysScheduledProgram: WorkoutProgram? { program(for: Date()) } + + /// True when a schedule exists but today has no program — a rest day. + var isRestDayToday: Bool { + let weekday = Calendar.current.component(.weekday, from: Date()) + return !schedule.isEmpty && schedule[weekday] == nil + } + + /// On a new day, make the active program match the day's schedule so the + /// Workout tab shows today's workout instead of yesterday's. Rest days leave + /// the active program untouched (the view shows a rest state instead). + private func applyScheduledProgramForToday() { + guard let scheduled = todaysScheduledProgram, scheduled.id != activeProgramId else { return } + activeProgramId = scheduled.id + activeSections = scheduled.sections + save() + } + /// Unchecks every set across all programs and clears the in-progress session. private func clearAllDoneSets() { for pi in programs.indices { diff --git a/KisaniCal/Views/WorkoutView.swift b/KisaniCal/Views/WorkoutView.swift index bdcb8a6..8214164 100644 --- a/KisaniCal/Views/WorkoutView.swift +++ b/KisaniCal/Views/WorkoutView.swift @@ -62,6 +62,7 @@ struct WorkoutView: View { @State private var showManage = false @State private var showAddSection = false @State private var isReordering = false + @State private var trainAnyway = false // override the rest-day state for today @ObservedObject private var tm = TutorialManager.shared private let restTick = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -70,6 +71,10 @@ struct WorkoutView: View { return f.string(from: Date()) } + /// Show the rest-day state when today has no scheduled program and the user + /// hasn't chosen to train anyway. + private var isRestState: Bool { vm.isRestDayToday && !trainAnyway } + var body: some View { ZStack(alignment: .bottomTrailing) { if isReordering { @@ -85,8 +90,8 @@ struct WorkoutView: View { HStack { VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { - ProgramIcon(name: vm.workoutEmoji, size: 14, color: AppColors.accent) - Text(vm.workoutTitle) + ProgramIcon(name: isRestState ? "moon.zzz.fill" : vm.workoutEmoji, size: 14, color: AppColors.accent) + Text(isRestState ? "Rest Day" : vm.workoutTitle) .font(AppFonts.sans(15, weight: .semibold)) .foregroundColor(AppColors.text(cs)) } @@ -102,6 +107,15 @@ struct WorkoutView: View { .listRowInsets(EdgeInsets(top: 8, leading: 18, bottom: 4, trailing: 18)) .moveDisabled(true) + if isRestState { + // ── Rest Day ── + RestDayCard(onTrain: { withAnimation(KisaniSpring.snappy) { trainAnyway = true } }) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 16, leading: 14, bottom: 6, trailing: 14)) + .moveDisabled(true) + } else { + // ── Dot Grid Progress ── DotProgressCard(progress: vm.progress, done: vm.doneSets, total: vm.totalSets) .listRowBackground(Color.clear) @@ -200,6 +214,7 @@ struct WorkoutView: View { .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 4, leading: 14, bottom: 100, trailing: 14)) .moveDisabled(true) + } // end non-rest content } .listStyle(.plain) .scrollContentBackground(.hidden) @@ -537,6 +552,41 @@ struct DotProgressCard: View { } } +// MARK: - Rest Day Card +struct RestDayCard: View { + @Environment(\.colorScheme) private var cs + let onTrain: () -> Void + + var body: some View { + VStack(spacing: 12) { + Image(systemName: "moon.zzz.fill") + .font(.system(size: 30, weight: .light)) + .foregroundColor(AppColors.accent) + VStack(spacing: 4) { + Text("Rest Day") + .font(AppFonts.sans(17, weight: .bold)) + .foregroundColor(AppColors.text(cs)) + Text("No workout scheduled today. Recover and come back stronger.") + .font(AppFonts.sans(12)) + .foregroundColor(AppColors.text3(cs)) + .multilineTextAlignment(.center) + } + Button(action: onTrain) { + Text("Work out anyway") + .font(AppFonts.sans(13, weight: .semibold)) + .foregroundColor(AppColors.accent) + .padding(.horizontal, 16).padding(.vertical, 9) + .background(AppColors.accentSoft) + .clipShape(Capsule()) + } + .buttonStyle(PressButtonStyle(scale: 0.96)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 28).padding(.horizontal, 16) + .cardStyle() + } +} + // MARK: - Streak Banner struct StreakBannerView: View { @Environment(\.colorScheme) private var cs