Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
*.p12
*.pfx
*.mobileprovision
**/Directory.Build.signing.props

# Generated artifacts
.artifacts/
Expand Down
2 changes: 2 additions & 0 deletions MauverseIOS/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>MAUverseScheduleToken</key>
<string>$(MAUVERSE_SCHEDULE_TOKEN)</string>
<key>MAUVERSE_API_BASE_URL</key>
<string>$(MAUVERSE_API_BASE_URL)</string>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
Expand Down
8 changes: 7 additions & 1 deletion MauverseIOS/Sources/Core/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ extension Notification.Name {

final class APIClient {
static let shared = APIClient()
private let baseURL = URL(string: "https://app.mauniver.ru/dev/mauverse/")!
private let baseURL: URL
private let session: URLSession

private init() {
if let configured = Bundle.main.object(forInfoDictionaryKey: "MAUVERSE_API_BASE_URL") as? String,
let url = URL(string: configured), !configured.isEmpty {
baseURL = url
} else {
baseURL = URL(string: "https://app.mauniver.ru/dev/mauverse/")!
}
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
configuration.waitsForConnectivity = true
Expand Down
1 change: 0 additions & 1 deletion MauverseIOS/Sources/Core/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ struct UserDTO: Codable, Equatable {
subGroupId = values.decodeLossyStringIfPresent(forKey: .subGroupId)
?? values.decodeLossyStringIfPresent(forKey: .legacySubGroupId)
scheduleGroupUID = try values.decodeIfPresent(String.self, forKey: .scheduleGroupUID)
?? values.decodeLossyStringIfPresent(forKey: .apiGroupId)
groupName = try values.decodeIfPresent(String.self, forKey: .groupName)
?? values.decodeLossyStringIfPresent(forKey: .legacyGroupName)
speciality = try values.decodeIfPresent(String.self, forKey: .speciality)
Expand Down
2 changes: 1 addition & 1 deletion MauverseIOS/Sources/Core/OfficialNewsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ actor OfficialNewsService {
private func fetch(url: URL, cachePolicy: URLRequest.CachePolicy) async throws -> [NewsItem] {
var request = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: 30)
request.timeoutInterval = 30
request.setValue("MAUverse/1.12.2 (iOS)", forHTTPHeaderField: "User-Agent")
request.setValue("MAUverse/1.12.3 (iOS)", forHTTPHeaderField: "User-Agent")
request.setValue("application/rss+xml, application/xml, text/xml", forHTTPHeaderField: "Accept")

let (data, response) = try await URLSession.shared.data(for: request)
Expand Down
20 changes: 17 additions & 3 deletions MauverseIOS/Sources/Core/SessionStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ final class SessionStore: ObservableObject {
UserDefaults.standard.removeObject(forKey: storageKey)
}
}
if var current = user {
Self.clearMistakenScheduleUID(&current)
user = current
}
sessionObserver = NotificationCenter.default.addObserver(
forName: .mauverseSessionExpired,
object: nil,
Expand Down Expand Up @@ -63,9 +67,7 @@ final class SessionStore: ObservableObject {
)
}
authenticated.username = authenticated.username ?? normalizedUsername
if authenticated.scheduleGroupUID?.isEmpty != false {
authenticated.scheduleGroupUID = authenticated.groupId
}
Self.clearMistakenScheduleUID(&authenticated)
user = authenticated
persist()
await refreshProfile()
Expand All @@ -83,6 +85,7 @@ final class SessionStore: ObservableObject {

func signOut() {
user = nil
errorMessage = nil
KeychainStore.delete(account: storageKey)
UserDefaults.standard.removeObject(forKey: storageKey)
}
Expand Down Expand Up @@ -118,4 +121,15 @@ final class SessionStore: ObservableObject {
guard let user, let data = try? JSONEncoder().encode(user) else { return }
KeychainStore.write(data, account: storageKey)
}

/// Moodle numeric group ids were previously copied into scheduleGroupUID and broke lookups.
private static func clearMistakenScheduleUID(_ user: inout UserDTO) {
guard let uid = user.scheduleGroupUID, !uid.isEmpty else {
user.scheduleGroupUID = nil
return
}
if let groupId = user.groupId, uid == groupId {
user.scheduleGroupUID = nil
}
}
}
119 changes: 76 additions & 43 deletions MauverseIOS/Sources/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ final class HomeViewModel: ObservableObject {

struct HomeView: View {
@EnvironmentObject private var session: SessionStore
@Binding var selectedTab: Int
@StateObject private var model = HomeViewModel()

private var firstName: String {
Expand All @@ -106,17 +107,21 @@ struct HomeView: View {
HomeDataErrorCard(
title: "Расписание недоступно",
message: error,
icon: "calendar.badge.exclamationmark"
icon: "calendar.badge.exclamationmark",
retry: { Task { await model.load(user: session.user) } }
)
} else {
NextLessonCard(item: model.nextLesson, group: session.user?.groupName)
Button { selectedTab = 1 } label: {
NextLessonCard(item: model.nextLesson, group: session.user?.groupName)
}
.buttonStyle(.plain)
}

MauSectionHeader(title: "Быстрый доступ")
quickActions

if !model.notifications.isEmpty {
MauSectionHeader(title: "Обновления ЭИОС")
MauSectionHeader(title: "Уведомления портала")
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 12) {
ForEach(model.notifications) { notification in
Expand All @@ -132,7 +137,7 @@ struct HomeView: View {
.font(.title3.bold())
.foregroundStyle(MauTheme.ink)
Spacer()
NavigationLink(destination: NewsView()) {
Button { selectedTab = 3 } label: {
HStack(spacing: 4) {
Text("Все новости")
Image(systemName: "chevron.right")
Expand All @@ -147,7 +152,8 @@ struct HomeView: View {
HomeDataErrorCard(
title: "Новости не обновились",
message: error,
icon: "newspaper"
icon: "newspaper",
retry: { Task { await model.load(user: session.user) } }
)
}
}
Expand All @@ -171,7 +177,7 @@ struct HomeView: View {
.font(.system(size: 38, weight: .bold, design: .rounded))
.foregroundStyle(MauTheme.ink)
HStack(spacing: 8) {
MauStatusPill(title: "Сессия защищена", icon: "lock.fill")
MauStatusPill(title: "Вход сохранён", icon: "lock.fill")
if let group = session.user?.groupName, !group.isEmpty {
Text(group)
.font(.caption.weight(.semibold))
Expand All @@ -181,7 +187,7 @@ struct HomeView: View {
}
}
Spacer()
NavigationLink(destination: ProfileView()) {
Button { selectedTab = 4 } label: {
ZStack {
Circle().fill(MauTheme.blue.opacity(0.13))
Image(systemName: "person.crop.circle.fill")
Expand Down Expand Up @@ -226,17 +232,25 @@ struct HomeView: View {
spacing: 12
) {
PremiumQuickCard(title: "Расписание", subtitle: "Занятия и аудитории", icon: "calendar", color: MauTheme.blue) {
ScheduleView()
selectedTab = 1
}
PremiumQuickCard(title: "Сервисы", subtitle: "Все инструменты МАУ", icon: "square.grid.2x2.fill", color: MauTheme.violet) {
ServicesView()
selectedTab = 2
}
PremiumQuickCard(title: "Новости", subtitle: "События университета", icon: "newspaper.fill", color: .orange) {
NewsView()
selectedTab = 3
}
PremiumQuickCard(title: "ЭИОС", subtitle: "Учебный портал", icon: "graduationcap.fill", color: .teal) {
InAppBrowserView(url: URL(string: "https://eios.mauniver.ru/moodle/")!, title: "ЭИОС")
NavigationLink {
InAppBrowserView(url: URL(string: "https://eios.mauniver.ru/moodle/")!, title: "Учебный портал")
} label: {
PremiumQuickCardContent(
title: "Учёба",
subtitle: "Учебный портал",
icon: "graduationcap.fill",
color: .teal
)
}
.buttonStyle(.plain)
}
}

Expand Down Expand Up @@ -298,18 +312,26 @@ private struct HomeDataErrorCard: View {
let title: String
let message: String
let icon: String
var retry: (() -> Void)?

var body: some View {
HStack(spacing: 14) {
IconTile(systemName: icon, color: .orange)
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline)
Text(message)
.font(.caption)
.foregroundStyle(MauTheme.muted)
.lineLimit(2)
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 14) {
IconTile(systemName: icon, color: .orange)
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.headline)
Text(message)
.font(.caption)
.foregroundStyle(MauTheme.muted)
.lineLimit(3)
}
Spacer()
}
if let retry {
Button("Повторить", action: retry)
.font(.subheadline.weight(.semibold))
.foregroundStyle(MauTheme.blue)
}
Spacer()
}
.padding(17)
.mauSurface(radius: 22)
Expand Down Expand Up @@ -377,36 +399,47 @@ private struct NextLessonCard: View {
}
}

private struct PremiumQuickCard<Destination: View>: View {
private struct PremiumQuickCard: View {
let title: String
let subtitle: String
let icon: String
let color: Color
@ViewBuilder let destination: () -> Destination
let action: () -> Void

var body: some View {
NavigationLink(destination: destination()) {
VStack(alignment: .leading, spacing: 15) {
HStack {
IconTile(systemName: icon, color: color)
Spacer()
Image(systemName: "arrow.up.right")
.font(.caption.bold())
.foregroundStyle(MauTheme.muted)
}
VStack(alignment: .leading, spacing: 3) {
Text(title).font(.headline)
Text(subtitle)
.font(.caption)
.foregroundStyle(MauTheme.muted)
.lineLimit(1)
}
}
.foregroundStyle(MauTheme.ink)
.frame(maxWidth: .infinity, minHeight: 105, alignment: .topLeading)
.padding(16)
Button(action: action) {
PremiumQuickCardContent(title: title, subtitle: subtitle, icon: icon, color: color)
}
.buttonStyle(.plain)
}
}

private struct PremiumQuickCardContent: View {
let title: String
let subtitle: String
let icon: String
let color: Color

var body: some View {
VStack(alignment: .leading, spacing: 15) {
HStack {
IconTile(systemName: icon, color: color)
Spacer()
Image(systemName: "arrow.up.right")
.font(.caption.bold())
.foregroundStyle(MauTheme.muted)
}
VStack(alignment: .leading, spacing: 3) {
Text(title).font(.headline)
Text(subtitle)
.font(.caption)
.foregroundStyle(MauTheme.muted)
.lineLimit(1)
}
}
.foregroundStyle(MauTheme.ink)
.frame(maxWidth: .infinity, minHeight: 105, alignment: .topLeading)
.padding(16)
.mauSurface(radius: 22)
}
}
Expand Down
2 changes: 1 addition & 1 deletion MauverseIOS/Sources/Views/InAppBrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class InAppBrowserController: NSObject, ObservableObject, WKNavigationDele

let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .default()
configuration.applicationNameForUserAgent = "MAUverse/1.12.2"
configuration.applicationNameForUserAgent = "MAUverse/1.12.3"
configuration.defaultWebpagePreferences.allowsContentJavaScript = true
configuration.allowsInlineMediaPlayback = true
webView = WKWebView(frame: .zero, configuration: configuration)
Expand Down
2 changes: 1 addition & 1 deletion MauverseIOS/Sources/Views/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct LoginView: View {
}

VStack(spacing: 16) {
TextField("Логин ЭИОС", text: $username)
TextField("Логин учебного портала", text: $username)
.textContentType(.username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Expand Down
8 changes: 3 additions & 5 deletions MauverseIOS/Sources/Views/NewsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ private struct HeroNewsCard: View {
.lineLimit(4)
.fixedSize(horizontal: false, vertical: true)
if let description = item.description {
Text(description.strippingHTML)
Text(description.cleanedForDisplay)
.font(.system(size: 13, weight: .regular))
.foregroundStyle(MauTheme.muted)
.lineLimit(2)
Expand All @@ -252,9 +252,7 @@ private struct HeroNewsCard: View {
}

private extension String {
var strippingHTML: String {
replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
.replacingOccurrences(of: "&nbsp;", with: " ")
.replacingOccurrences(of: "&amp;", with: "&")
var cleanedForDisplay: String {
HTMLTextCleaning.cleanRSS(self)
}
}
6 changes: 3 additions & 3 deletions MauverseIOS/Sources/Views/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct ProfileView: View {
.font(.subheadline)
.foregroundStyle(.white.opacity(0.72))
HStack(spacing: 8) {
Label("Сессия в Keychain", systemImage: "lock.fill")
Label("Вход сохранён", systemImage: "lock.fill")
if let group = session.user?.groupName, !group.isEmpty {
Text("•")
Text(group)
Expand Down Expand Up @@ -73,7 +73,7 @@ struct ProfileView: View {
.buttonStyle(.plain)
.mauSurface()

Text("MAUverse 1.12.2 (31)")
Text("MAUverse 1.12.3 (32)")
.font(.caption)
.foregroundStyle(MauTheme.muted)
}
Expand Down Expand Up @@ -280,7 +280,7 @@ private struct SettingsView: View {
.foregroundStyle(MauTheme.blue)
}
}
Text("Версия 1.12.2 • сборка 31").font(.caption)
Text("Версия 1.12.3 • сборка 32").font(.caption)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(20)
Expand Down
Loading
Loading