commit 52a280b0f42a21ccc95f8b10879fa59dda9f2c5a Author: Vladimir Dubovik Date: Wed May 14 13:35:58 2025 +0300 Initial Commit diff --git a/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents b/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents new file mode 100644 index 0000000..ebf694f --- /dev/null +++ b/ClassDataModel.xcdatamodeld/ClassDataModel.xcdatamodel/contents @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Schedule-ICTIS.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Schedule-ICTIS.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Schedule-ICTIS.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Schedule-ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcschemes/xcschememanagement.plist b/Schedule-ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..f754700 --- /dev/null +++ b/Schedule-ICTIS.xcodeproj/xcuserdata/g412.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Schedule-ICTIS.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/Schedule-ICTIS/Assets.xcassets/AccentColor.colorset/Contents.json b/Schedule-ICTIS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Schedule-ICTIS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Schedule-ICTIS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Schedule-ICTIS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "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 + } +} diff --git a/Schedule-ICTIS/Assets.xcassets/Contents.json b/Schedule-ICTIS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Schedule-ICTIS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/ContentView.swift b/Schedule-ICTIS/ContentView.swift new file mode 100644 index 0000000..67410f1 --- /dev/null +++ b/Schedule-ICTIS/ContentView.swift @@ -0,0 +1,74 @@ +// +// ContentView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.11.2024. +// + +import SwiftUI + +struct ContentView: View { + @State private var selectedTab: TabBarModel = .schedule + @State private var isTabBarHidden = false + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var networkMonitor: NetworkMonitor + var body: some View { + ZStack (alignment: .bottom) { + TabView(selection: $selectedTab) { + Text("Tasks") + .tag(TabBarModel.tasks) + + MainView(vm: vm, networkMonitor: networkMonitor) + .tag(TabBarModel.schedule) + .background { + if !isTabBarHidden { + HideTabBar { + print("TabBar is hidden") + isTabBarHidden = true + } + } + } + + SettingsView(vm: vm, networkMonitor: networkMonitor) + .tag(TabBarModel.settings) + } + TabBarView(selectedTab: $selectedTab) + } + } +} + +struct HideTabBar: UIViewRepresentable { + var result: () -> () + func makeUIView(context: Context) -> UIView { + let view = UIView(frame: .zero) + view.backgroundColor = .clear + + DispatchQueue.main.async { + if let tabController = view.tabController { + tabController.tabBar.isHidden = true + result() + } + } + return view + } + func updateUIView(_ uiView: UIView, context: Context) { + + } +} + +extension UIView { + var tabController: UITabBarController? { + if let controller = sequence(first: self, next: { + $0.next + }).first(where: { $0 is UITabBarController}) as? UITabBarController { + return controller + } + return nil + } +} + +#Preview { + @Previewable @StateObject var vm1 = ScheduleViewModel() + @Previewable @StateObject var vm2 = NetworkMonitor() + ContentView(vm: vm1, networkMonitor: vm2) +} diff --git a/Schedule-ICTIS/ErrorsView/NetworkErrorView.swift b/Schedule-ICTIS/ErrorsView/NetworkErrorView.swift new file mode 100644 index 0000000..d37e257 --- /dev/null +++ b/Schedule-ICTIS/ErrorsView/NetworkErrorView.swift @@ -0,0 +1,31 @@ +// +// NetworkErrorView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 26.03.2025. +// + +import SwiftUI + +struct NetworkErrorView: View { + var message: String + var body: some View { + VStack { + Spacer() + VStack { + Image(systemName: "wifi.slash") + .font(.system(size: 60, weight: .light)) + .frame(width: 70, height: 70) + Text(message) + .font(.custom("Montserrat-Medium", fixedSize: 15)) + .padding(.top, 5) + } + .padding(.horizontal, 30) + Spacer() + } + } +} + +#Preview { + NetworkErrorView(message: "Восстановите подключение к интернету чтобы мы смогли загрузить расписание") +} diff --git a/Schedule-ICTIS/Fonts/Montserrat-Black.ttf b/Schedule-ICTIS/Fonts/Montserrat-Black.ttf new file mode 100755 index 0000000..f0d24ad Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Black.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-BlackItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-BlackItalic.ttf new file mode 100755 index 0000000..5ce4afc Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-BlackItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-Bold.ttf b/Schedule-ICTIS/Fonts/Montserrat-Bold.ttf new file mode 100755 index 0000000..9a425b9 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Bold.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-BoldItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-BoldItalic.ttf new file mode 100755 index 0000000..ed61ca7 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-BoldItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-ExtraBold.ttf b/Schedule-ICTIS/Fonts/Montserrat-ExtraBold.ttf new file mode 100755 index 0000000..6725d53 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-ExtraBold.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-ExtraBoldItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-ExtraBoldItalic.ttf new file mode 100755 index 0000000..107f98d Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-ExtraBoldItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-ExtraLight.ttf b/Schedule-ICTIS/Fonts/Montserrat-ExtraLight.ttf new file mode 100755 index 0000000..2967eb6 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-ExtraLight.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-ExtraLightItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-ExtraLightItalic.ttf new file mode 100755 index 0000000..5d62085 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-ExtraLightItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-Italic.ttf b/Schedule-ICTIS/Fonts/Montserrat-Italic.ttf new file mode 100755 index 0000000..00fadbe Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Italic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-Light.ttf b/Schedule-ICTIS/Fonts/Montserrat-Light.ttf new file mode 100755 index 0000000..a3cf5f5 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Light.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-LightItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-LightItalic.ttf new file mode 100755 index 0000000..6dba219 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-LightItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-Medium.ttf b/Schedule-ICTIS/Fonts/Montserrat-Medium.ttf new file mode 100755 index 0000000..db5b1af Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Medium.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-MediumItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-MediumItalic.ttf new file mode 100755 index 0000000..16dbf4c Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-MediumItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-Regular.ttf b/Schedule-ICTIS/Fonts/Montserrat-Regular.ttf new file mode 100755 index 0000000..2a2b2aa Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Regular.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-SemiBold.ttf b/Schedule-ICTIS/Fonts/Montserrat-SemiBold.ttf new file mode 100755 index 0000000..0ecc667 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-SemiBold.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-SemiBoldItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-SemiBoldItalic.ttf new file mode 100755 index 0000000..39f2393 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-SemiBoldItalic.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-Thin.ttf b/Schedule-ICTIS/Fonts/Montserrat-Thin.ttf new file mode 100755 index 0000000..6a394e7 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-Thin.ttf differ diff --git a/Schedule-ICTIS/Fonts/Montserrat-ThinItalic.ttf b/Schedule-ICTIS/Fonts/Montserrat-ThinItalic.ttf new file mode 100755 index 0000000..8eeeaa4 Binary files /dev/null and b/Schedule-ICTIS/Fonts/Montserrat-ThinItalic.ttf differ diff --git a/Schedule-ICTIS/Info.plist b/Schedule-ICTIS/Info.plist new file mode 100644 index 0000000..85932e1 --- /dev/null +++ b/Schedule-ICTIS/Info.plist @@ -0,0 +1,27 @@ + + + + + UIAppFonts + + Montserrat-Black.ttf + Montserrat-BlackItalic.ttf + Montserrat-Bold.ttf + Montserrat-BoldItalic.ttf + Monsterrat-ExtraBold.ttf + Montserrat-ExtraBoldItalic.ttf + Montserrat-ExtraLight.ttf + Montserrat-ExtraLightItalic.ttf + Montserrat-Italic.ttf + Montserrat-Light.ttf + Montserrat-LightItalic.ttf + Montserrat-Medium.ttf + Montserrat-MediumItalic.ttf + Montserrat-Regular.ttf + Montserrat-SemiBold.ttf + Montserrat-SemiBoldItalic.ttf + Montserrat-Thin.ttf + Montserrat-ThinItalic.ttf + + + diff --git a/Schedule-ICTIS/Launch Screen.storyboard b/Schedule-ICTIS/Launch Screen.storyboard new file mode 100644 index 0000000..de32c6b --- /dev/null +++ b/Schedule-ICTIS/Launch Screen.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Schedule-ICTIS/LoadingViews/ConnectingToNetworkView.swift b/Schedule-ICTIS/LoadingViews/ConnectingToNetworkView.swift new file mode 100644 index 0000000..7dcae08 --- /dev/null +++ b/Schedule-ICTIS/LoadingViews/ConnectingToNetworkView.swift @@ -0,0 +1,32 @@ +// +// LoadingView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 11.12.2024. +// + +import SwiftUI + +struct ConnectingToNetworkView: View { + @State private var isAnimating = false + var body: some View { + VStack { + Text("Ожидание сети") + .font(.custom("Montserrat-Medium", fixedSize: 18)) + Circle() + .trim(from: 0.2, to: 1.0) + .stroke(Color("blueColor"), lineWidth: 3) + .frame(width: 30, height: 30) + .rotationEffect(Angle(degrees: isAnimating ? 360 : 0)) + .animation( + Animation.linear(duration: 0.6).repeatForever(autoreverses: false), + value: isAnimating + ) + .onAppear { isAnimating = true } + } + } +} + +#Preview { + ConnectingToNetworkView() +} diff --git a/Schedule-ICTIS/LoadingViews/LoadingScheduleView.swift b/Schedule-ICTIS/LoadingViews/LoadingScheduleView.swift new file mode 100644 index 0000000..2a82334 --- /dev/null +++ b/Schedule-ICTIS/LoadingViews/LoadingScheduleView.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct LoadingScheduleView: View { + @State private var isAnimated = false + var body: some View { + ZStack { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 20) { + ForEach(0..<5, id: \.self) { _ in + VStack (alignment: .trailing) { + RoundedRectangle(cornerRadius: 20) + .fill( + LinearGradient( + gradient: Gradient(colors: [ + isAnimated ? Color.gray.opacity(0.6) : Color.gray.opacity(0.3), + isAnimated ? Color.gray.opacity(0.3) : Color.gray.opacity(0.6) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 45, height: 20) + .padding(.horizontal, 20) + .animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated) + RoundedRectangle(cornerRadius: 20) + .fill( + LinearGradient( + gradient: Gradient(colors: [ + isAnimated ? Color.gray.opacity(0.6) : Color.gray.opacity(0.3), + isAnimated ? Color.gray.opacity(0.3) : Color.gray.opacity(0.6) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 70) + .padding(.horizontal, 20) + .animation(.linear(duration: 0.8).repeatForever(autoreverses: true), value: isAnimated) + } + } + } + .onAppear { + isAnimated.toggle() + } + .padding(.top, 10) + } + } + } +} + +#Preview { + LoadingScheduleView() +} diff --git a/Schedule-ICTIS/LoadingViews/LoadingView.swift b/Schedule-ICTIS/LoadingViews/LoadingView.swift new file mode 100644 index 0000000..70e6b3b --- /dev/null +++ b/Schedule-ICTIS/LoadingViews/LoadingView.swift @@ -0,0 +1,29 @@ +// +// LoadingView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 04.04.2025. +// + +import SwiftUI + +struct LoadingView: View { + @State private var isAnimating = false + + var body: some View { + Circle() + .trim(from: 0.2, to: 1.0) + .stroke(Color("blueColor"), lineWidth: 3) + .frame(width: 30, height: 30) + .rotationEffect(Angle(degrees: isAnimating ? 360 : 0)) + .animation( + Animation.linear(duration: 0.6).repeatForever(autoreverses: false), + value: isAnimating + ) + .onAppear { isAnimating = true } + } +} + +#Preview { + LoadingView() +} diff --git a/Schedule-ICTIS/Main/Views/CreatedClassView.swift b/Schedule-ICTIS/Main/Views/CreatedClassView.swift new file mode 100644 index 0000000..3d63a7f --- /dev/null +++ b/Schedule-ICTIS/Main/Views/CreatedClassView.swift @@ -0,0 +1,52 @@ +// +// CreatedClassView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 23.12.2024. +// + +import SwiftUI + +struct CreatedClassView: View { + @ObservedObject var _class: CoreDataClassModel + var provider = ClassProvider.shared + var body: some View { + let existingCopy = try? provider.viewContext.existingObject(with: _class.objectID) + if existingCopy != nil { + HStack(spacing: 15) { + VStack { + Text(getTimeString(_class.starttime)) + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .padding(.bottom, 1) + Text(getTimeString(_class.endtime)) + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .padding(.top, 1) + } + .frame(width: 48) + .padding(.top, 7) + .padding(.bottom, 7) + .padding(.leading, 10) + Rectangle() + .frame(width: 2) + .frame(maxHeight: UIScreen.main.bounds.height - 18) + .padding(.top, 7) + .padding(.bottom, 7) + .foregroundColor(_class.important ? Color("redForImportant") : onlineOrNot(_class.online)) + Text(getSubjectName(_class.subject, _class.professor, _class.auditory)) + .font(.custom("Montserrat-Medium", fixedSize: 15)) + .lineSpacing(3) + .padding(.top, 9) + .padding(.bottom, 9) + Spacer() + } + .frame(maxWidth: UIScreen.main.bounds.width - 40, maxHeight: 230) + .background(Color.white) + .cornerRadius(20) + .shadow(color: .black.opacity(0.25), radius: 4, x: 2, y: 2) + } + } +} + +#Preview { + CreatedClassView(_class: .preview()) +} diff --git a/Schedule-ICTIS/Main/Views/Fields/AuditoryFieldView.swift b/Schedule-ICTIS/Main/Views/Fields/AuditoryFieldView.swift new file mode 100644 index 0000000..2fc69bb --- /dev/null +++ b/Schedule-ICTIS/Main/Views/Fields/AuditoryFieldView.swift @@ -0,0 +1,47 @@ +// +// AuditoryFieldView.swift +// Schedule ICTIS +// +// Created by G412 on 23.01.2025. +// + +import SwiftUI + +struct AuditoryFieldView: View { + @Binding var text: String + var labelForField: String + @FocusState var isFocused: Bool + var body: some View { + HStack(spacing: 0) { + Image(systemName: "mappin.and.ellipse") + .foregroundColor(Color.gray) + .padding(.leading, 12) + .padding(.trailing, 14) + TextField(labelForField, text: $text) + .font(.custom("Montserrat-Meduim", fixedSize: 17)) + .disableAutocorrection(true) + .submitLabel(.done) + .focused($isFocused) + if isFocused { + Button { + self.text = "" + self.isFocused = false + } label: { + Image(systemName: "xmark.circle.fill") + .padding(.trailing, 20) + .offset(x: 10) + .foregroundColor(.gray) + } + } + } + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + } +} + +#Preview { + AuditoryFieldView(text: .constant(""), labelForField: "Корпус-аудитория") +} diff --git a/Schedule-ICTIS/Main/Views/Fields/CommentFieldView.swift b/Schedule-ICTIS/Main/Views/Fields/CommentFieldView.swift new file mode 100644 index 0000000..fb64d55 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/Fields/CommentFieldView.swift @@ -0,0 +1,44 @@ +// +// CommentView.swift +// Schedule ICTIS +// +// Created by G412 on 17.12.2024. +// + +import SwiftUI + +struct CommentFieldView: View { + @Binding var textForComment: String + @FocusState var isFocused: Bool + + var body: some View { + HStack { + TextField("Комментарий", text: $textForComment) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .submitLabel(.done) + .multilineTextAlignment(.leading) + .focused($isFocused) + .padding(.top, 6) + .padding(.bottom, 6) + + if isFocused { + Button { + textForComment = "" + self.isFocused = false + } label: { + Image(systemName: "xmark.circle.fill") + .padding(.trailing, 20) + .offset(x: 10) + .foregroundColor(.gray) + } + } + } + .frame(minHeight: 40) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + } +} + diff --git a/Schedule-ICTIS/Main/Views/Fields/ProfessorFieldView.swift b/Schedule-ICTIS/Main/Views/Fields/ProfessorFieldView.swift new file mode 100644 index 0000000..a34ae66 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/Fields/ProfessorFieldView.swift @@ -0,0 +1,47 @@ +// +// ProfessorFieldView.swift +// Schedule ICTIS +// +// Created by G412 on 23.01.2025. +// + +import SwiftUI + +struct ProfessorFieldView: View { + @Binding var text: String + var labelForField: String + @FocusState var isFocused: Bool + var body: some View { + HStack(spacing: 0) { + Image(systemName: "graduationcap") + .foregroundColor(Color.gray) + .padding(.leading, 12) + .padding(.trailing, 7) + TextField(labelForField, text: $text) + .font(.custom("Montserrat-Meduim", fixedSize: 17)) + .disableAutocorrection(true) + .submitLabel(.done) + .focused($isFocused) + if isFocused { + Button { + self.text = "" + self.isFocused = false + } label: { + Image(systemName: "xmark.circle.fill") + .padding(.trailing, 20) + .offset(x: 10) + .foregroundColor(.gray) + } + } + } + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + } +} + +#Preview { + ProfessorFieldView(text: .constant(""), labelForField: "Преподаватель") +} diff --git a/Schedule-ICTIS/Main/Views/Fields/StartEndTimeFieldView.swift b/Schedule-ICTIS/Main/Views/Fields/StartEndTimeFieldView.swift new file mode 100644 index 0000000..5ef69ed --- /dev/null +++ b/Schedule-ICTIS/Main/Views/Fields/StartEndTimeFieldView.swift @@ -0,0 +1,61 @@ +// +// StartEndTimeView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 17.12.2024. +// + +import SwiftUI + +struct StartEndTimeFieldView: View { + @Binding var isIncorrectDate: Bool + @Binding var selectedDay: Date + @Binding var selectedTime: Date + var imageName: String + var text: String + @Binding var isTimeSelected: Bool + var body: some View { + HStack { + Image(systemName: imageName) + .foregroundColor(isIncorrectDate ? .red : Color("grayForFields")) + .padding(.leading, 12) + .padding(.trailing, 5) + + if !isTimeSelected || isIncorrectDate { + Text(text) + .font(.custom("Montserrat-Meduim", fixedSize: 17)) + .foregroundColor(.gray.opacity(0.5)) + } + else { + Text("\(selectedTime, formatter: timeFormatter)") + .foregroundColor(isIncorrectDate ? .red : .black) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .padding(.trailing, 10) + } + Spacer() + } + .frame(width: (UIScreen.main.bounds.width / 2) - 22, height: 40) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + .overlay { + if selectedDay.isToday { + DatePicker("", selection: $selectedTime, in: Date()..., displayedComponents: .hourAndMinute) + .padding(.trailing, 35) + .blendMode(.destinationOver) + .onChange(of: selectedTime) { newValue, oldValue in + isTimeSelected = true + } + } + else { + DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute) + .padding(.trailing, 35) + .blendMode(.destinationOver) + .onChange(of: selectedTime) { newValue, oldValue in + isTimeSelected = true + } + } + } + } +} diff --git a/Schedule-ICTIS/Main/Views/Fields/SubjectFieldView.swift b/Schedule-ICTIS/Main/Views/Fields/SubjectFieldView.swift new file mode 100644 index 0000000..60ab5b0 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/Fields/SubjectFieldView.swift @@ -0,0 +1,65 @@ +// +// Field.swift +// Schedule ICTIS +// +// Created by G412 on 16.12.2024. +// КТбо2-6 + +import SwiftUI + +struct SubjectFieldView: View { + @Binding var text: String + @Binding var isShowingSubjectFieldRed: Bool + @Binding var labelForField: String + @FocusState var isFocused: Bool + var body: some View { + HStack(spacing: 0) { + Image(systemName: "book") + .foregroundColor(Color.gray) + .padding(.leading, 12) + .padding(.trailing, 9) + TextField(labelForField, text: $text) + .font(.custom("Montserrat-Meduim", fixedSize: 17)) + .disableAutocorrection(true) + .submitLabel(.done) + .focused($isFocused) + .onChange(of: isFocused, initial: false) { oldValue, newValue in + if newValue { + self.isShowingSubjectFieldRed = false + self.labelForField = "Предмет" + } + } + .background { + Group { + if isShowingSubjectFieldRed { + Text("Поле должно быть заполнено!") + .font(.custom("Montserrat-Meduim", fixedSize: 17)) + .foregroundColor(.red) + .frame(width: 290) + .padding(.leading, -38) + } + } + } + if isFocused { + Button { + self.text = "" + self.isFocused = false + } label: { + Image(systemName: "xmark.circle.fill") + .padding(.trailing, 20) + .offset(x: 10) + .foregroundColor(.gray) + } + } + } + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + } +} + +#Preview { + SubjectFieldView(text: .constant(""), isShowingSubjectFieldRed: .constant(false), labelForField: .constant("Предмет")) +} diff --git a/Schedule-ICTIS/Main/Views/FilterGroupsView.swift b/Schedule-ICTIS/Main/Views/FilterGroupsView.swift new file mode 100644 index 0000000..de03064 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/FilterGroupsView.swift @@ -0,0 +1,51 @@ +// +// FilterGroupsView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 21.03.2025. +// + +import SwiftUI + +struct FilterGroupsView: View { + @ObservedObject var vm: ScheduleViewModel + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 12) { + ForEach(vm.filteringGroups, id: \.self) { group in + VStack { + Text(group) + .foregroundColor(Color("customGray3")) + .font(.custom("Montserrat-Medium", fixedSize: 14)) + .padding(.horizontal, 15) + .padding(.vertical, 7) + } + .background(Color.white) + .overlay ( + Group { + if vm.showOnlyChoosenGroup == group { + RoundedRectangle(cornerRadius: 20) + .stroke(Color("blueColor"), lineWidth: 3) + } + } + ) + .cornerRadius(20) + .onTapGesture { + vm.showOnlyChoosenGroup = group + } + } + } + .padding(.horizontal) + } + .frame(height: 40) + .onAppear { + vm.updateFilteringGroups() + } + .padding(.bottom, 4) + } +} + +#Preview { + @Previewable @ObservedObject var vm = ScheduleViewModel() + FilterGroupsView(vm: vm) +} diff --git a/Schedule-ICTIS/Main/Views/FirstLaunchScheduleView.swift b/Schedule-ICTIS/Main/Views/FirstLaunchScheduleView.swift new file mode 100644 index 0000000..6434146 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/FirstLaunchScheduleView.swift @@ -0,0 +1,20 @@ +// +// FirstLaunchScheduleView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 06.12.2024. +// + +import SwiftUI + +struct FirstLaunchScheduleView: View { + var body: some View { + VStack () { + Spacer() + } + } +} + +#Preview { + FirstLaunchScheduleView() +} diff --git a/Schedule-ICTIS/Main/Views/MainView.swift b/Schedule-ICTIS/Main/Views/MainView.swift new file mode 100644 index 0000000..bf70521 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/MainView.swift @@ -0,0 +1,96 @@ +// +// ScheduleView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.11.2024. +// + +import SwiftUI + +struct MainView: View { + @State private var isShowingMonthSlider: Bool = false + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var networkMonitor: NetworkMonitor + @FocusState private var isFocusedSearchBar: Bool + @State private var isScrolling: Bool = false + var body: some View { + VStack { + SearchBarView(isFocused: _isFocusedSearchBar, vm: vm, isShowingMonthSlider: $isShowingMonthSlider) + .onChange(of: isScrolling, initial: false) { oldValue, newValue in + if newValue && isScrolling { + isFocusedSearchBar = false + } + } + CurrentDateView() + FilterGroupsView(vm: vm) + if vm.isLoading { + LoadingScheduleView() + } + else { + ScheduleView(vm: vm, networkMonitor: networkMonitor, isScrolling: $isScrolling) + } + } + .alert(isPresented: $vm.isShowingAlertForIncorrectGroup, error: vm.errorInNetwork) { error in + Button("ОК") { + print("This alert") + vm.isShowingAlertForIncorrectGroup = false + vm.errorInNetwork = nil + } + } message: { error in + Text(error.failureReason) + } + .background(Color("background")) + } + + @ViewBuilder + func CurrentDateView() -> some View { + VStack (alignment: .leading, spacing: 6) { + HStack { + VStack (alignment: .leading, spacing: 0) { + Text(vm.selectedDay.format("EEEE")) + .font(.custom("Montserrat-SemiBold", fixedSize: 30)) + .foregroundStyle(.black) + HStack (spacing: 5) { + Text(vm.selectedDay.format("dd")) + .font(.custom("Montserrat-Bold", fixedSize: 17)) + .foregroundStyle(Color("grayForDate")) + Text(vm.selectedDay.format("MMMM")) + .font(.custom("Montserrat-Bold", fixedSize: 17)) + .foregroundStyle(Color("grayForDate")) + Spacer() + Button(action: { + withAnimation(.easeInOut(duration: 0.5)) { + isShowingMonthSlider.toggle() + } + }) { + HStack(spacing: 2) { + Text(isShowingMonthSlider ? "Свернуть" : "Развернуть") + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .foregroundStyle(Color.blue) + Image(isShowingMonthSlider ? "arrowup" : "arrowdown") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + } + } + } + } + .padding(.top, 8) + .padding(.leading, 5) + Spacer() + } + if (!isShowingMonthSlider) { + WeekTabView(vm: vm) + .transition(.opacity) + .animation(.easeInOut(duration: 0.25), value: isShowingMonthSlider) + } + else { + MonthTabView(vm: vm) + .transition(.opacity) + .animation(.linear(duration: 0.5), value: isShowingMonthSlider) + } + } + .padding(.horizontal) + } +} + diff --git a/Schedule-ICTIS/Main/Views/NoScheduleView.swift b/Schedule-ICTIS/Main/Views/NoScheduleView.swift new file mode 100644 index 0000000..dae758c --- /dev/null +++ b/Schedule-ICTIS/Main/Views/NoScheduleView.swift @@ -0,0 +1,24 @@ +// +// NoScheduleView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 12.12.2024. +// + +import SwiftUI + +struct NoScheduleView: View { + var body: some View { + VStack { + ScrollView (showsIndicators: false) { + Text("Расслабся братан, расписания еще нет") + .padding(.top, 100) + .font(.custom("Montserrat-SemiBold", fixedSize: 17)) + } + } + } +} + +#Preview { + NoScheduleView() +} diff --git a/Schedule-ICTIS/Main/Views/ScheduleView.swift b/Schedule-ICTIS/Main/Views/ScheduleView.swift new file mode 100644 index 0000000..38b318d --- /dev/null +++ b/Schedule-ICTIS/Main/Views/ScheduleView.swift @@ -0,0 +1,170 @@ +import SwiftUI +import CoreData + +struct ScheduleView: View { + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var networkMonitor: NetworkMonitor + @FetchRequest(fetchRequest: CoreDataClassModel.all()) var classes // Список пар добавленных пользователем + @FetchRequest(fetchRequest: JsonClassModel.all()) private var subjects // Список пар сохраненных в CoreData(для отсутствия интернета) + @FetchRequest(fetchRequest: FavouriteGroupModel.all()) private var favGroups + @FetchRequest(fetchRequest: FavouriteVpkModel.all()) private var favVpk + @State private var selectedClass: CoreDataClassModel? = nil + @State private var lastOffset: CGFloat = 0 + @State private var scrollTimer: Timer? = nil + @Binding var isScrolling: Bool + var provider = ClassProvider.shared + + private var hasSubjectsToShow: Bool { + subjects.contains { subject in + subject.week == vm.week + } + } + + private var hasClassesToShow: Bool { + classes.contains { _class in + _class.day == vm.selectedDay + } + } + + var body: some View { + ZStack(alignment: .top) { + if networkMonitor.isConnected { + onlineContent + } else { + offlineContent + } + gradientOverlay + } + .onAppear { + deleteClassesFormCoreDataIfMonday() + if networkMonitor.isConnected { + checkSavingOncePerDay() + } + } + .sheet(item: $selectedClass, onDismiss: { selectedClass = nil }) { _class in + CreateEditClassView(vm: .init(provider: provider, _class: _class), day: vm.selectedDay) + } + } + + private var onlineContent: some View { + Group { + if vm.errorInNetwork == .timeout { + NetworkErrorView(message: "Проверьте подключение к интернету") + } else if vm.errorInNetwork == .invalidResponse { + NoScheduleView() + } + else if vm.errorInNetwork == .noError { + scheduleScrollView(isOnline: true) + } + else { + NoScheduleView() + } + } + .onAppear { + if vm.classesGroups.isEmpty { + vm.fetchWeekSchedule() + } + } + } + + private var offlineContent: some View { + scheduleScrollView(isOnline: false) + } + + private func scheduleScrollView(isOnline: Bool) -> some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 30) { + subjectsSection(isOnline: isOnline) + myPairsSection + } + .frame(width: UIScreen.main.bounds.width) + .padding(.bottom, 100) + .padding(.top, 10) + .background(GeometryReader { geometry in + Color.clear.preference(key: ViewOffsetKey.self, value: geometry.frame(in: .global).minY) + }) + } + .onPreferenceChange(ViewOffsetKey.self) { offset in + if offset != lastOffset { + isScrolling = true + scrollTimer?.invalidate() + scrollTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + isScrolling = false + } + } + lastOffset = offset + } + .onDisappear { + scrollTimer?.invalidate() + } + } + + // Секция с парами + private func subjectsSection(isOnline: Bool) -> some View { + VStack(alignment: .leading, spacing: 10) { + if isOnline { + ForEach(0.. Value) { + value += nextValue() + } +} diff --git a/Schedule-ICTIS/Main/Views/SearchBarView.swift b/Schedule-ICTIS/Main/Views/SearchBarView.swift new file mode 100644 index 0000000..ee2c445 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/SearchBarView.swift @@ -0,0 +1,99 @@ +// +// SearchBarView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.11.2024. +// + +import SwiftUI + +struct SearchBarView: View { + @State private var text: String = "" + @FocusState var isFocused: Bool + @State private var isShowingSheet: Bool = false + @ObservedObject var vm: ScheduleViewModel + @Binding var isShowingMonthSlider: Bool + + var provider = ClassProvider.shared + + var body: some View { + HStack (spacing: 11) { + HStack (spacing: 0) { + Image(systemName: "magnifyingglass") + .foregroundColor(Color.gray) + .padding(.leading, 12) + .padding(.trailing, 7) + TextField("Поиск группы", text: $text) + .disableAutocorrection(true) + .focused($isFocused) + .onSubmit { + self.isFocused = false + if (!text.isEmpty) { + vm.fetchWeekForSingleGroup(groupName: text) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + guard vm.errorInNetwork == .noError else { + vm.isShowingAlertForIncorrectGroup = true + return + } + vm.removeFromSchedule(group: vm.searchingGroup) + text = transformStringToFormat(text) + vm.searchingGroup = text + vm.nameToHtml[text] = "" + print("Ключи: \(vm.nameToHtml.keys)") + vm.updateFilteringGroups() + vm.fetchWeekSchedule() + self.text = "" + } + } + } + .submitLabel(.search) + if isFocused { + Button { + self.text = "" + self.isFocused = false + } label: { + Image(systemName: "xmark.circle.fill") + .padding(.trailing, 20) + .offset(x: 10) + .foregroundColor(.gray) + .background( + ) + } + } + } + .simultaneousGesture(TapGesture().onEnded { + self.isShowingMonthSlider = false + }) + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + if !isFocused { + Button { + isShowingSheet = true + } label: { + ZStack { + Rectangle() + .frame(width: 40, height: 40) + .foregroundStyle(Color("blueColor")) + .cornerRadius(15) + Image(systemName: "plus") + .resizable() + .foregroundStyle(.white) + .scaledToFit() + .frame(width: 16) + } + } + } + } + .padding(.horizontal) + .padding(.top, 5) + .frame(height: 40) + .accentColor(.blue) + .sheet(isPresented: $isShowingSheet) { + CreateEditClassView(vm: .init(provider: provider), day: vm.selectedDay) + } + } +} + diff --git a/Schedule-ICTIS/Main/Views/Sheets/CreateEditClassView.swift b/Schedule-ICTIS/Main/Views/Sheets/CreateEditClassView.swift new file mode 100644 index 0000000..6b9ccf6 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/Sheets/CreateEditClassView.swift @@ -0,0 +1,301 @@ +// +// SheetCreateClassView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 12.12.2024. +// + +import SwiftUI + +struct CreateEditClassView: View { + @Environment(\.dismiss) private var dismiss + @State private var isShowingDatePickerForDate: Bool = false + @ObservedObject var vm: EditClassViewModel + var day: Date + @State private var isIncorrectDate1: Bool = false + @State private var isIncorrectDate2: Bool = false + @State private var isShowingSubjectFieldRed: Bool = false + @State private var isSelectedTime1 = false + @State private var isSelectedTime2 = false + @State private var textForLabelInSubjectField: String = "Предмет" + @State private var selectedType: String = "Оффлайн" + @FocusState private var isFocusedSubject: Bool + @FocusState private var isFocusedAuditory: Bool + @FocusState private var isFocusedProfessor: Bool + @FocusState private var isFocusedComment: Bool + var provider = ClassProvider.shared + var body: some View { + NavigationView { + ScrollView(.vertical, showsIndicators: false) { + VStack { + SubjectFieldView(text: $vm._class.subject, isShowingSubjectFieldRed: $isShowingSubjectFieldRed, labelForField: $textForLabelInSubjectField, isFocused: _isFocusedSubject) + .padding(.bottom, 10) + + HStack { + HStack { + Text("Тип") + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(.black) + Spacer() + HStack { + Text(vm._class.online) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(Color("customGray3")) + Image("upDownArrows") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + } + .padding(.horizontal) + } + .padding(.horizontal) + .padding(.top, 10) + .padding(.bottom, 10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + .overlay { + HStack { + Spacer() + Picker("Тип", selection: $vm._class.online, content: { + ForEach(MockData.onlineOrOffline, id: \.self) { + Text($0) + } + }) + .accentColor(Color("grayForFields")) + .padding(.trailing, 35) + .blendMode(.destinationOver) + } + .frame(width: UIScreen.main.bounds.width) + } + } + .padding(.bottom, 10) + + ZStack { + if vm._class.online == "Оффлайн" { + AuditoryFieldView(text: $vm._class.auditory, labelForField: "Корпус-аудитория", isFocused: _isFocusedAuditory) + .padding(.bottom, 10) + .transition(.asymmetric( + insertion: .offset(y: -50).combined(with: .identity), + removal: .offset(y: -50).combined(with: .opacity) + )) + } + } + .animation( + vm._class.online == "Оффлайн" ? + .linear(duration: 0.3) : // Анимация для появления + .linear(duration: 0.2), // Анимация для исчезновения + value: vm._class.online + ) + + ProfessorFieldView(text: $vm._class.professor, labelForField: "Преподаватель", isFocused: _isFocusedProfessor) + .padding(.bottom, 10) + + HStack { + Image(systemName: "calendar") + .foregroundColor(Color.gray) + .padding(.leading, 12) + .padding(.trailing, 5) + Text("Дата") + .foregroundColor(Color("grayForFields").opacity(0.5)) + .font(.custom("Montserrat-Meduim", fixedSize: 17)) + Spacer() + Text("\(vm._class.day, formatter: dateFormatter)") + .foregroundColor(.black) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .padding(.trailing, 20) + } + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + .overlay { + DatePicker("", selection: $vm._class.day, in: Date()..., displayedComponents: .date) + .blendMode(.destinationOver) + + } + .padding(.bottom, 10) + HStack { + StartEndTimeFieldView(isIncorrectDate: $isIncorrectDate1, selectedDay: $vm._class.day, selectedTime: $vm._class.starttime, imageName: "clock", text: "Начало", isTimeSelected: $isSelectedTime1) + .onChange(of: vm._class.starttime) { oldValue, newValue in + if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) { + self.isIncorrectDate1 = true + self.isSelectedTime1 = false + print("Первый") + print(self.isSelectedTime1) + print(self.isSelectedTime2) + } + else { + self.isIncorrectDate1 = false + self.isIncorrectDate2 = false + } + } + Spacer() + StartEndTimeFieldView(isIncorrectDate: $isIncorrectDate2, selectedDay: $vm._class.day, selectedTime: $vm._class.endtime, imageName: "clock.badge.xmark", text: "Конец", isTimeSelected: $isSelectedTime2) + .onChange(of: vm._class.endtime) { oldValue, newValue in + if !checkStartTimeLessThenEndTime(vm._class.starttime, vm._class.endtime) { + self.isIncorrectDate2 = true + self.isSelectedTime2 = false + print("Второй") + print(self.isSelectedTime1) + print(self.isSelectedTime2) + } + else { + self.isIncorrectDate1 = false + self.isIncorrectDate2 = false + } + } + } + .frame(height: 40) + .padding(.bottom, 10) + Toggle("Пометить как важную", isOn: $vm._class.important) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .frame(height: 40) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + .padding(.bottom, 10) + + HStack { + HStack { + Text("Напоминание") + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(.black) + Spacer() + HStack { + Text(vm._class.notification) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(Color("customGray3")) + Image("upDownArrows") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + } + .padding(.horizontal) + } + .padding(.horizontal) + .padding(.top, 10) + .padding(.bottom, 10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(.white) + ) + .overlay { + HStack { + Spacer() + Picker("", selection: $vm._class.notification , content: { + ForEach(MockData.notifications, id: \.self) { + Text($0) + } + }) + .accentColor(Color("grayForFields")) + .padding(.trailing, 35) + .blendMode(.destinationOver) + } + .frame(width: UIScreen.main.bounds.width) + } + } + .padding(.bottom, 10) + + CommentFieldView(textForComment: $vm._class.comment, isFocused: _isFocusedComment) + .padding(.bottom, 20) + + + if !vm.isNew { + Button { + do { + try provider.delete(vm._class, in: provider.viewContext) + dismiss() + } catch { + print(error) + } + } label: { + HStack { + Spacer() + Image(systemName: "trash") + Text("Удалить занятие") + .font(.custom("Montserrat-Medium", fixedSize: 17)) + Spacer() + } + .frame(height: 40) + .background(Color.white) + .foregroundColor(Color.red) + .cornerRadius(10) + } + } + Spacer() + } + .padding(.horizontal) + .padding(.bottom, 60) + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Отменить") { + dismiss() + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button("Сохранить") { + isFocusedSubject = false + isFocusedProfessor = false + isFocusedAuditory = false + isFocusedComment = false + if (vm._class.subject.isEmpty || (isIncorrectDate1 || isIncorrectDate2) || (!isSelectedTime1 || !isSelectedTime2)) { + if (vm._class.subject.isEmpty) { + self.isShowingSubjectFieldRed = true + self.textForLabelInSubjectField = "" + } + if !isSelectedTime1 { + self.isIncorrectDate1 = true + } + if !isSelectedTime2 { + self.isIncorrectDate2 = true + } + } + else { + do { + try vm.save() + dismiss() + } catch { + print(error) + } + } + } + } + } + .navigationTitle(vm.isNew ? "Новая пара" : "Изменить данные") + .background(Color("background")) + .onAppear { + let temp = Calendar.current.date(byAdding: .hour, value: 1, to: Date.init()) + if let endTime = temp { + if (!hoursMinutesAreEqual(date1: vm._class.starttime, isEqualTo: Date()) && !hoursMinutesAreEqual(date1: vm._class.endtime, isEqualTo: endTime)) { + self.isSelectedTime1 = true + self.isSelectedTime2 = true + print(vm._class.starttime) + print(vm._class.endtime) + print(endTime) + print(Date()) + } + } + if day > Calendar.current.startOfDay(for: Date()) { + vm._class.day = day + } + } + .onTapGesture { + isFocusedSubject = false + isFocusedProfessor = false + isFocusedAuditory = false + isFocusedComment = false + } + } + } +} + +#Preview { + let day: Date = .init() + CreateEditClassView(vm: .init(provider: .shared), day: day) +} diff --git a/Schedule-ICTIS/Main/Views/SubjectView.swift b/Schedule-ICTIS/Main/Views/SubjectView.swift new file mode 100644 index 0000000..4030bc0 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/SubjectView.swift @@ -0,0 +1,61 @@ +// +// SubjectView.swift +// Schedule ICTIS +// +// Created by Egor Mironov on 02.04.2025. +// + +import SwiftUI + +struct SubjectView: View { + let info: ClassInfo + @ObservedObject var vm: ScheduleViewModel + @State private var onlyOneGroup: Bool = false + + var body: some View { + VStack(alignment: .trailing) { + if !onlyOneGroup { + Text(info.group) + .font(.custom("Montserrat-Regular", fixedSize: 11)) + .foregroundColor(Color("grayForNameGroup")) + } + HStack(spacing: 15) { + VStack { + Text(convertTimeString(info.time)[0]) + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .padding(.bottom, 1) + Text(convertTimeString(info.time)[1]) + .font(.custom("Montserrat-Regular", fixedSize: 15)) + .padding(.top, 1) + } + .frame(width: 48) + .padding(.top, 7) + .padding(.bottom, 7) + .padding(.leading, 10) + Rectangle() + .frame(width: 2) + .frame(maxHeight: UIScreen.main.bounds.height - 18) + .padding(.top, 7) + .padding(.bottom, 7) + .foregroundColor(getColorForClass(info.subject)) + Text(info.subject) + .font(.custom("Montserrat-Medium", fixedSize: 16)) + .lineSpacing(3) + .padding(.top, 9) + .padding(.bottom, 9) + Spacer() + } + .frame(maxWidth: UIScreen.main.bounds.width - 40, maxHeight: 230) + .background(Color.white) + .cornerRadius(20) + .shadow(color: .black.opacity(0.25), radius: 4, x: 2, y: 2) + } + .onAppear { + onlyOneGroup = (vm.showOnlyChoosenGroup != vm.filteringGroups[0]) + } + .padding(.bottom, onlyOneGroup ? 17 : 0) + .onChange(of: vm.showOnlyChoosenGroup) { oldValue, newValue in + onlyOneGroup = (newValue != vm.filteringGroups[0]) + } + } +} diff --git a/Schedule-ICTIS/Main/Views/TabViews/MonthTabView.swift b/Schedule-ICTIS/Main/Views/TabViews/MonthTabView.swift new file mode 100644 index 0000000..1f92190 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/TabViews/MonthTabView.swift @@ -0,0 +1,84 @@ +// +// MonthTabView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 10.12.2024. +// + +import SwiftUI + +struct MonthTabView: View { + @State var currentMonthIndex: Int = 1 + @State var monthSlider: [[Date.MonthWeek]] = [] + @State private var createMonth: Bool = false + @State private var currentWeekIndex: Int = 0 + @ObservedObject var vm: ScheduleViewModel + var body: some View { + VStack { + HStack (spacing: 33) { + ForEach(MockData.daysOfWeek.indices, id: \.self) { index in + Text(MockData.daysOfWeek[index]) + .font(.custom("Montserrat-SemiBold", fixedSize: 15)) + .foregroundColor(MockData.daysOfWeek[index] == "Вс" ? Color(.red) : Color("customGray2")) + } + } + TabView(selection: $currentMonthIndex) { + ForEach(monthSlider.indices, id: \.self) { index in + let month = monthSlider[index] + MonthView(month) + .tag(index) + .transition(.slide) + } + } + .padding(.horizontal, -15) + .tabViewStyle(.page(indexDisplayMode: .never)) + //.animation(.easeIn(duration: 0.3), value: currentMonthIndex) + } + .frame(height: 220) + .padding(.top, 26) + .padding(.bottom, 20) + .onAppear { + updateMonthScreenViewForNewGroup() + } + .onChange(of: currentMonthIndex, initial: false) { oldValue, newValue in + if newValue == 0 || newValue == (monthSlider.count - 1) { + createMonth = true + } + } + .onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in + if newValue { + monthSlider.removeAll() + currentMonthIndex = 1 + updateMonthScreenViewForNewGroup() + vm.isNewGroup = false + } + } + } + + @ViewBuilder + func MonthView(_ month: [Date.MonthWeek]) -> some View { + VStack (spacing: 10) { + ForEach(month.indices, id: \.self) { index in + let week = month[index].week + WeekViewForMonth(week: week, vm: vm) + } + } + .background { + GeometryReader { + let minX = $0.frame(in: .global).minX + + Color.clear + .preference(key: OffsetKey.self, value: minX) + .onPreferenceChange(OffsetKey.self) { value in + if (abs(value.rounded()) - 20) < 5 && createMonth { + paginateMonth() + + createMonth = false + } + } + } + } + } +} + + diff --git a/Schedule-ICTIS/Main/Views/TabViews/WeekTabView.swift b/Schedule-ICTIS/Main/Views/TabViews/WeekTabView.swift new file mode 100644 index 0000000..30a896a --- /dev/null +++ b/Schedule-ICTIS/Main/Views/TabViews/WeekTabView.swift @@ -0,0 +1,46 @@ +// +// WeekTabView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 10.12.2024. +// + +import SwiftUI + +struct WeekTabView: View { + @State private var currentWeekIndex: Int = 1 + @State var weekSlider: [[Date.WeekDay]] = [] + @State private var createWeek: Bool = false + @ObservedObject var vm: ScheduleViewModel + var body: some View { + HStack { + TabView(selection: $currentWeekIndex) { + ForEach(weekSlider.indices, id: \.self) { index in + let week = weekSlider[index] + WeekViewForWeek(weekSlider: $weekSlider, currentWeekIndex: $currentWeekIndex, createWeek: $createWeek, week: week, vm: vm) + .padding(.horizontal, 15) + .tag(index) + } + } + .padding(.horizontal, -15) + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(height: 90) + } + .onAppear(perform: { + updateWeekScreenViewForNewGroup() + }) + .onChange(of: currentWeekIndex, initial: false) { oldValue, newValue in + if newValue == 0 || newValue == (weekSlider.count - 1) { + createWeek = true + } + } + .onChange(of: vm.isNewGroup, initial: false) { oldValue, newValue in + if newValue { + weekSlider.removeAll() + currentWeekIndex = 1 + updateWeekScreenViewForNewGroup() + vm.isNewGroup = false + } + } + } +} diff --git a/Schedule-ICTIS/Main/Views/TabViews/WeekViewForMonth.swift b/Schedule-ICTIS/Main/Views/TabViews/WeekViewForMonth.swift new file mode 100644 index 0000000..98a7747 --- /dev/null +++ b/Schedule-ICTIS/Main/Views/TabViews/WeekViewForMonth.swift @@ -0,0 +1,32 @@ +// +// WeekViewForMonth.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 20.12.2024. +// + +import SwiftUI + +struct WeekViewForMonth: View { + let week: [Date.WeekDay] + @ObservedObject var vm: ScheduleViewModel + + var body: some View { + HStack(spacing: 23) { + ForEach(week) { day in + VStack { + Text(day.date.format("dd")) + .font(.custom("Montserrat-SemiBold", fixedSize: 15)) + .foregroundStyle(getForegroundColor(day: day)) + } + .frame(width: 30, height: 30, alignment: .center) + .background(getBackgroundColor(day: day)) + .overlay(overlay(day: day)) + .cornerRadius(15) + .onTapGesture { + handleTap(day: day) + } + } + } + } +} diff --git a/Schedule-ICTIS/Main/Views/TabViews/WeekViewForWeek.swift b/Schedule-ICTIS/Main/Views/TabViews/WeekViewForWeek.swift new file mode 100644 index 0000000..37c6e5a --- /dev/null +++ b/Schedule-ICTIS/Main/Views/TabViews/WeekViewForWeek.swift @@ -0,0 +1,76 @@ +// +// WeekView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 20.12.2024. +// + +import SwiftUI + +struct WeekViewForWeek: View { + @Binding var weekSlider: [[Date.WeekDay]] + @Binding var currentWeekIndex: Int + @Binding var createWeek: Bool + let week: [Date.WeekDay] + @ObservedObject var vm: ScheduleViewModel + var body: some View { + HStack (spacing: 10) { + ForEach(week) { day in + VStack (spacing: 1) { + Text(day.date.format("E")) + .font(.custom("Montserrat-SemiBold", fixedSize: 15)) + .foregroundColor(day.date.format("E") == "Вс" ? Color(.red) : isSameDate(day.date, vm.selectedDay) ? Color("customGray1") : Color("customGray3")) + .padding(.top, 13) + .foregroundColor(.gray) + Text(day.date.format("dd")) + .font(.custom("Montserrat-Semibold", fixedSize: 15)) + .foregroundStyle(isSameDate(day.date, vm.selectedDay) ? .white : .black) + .padding(.bottom, 13) + } + .frame(width: 43, height: 55, alignment: .center) + .background( content: { + Group { + if isSameDate(day.date, vm.selectedDay) { + Color("blueColor") + } + else { + Color(.white) + } + if isSameDate(day.date, vm.selectedDay) { + Color("blueColor") + } + } + } + ) + .overlay ( + Group { + if day.date.isToday && !isSameDate(day.date, vm.selectedDay) { + RoundedRectangle(cornerRadius: 15) + .stroke(Color("blueColor"), lineWidth: 2) + } + } + ) + .cornerRadius(15) + .onTapGesture { + vm.selectedDay = day.date + vm.updateSelectedDayIndex() + } + } + } + .background { + GeometryReader { + let minX = $0.frame(in: .global).minX + + Color.clear + .preference(key: OffsetKey.self, value: minX) + .onPreferenceChange(OffsetKey.self) { value in + if value.rounded() == 15 && createWeek { + paginateWeek() + + createWeek = false + } + } + } + } + } +} diff --git a/Schedule-ICTIS/MockData.swift b/Schedule-ICTIS/MockData.swift new file mode 100644 index 0000000..e191786 --- /dev/null +++ b/Schedule-ICTIS/MockData.swift @@ -0,0 +1,24 @@ +// +// MockData.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 06.12.2024. +// + +import Foundation + + +struct MockData { + static let daysOfWeek = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] + + // MARK: SheetCreateClassView + static let notifications = ["Нет", "За 10 минут", "За 30 миннут", "За 1 час"] + + static let onlineOrOffline = ["Оффлайн", "Онлайн"] + + static let themes = ["Светлая", "Темная", "Системная"] + + static let languages = ["Русский", "Английский", "Китайский", "Испанский"] + + static let groups = ["КТбо2-6", "КТбо1-9", "КТбо3-3", "ВУЦ", "КТао1-1", "КТсо2-2"] +} diff --git a/Schedule-ICTIS/Model/CoreDataClassModel.swift b/Schedule-ICTIS/Model/CoreDataClassModel.swift new file mode 100644 index 0000000..71f904f --- /dev/null +++ b/Schedule-ICTIS/Model/CoreDataClassModel.swift @@ -0,0 +1,91 @@ +// +// Class.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 18.12.2024. +// + +import Foundation +import CoreData + +final class CoreDataClassModel: NSManagedObject, Identifiable { + @NSManaged var auditory: String + @NSManaged var professor: String + @NSManaged var subject: String + @NSManaged var comment: String + @NSManaged var notification: String + @NSManaged var day: Date + @NSManaged var starttime: Date + @NSManaged var endtime: Date + @NSManaged var important: Bool + @NSManaged var online: String + + static var dateNow: Date = .now + + // Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию + // Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст + override func awakeFromInsert() { + super.awakeFromInsert() + + let moscowTimeZone = TimeZone(identifier: "Europe/Moscow")! + var calendar = Calendar.current + calendar.timeZone = moscowTimeZone + let startTime = Date() + let endTime = calendar.date(byAdding: .hour, value: 1, to: Date.init()) + + setPrimitiveValue("", forKey: "auditory") + setPrimitiveValue("", forKey: "professor") + setPrimitiveValue("", forKey: "subject") + setPrimitiveValue("", forKey: "comment") + setPrimitiveValue("Нет", forKey: "notification") + setPrimitiveValue(false, forKey: "important") + setPrimitiveValue("Оффлайн", forKey: "online") + setPrimitiveValue(startTime, forKey: "day") + setPrimitiveValue(startTime, forKey: "starttime") + setPrimitiveValue(endTime, forKey: "endtime") + } +} + +// Расширение для загрузки данных из памяти +extension CoreDataClassModel { + // Получаем все данные из памяти + private static var classesFetchRequest: NSFetchRequest { + NSFetchRequest(entityName: "CoreDataClassModel") + } + + // Получаем все данные и сортируем их по дню + // Этот метод будет использоваться на View(ScheduleView), где отображаются пары + static func all() -> NSFetchRequest { + let request: NSFetchRequest = classesFetchRequest + request.sortDescriptors = [ + NSSortDescriptor(keyPath: \CoreDataClassModel.day, ascending: true) + ] + return request + } +} + +extension CoreDataClassModel { + @discardableResult + static func makePreview(count: Int, in context: NSManagedObjectContext) -> [CoreDataClassModel] { + var classes = [CoreDataClassModel]() + for i in 0.. CoreDataClassModel { + return makePreview(count: 1, in: context)[0] + } + + static func empty(context: NSManagedObjectContext = ClassProvider.shared.viewContext) -> CoreDataClassModel { + return CoreDataClassModel(context: context) + } +} diff --git a/Schedule-ICTIS/Model/FavouriteGroupModel.swift b/Schedule-ICTIS/Model/FavouriteGroupModel.swift new file mode 100644 index 0000000..2178d1a --- /dev/null +++ b/Schedule-ICTIS/Model/FavouriteGroupModel.swift @@ -0,0 +1,39 @@ +// +// FavouriteGroupsModel.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 04.04.2025. +// + +import Foundation +import CoreData + +final class FavouriteGroupModel: NSManagedObject, Identifiable { + @NSManaged var name: String + + // Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию + // Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст + override func awakeFromInsert() { + super.awakeFromInsert() + + setPrimitiveValue("", forKey: "name") + } +} + +// Расширение для загрузки данных из памяти +extension FavouriteGroupModel { + // Получаем все данные из памяти + private static var favGroupsFetchRequest: NSFetchRequest { + NSFetchRequest(entityName: "FavouriteGroupModel") + } + + // Получаем все данные и сортируем их по дню + // Этот метод будет использоваться на View(ScheduleView), где отображаются пары + static func all() -> NSFetchRequest { + let request: NSFetchRequest = favGroupsFetchRequest + request.sortDescriptors = [ + NSSortDescriptor(keyPath: \FavouriteGroupModel.name, ascending: true) + ] + return request + } +} diff --git a/Schedule-ICTIS/Model/FavouriteVpkModel.swift b/Schedule-ICTIS/Model/FavouriteVpkModel.swift new file mode 100644 index 0000000..165e458 --- /dev/null +++ b/Schedule-ICTIS/Model/FavouriteVpkModel.swift @@ -0,0 +1,39 @@ +// +// FavouriteGroupsModel.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 04.04.2025. +// + +import Foundation +import CoreData + +final class FavouriteVpkModel: NSManagedObject, Identifiable { + @NSManaged var name: String + + // Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию + // Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст + override func awakeFromInsert() { + super.awakeFromInsert() + + setPrimitiveValue("", forKey: "name") + } +} + +// Расширение для загрузки данных из памяти +extension FavouriteVpkModel { + // Получаем все данные из памяти + private static var favVpkFetchRequest: NSFetchRequest { + NSFetchRequest(entityName: "FavouriteVpkModel") + } + + // Получаем все данные и сортируем их по дню + // Этот метод будет использоваться на View(ScheduleView), где отображаются пары + static func all() -> NSFetchRequest { + let request: NSFetchRequest = favVpkFetchRequest + request.sortDescriptors = [ + NSSortDescriptor(keyPath: \FavouriteVpkModel.name, ascending: true) + ] + return request + } +} diff --git a/Schedule-ICTIS/Model/GroupsModel.swift b/Schedule-ICTIS/Model/GroupsModel.swift new file mode 100644 index 0000000..09b32ef --- /dev/null +++ b/Schedule-ICTIS/Model/GroupsModel.swift @@ -0,0 +1,20 @@ +// +// SubjectsModel.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 19.02.2025. +// + +import Foundation + +// MARK: - Welcome +struct Welcome: Decodable { + let choices: [Subject] +} + +// MARK: - Choice +struct Subject: Decodable, Identifiable { + let name: String + let id: String + let group: String +} diff --git a/Schedule-ICTIS/Model/JsonClassModel.swift b/Schedule-ICTIS/Model/JsonClassModel.swift new file mode 100644 index 0000000..f0ee6ec --- /dev/null +++ b/Schedule-ICTIS/Model/JsonClassModel.swift @@ -0,0 +1,65 @@ +// +// JsonClassModel.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 27.03.2025. +// + +import Foundation +import CoreData + +final class JsonClassModel: NSManagedObject, Identifiable { + @NSManaged var name: String + @NSManaged var group: String + @NSManaged var time: String + @NSManaged var day: Int16 + @NSManaged var week: Int16 + + // Здесь мы выполняем дополнительную инициализацию, назначая значения по умолчанию + // Этот метод вызывается всякий раз, когда объект Core Data вставляется в контекст + override func awakeFromInsert() { + super.awakeFromInsert() + + setPrimitiveValue("", forKey: "name") + setPrimitiveValue("", forKey: "group") + setPrimitiveValue("", forKey: "time") + setPrimitiveValue(0, forKey: "day") + setPrimitiveValue(0, forKey: "week") + } +} + +// Расширение для загрузки данных из памяти +extension JsonClassModel { + // Получаем все данные из памяти + private static var subjectsFetchRequest: NSFetchRequest { + NSFetchRequest(entityName: "JsonClassModel") + } + + // Получаем все данные и сортируем их по дню + // Этот метод будет использоваться на View(ScheduleView), где отображаются пары + static func all() -> NSFetchRequest { + let request: NSFetchRequest = subjectsFetchRequest + request.sortDescriptors = [ + NSSortDescriptor(keyPath: \JsonClassModel.time, ascending: true) + ] + return request + } + + static func deleteClasses(withName name: String, in context: NSManagedObjectContext) throws { + let fetchRequest: NSFetchRequest = JsonClassModel.all() + fetchRequest.predicate = NSPredicate(format: "group == %@", name) + + let groupsToDelete = try context.fetch(fetchRequest) + + print("Пары для удаления: \(groupsToDelete)") + + for group in groupsToDelete { + do { + try ClassProvider.shared.delete(group, in: context) + } + catch { + print(error) + } + } + } +} diff --git a/Schedule-ICTIS/Model/ScheduleModel.swift b/Schedule-ICTIS/Model/ScheduleModel.swift new file mode 100644 index 0000000..2f08aa4 --- /dev/null +++ b/Schedule-ICTIS/Model/ScheduleModel.swift @@ -0,0 +1,30 @@ +// +// Model.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.11.2024. +// + +import Foundation + +// MARK: - Welcome +struct Schedule: Decodable { + let table: Table + let weeks: [Int] +} + +// MARK: - Table +struct Table: Decodable { + let type, name: String + let week: Int + let group: String + let table: [[String]] + let link: String +} + +struct ClassInfo: Identifiable { + let id = UUID() + let subject: String + let group: String + let time: String +} diff --git a/Schedule-ICTIS/Model/TabBarModel.swift b/Schedule-ICTIS/Model/TabBarModel.swift new file mode 100644 index 0000000..13b1bf5 --- /dev/null +++ b/Schedule-ICTIS/Model/TabBarModel.swift @@ -0,0 +1,14 @@ +// +// Tab.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.11.2024. +// + +import SwiftUI + +enum TabBarModel: String, CaseIterable { + case tasks = "books.vertical" + case schedule = "house" + case settings = "gear" +} diff --git a/Schedule-ICTIS/Model/Utilities/Extensions/Date+Extensions.swift b/Schedule-ICTIS/Model/Utilities/Extensions/Date+Extensions.swift new file mode 100644 index 0000000..824689a --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Extensions/Date+Extensions.swift @@ -0,0 +1,125 @@ +// +// Date+Extensions.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 14.11.2024. +// + +import SwiftUI + + +extension Date { + func format(_ format: String, locale: Locale = Locale(identifier: "ru_RU")) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.locale = locale + let formattedString = formatter.string(from: self) + + if format == "EEEE" { + return formattedString.prefix(1).capitalized + formattedString.dropFirst() + } + + return formattedString + } + + var isToday: Bool { + return Calendar.current.isDateInToday(self) + } + + func fetchWeek(_ date: Date = .init()) -> [WeekDay] { + let calendar = Calendar.current + let startOfDate = calendar.startOfDay(for: date) + + var week: [WeekDay] = [] + // Создаем дату начала и конца недели + let weekForDate = calendar.dateInterval(of: .weekOfMonth, for: startOfDate) + //print("Start: \(weekForDate?.start)") + //print("End: \(weekForDate?.end)") + + guard let startOfWeek = weekForDate?.start else { + return [] + } + + // Создаем массив дней для недели + (0..<7).forEach { index in + if let weekDay = calendar.date(byAdding: .day, value: index, to: startOfWeek) { + week.append(WeekDay(date: weekDay)) + } + } + + return week + } + + func fetchMonth(_ date: Date = .init()) -> [MonthWeek] { + let calendar = Calendar.current + let startOfDate = calendar.startOfDay(for: date) + + let weekForDate = calendar.dateInterval(of: .weekOfMonth, for: startOfDate) + + guard let startOfWeek = weekForDate?.start else { + return [] + } + + var month: [MonthWeek] = [] + + for weekIndex in 0..<5 { + var week: [WeekDay] = [] + for dayIndex in 0..<7 { + if let weekDay = calendar.date(byAdding: .day, value: (weekIndex * 7 + dayIndex), to: startOfWeek) { + week.append(WeekDay(date: weekDay)) + } + } + month.append(MonthWeek(week: week)) + } + + return month + } + + func createNextMonth() -> [MonthWeek] { + let calendar = Calendar.current + let startOfLastDate = calendar.startOfDay(for: self) + guard let nextDate = calendar.date(byAdding: .day, value: 1, to: startOfLastDate) else { + return [] + } + return fetchMonth(nextDate) + } + + func createPreviousMonth() -> [MonthWeek] { + let calendar = Calendar.current + let startOfFirstDate = calendar.startOfDay(for: self) + guard let previousDate = calendar.date(byAdding: .weekOfMonth, value: -5, to: startOfFirstDate) else { + return [] + } + print("Start of first date \(startOfFirstDate)") + print("Previous date \(previousDate)") + return fetchMonth(previousDate) + } + + func createNextWeek() -> [WeekDay] { + let calendar = Calendar.current + let startOfLastDate = calendar.startOfDay(for: self) + guard let nextDate = calendar.date(byAdding: .day, value: 1, to: startOfLastDate) else { + return [] + } + return fetchWeek(nextDate) + } + + func createPrevioustWeek() -> [WeekDay] { + let calendar = Calendar.current + let startOfFirstDate = calendar.startOfDay(for: self) + guard let previousDate = calendar.date(byAdding: .day, value: -1, to: startOfFirstDate) else { + return [] + } + return fetchWeek(previousDate) + } + + struct WeekDay: Identifiable { + var id: UUID = .init() + var date: Date + } + + struct MonthWeek: Identifiable { + var id: UUID = .init() + var week: [WeekDay] + } +} diff --git a/Schedule-ICTIS/Model/Utilities/Extensions/OffsetKey.swift b/Schedule-ICTIS/Model/Utilities/Extensions/OffsetKey.swift new file mode 100644 index 0000000..79e697e --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Extensions/OffsetKey.swift @@ -0,0 +1,15 @@ +// +// OffsetKey.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 18.11.2024. +// + +import SwiftUI + +struct OffsetKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} diff --git a/Schedule-ICTIS/Model/Utilities/Extensions/ScheduleView+Extensions.swift b/Schedule-ICTIS/Model/Utilities/Extensions/ScheduleView+Extensions.swift new file mode 100644 index 0000000..94e56f4 --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Extensions/ScheduleView+Extensions.swift @@ -0,0 +1,102 @@ +// +// ScheduleView+Extensions.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 04.04.2025. +// + +import SwiftUI +import CoreData + +extension ScheduleView { + // Удаляем пары добавленные пользователем, если сегодня понедельник + func deleteClassesFormCoreDataIfMonday() { + let today = Date() + let calendar = Calendar.current + let weekday = calendar.component(.weekday, from: today) + + if weekday == 6 { + for _class in classes { + if _class.day < today { + do { + try provider.delete(_class, in: provider.viewContext) + } catch { + print("❌ - Ошибка при удалении, добавленных пользователем пар: \(error)") + } + } + } + } + } + + func saveGroupsToMemory() { + var indexOfTheDay: Int16 = 0 + let context = provider.newContext // Создаем новый контекст + + context.perform { + for dayIndex in 0.. = JsonClassModel.fetchRequest() + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + try context.execute(deleteRequest) + try context.save() + print("✅ Все объекты JsonClassModel успешно удалены") + } + + func checkSavingOncePerDay() { + let today = Date() + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: today) // Начало текущего дня + + // Получаем дату последнего выполнения из UserDefaults + let lastCheckDate = UserDefaults.standard.object(forKey: "LastSaving") as? Date ?? .distantPast + let lastCheckStart = calendar.startOfDay(for: lastCheckDate) + + print("Дата последнего сохранения расписания в CoreData: \(lastCheckDate)") + + // Проверяем, был ли уже выполнен код сегодня + if lastCheckStart < todayStart && networkMonitor.isConnected { + print("✅ Интернет есть, сохранение пар в CoreData") + vm.fetchWeekSchedule() + do { + try deleteAllJsonClassModelsSync() + } catch { + print("Ошибка при удалении: \(error)") + return + } + saveGroupsToMemory() + + // Сохраняем текущую дату как дату последнего выполнения + UserDefaults.standard.set(today, forKey: "LastSaving") + } + } +} diff --git a/Schedule-ICTIS/Model/Utilities/Extensions/View+Extensions.swift b/Schedule-ICTIS/Model/Utilities/Extensions/View+Extensions.swift new file mode 100644 index 0000000..aec0a5c --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Extensions/View+Extensions.swift @@ -0,0 +1,350 @@ +// +// View+Extensions.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 15.11.2024. +// + +import SwiftUI +import CoreData + +extension View { + + func transformStringToFormat(_ input: String) -> String { + var result = input + + // Условие 1: начинается с "кт" + if result.lowercased().hasPrefix("кт") { + result = result.lowercased() + let firstTwo = String(result.prefix(2)).uppercased() + let rest = String(result.dropFirst(2)) + result = firstTwo + rest + return result + } + + // Условие 2: содержит "впк" + if result.lowercased().contains("впк") { + result = result.lowercased() + result = result.replacingOccurrences(of: "впк", with: "ВПК") + return result + } + + // Условие 3: содержит "мвпк" + if result.lowercased().contains("мвпк") { + result = result.lowercased() + result = result.replacingOccurrences(of: "впк", with: "ВПК") + return result + } + + return result + } + + func isSameDate(_ date1: Date, _ date2: Date) -> Bool { + return Calendar.current.isDate(date1, inSameDayAs: date2) + } + + func isDateInCurrentMonth(_ date: Date) -> Bool { + let calendar = Calendar.current + let currentDate = Date() + + let currentMonth = calendar.component(.month, from: currentDate) + let currentYear = calendar.component(.year, from: currentDate) + + let dateMonth = calendar.component(.month, from: date) + let dateYear = calendar.component(.year, from: date) + + return currentMonth == dateMonth && currentYear == dateYear + } + + func isSameWeek(_ date1: Date, _ date2: Date) -> Bool { + return Calendar.current.compare(date1, to: date2, toGranularity: .weekOfYear) == .orderedSame + } + + func weeksBetween(startDate: Date, endDate: Date) -> Int { + let calendar = Calendar.current + let startOfFirstDate = calendar.startOfDay(for: startDate) + let startOfEndDate = calendar.startOfDay(for: endDate) + + let weekForDate1 = calendar.dateInterval(of: .weekOfMonth, for: startOfFirstDate) + let weekForDate2 = calendar.dateInterval(of: .weekOfMonth, for: startOfEndDate) + + guard let startOfWeek1 = weekForDate1?.start else { + return 0 + } + + guard let startOfWeek2 = weekForDate2?.start else { + return 0 + } + + let components = calendar.dateComponents([.day], from: startOfWeek1, to: startOfWeek2) + let daysDifference = components.day ?? 0 + return Int(ceil(Double(abs(daysDifference)) / 7.0)) + } + + func convertTimeString(_ input: String) -> [String] { + let parts = input.split(separator: "-") + if let firstPart = parts.first, let lastPart = parts.last { + return [String(firstPart), String(lastPart)] + } else { + return [] + } + } + + func getColorForClass(_ str: String) -> Color { + if (str.contains("LMS")) { + return Color("blueForOnline") + } + else if (str.contains("ВПК")) { + return Color("turquoise") + } + else { + return Color("greenForOffline") + } + } + + func hoursMinutesAreEqual(date1: Date, isEqualTo date2: Date) -> Bool { + let calendar = Calendar.current + + let components1 = calendar.dateComponents([.day, .hour, .minute], from: date1) + let components2 = calendar.dateComponents([.day, .hour, .minute], from: date2) + + return components1.day == components2.day && components1.hour == components2.hour && components1.minute == components2.minute + } + + // MARK: ScheduleView + func daysAreEqual(_ date1: Date, _ date2: Date) -> Bool { + let calendar = Calendar.current + + let components1 = calendar.dateComponents([.year, .month, .day], from: date1) + let components2 = calendar.dateComponents([.year, .month, .day], from: date2) + + return components1.year == components2.year && + components1.month == components2.month && + components1.day == components2.day + } + + func onlineOrNot(_ str: String) -> Color { + if (str == "Онлайн") { + return Color("blueForOnline") + } + else { + return Color("greenForOffline") + } + } + + func getSubjectName(_ subject: String, _ professor: String, _ auditory: String) -> String { + return "\(subject) \(professor) \(auditory)" + } + + func getTimeString(_ date: Date) -> String { + let calendar = Calendar.current + let components = calendar.dateComponents([.hour, .minute], from: date) + + guard let hour = components.hour, let minute = components.minute else { + return "Invalid time" + } + + return String(format: "%02d:%02d", hour, minute) + } + + var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + } + + var timeFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm" + return formatter + } + + func checkStartTimeLessThenEndTime(_ startTime: Date, _ endTime: Date) -> Bool { + let calendar = Calendar.current + + let firstComponents = calendar.dateComponents([.hour, .minute], from: startTime) + let secondComponents = calendar.dateComponents([.hour, .minute], from: endTime) + + guard let startHours = firstComponents.hour, let startMinutes = firstComponents.minute else { + return false + } + guard let endHours = secondComponents.hour, let endMinutes = secondComponents.minute else { + return false + } + + print("\(startHours) - \(endHours)") + print("\(startMinutes) - \(endMinutes)") + if Int(startHours) > Int(endHours) { + return false + } + else if startHours == endHours { + if startMinutes < endMinutes { + return true + } + else { + return false + } + } + else { + return true + } + } +} + +extension WeekTabView { + func updateWeekScreenViewForNewGroup() { + vm.updateSelectedDayIndex() + if weekSlider.isEmpty { + let currentWeek = Date().fetchWeek(vm.selectedDay) + + if let firstDate = currentWeek.first?.date { + weekSlider.append(firstDate.createPrevioustWeek()) + } + + weekSlider.append(currentWeek) + + if let lastDate = currentWeek.last?.date { + weekSlider.append(lastDate.createNextWeek()) + } + } + } +} + +extension WeekViewForWeek { + func paginateWeek() { + let calendar = Calendar.current + let groupsKeys = Array (vm.nameToHtml.keys) + if weekSlider.indices.contains(currentWeekIndex) { + if let firstDate = weekSlider[currentWeekIndex].first?.date, + currentWeekIndex == 0 { + vm.week -= 1 + if !groupsKeys.isEmpty { + vm.fetchWeekSchedule(isOtherWeek: true) + } + if UserDefaults.standard.string(forKey: "vpk") != nil { + //vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk")) + } + weekSlider.insert(firstDate.createPrevioustWeek(), at: 0) + weekSlider.removeLast() + currentWeekIndex = 1 + vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -1, to: vm.selectedDay) ?? Date.init() + vm.updateSelectedDayIndex() + } + + if let lastDate = weekSlider[currentWeekIndex].last?.date, + currentWeekIndex == (weekSlider.count - 1) { + vm.week += 1 + if !groupsKeys.isEmpty { + vm.fetchWeekSchedule(isOtherWeek: true) + } + weekSlider.append(lastDate.createNextWeek()) + weekSlider.removeFirst() + currentWeekIndex = weekSlider.count - 2 + vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 1, to: vm.selectedDay) ?? Date.init() + vm.updateSelectedDayIndex() + } + } + } +} + +extension WeekViewForMonth { + func getForegroundColor(day: Date.WeekDay) -> Color { + if isDateInCurrentMonth(day.date) { + return isSameDate(day.date, vm.selectedDay) ? .white : .black + } else { + return isSameDate(day.date, vm.selectedDay) ? .white : Color("greyForDaysInMonthTabView") + } + } + + func getBackgroundColor(day: Date.WeekDay) -> Color { + return isSameDate(day.date, vm.selectedDay) ? Color("blueColor") : Color("background") + } + + func overlay(day: Date.WeekDay) -> some View { + Group { + if day.date.isToday && !isSameDate(day.date, vm.selectedDay) { + RoundedRectangle(cornerRadius: 100) + .stroke(Color("blueColor"), lineWidth: 2) + } + } + } + + func handleTap(day: Date.WeekDay) { + if isSameWeek(day.date, vm.selectedDay) { + print("На одной неделе") + } + else { + let groupsKeys = Array(vm.nameToHtml.keys) + var difBetweenWeeks = weeksBetween(startDate: vm.selectedDay, endDate: day.date) + if day.date < vm.selectedDay { + difBetweenWeeks = difBetweenWeeks * -1 + } + print(difBetweenWeeks) + vm.week += difBetweenWeeks + if !groupsKeys.isEmpty { + vm.fetchWeekSchedule(isOtherWeek: true) + } + if UserDefaults.standard.string(forKey: "vpk") != nil { + //vm.fetchWeekVPK(isOtherWeek: true, vpk: UserDefaults.standard.string(forKey: "vpk")) + } + } + vm.selectedDay = day.date + vm.updateSelectedDayIndex() + } +} + +extension MonthTabView { + func updateMonthScreenViewForNewGroup() { + vm.updateSelectedDayIndex() + if monthSlider.isEmpty { + let currentMonth = Date().fetchMonth(vm.selectedDay) + + if let firstDate = currentMonth.first?.week[0].date { + let temp = firstDate.createPreviousMonth() + print("First date - \(firstDate)") + print(temp) + monthSlider.append(temp) + } + + monthSlider.append(currentMonth) + + if let lastDate = currentMonth.last?.week[6].date { + let temp = lastDate.createNextMonth() + monthSlider.append(temp) + } + } + } + + func paginateMonth(_ indexOfWeek: Int = 0) { + let calendar = Calendar.current + let groupsKeys = Array (vm.nameToHtml.keys) + if monthSlider.indices.contains(currentMonthIndex) { + if let firstDate = monthSlider[currentMonthIndex].first?.week[0].date, + currentMonthIndex == 0 { + monthSlider.insert(firstDate.createPreviousMonth(), at: 0) + monthSlider.removeLast() + currentMonthIndex = 1 + vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: -5, to: vm.selectedDay) ?? Date.init() + vm.updateSelectedDayIndex() + vm.week -= 5 + if !groupsKeys.isEmpty { + vm.fetchWeekSchedule(isOtherWeek: true) + } + } + + if let lastDate = monthSlider[currentMonthIndex].last?.week[6].date, + currentMonthIndex == (monthSlider.count - 1) { + monthSlider.append(lastDate.createNextMonth()) + monthSlider.removeFirst() + currentMonthIndex = monthSlider.count - 2 + vm.selectedDay = calendar.date(byAdding: .weekOfYear, value: 5, to: vm.selectedDay) ?? Date.init() + vm.updateSelectedDayIndex() + vm.week += 5 + if !groupsKeys.isEmpty { + vm.fetchWeekSchedule(isOtherWeek: true) + } + } + } + } +} diff --git a/Schedule-ICTIS/Model/Utilities/Network/NetworkError.swift b/Schedule-ICTIS/Model/Utilities/Network/NetworkError.swift new file mode 100644 index 0000000..bfaba72 --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Network/NetworkError.swift @@ -0,0 +1,46 @@ +// +// NetworkError.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 18.11.2024. +// + +import Foundation + +enum NetworkError: String, Error, LocalizedError { + case invalidUrl + case invalidResponse + case invalidData + case noError + case timeout + + var errorDescription: String? { + switch self { + case .invalidUrl: + "InvalidUrl" + case .invalidResponse: + "InvalidResponse" + case .invalidData: + "Проверьте номер группы" + case .timeout: + "Ошибка сети" + case .noError: + "Нет ошибки" + } + } + + var failureReason: String { + switch self { + case .invalidUrl: + "Похоже не удалось составить ссылку для api" + case .invalidResponse: + "Для этой недели расписания еще нет" + case .invalidData: + "Похоже такой группы не существует" + case .timeout: + "Проверьте соединение с интернетом" + case .noError: + "Ошибки нет" + } + } +} diff --git a/Schedule-ICTIS/Model/Utilities/Network/NetworkManager.swift b/Schedule-ICTIS/Model/Utilities/Network/NetworkManager.swift new file mode 100644 index 0000000..18cb4bb --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Network/NetworkManager.swift @@ -0,0 +1,81 @@ +// +// NetworkManager.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 18.11.2024. +// + +import Foundation + +final class NetworkManager { + //"https://webictis.sfedu.ru/schedule-api/?group=51.html&week=15" + //MARK: Properties + static let shared = NetworkManager() + private let decoder = JSONDecoder() + private let urlForGroup = "https://shedule.rdcenter.ru/schedule-api/?query=" + private let urlForWeek = "https://shedule.rdcenter.ru/schedule-api/?group=" + private let customSession: URLSession // Кастомная сессия для ограничения времени ответа от сервера + + //MARK: Initializer + private init() { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = 4 // Таймаут запроса + configuration.timeoutIntervalForResource = 6 // Таймаут ресурса + self.customSession = URLSession(configuration: configuration) + decoder.dateDecodingStrategy = .iso8601 + } + + //MARK: Methods + func makeUrlForGroup(_ group: String) -> String { + return urlForGroup + group + } + + func makeUrlForWeek(_ numOfWeek: Int, _ htmlNameOfGroup: String) -> String { + return urlForWeek + htmlNameOfGroup + "&week=" + String(numOfWeek) + } + + func getSchedule(_ group: String) async throws -> Schedule { + let newUrlForGroup = makeUrlForGroup(group) + print(newUrlForGroup) + guard let url = URL(string: newUrlForGroup) else { throw NetworkError.invalidUrl } + let (data, response) = try await customSession.data(from: url) + guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } + + do { + return try decoder.decode(Schedule.self, from: data) + } + catch { + throw NetworkError.invalidData + } + } + + func getScheduleForOtherWeek(_ numOfWeek: Int, _ htmlNameOfGroup: String) async throws -> Schedule { + let newUrlForWeek = makeUrlForWeek(numOfWeek, htmlNameOfGroup) + print(newUrlForWeek) + guard let url = URL(string: newUrlForWeek) else { throw NetworkError.invalidUrl } + let (data, response) = try await customSession.data(from: url) + guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } + + do { + return try decoder.decode(Schedule.self, from: data) + } + catch { + throw NetworkError.invalidData + } + } + + func getGroups(group: String) async throws -> Welcome { + let newUrlForGroups = makeUrlForGroup(group) + print(newUrlForGroups) + guard let url = URL(string: newUrlForGroups) else { throw NetworkError.invalidUrl } + let (data, response) = try await customSession.data(from: url) + guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw NetworkError.invalidResponse } + + do { + return try decoder.decode(Welcome.self, from: data) + } + catch { + throw NetworkError.invalidData + } + } +} diff --git a/Schedule-ICTIS/Model/Utilities/Network/NetworkMonitor.swift b/Schedule-ICTIS/Model/Utilities/Network/NetworkMonitor.swift new file mode 100644 index 0000000..2ca2ba3 --- /dev/null +++ b/Schedule-ICTIS/Model/Utilities/Network/NetworkMonitor.swift @@ -0,0 +1,37 @@ +// +// NetworkMonitor.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 27.03.2025. +// + +import Network +import SwiftUI + +class NetworkMonitor: ObservableObject { + @Published var isConnected: Bool = false + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "NetworkMonitorQueue") + + init() { + startMonitoring() + } + + func startMonitoring() { + monitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + self?.isConnected = path.status == .satisfied + print(self?.isConnected == true ? "✅ Интернет подключен!" : "❌ Нет подключения к интернету.") + } + } + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } + + deinit { + stopMonitoring() + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/AccentColor.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/AppIcon.appiconset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..080ba9c --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "ICTIS_logo.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/AppIcon.appiconset/ICTIS_logo.png b/Schedule-ICTIS/Preview Content/Assets.xcassets/AppIcon.appiconset/ICTIS_logo.png new file mode 100644 index 0000000..44b05e5 Binary files /dev/null and b/Schedule-ICTIS/Preview Content/Assets.xcassets/AppIcon.appiconset/ICTIS_logo.png differ diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/Contents.json new file mode 100644 index 0000000..985ff81 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "arrowRight.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/arrowRight.svg b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/arrowRight.svg new file mode 100644 index 0000000..394cec0 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowRight.imageset/arrowRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/Contents.json new file mode 100644 index 0000000..cfd47c3 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "arrowdown.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/arrowdown.svg b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/arrowdown.svg new file mode 100644 index 0000000..702229d --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowdown.imageset/arrowdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/Contents.json new file mode 100644 index 0000000..c50124f --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "arrowup.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/arrowup.svg b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/arrowup.svg new file mode 100644 index 0000000..4c077ab --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/arrowup.imageset/arrowup.svg @@ -0,0 +1,3 @@ + + + diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/Contents.json new file mode 100644 index 0000000..0d905b9 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "auditoryImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/auditoryImage.png b/Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/auditoryImage.png new file mode 100644 index 0000000..6952f6a Binary files /dev/null and b/Schedule-ICTIS/Preview Content/Assets.xcassets/auditoryImage.imageset/auditoryImage.png differ diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/background.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/background.colorset/Contents.json new file mode 100644 index 0000000..5cdfbad --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF6", + "green" : "0xF0", + "red" : "0xF1" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/blueColor.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/blueColor.colorset/Contents.json new file mode 100644 index 0000000..5b28f5e --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/blueColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA4", + "green" : "0x60", + "red" : "0x28" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA4", + "green" : "0x60", + "red" : "0x28" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/blueForOnline.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/blueForOnline.colorset/Contents.json new file mode 100644 index 0000000..213c5b9 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/blueForOnline.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x7A", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/Contents.json new file mode 100644 index 0000000..83777ce --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "bookImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/bookImage.png b/Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/bookImage.png new file mode 100644 index 0000000..dd06b99 Binary files /dev/null and b/Schedule-ICTIS/Preview Content/Assets.xcassets/bookImage.imageset/bookImage.png differ diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray1.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray1.colorset/Contents.json new file mode 100644 index 0000000..ea75ab6 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray1.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD9", + "green" : "0xD9", + "red" : "0xD9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD9", + "green" : "0xD9", + "red" : "0xD9" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray2.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray2.colorset/Contents.json new file mode 100644 index 0000000..80b1768 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray2.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7D", + "green" : "0x7D", + "red" : "0x7D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7D", + "green" : "0x7D", + "red" : "0x7D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray3.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray3.colorset/Contents.json new file mode 100644 index 0000000..80b1768 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/customGray3.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7D", + "green" : "0x7D", + "red" : "0x7D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7D", + "green" : "0x7D", + "red" : "0x7D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForDate.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForDate.colorset/Contents.json new file mode 100644 index 0000000..a039c0b --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForDate.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x7A", + "red" : "0x7A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x7A", + "red" : "0x7A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForFields.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForFields.colorset/Contents.json new file mode 100644 index 0000000..58b6ec3 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForFields.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x84", + "green" : "0x80", + "red" : "0x80" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x84", + "green" : "0x80", + "red" : "0x80" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForNameGroup.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForNameGroup.colorset/Contents.json new file mode 100644 index 0000000..cf4b210 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/grayForNameGroup.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x89", + "green" : "0x89", + "red" : "0x89" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x89", + "green" : "0x89", + "red" : "0x89" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/greenForOffline.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/greenForOffline.colorset/Contents.json new file mode 100644 index 0000000..7aba1dd --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/greenForOffline.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x0A", + "green" : "0x97", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/greyForDaysInMonthTabView.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/greyForDaysInMonthTabView.colorset/Contents.json new file mode 100644 index 0000000..bfc2a11 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/greyForDaysInMonthTabView.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/Contents.json new file mode 100644 index 0000000..351e621 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "professorHatImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/professorHatImage.png b/Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/professorHatImage.png new file mode 100644 index 0000000..cbee224 Binary files /dev/null and b/Schedule-ICTIS/Preview Content/Assets.xcassets/professorHatImage.imageset/professorHatImage.png differ diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/redForImportant.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/redForImportant.colorset/Contents.json new file mode 100644 index 0000000..1421f31 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/redForImportant.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/turquoise.colorset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/turquoise.colorset/Contents.json new file mode 100644 index 0000000..ba13876 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/turquoise.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDE", + "green" : "0xE4", + "red" : "0x22" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDE", + "green" : "0xE4", + "red" : "0x22" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/Contents.json b/Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/Contents.json new file mode 100644 index 0000000..0e744ce --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "upDownArrows.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/upDownArrows.svg b/Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/upDownArrows.svg new file mode 100644 index 0000000..5c4964b --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Assets.xcassets/upDownArrows.imageset/upDownArrows.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Schedule-ICTIS/Preview Content/Preview Assets.xcassets/Contents.json b/Schedule-ICTIS/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Schedule-ICTIS/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Schedule-ICTIS/Provider/ClassProvider.swift b/Schedule-ICTIS/Provider/ClassProvider.swift new file mode 100644 index 0000000..eabca35 --- /dev/null +++ b/Schedule-ICTIS/Provider/ClassProvider.swift @@ -0,0 +1,129 @@ +// +// ClassProvider.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 18.12.2024. +// + +import Foundation +import CoreData +import SwiftUI + +// Это класс служит посредником между View и моделью данных +// Он позволяет открыть наш файл данных чтобы записывать и извлекать значения +// Объект этого класса должен быть единственным за весь жизненный цикл приложения, чтобы не было рассинхронизации +// Для этого мы делаем его синглтоном +final class ClassProvider { + static let shared = ClassProvider() + + // Это свойство для хранения открытого файла модели данных + private let persistentContainer: NSPersistentContainer + + var viewContext: NSManagedObjectContext { + persistentContainer.viewContext + } + + var newContext: NSManagedObjectContext { + //persistentContainer.newBackgroundContext() + //Можно использовать объявление newContext с помощью строки, которая написана выше, но вариант ниже потокобезопаснее + let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + context.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator + return context + } + + private init() { + // Открытие файла + persistentContainer = NSPersistentContainer(name: "ClassDataModel") + if EnvironmentValues.isPreview { + persistentContainer.persistentStoreDescriptions.first?.url = .init(filePath: "/dev/null") + } + + // Выставляем флаг для автоматического слияния данных из фонового контекста в основной + persistentContainer.viewContext.automaticallyMergesChangesFromParent = true + + // Выполняем открытие файла с данными + persistentContainer.loadPersistentStores {_, error in + if let error { + fatalError("Unable to load store. Error: \(error)") + } + } + } + + func exists(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) -> CoreDataClassModel? { + try? context.existingObject(with: lesson.objectID) as? CoreDataClassModel + } + + func delete(_ lesson: CoreDataClassModel, in context: NSManagedObjectContext) throws { + if let existingClass = exists(lesson, in: context) { + context.delete(existingClass) + Task(priority: .background) { + try await context.perform { + try context.save() + } + } + } + } + + func persist(in context: NSManagedObjectContext) throws { + if context.hasChanges { + try context.save() + } + } +} + +extension EnvironmentValues { + static var isPreview: Bool { + return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + } +} + +extension ClassProvider { + func exists(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) -> JsonClassModel? { + try? context.existingObject(with: jsonClass.objectID) as? JsonClassModel + } + + func delete(_ jsonClass: JsonClassModel, in context: NSManagedObjectContext) throws { + if let existingJsonClass = exists(jsonClass, in: context) { + context.delete(existingJsonClass) + Task(priority: .background) { + try await context.perform { + try context.save() + } + } + } + } + + func exists(_ favGroup: FavouriteGroupModel, in context: NSManagedObjectContext) -> FavouriteGroupModel? { + try? context.existingObject(with: favGroup.objectID) as? FavouriteGroupModel + } + + func delete(_ favGroup: FavouriteGroupModel, in context: NSManagedObjectContext) throws { + if let existingFavGroup = exists(favGroup, in: context) { + context.delete(existingFavGroup) + Task(priority: .background) { + try await context.perform { + try context.save() + } + } + } + } + + func exists(_ favVpk: FavouriteVpkModel, in context: NSManagedObjectContext) -> FavouriteVpkModel? { + try? context.existingObject(with: favVpk.objectID) as? FavouriteVpkModel + } + + func delete(_ favVpk: FavouriteVpkModel, in context: NSManagedObjectContext) throws { + if let existingFavVpk = exists(favVpk, in: context) { + context.delete(existingFavVpk) + Task(priority: .background) { + try await context.perform { + try context.save() + } + } + } + } +} + +extension ClassProvider { + +} diff --git a/Schedule-ICTIS/Schedule_ICTISApp.swift b/Schedule-ICTIS/Schedule_ICTISApp.swift new file mode 100644 index 0000000..994e4bf --- /dev/null +++ b/Schedule-ICTIS/Schedule_ICTISApp.swift @@ -0,0 +1,47 @@ +// +// Schedule_ICTISApp.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.11.2024. +// + +import SwiftUI + +@main +struct Schedule_ICTISApp: App { + @StateObject private var networkMonitor = NetworkMonitor() + @StateObject var vm = ScheduleViewModel() + var body: some Scene { + WindowGroup { + ContentView(vm: vm, networkMonitor: networkMonitor) + .environment(\.managedObjectContext, ClassProvider.shared.viewContext) + .onAppear { + fillDictForVm() + } + } + } +} + +extension Schedule_ICTISApp { + func fillDictForVm() { + let context = ClassProvider.shared.viewContext + + do { + // Используем ваш метод all() для групп + let groupRequest = FavouriteGroupModel.all() + let groups = try context.fetch(groupRequest) + for group in groups { + vm.nameToHtml[group.name] = "" + } + + // Аналогично для ВПК (предполагая, что у вас есть аналогичный метод) + let vpkRequest = FavouriteVpkModel.all() + let vpks = try context.fetch(vpkRequest) + for vpk in vpks { + vm.nameToHtml[vpk.name] = "" + } + } catch { + print("Ошибка при загрузке данных: \(error.localizedDescription)") + } + } +} diff --git a/Schedule-ICTIS/Settings/FavGroupsView.swift b/Schedule-ICTIS/Settings/FavGroupsView.swift new file mode 100644 index 0000000..6d2dca6 --- /dev/null +++ b/Schedule-ICTIS/Settings/FavGroupsView.swift @@ -0,0 +1,71 @@ +// +// FavGroupsView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 05.03.2025. +// + +import SwiftUI +import CoreData + +struct FavGroupsView: View { + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var networkMonitor: NetworkMonitor + @FetchRequest(fetchRequest: FavouriteGroupModel.all()) private var favGroups // Список групп сохраненных в CoreData + var provider = ClassProvider.shared + var body: some View { + VStack (spacing: 0) { + List { + ForEach(favGroups, id: \.self) {favGroup in + HStack { + Text(favGroup.name) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + Spacer() + } + .background(Color.white) + .cornerRadius(10) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + vm.removeFromSchedule(group: favGroup.name) + do { + try JsonClassModel.deleteClasses(withName: favGroup.name, in: provider.viewContext) + try provider.delete(favGroup, in: provider.viewContext) + } catch { + print(error) + } + } label: { + Label("Удалить", systemImage: "trash") + } + } + } + } + + Spacer() + + HStack { + Spacer() + if favGroups.count < 10 { + NavigationLink(destination: SelectingGroupView(vm: vm, networkMonitor: networkMonitor)) { + HStack { + Image(systemName: "plus") + .foregroundColor(.white) + .font(.system(size: 22)) + .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) + } + .background(Color("blueColor")) + .cornerRadius(10) + .padding(.trailing, 20) + } + } + } + .padding(.bottom, 90) + } + .background(Color("background")) + } +} + +#Preview { + @Previewable @StateObject var vm = ScheduleViewModel() + @Previewable @StateObject var vm2 = NetworkMonitor() + FavGroupsView(vm: vm, networkMonitor: vm2) +} diff --git a/Schedule-ICTIS/Settings/FavVPKView.swift b/Schedule-ICTIS/Settings/FavVPKView.swift new file mode 100644 index 0000000..4bd2840 --- /dev/null +++ b/Schedule-ICTIS/Settings/FavVPKView.swift @@ -0,0 +1,70 @@ +// +// FavGroupsView.swift +// Schedule ICTIS +// +// Created by Egor Mironov on 05.03.2025. +// + +import SwiftUI + +struct FavVPKView: View { + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var networkMonitor: NetworkMonitor + @FetchRequest(fetchRequest: FavouriteVpkModel.all()) private var favVpk // Список ВПК сохраненных в CoreData + var provider = ClassProvider.shared + var body: some View { + VStack (spacing: 0) { + List { + ForEach(favVpk, id: \.self) {favVpk in + HStack { + Text(favVpk.name) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + Spacer() + } + .background(Color.white) + .cornerRadius(10) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + vm.removeFromSchedule(group: favVpk.name) + do { + try JsonClassModel.deleteClasses(withName: favVpk.name, in: provider.viewContext) + try provider.delete(favVpk, in: provider.viewContext) + } catch { + print(error) + } + } label: { + Label("Удалить", systemImage: "trash") + } + } + } + } + + Spacer() + + HStack { + Spacer() + if favVpk.count < 5 { + NavigationLink(destination: SelectingVPKView(vm: vm, networkMonitor: networkMonitor)) { + HStack { + Image(systemName: "plus") + .foregroundColor(.white) + .font(.system(size: 22)) + .padding(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) + } + .background(Color("blueColor")) + .cornerRadius(10) + .padding(.trailing, 20) + } + } + } + .padding(.bottom, 90) + } + .background(Color("background")) + } +} + +#Preview { + @Previewable @StateObject var vm = ScheduleViewModel() + @Previewable @StateObject var vm2 = NetworkMonitor() + FavVPKView(vm: vm, networkMonitor: vm2) +} diff --git a/Schedule-ICTIS/Settings/GeneralGroupSettings.swift b/Schedule-ICTIS/Settings/GeneralGroupSettings.swift new file mode 100644 index 0000000..f1947fd --- /dev/null +++ b/Schedule-ICTIS/Settings/GeneralGroupSettings.swift @@ -0,0 +1,91 @@ +// +// GeneralGroupSettings.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 25.02.2025. +// + +import SwiftUI + +struct GeneralGroupSettings: View { + @Binding var selectedTheme: String + @Binding var selectedLanguage: String + var body: some View { + VStack { + HStack { + Text("Тема") + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(.black) + Spacer() + HStack { + Text(selectedTheme) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(Color("customGray3")) + Image("upDownArrows") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + } + .padding(.horizontal) + } + .padding(.horizontal) + .padding(.top, 17) + .padding(.bottom, 7) + .overlay { + HStack { + Spacer() + Picker("", selection: $selectedTheme, content: { + ForEach(MockData.themes, id: \.self) { + Text($0) + } + }) + .padding(.trailing, 35) + .blendMode(.destinationOver) + } + .frame(width: UIScreen.main.bounds.width) + } + Rectangle() + .foregroundColor(Color("customGray1")) + .frame(height: 1) + .padding(.horizontal) + HStack { + Text("Язык") + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(.black) + Spacer() + HStack { + Text(selectedLanguage) + .font(.custom("Montserrat-Medium", fixedSize: 17)) + .foregroundColor(Color("customGray3")) + Image("upDownArrows") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + } + .padding(.horizontal) + } + .padding(.horizontal) + .padding(.top, 7) + .padding(.bottom, 17) + .overlay { + HStack { + Spacer() + Picker("", selection: $selectedLanguage, content: { + ForEach(MockData.languages, id: \.self) { + Text($0) + } + }) + .padding(.trailing, 35) + .blendMode(.destinationOver) + } + .frame(width: UIScreen.main.bounds.width) + } + } + .background(Color.white) + .cornerRadius(20) + } +} + +#Preview { + GeneralGroupSettings(selectedTheme: .constant("Темная"), selectedLanguage: .constant("Русский")) +} diff --git a/Schedule-ICTIS/Settings/ListOfGroupsView.swift b/Schedule-ICTIS/Settings/ListOfGroupsView.swift new file mode 100644 index 0000000..2f27c77 --- /dev/null +++ b/Schedule-ICTIS/Settings/ListOfGroupsView.swift @@ -0,0 +1,105 @@ +// +// ListOfGroupsView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 13.03.2025. +// + +import SwiftUI +import CoreData + +struct ListOfGroupsView: View { + @Environment(\.dismiss) private var dismiss + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var serchGroupsVM: SearchGroupsViewModel + var provider = ClassProvider.shared + var body: some View { + ScrollView(.vertical, showsIndicators: true) { + ForEach(serchGroupsVM.groups) { item in + if item.name.starts(with: "ВПК") || item.name.starts(with: "мВПК") { + VStack { + Rectangle() + .frame(height: 1) + .foregroundColor(Color("customGray1")) + .padding(.horizontal, 10) + HStack { + Text(item.name) + .foregroundColor(.black) + .font(.custom("Montserrat-SemiBold", fixedSize: 15)) + Spacer() + } + .padding(.horizontal, 10) + .padding(.top, 2) + .padding(.bottom, 2) + .frame(width: UIScreen.main.bounds.width, height: 30) + .background(Color("background")) + .onTapGesture { + do { + try saveGroup(name: item.name) + saveScheduleForVpkToMemory(withName: item.name) + vm.nameToHtml[item.name] = "" + vm.updateFilteringGroups() + vm.fetchWeekSchedule() + dismiss() + } catch { + print("Ошибка сохранения: \(error.localizedDescription)") + vm.isShowingAlertForIncorrectGroup = true + } + } + } + } + } + } + } +} + +extension ListOfGroupsView { + func saveGroup(name: String) throws { + let context = ClassProvider.shared.viewContext + + // Создаем fetch request с правильным типом + let fetchRequest: NSFetchRequest = FavouriteVpkModel.all() + fetchRequest.predicate = NSPredicate(format: "name == %@", name) + + let existingGroups = try context.fetch(fetchRequest) + guard existingGroups.isEmpty else { return } + + let newGroup = FavouriteVpkModel(context: context) + newGroup.name = name + try context.save() + } + + + func saveScheduleForVpkToMemory(withName name: String) { + vm.fetchWeekForSingleGroup(groupName: name) + var indexOfTheDay: Int16 = 0 + let context = provider.newContext // Создаем новый контекст + + context.perform { + for dayIndex in 0.. = FavouriteGroupModel.all() + fetchRequest.predicate = NSPredicate(format: "name == %@", name) + + let existingGroups = try context.fetch(fetchRequest) + guard existingGroups.isEmpty else { return } + + let newGroup = FavouriteGroupModel(context: context) + newGroup.name = name + try context.save() + } + + func saveScheduleForGroupToMemory(withName name: String) { + vm.fetchWeekForSingleGroup(groupName: name) + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + var indexOfTheDay: Int16 = 0 + let context = provider.newContext // Создаем новый контекст + + context.perform { + for dayIndex in 0.. = JsonClassModel.all() + do { + let results = try context.fetch(fetchRequest) + for group in results { + print(group.group) + } + } catch { + print("Ошибка при выполнении fetch-запроса: \(error)") + } + } + } + } +} + +#Preview { + @Previewable @StateObject var vm = ScheduleViewModel() + @Previewable @StateObject var vm2 = NetworkMonitor() + SelectingGroupView(vm: vm, networkMonitor: vm2) +} diff --git a/Schedule-ICTIS/Settings/SelectingVPKView.swift b/Schedule-ICTIS/Settings/SelectingVPKView.swift new file mode 100644 index 0000000..1173a3d --- /dev/null +++ b/Schedule-ICTIS/Settings/SelectingVPKView.swift @@ -0,0 +1,167 @@ +// +// SelectedGroupView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 30.01.2025. +// + +import SwiftUI +import CoreData + +struct SelectingVPKView: View { + @Environment(\.dismiss) private var dismiss + @FocusState private var isFocused: Bool + @State private var text: String = "" + @ObservedObject var vm: ScheduleViewModel + @ObservedObject var networkMonitor: NetworkMonitor + @State private var isLoading = false + @State private var searchTask: DispatchWorkItem? + @StateObject private var serchGroupsVM = SearchGroupsViewModel() + var provider = ClassProvider.shared + var body: some View { + VStack { + HStack (spacing: 0) { + Image(systemName: "magnifyingglass") + .foregroundColor(Color.gray) + .padding(.leading, 12) + .padding(.trailing, 7) + TextField("Поиск ВПК", text: $text) + .disableAutocorrection(true) + .focused($isFocused) + .onChange(of: text) { oldValue, newValue in + searchTask?.cancel() + let task = DispatchWorkItem { + if !text.isEmpty { + serchGroupsVM.fetchGroups(group: text) + } + else { + serchGroupsVM.fetchGroups(group: "ВПК") + } + } + searchTask = task + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: task) + } + .onSubmit { + self.isFocused = false + guard !text.isEmpty else { return } + + vm.fetchWeekForSingleGroup(groupName: text) + self.isLoading = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + guard vm.errorInNetwork == .noError else { + vm.isShowingAlertForIncorrectGroup = true + return + } + + vm.errorInNetwork = nil + let formattedText = transformStringToFormat(text) + + do { + try saveGroup(name: formattedText) + saveScheduleForVpkToMemory(withName: formattedText) + vm.nameToHtml[formattedText] = "" + vm.updateFilteringGroups() + vm.fetchWeekSchedule() + self.isLoading = false + self.text = "" + dismiss() + } catch { + print("Ошибка сохранения: \(error.localizedDescription)") + vm.isShowingAlertForIncorrectGroup = true + } + } + } + .submitLabel(.done) + if isFocused { + Button { + self.text = "" + self.isFocused = false + } label: { + Image(systemName: "xmark.circle.fill") + .padding(.trailing, 20) + .offset(x: 10) + .foregroundColor(.gray) + .background( + ) + } + } + } + .frame(height: 40) + .background( + RoundedRectangle(cornerRadius: 15) + .fill(.white) + ) + Spacer() + if isLoading { + LoadingView() + Spacer() + } else if networkMonitor.isConnected { + ListOfGroupsView(vm: vm, serchGroupsVM: serchGroupsVM) + } else { + ConnectingToNetworkView() + } + } + .padding(.horizontal, 10) + .background(Color("background")) + .onAppear { + serchGroupsVM.fetchGroups(group: "ВПК") + } + } +} + +extension SelectingVPKView { + func saveGroup(name: String) throws { + let context = ClassProvider.shared.viewContext + + // Создаем fetch request с правильным типом + let fetchRequest: NSFetchRequest = FavouriteVpkModel.all() + fetchRequest.predicate = NSPredicate(format: "name == %@", name) + + let existingGroups = try context.fetch(fetchRequest) + guard existingGroups.isEmpty else { return } + + let newGroup = FavouriteVpkModel(context: context) + newGroup.name = name + try context.save() + } + + func saveScheduleForVpkToMemory(withName name: String) { + vm.fetchWeekForSingleGroup(groupName: name) + var indexOfTheDay: Int16 = 0 + let context = provider.newContext // Создаем новый контекст + + context.perform { + for dayIndex in 0.. 0 { // Пропускаем первый столбец (день и дату) + let time = table[1][timeIndex] // Время берем из второй строки + let classInfo = ClassInfo(subject: subject, group: nameOfGroup, time: time) + updatedClassesGroups[dayIndex].append(classInfo) + } + } + } + } + } else { + let groupNames = Array(self.nameToHtml.keys) + for groupName in groupNames { + let schedule = try await NetworkManager.shared.getSchedule(groupName) + let numberHTML = schedule.table.group + self.nameToHtml[groupName] = numberHTML + let table = schedule.table.table + self.week = schedule.table.week +// print("Группа: \(schedule.table.name), неделя: \(schedule.table.week)") +// print(table) + + // Преобразуем данные в формат ClassInfo + for (dayIndex, day) in table[2...].enumerated() { // Пропускаем первые две строки (заголовки) + for (timeIndex, subject) in day.enumerated() { + if !subject.isEmpty && timeIndex > 0 { // Пропускаем первый столбец (день и дату) + let time = table[1][timeIndex] // Время берем из второй строки + let classInfo = ClassInfo(subject: subject, group: groupName, time: time) + updatedClassesGroups[dayIndex].append(classInfo) + } + } + } + } + } + + // Обновляем данные + self.classesGroups = updatedClassesGroups + self.isFirstStartOffApp = false + self.isShowingAlertForIncorrectGroup = false + self.isLoading = false + self.errorInNetwork = .noError + + // Сортируем по времени + self.sortClassesByTime() + } catch { + if let urlError = error as? URLError, urlError.code == .timedOut { + errorInNetwork = .timeout + print("Ошибка: превышено время ожидания ответа от сервера") + } else if let error = error as? NetworkError { + switch error { + case .invalidResponse: + errorInNetwork = .invalidResponse + case .invalidData: + errorInNetwork = .invalidData + self.isShowingAlertForIncorrectGroup = true + default: + print("Неизвестная ошибка: \(error)") + } + print("Есть ошибка: \(error)") + } + isLoading = false + } + } + } + + func fetchWeekForSingleGroup(groupName name: String) { + isLoading = true + Task { + do { + var singleSchedule: [[ClassInfo]] = Array(repeating: [], count: 6) // 6 дней (пн-сб) + let schedule = try await NetworkManager.shared.getSchedule(name) + let table = schedule.table.table + let groupName = schedule.table.name + self.weekForSingleGroup = schedule.table.week + + // Преобразуем данные в формат ClassInfo + for (dayIndex, day) in table[2...].enumerated() { // Пропускаем первые две строки (заголовки) + for (timeIndex, subject) in day.enumerated() { + if !subject.isEmpty && timeIndex > 0 { // Пропускаем первый столбец (день и дату) + let time = table[1][timeIndex] // Время берем из второй строки + let classInfo = ClassInfo(subject: subject, group: groupName, time: time) + singleSchedule[dayIndex].append(classInfo) + } + } + } + + // Обновляем данные + self.classesForSingleGroup = singleSchedule + self.isShowingAlertForIncorrectGroup = false + self.isLoading = false + self.errorInNetwork = .noError + + } catch { + if let urlError = error as? URLError, urlError.code == .timedOut { + errorInNetwork = .timeout + print("Ошибка: превышено время ожидания ответа от сервера") + } else if let error = error as? NetworkError { + switch error { + case .invalidResponse: + errorInNetwork = .invalidResponse + case .invalidData: + errorInNetwork = .invalidData + print("FetchSingle: InvalidData") + self.isShowingAlertForIncorrectGroup = true + default: + print("Неизвестная ошибка: \(error)") + } + print("Есть ошибка: \(error)") + } + isLoading = false + } + } + } + + func updateSelectedDayIndex() { + switch selectedDay.format("E") { + case "Пн": + selectedIndex = 0 + case "Вт": + selectedIndex = 1 + case "Ср": + selectedIndex = 2 + case "Чт": + selectedIndex = 3 + case "Пт": + selectedIndex = 4 + case "Сб": + selectedIndex = 5 + default: + selectedIndex = 6 + } + } + + private func parseTime(_ timeString: String) -> Int { + // Разделяем строку по дефису и берем первую часть (время начала) + let startTimeString = timeString.components(separatedBy: "-").first ?? "" + + // Разделяем время на часы и минуты + let components = startTimeString.components(separatedBy: ":") + guard components.count == 2, + let hours = Int(components[0]), + let minutes = Int(components[1]) else { + return 0 // В случае ошибки возвращаем 0 + } + + // Преобразуем время в минуты с начала дня + return hours * 60 + minutes + } + + // Method for sorting classes by time + private func sortClassesByTime() { + // Проходим по каждому дню (подмассиву) в classesGroups + for dayIndex in 0.. GroupComponents { + // Находим индекс дефиса + guard let hyphenIndex = name.firstIndex(of: "-") else { + return GroupComponents(courseNumber: 0, groupNumber: 0) + } + + // Извлекаем часть до дефиса (буквы и номер курса) + let prefix = String(name[.. VPKComponents { + let isMinor = name.hasPrefix("мВПК") + + // Убираем префикс и разбиваем по дефису + let cleanName = isMinor ? name.replacingOccurrences(of: "мВПК-", with: "") : name.replacingOccurrences(of: "ВПК ", with: "") + let components = cleanName.split(separator: "-") + + // Извлекаем первый номер + let firstNumberString = String(components[0]).trimmingCharacters(in: .whitespaces) + let firstNumber = Int(firstNumberString) ?? 0 + + // Извлекаем второй номер, если он есть + let secondNumber: Int? + if components.count > 1 { + secondNumber = Int(components[1]) ?? 0 + } else { + secondNumber = nil + } + + return VPKComponents(isMinor: isMinor, firstNumber: firstNumber, secondNumber: secondNumber) + } +} diff --git a/Schedule-ICTIS/WebView.swift b/Schedule-ICTIS/WebView.swift new file mode 100644 index 0000000..48e9571 --- /dev/null +++ b/Schedule-ICTIS/WebView.swift @@ -0,0 +1,41 @@ +// +// WebView.swift +// Schedule ICTIS +// +// Created by Mironov Egor on 17.04.2025. +// + +import SwiftUI +import WebKit + +struct WebView: UIViewRepresentable { + let url: URL + + func makeUIView(context: Context) -> WKWebView { + let webView = WKWebView() + webView.navigationDelegate = context.coordinator + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + let request = URLRequest(url: url) + uiView.load(request) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, WKNavigationDelegate { + var parent: WebView + + init(_ parent: WebView) { + self.parent = parent + } + + // Здесь можно реализовать методы делегата + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + print("Загрузка завершена") + } + } +}