Skip to content
82 changes: 78 additions & 4 deletions Horizon/Horizon/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
"sourceLanguage" : "en",
"strings" : {
"" : {

},
" , " : {
"comment" : "A comma used to separate items in a list.",
"isCommentAutoGenerated" : true
},
"-" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -257,6 +264,10 @@
}
}
},
"," : {
"comment" : "A comma used to separate items in a list.",
"isCommentAutoGenerated" : true
},
"'s score is now available" : {
"extractionState" : "stale",
"localizations" : {
Expand Down Expand Up @@ -4633,6 +4644,10 @@
}
}
},
"Adjust your filters to see more." : {
"comment" : "A description below the title that instructs the user to adjust their filters to see more content.",
"isCommentAutoGenerated" : true
},
"Advanced" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -5400,6 +5415,9 @@
}
}
}
},
"All courses" : {

},
"All messages" : {
"localizations" : {
Expand Down Expand Up @@ -24886,6 +24904,10 @@
}
}
},
"Count of visible courses is %@" : {
"comment" : "A label displaying the number of courses that are currently visible in the list. The argument is the count of visible courses.",
"isCommentAutoGenerated" : true
},
"Course %@" : {
"comment" : "A string that can be read by VoiceOver that describes the course name. The argument is the course name.",
"isCommentAutoGenerated" : true
Expand Down Expand Up @@ -26958,6 +26980,10 @@
"comment" : "A hint that appears when hovering over the button to select a different course.",
"isCommentAutoGenerated" : true
},
"Double tap to load more courses" : {
"comment" : "An accessibility hint for the \"Show more\" button in the course list view.",
"isCommentAutoGenerated" : true
},
"Double tap to load more notifications" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -27214,11 +27240,17 @@
}
}
},
"Double tap to navigate to all courses" : {
"comment" : "A hint that describes an action to perform on a view.",
"isCommentAutoGenerated" : true
},
"Double tap to open course" : {

"comment" : "Accessibility hint text for a course in the course list.",
"isCommentAutoGenerated" : true
},
"Double tap to open learning object" : {

"comment" : "Accessibility hint string for a cell that represents a learning object.",
"isCommentAutoGenerated" : true
},
"Double tap to open skillspace" : {
"comment" : "A hint that appears when hovering over a button in the SkillListWidgetView.",
Expand Down Expand Up @@ -40559,6 +40591,10 @@
}
}
},
"In Progress" : {
"comment" : "Name of the course status when the user has completed some content.",
"isCommentAutoGenerated" : true
},
"Indiana (East) (-05:00/-04:00)" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -56726,6 +56762,10 @@
"comment" : "An accessibility label describing the content of the empty view.",
"isCommentAutoGenerated" : true
},
"No due date" : {
"comment" : "Text for the due date chip when a learning object does not have a due date.",
"isCommentAutoGenerated" : true
},
"No notification activity yet." : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -57750,6 +57790,10 @@
}
}
},
"Not Started" : {
"comment" : "Name of the course status when the user has not started the course.",
"isCommentAutoGenerated" : true
},
"Not submitted" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -58518,6 +58562,10 @@
}
}
},
"Nothing here yet" : {
"comment" : "A message displayed when there are no courses to display.",
"isCommentAutoGenerated" : true
},
"Notifications" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -60058,7 +60106,8 @@

},
"Open course" : {

"comment" : "A button that opens a course.",
"isCommentAutoGenerated" : true
},
"Open in a New Tab" : {
"localizations" : {
Expand Down Expand Up @@ -60317,7 +60366,8 @@
}
},
"Open learning object" : {

"comment" : "A button that opens a learning object.",
"isCommentAutoGenerated" : true
},
"Optional" : {
"localizations" : {
Expand Down Expand Up @@ -62374,8 +62424,16 @@
}
}
},
"Part of :" : {
"comment" : "Label for the status chip in the Program Name List Chip View.",
"isCommentAutoGenerated" : true
},
"Part of %@. " : {

},
"Part of program: %@" : {
"comment" : "A label for a chip that lists a program.",
"isCommentAutoGenerated" : true
},
"Perth (+08:00)" : {
"localizations" : {
Expand Down Expand Up @@ -72392,6 +72450,14 @@
}
}
},
"See all" : {
"comment" : "Text for a button that links to a view showing all courses.",
"isCommentAutoGenerated" : true
},
"See all courses" : {
"comment" : "A button label that links to a view showing all courses.",
"isCommentAutoGenerated" : true
},
"Select a course" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -72904,6 +72970,10 @@
}
}
},
"Selected filter is %@. Double tap to select another filter" : {
"comment" : "A hint that appears when a user taps on a filter button in the course filtering view. The argument is the name of the currently selected filter.",
"isCommentAutoGenerated" : true
},
"Send" : {
"localizations" : {
"ar" : {
Expand Down Expand Up @@ -94000,6 +94070,10 @@
}
}
},
"You’re enrolled in %d courses." : {
"comment" : "A label that announces the number of courses a user is enrolled in. The argument is the number of courses.",
"isCommentAutoGenerated" : true
},
"You’ve been enrolled in Course" : {
"localizations" : {
"ar" : {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import Core
import UIKit

enum CourseListAssembly {
static func makeViewModel(courses: [CourseCardModel]) -> CourseListViewModel {
let onTapProgram: (ProgramSwitcherModel?, WeakViewController) -> Void = { program, viewController in
AppEnvironment.shared.switchToLearnTab(with: program, from: viewController)
}
return CourseListViewModel(
courses: courses,
router: AppEnvironment.shared.router,
onTapProgram: onTapProgram
)
}

static func makeView(courses: [CourseCardModel]) -> UIViewController {
CoreHostingController(CourseListView(viewModel: makeViewModel(courses: courses)))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// This file is part of Canvas.
// Copyright (C) 2025-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import Core
import Foundation

struct CourseCardModel: Identifiable, Equatable {
let id: String
let enrollmentID: String
let name: String
let progress: Double
let programs: [CourseListWidgetModel.ProgramInfo]
let hasPrograms: Bool
let status: CourseStatus
let firstProgramID: String?
var progressPercentage: String {
let percentage = Int(progress.rounded())
return String(format: "%d%%", percentage)
}

init(course: HCourse) {
self.id = course.id
self.enrollmentID = course.enrollmentID
self.name = course.name
self.progress = course.progress
self.programs = course.programs.map { .init(id: $0.id, name: $0.name) }
self.hasPrograms = course.programs.isNotEmpty
self.status = .init(progress: progress)
self.firstProgramID = course.programs.first?.id
}

var accessibilityDescription: String {
var description = String.localizedStringWithFormat(
String(localized: "Course: %@. ", bundle: .horizon),
name
)

if programs.isNotEmpty {
let programsSeparated = programs.map(\.name).joined(separator: ", ")
if programsSeparated.isNotEmpty {
description += String.localizedStringWithFormat(
String(localized: "Part of %@. ", bundle: .horizon),
programsSeparated
)
}
}

description += String.localizedStringWithFormat(
String(localized: "Progress: %d percent complete. ", bundle: .horizon),
Int(progress.rounded())
)

return description
}
func viewProgramAccessibilityString(_ programName: String) -> String {
String.localizedStringWithFormat(
String(localized: "Open %@", bundle: .horizon),
programName
)
}

enum CourseStatus: CaseIterable {
case all
case notStarted
case inProgress
case completed

init(progress: Double) {
switch progress {
case 100.0:
self = .completed
case 0.0:
self = .notStarted
default:
self = .inProgress
}
}

var name: String {
switch self {
case .all: String(localized: "All courses", bundle: .horizon)
case .inProgress: String(localized: "In Progress", bundle: .horizon)
case .completed: String(localized: "Completed", bundle: .horizon)
case .notStarted: String(localized: "Not Started", bundle: .horizon)
}
}
}
}
Loading