Skip to content

Commit e1649aa

Browse files
authored
Merge pull request #25 from Ditectrev/develop
added bookmark feature
2 parents 7baf030 + d6d96f7 commit e1649aa

File tree

8 files changed

+339
-45
lines changed

8 files changed

+339
-45
lines changed

CloudMaster.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
8D26A31C2C0EA9C100E9B015 /* QuestionNavbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A31B2C0EA9C100E9B015 /* QuestionNavbar.swift */; };
1111
8D26A31E2C0EE3F400E9B015 /* QuestionImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A31D2C0EE3F400E9B015 /* QuestionImages.swift */; };
1212
8D26A3202C0EE4A000E9B015 /* QuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A31F2C0EE4A000E9B015 /* QuestionView.swift */; };
13+
8D26A3222C101C5000E9B015 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D26A3212C101C5000E9B015 /* BookmarksView.swift */; };
1314
8D8D8A862C05A23600ACC61C /* CloudMasterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8D8A852C05A23600ACC61C /* CloudMasterTests.swift */; };
1415
8D8D8A902C05A23600ACC61C /* CloudMasterUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8D8A8F2C05A23600ACC61C /* CloudMasterUITests.swift */; };
1516
8D8D8A922C05A23600ACC61C /* CloudMasterUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8D8A912C05A23600ACC61C /* CloudMasterUITestsLaunchTests.swift */; };
@@ -67,6 +68,7 @@
6768
8D26A31B2C0EA9C100E9B015 /* QuestionNavbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionNavbar.swift; sourceTree = "<group>"; };
6869
8D26A31D2C0EE3F400E9B015 /* QuestionImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionImages.swift; sourceTree = "<group>"; };
6970
8D26A31F2C0EE4A000E9B015 /* QuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionView.swift; sourceTree = "<group>"; };
71+
8D26A3212C101C5000E9B015 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
7072
8D8D8A712C05A23400ACC61C /* CloudMaster Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CloudMaster Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
7173
8D8D8A812C05A23600ACC61C /* CloudMasterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CloudMasterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7274
8D8D8A852C05A23600ACC61C /* CloudMasterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudMasterTests.swift; sourceTree = "<group>"; };
@@ -225,6 +227,7 @@
225227
isa = PBXGroup;
226228
children = (
227229
8D8D8AA42C05A27800ACC61C /* CourseView.swift */,
230+
8D26A3212C101C5000E9B015 /* BookmarksView.swift */,
228231
);
229232
path = Views;
230233
sourceTree = "<group>";
@@ -603,6 +606,7 @@
603606
8D8D8AE12C05A27800ACC61C /* Courses.swift in Sources */,
604607
8D26A31C2C0EA9C100E9B015 /* QuestionNavbar.swift in Sources */,
605608
8D8D8AD12C05A27800ACC61C /* CourseView.swift in Sources */,
609+
8D26A3222C101C5000E9B015 /* BookmarksView.swift in Sources */,
606610
8DABB7742C0D7D0300B40E25 /* DownloadViewModel.swift in Sources */,
607611
);
608612
runOnlyForDeploymentPostprocessing = 0;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import SwiftUI
2+
3+
struct BookmarksView: View {
4+
@State private var bookmarks: [Bookmark] = []
5+
6+
var body: some View {
7+
NavigationView {
8+
9+
if bookmarks.isEmpty {
10+
VStack {
11+
Image(systemName: "bookmark")
12+
.resizable()
13+
.frame(width: 80, height: 100)
14+
.foregroundColor(.gray)
15+
.padding(.bottom, 20)
16+
Text("No questions saved")
17+
.font(.title)
18+
.foregroundColor(.gray)
19+
}
20+
.frame(maxWidth: .infinity, maxHeight: .infinity)
21+
} else {
22+
List {
23+
ForEach(bookmarks) { bookmark in
24+
NavigationLink(destination: QuestionDetailView(question: bookmark.question, bookmarks: $bookmarks)) {
25+
VStack(alignment: .leading) {
26+
Text(bookmark.question.question.prefix(40) + "...")
27+
.font(.headline)
28+
.lineLimit(2)
29+
.padding(.vertical,5)
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
.navigationTitle("Bookmarks")
37+
.onAppear {
38+
bookmarks = FavoritesStorage.shared.loadBookmarks()
39+
}
40+
}
41+
}
42+
43+
struct QuestionDetailView: View {
44+
let question: Question
45+
@Binding var bookmarks: [Bookmark]
46+
@State private var isBookmarked: Bool = false
47+
@Environment(\.presentationMode) var presentationMode
48+
49+
var body: some View {
50+
QuestionView(
51+
mode: .bookmarked,
52+
question: question,
53+
selectedChoices: nil,
54+
isMultipleResponse: question.multipleResponse,
55+
isResultShown: true,
56+
onChoiceSelected: { _ in }
57+
)
58+
.navigationTitle("Question")
59+
.navigationBarTitleDisplayMode(.inline)
60+
.navigationBarItems(trailing: bookmarkButton)
61+
.onAppear {
62+
isBookmarked = FavoritesStorage.shared.isBookmarked(question)
63+
}
64+
}
65+
66+
private var bookmarkButton: some View {
67+
Button(action: {
68+
toggleBookmark()
69+
}) {
70+
Image(systemName: isBookmarked ? "bookmark.fill" : "bookmark")
71+
72+
}
73+
}
74+
75+
private func toggleBookmark() {
76+
if isBookmarked {
77+
FavoritesStorage.shared.removeBookmarkByQuestionText(question.question)
78+
bookmarks = FavoritesStorage.shared.loadBookmarks()
79+
presentationMode.wrappedValue.dismiss()
80+
} else {
81+
let newBookmark = Bookmark(id: UUID(), question: question, answer: question.choices)
82+
FavoritesStorage.shared.addBookmark(newBookmark)
83+
bookmarks = FavoritesStorage.shared.loadBookmarks()
84+
}
85+
isBookmarked.toggle()
86+
}
87+
}

CloudMaster/Features/Course/Views/CourseView.swift

+68-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import SwiftUI
2-
32
struct CourseView: View {
43
@State private var isLoading = false
54
@State private var downloadProgress: [Course: Progress] = [:]
65
@State private var userTrainingData = UserTrainingData()
76
@State private var showingNotificationSettings = false
87
@State private var notificationsEnabled = false
8+
@State private var showingInfoPopup = false
9+
910
@StateObject private var viewModel = DownloadViewModel()
1011
@StateObject private var questionLoader: QuestionLoader
1112

13+
@Environment(\.colorScheme) var colorScheme
14+
1215
let course: Course
1316

1417
init(course: Course) {
@@ -41,7 +44,7 @@ struct CourseView: View {
4144

4245
Spacer()
4346
VStack(spacing: 20) {
44-
NavigationLink(destination: TrainingView(course: course, questionLoader: questionLoader)) {
47+
NavigationLink(destination: TrainingView(course: course)) {
4548
VStack {
4649
Text("Training")
4750
.font(.title)
@@ -55,7 +58,7 @@ struct CourseView: View {
5558
.cornerRadius(10)
5659
}
5760

58-
NavigationLink(destination: TrainingView(course: course, questionLoader: questionLoader)) {
61+
NavigationLink(destination: TrainingView(course: course)) {
5962
VStack {
6063
Text("Intelligent Training")
6164
.font(.title)
@@ -85,14 +88,16 @@ struct CourseView: View {
8588
}
8689
Spacer()
8790

88-
HStack(spacing: 20) {
89-
Link("Certification", destination: URL(string: course.url)!)
90-
.padding()
91-
.font(.subheadline)
92-
93-
Link("Sources", destination: URL(string: course.repositoryURL)!)
94-
.padding()
95-
.font(.subheadline)
91+
NavigationLink(destination: BookmarksView()) {
92+
HStack {
93+
Image(systemName: "bookmark")
94+
.font(.title3)
95+
.foregroundColor(colorScheme == .dark ? .white : .black)
96+
Text("Bookmarks")
97+
.font(.title3)
98+
.foregroundColor(colorScheme == .dark ? .white : .black)
99+
}
100+
.cornerRadius(10)
96101
}
97102
}
98103
.onAppear {
@@ -104,14 +109,20 @@ struct CourseView: View {
104109
}
105110
}
106111
.navigationBarTitle(course.shortName, displayMode: .inline)
107-
.navigationBarItems(trailing: notificationButton)
112+
.navigationBarItems(trailing: HStack {
113+
notificationButton
114+
infoButton
115+
})
108116
.navigationBarBackButtonHidden(false)
109117
.sheet(isPresented: $showingNotificationSettings) {
110118
NotificationSettingsView(isPresented: $showingNotificationSettings, notificationsEnabled: $notificationsEnabled, course: course)
111119
.onDisappear {
112120
checkNotificationSettings()
113121
}
114122
}
123+
.sheet(isPresented: $showingInfoPopup) {
124+
CourseInformationPopup(course: course)
125+
}
115126
.overlay(
116127
DownloadOverlayView(
117128
isShowing: $viewModel.isDownloading,
@@ -135,6 +146,14 @@ struct CourseView: View {
135146
}
136147
}
137148

149+
private var infoButton: some View {
150+
Button(action: {
151+
showingInfoPopup = true
152+
}) {
153+
Image(systemName: "info.circle")
154+
}
155+
}
156+
138157
func loadUserTrainingData(for course: Course) {
139158
if let data = UserDefaults.standard.data(forKey: course.shortName) {
140159
if let decodedData = try? JSONDecoder().decode(UserTrainingData.self, from: data) {
@@ -163,7 +182,7 @@ struct CourseView: View {
163182
func downloadCourse() {
164183
viewModel.downloadCourse(course)
165184
viewModel.$isDownloading.sink { isDownloading in
166-
if !isDownloading {
185+
if (!isDownloading) {
167186
DispatchQueue.main.async {
168187
questionLoader.reloadQuestions(from: course.shortName + ".json")
169188
}
@@ -181,3 +200,39 @@ struct CourseView: View {
181200
.store(in: &viewModel.cancellables)
182201
}
183202
}
203+
204+
struct CourseInformationPopup: View {
205+
let course: Course
206+
207+
var body: some View {
208+
VStack(spacing: 20) {
209+
Text("Course information")
210+
.font(.title2)
211+
.multilineTextAlignment(.center)
212+
213+
VStack(spacing: 10) {
214+
HStack {
215+
Spacer()
216+
Image(systemName: "book.pages.fill")
217+
Text("Certification")
218+
Spacer()
219+
}
220+
Link(course.url, destination: URL(string: course.url)!)
221+
}
222+
223+
VStack(spacing: 10) {
224+
HStack {
225+
Spacer()
226+
Image(systemName: "link")
227+
Text("Sources")
228+
Spacer()
229+
}
230+
Link(course.repositoryURL, destination: URL(string: course.repositoryURL)!)
231+
}
232+
233+
Spacer()
234+
}
235+
.padding()
236+
}
237+
238+
}

CloudMaster/Features/Exam/Views/ExamModesView.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ struct ExamModeView: View {
44
let course: Course
55
@ObservedObject var examDataStore = UserExamDataStore.shared
66

7+
@Environment(\.colorScheme) var colorScheme
8+
79
var body: some View {
810
VStack {
911

@@ -68,12 +70,13 @@ struct ExamModeView: View {
6870
NavigationLink(destination: PreviousExamsView(exams: filteredExams)) {
6971
HStack {
7072
Image(systemName: "clock.arrow.circlepath")
73+
.font(.title3)
74+
.foregroundColor(colorScheme == .dark ? .white : .black)
7175
Text("Exam History")
76+
.font(.title3)
77+
.foregroundColor(filteredExams.isEmpty ? .gray : (colorScheme == .dark ? .white : .black))
7278
}
7379
.padding()
74-
.background(filteredExams.isEmpty ? Color.customAccent : Color.customPrimary)
75-
.foregroundColor(.white)
76-
.cornerRadius(10)
7780
}
7881
.disabled(filteredExams.isEmpty)
7982
}

CloudMaster/Features/Shared/Components/QuestionNavbar.swift

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import SwiftUI
2-
32
struct QuestionNavbar: View {
43
@Environment(\.presentationMode) var presentationMode
54
let currentQuestionIndex: Int
65
let totalQuestions: Int
6+
let question: Question
77

8+
@Binding var isBookmarked: Bool
9+
810
var body: some View {
911
HStack {
1012
Button(action: {
@@ -22,6 +24,21 @@ struct QuestionNavbar: View {
2224
.foregroundColor(.secondary)
2325

2426
Spacer()
27+
28+
Button(action: {
29+
if isBookmarked {
30+
FavoritesStorage.shared.removeBookmarkByQuestionText(question.question)
31+
isBookmarked = false
32+
} else {
33+
let newBookmark = Bookmark(id: UUID(), question: question, answer: question.choices)
34+
FavoritesStorage.shared.addBookmark(newBookmark)
35+
isBookmarked = true
36+
}
37+
}) {
38+
Image(systemName: isBookmarked ? "bookmark.fill" : "bookmark")
39+
.font(.title3)
40+
.foregroundColor(.blue)
41+
}
2542
}
2643
.padding()
2744
}

CloudMaster/Features/Shared/Components/QuestionView.swift

+25-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
//
2-
// QuestionView.swift
3-
// CloudMaster
4-
//
5-
// Created by Benedikt Wagner on 04.06.24.
6-
//
7-
81
import Foundation
92
import SwiftUI
103

114
struct QuestionView: View {
125
enum Mode {
136
case training
147
case exam
8+
case bookmarked
159
}
1610

1711
let mode: Mode
@@ -67,12 +61,17 @@ struct QuestionView: View {
6761
isResultShown: isResultShown ?? false,
6862
onChoiceSelected: onChoiceSelected
6963
)
70-
} else {
64+
} else if mode == .exam {
7165
ExamChoice(
7266
choice: choice,
7367
isSelected: selectedChoices?.contains(choice.id) == true,
7468
onChoiceSelected: onChoiceSelected
7569
)
70+
} else if mode == .bookmarked {
71+
BookmarkedChoice(
72+
choice: choice,
73+
isSelected: selectedChoices?.contains(choice.id) == true
74+
)
7675
}
7776
}
7877
}
@@ -163,7 +162,24 @@ struct ExamChoice: View {
163162
.background(isSelected ? Color.gray.opacity(0.3) : Color.clear)
164163
.cornerRadius(10)
165164
.padding(.horizontal)
166-
.foregroundColor(.white)
165+
166+
Divider()
167+
}
168+
}
169+
170+
struct BookmarkedChoice: View {
171+
let choice: Choice
172+
let isSelected: Bool
173+
174+
var body: some View {
175+
Text(choice.text)
176+
.padding()
177+
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
178+
.multilineTextAlignment(.center)
179+
.background(choice.correct ? Color.correct : (isSelected ? Color.wrong : Color.gray.opacity(0.3)))
180+
.foregroundColor(.white)
181+
.cornerRadius(10)
182+
.padding(.horizontal)
167183

168184
Divider()
169185
}

0 commit comments

Comments
 (0)