Skip to content

Commit db6a719

Browse files
authored
테마 WIP (#349)
* wip * Fix for xcode 16.4 * Apply SwiftFormat changes --------- Co-authored-by: shp7724 <shp7724@users.noreply.github.com>
1 parent b2b5de9 commit db6a719

File tree

18 files changed

+300
-54
lines changed

18 files changed

+300
-54
lines changed

SNUTT/Modules/Feature/APIClientInterface/Sources/LocalizedErrorCode.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ public enum LocalizedErrorCode: Int, LocalizedError {
6464

6565
case tagNotFound = 0x4000
6666
case timetableNotFound = 0x4001
67-
case lectureNotFound = 0x4002
68-
case refLectureNotFound = 0x4003
67+
case lectureNotFound = 0x4003
6968
case userNotFound = 0x4004
7069
case colorlistNotFound = 0x4005
7170
case emailNotFound = 0x4006
@@ -146,7 +145,7 @@ public enum LocalizedErrorCode: Int, LocalizedError {
146145
APIClientInterfaceStrings.errorDescriptionCustomLecture
147146
case .userHasNoFCMKey:
148147
APIClientInterfaceStrings.errorDescriptionNoFCMKey
149-
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound,
148+
case .tagNotFound, .timetableNotFound, .lectureNotFound, .userNotFound,
150149
.colorlistNotFound,
151150
.emailNotFound:
152151
APIClientInterfaceStrings.errorDescriptionNotFound
@@ -233,7 +232,7 @@ public enum LocalizedErrorCode: Int, LocalizedError {
233232
APIClientInterfaceStrings.errorFailureReasonCustomLecture
234233
case .userHasNoFCMKey:
235234
APIClientInterfaceStrings.errorFailureReasonNoFCMKey
236-
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound,
235+
case .tagNotFound, .timetableNotFound, .lectureNotFound, .userNotFound,
237236
.colorlistNotFound,
238237
.emailNotFound:
239238
APIClientInterfaceStrings.errorFailureReasonNotFound
@@ -320,7 +319,7 @@ public enum LocalizedErrorCode: Int, LocalizedError {
320319
APIClientInterfaceStrings.errorRecoverySuggestionCustomLecture
321320
case .userHasNoFCMKey:
322321
APIClientInterfaceStrings.errorRecoverySuggestionNoFCMKey
323-
case .tagNotFound, .timetableNotFound, .lectureNotFound, .refLectureNotFound, .userNotFound,
322+
case .tagNotFound, .timetableNotFound, .lectureNotFound, .userNotFound,
324323
.colorlistNotFound,
325324
.emailNotFound:
326325
APIClientInterfaceStrings.errorRecoverySuggestionNotFound

SNUTT/Modules/Feature/Themes/Sources/UI/MenuThemeSelectionSheet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ struct MenuThemeSelectionSheet: View {
107107
ThemeIcon(theme: theme)
108108
}
109109
}
110-
.frame(width: 80, height: 78)
110+
.frame(width: 80, height: 80)
111111
.cornerRadius(6)
112112

113113
let selectionBackgroundColor: Color = if isSelectionHighlighted(selection) {

SNUTT/Modules/Feature/ThemesInterface/Sources/Theme.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ public struct Theme: Identifiable, Sendable, Codable, Equatable {
2222

2323
@MemberwiseInit(.public)
2424
public struct LectureColor: Hashable, Sendable, Codable {
25-
public let fgHex: String
26-
public let bgHex: String
25+
public var fgHex: String
26+
public var bgHex: String
2727
public var fg: Color { Color(hex: fgHex) }
2828
public var bg: Color { Color(hex: bgHex) }
2929

30-
public static let temporary: Self = .init(fgHex: "#000000", bgHex: "#121213")
30+
public static let temporary: Self = .init(fgHex: "#000000", bgHex: "#C4C4C4")
3131
}

SNUTT/Modules/Feature/Timetable/Sources/Infra/LectureAPIRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public struct LectureAPIRepository: LectureRepository {
2424
public func updateLecture(timetableID: String, lecture: Lecture,
2525
overrideOnConflict: Bool) async throws -> Timetable
2626
{
27-
guard let lectureID = lecture.lectureID else { throw LocalizedErrorCode.lectureNotFound }
27+
let lectureID = lecture.id
2828
let timePlaces = try lecture.timePlaces
2929
.map {
3030
try Components.Schemas.ClassPlaceAndTimeLegacyRequestDto(

SNUTT/Modules/Feature/Timetable/Sources/Presentation/LectureEditDetailViewModel.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ import TimetableInterface
1111

1212
@MainActor
1313
@Observable
14-
final class LectureEditDetailViewModel {
14+
public final class LectureEditDetailViewModel {
1515
@ObservationIgnored
1616
@Dependency(\.lectureRepository) private var lectureRepository
1717

18-
let entryLecture: Lecture
18+
/// Might be `nil` if write operation is not necessary.
19+
private let timetableViewModel: TimetableViewModel?
20+
21+
let lectureID: String
22+
var entryLecture: Lecture
1923
var editableLecture: Lecture
2024

2125
var buildings: [Building] = []
@@ -34,7 +38,9 @@ final class LectureEditDetailViewModel {
3438
buildings.allSatisfy { $0.campus == .GWANAK }
3539
}
3640

37-
init(entryLecture: Lecture) {
41+
init(timetableViewModel: TimetableViewModel?, entryLecture: Lecture) {
42+
self.timetableViewModel = timetableViewModel
43+
lectureID = entryLecture.id
3844
self.entryLecture = entryLecture
3945
editableLecture = entryLecture
4046
}
@@ -43,15 +49,28 @@ final class LectureEditDetailViewModel {
4349

4450
func fetchBuildingList() async {
4551
let lecturePlaces = entryLecture.timePlaces.map { $0.place }
52+
buildings = (try? await lectureRepository.fetchBuildingList(places: lecturePlaces)) ?? []
53+
}
54+
55+
func saveEditableLecture() async throws {
56+
guard let timetableViewModel, let timetableID = timetableViewModel.currentTimetable?.id else { return }
57+
let lectureID = editableLecture.id
4658
do {
47-
buildings = try await lectureRepository.fetchBuildingList(places: lecturePlaces)
59+
let timetable = try await lectureRepository.updateLecture(
60+
timetableID: timetableID,
61+
lecture: editableLecture,
62+
overrideOnConflict: false
63+
)
64+
try timetableViewModel.setCurrentTimetable(timetable)
65+
guard let updatedLecture = timetable.lectures.first(where: { $0.id == lectureID }) else { return }
66+
entryLecture = updatedLecture
4867
} catch {
49-
print(error)
68+
editableLecture = entryLecture
69+
throw error
5070
}
5171
}
5272
}
5373

54-
5574
extension Lecture {
5675
var quotaDescription: String? {
5776
get {

SNUTT/Modules/Feature/Timetable/Sources/Presentation/TimetableViewModel.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class TimetableViewModel: TimetableViewModelProtocol {
2424
@ObservationIgnored
2525
@Dependency(\.timetableRepository) private var timetableRepository
2626

27+
@ObservationIgnored
28+
@Dependency(\.timetableLocalRepository) private var timetableLocalRepository
29+
2730
private let router: TimetableRouter
2831
var paths: [TimetableDetailSceneTypes] {
2932
get { router.navigationPaths }
@@ -35,7 +38,7 @@ public class TimetableViewModel: TimetableViewModelProtocol {
3538
currentTimetable = timetableUseCase.loadLocalRecentTimetable()
3639
}
3740

38-
public var currentTimetable: Timetable?
41+
public private(set) var currentTimetable: Timetable?
3942
private(set) var metadataLoadState: MetadataLoadState = .loading
4043

4144
var isMenuPresented = false
@@ -70,6 +73,11 @@ public class TimetableViewModel: TimetableViewModelProtocol {
7073
currentTimetable = try await timetableUseCase.fetchRecentTimetable()
7174
}
7275

76+
public func setCurrentTimetable(_ timetable: Timetable) throws {
77+
try timetableLocalRepository.storeSelectedTimetable(timetable)
78+
currentTimetable = timetable
79+
}
80+
7381
func loadTimetableList() async throws {
7482
let metadataList = try await timetableRepository.fetchTimetableMetadataList()
7583
metadataLoadState = .loaded(metadataList)
@@ -157,13 +165,16 @@ public enum TimetableDetailSceneTypes: Hashable, Equatable {
157165
case lectureList
158166
case notificationList
159167
case lectureDetail(Lecture)
168+
case lectureColorSelection(LectureEditDetailViewModel)
160169

161170
public static func == (lhs: TimetableDetailSceneTypes, rhs: TimetableDetailSceneTypes) -> Bool {
162171
switch (lhs, rhs) {
163172
case (.lectureList, .lectureList):
164173
true
165174
case (.notificationList, .notificationList):
166175
true
176+
case let (.lectureColorSelection(lhs), .lectureColorSelection(rhs)):
177+
lhs.lectureID == rhs.lectureID
167178
case let (.lectureDetail(lhs), .lectureDetail(rhs)):
168179
lhs.id == rhs.id
169180
default:

SNUTT/Modules/Feature/Timetable/Sources/UI/LectureEditDetail/EditableRow.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ extension ValueContainer where T == Int64? {
9999
}
100100
}
101101

102-
private struct DetailLabel: View {
102+
struct DetailLabel: View {
103103
let text: String
104104
var body: some View {
105105
VStack {
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//
2+
// LectureColorSelectionListView.swift
3+
// SNUTT
4+
//
5+
// Copyright © 2025 wafflestudio.com. All rights reserved.
6+
//
7+
8+
import SwiftUI
9+
import SwiftUIUtility
10+
import ThemesInterface
11+
import TimetableInterface
12+
13+
struct LectureColorSelectionListView: View {
14+
let theme: Theme
15+
let viewModel: LectureEditDetailViewModel
16+
17+
var body: some View {
18+
Form {
19+
fixedColorsSection
20+
customColorSection
21+
}
22+
.navigationBarTitleDisplayMode(.inline)
23+
.navigationTitle("색상 선택")
24+
}
25+
26+
private var checkMarkImage: Image {
27+
Image(systemName: "checkmark")
28+
}
29+
30+
private var fixedColorsSection: some View {
31+
Section {
32+
ForEach(Array(theme.colors.enumerated()), id: \.offset) { index, color in
33+
let isSelected = if theme.isCustom {
34+
viewModel.editableLecture.colorIndex == 0 && viewModel.editableLecture.customColor == color
35+
} else {
36+
viewModel.editableLecture.colorIndex == index + 1
37+
}
38+
LectureColorPreviewButton(
39+
lectureColor: color,
40+
title: "이름",
41+
trailingImage: isSelected ? checkMarkImage : nil
42+
) {
43+
withAnimation(.defaultSpring) {
44+
if theme.isCustom {
45+
viewModel.editableLecture.colorIndex = 0
46+
viewModel.editableLecture.customColor = color
47+
} else {
48+
viewModel.editableLecture.colorIndex = index + 1
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
extension LectureColorSelectionListView {
58+
@ViewBuilder private var customColorSection: some View {
59+
if theme.isCustom {
60+
EmptyView()
61+
} else {
62+
Section {
63+
let isSelected = viewModel.editableLecture.colorIndex == 0
64+
DisclosureGroup(isExpanded: .init(get: { isSelected }, set: { _ in })) {
65+
Group {
66+
ColorPicker("글꼴색", selection: fgColorBinding(), supportsOpacity: false)
67+
ColorPicker("배경색", selection: bgColorBinding(), supportsOpacity: false)
68+
}
69+
} label: {
70+
LectureColorPreviewButton(
71+
lectureColor: viewModel.editableLecture.customColor ?? .temporary,
72+
title: "이름",
73+
trailingImage: isSelected ? checkMarkImage : nil
74+
) {
75+
withAnimation(.defaultSpring) {
76+
viewModel.editableLecture.colorIndex = 0
77+
viewModel.editableLecture.customColor = viewModel.editableLecture.customColor ?? .temporary
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
private func fgColorBinding() -> Binding<Color> {
86+
.init {
87+
(viewModel.editableLecture.customColor ?? .temporary).fg
88+
} set: { color in
89+
viewModel.editableLecture.customColor?.fgHex = color.toHex()
90+
}
91+
}
92+
93+
private func bgColorBinding() -> Binding<Color> {
94+
.init {
95+
(viewModel.editableLecture.customColor ?? .temporary).bg
96+
} set: { color in
97+
viewModel.editableLecture.customColor?.bgHex = color.toHex()
98+
}
99+
}
100+
}
101+
102+
struct LectureColorPreviewButton: View {
103+
let lectureColor: LectureColor
104+
let title: String?
105+
let trailingImage: Image?
106+
let action: () -> Void
107+
108+
var body: some View {
109+
Button {
110+
action()
111+
} label: {
112+
HStack(spacing: 15) {
113+
colorPreview
114+
if let title {
115+
Text(title)
116+
}
117+
Spacer()
118+
119+
trailingImage
120+
}
121+
}
122+
}
123+
124+
private var colorPreview: some View {
125+
HStack(spacing: 0) {
126+
Rectangle()
127+
.fill(lectureColor.fg)
128+
.border(Color.black.opacity(0.1), width: 1)
129+
.aspectRatio(1.0, contentMode: .fit)
130+
Rectangle()
131+
.fill(lectureColor.bg)
132+
.border(Color.black.opacity(0.1), width: 1)
133+
.aspectRatio(1.0, contentMode: .fit)
134+
}
135+
.frame(height: 25)
136+
}
137+
}
138+
139+
#Preview {
140+
NavigationStack {
141+
LectureColorSelectionListView(
142+
theme: .snutt,
143+
viewModel: .init(timetableViewModel: .init(), entryLecture: PreviewHelpers.preview.lectures.first!)
144+
)
145+
}
146+
}

0 commit comments

Comments
 (0)