Compare commits

..

2 Commits

Author SHA1 Message Date
420f49cafc Oops, you can't send emails to a noreply address 2026-02-17 00:25:24 -05:00
0c07c509f3 Add feedback submission sheet
Also fixed a bug where locations with overlapping close and open times would show "Closing Soon" for 30 minutes just to then switch to "Open" when it rolled over.
2026-02-17 00:01:55 -05:00
8 changed files with 256 additions and 10 deletions

View File

@@ -42,6 +42,16 @@ func parseMultiOpenStatus(diningTimes: [DiningTimes]?, referenceTime: Date) -> O
closeTime: diningTimes[i].closeTime, closeTime: diningTimes[i].closeTime,
referenceTime: referenceTime referenceTime: referenceTime
) )
// If we're closing soon for the current opening period, but then we're opening again at the same time we close in the next
// period, we should just stick with "Open" because showing "Closing Soon" for 30 minutes and then going straight back to
// "Open" is kinda confusing. This issue has been observed with Petals specifically.
if openStatus == .closingSoon && i != diningTimes.indices.last {
if diningTimes[i].closeTime == diningTimes[i + 1].openTime {
openStatus = .open
}
}
// If the first event pass came back closed, loop again in case a later event has a different status. This is mostly to // If the first event pass came back closed, loop again in case a later event has a different status. This is mostly to
// accurately catch Gracie's/Brick City Cafe's multiple open periods each day. // accurately catch Gracie's/Brick City Cafe's multiple open periods each day.
if openStatus != .closed { if openStatus != .closed {

View File

@@ -57,8 +57,8 @@
membershipExceptions = ( membershipExceptions = (
Components/SharedUtils.swift, Components/SharedUtils.swift,
Components/TigerCenterParsers.swift, Components/TigerCenterParsers.swift,
Data/Static/FDMPMealPeriods.swift, Data/Constant/FDMPMealPeriods.swift,
Data/Static/TCtoFDMPMap.swift, Data/Constant/TCtoFDMPMap.swift,
Types/TigerCenterTypes.swift, Types/TigerCenterTypes.swift,
); );
target = 374CDA572F10A19500D8C50A /* TigerDineWidgets */; target = 374CDA572F10A19500D8C50A /* TigerDineWidgets */;
@@ -292,7 +292,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 32; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TigerDineWidgets/Info.plist; INFOPLIST_FILE = TigerDineWidgets/Info.plist;
@@ -304,7 +304,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining.Widgets"; PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining.Widgets";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -325,7 +325,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDineWidgets/TigerDineWidgets.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 32; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TigerDineWidgets/Info.plist; INFOPLIST_FILE = TigerDineWidgets/Info.plist;
@@ -337,7 +337,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining.Widgets"; PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining.Widgets";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -481,7 +481,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 32; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -500,7 +500,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining"; PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -518,7 +518,7 @@
CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements; CODE_SIGN_ENTITLEMENTS = TigerDine/TigerDine.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 32; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 5GF7GKNTK4; DEVELOPMENT_TEAM = 5GF7GKNTK4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -537,7 +537,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.0; MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining"; PRODUCT_BUNDLE_IDENTIFIER = "dev.ninjacheetah.RIT-Dining";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -19,6 +19,7 @@ struct ContentView: View {
@State private var loadFailed: Bool = false @State private var loadFailed: Bool = false
@State private var showingDonationSheet: Bool = false @State private var showingDonationSheet: Bool = false
@State private var showingFeedbackSheet: Bool = false
@State private var searchText: String = "" @State private var searchText: String = ""
@State private var path = NavigationPath() @State private var path = NavigationPath()
@@ -149,6 +150,11 @@ struct ContentView: View {
Image(systemName: "info.circle") Image(systemName: "info.circle")
Text("About") Text("About")
} }
Button(action: {
showingFeedbackSheet = true
}) {
Label("Feedback", systemImage: "paperplane")
}
Button(action: { Button(action: {
showingDonationSheet = true showingDonationSheet = true
}) { }) {
@@ -187,6 +193,9 @@ struct ContentView: View {
.sheet(isPresented: $showingDonationSheet) { .sheet(isPresented: $showingDonationSheet) {
DonationView() DonationView()
} }
.sheet(isPresented: $showingFeedbackSheet) {
FeedbackView()
}
} }
} }

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct DonationView: View { struct DonationView: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.openURL) private var openURL @Environment(\.openURL) private var openURL
@State private var symbolDrawn: Bool = true @State private var symbolDrawn: Bool = true
var body: some View { var body: some View {

View File

@@ -0,0 +1,170 @@
//
// FeedbackView.swift
// TigerDine
//
// Created by Campbell on 2/16/26.
//
import SwiftUI
import MessageUI
struct FeedbackView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.openURL) private var openURL
@State private var showingMailView = false
@State private var mailResult: Result<MFMailComposeResult, Error>? = nil
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "paperplane")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundStyle(Color.accentColor)
Text("Submit Feedback")
.fontWeight(.bold)
.font(.title)
}
Text("Did I break something? Oops.")
Text("Or maybe you just have a suggestion to make TigerDine even cooler. Either way, I'd love to hear your feedback! (Or maybe the hours for a location are off, in which case that feedback is RIT's to handle.)")
.foregroundStyle(.secondary)
Text("Incorrect Location Hours")
.padding(.top, 12)
Button(action: {
openURL(URL(string: "https://www.rit.edu/dining/locations")!)
}) {
HStack(alignment: .center) {
Image(systemName: "clock.badge.questionmark")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading) {
Text("Confirm Against the RIT Website")
.fontWeight(.bold)
Text("Check that the hours displayed in TigerDine match RIT's website.")
.foregroundStyle(.secondary)
.multilineTextAlignment(.leading)
}
Spacer()
Image(systemName: "chevron.forward")
}
.padding(.all, 6)
.background (
RoundedRectangle(cornerRadius: 8)
.fill(Color.secondary.opacity(0.1))
)
}
.buttonStyle(.plain)
Button(action: {
openURL(URL(string: "https://www.rit.edu/its/support")!)
}) {
HStack(alignment: .center) {
Image(systemName: "clock.badge.exclamationmark")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading) {
Text("Submit an ITS Ticket")
.fontWeight(.bold)
Text("If hours are also incorrect on RIT's website, submit a ticket to ITS.")
.foregroundStyle(.secondary)
.multilineTextAlignment(.leading)
}
Spacer()
Image(systemName: "chevron.forward")
}
.padding(.all, 6)
.background (
RoundedRectangle(cornerRadius: 8)
.fill(Color.secondary.opacity(0.1))
)
}
.buttonStyle(.plain)
Text("If the hours do not match between TigerDine and RIT's website, please contact me instead and I'll look into it.")
.foregroundStyle(.secondary)
.font(.caption)
Text("TigerDine Issues and Feedback")
.padding(.top, 12)
Button(action: {
openURL(URL(string: "https://github.com/NinjaCheetah/TigerDine/issues")!)
}) {
HStack(alignment: .center) {
Image(systemName: "ant.circle")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading) {
Text("Submit a GitHub Issue")
.fontWeight(.bold)
Text("Report a bug or suggest a feature on TigerDine's GitHub repository.")
.foregroundStyle(.secondary)
.multilineTextAlignment(.leading)
}
Spacer()
Image(systemName: "chevron.forward")
}
.padding(.all, 6)
.background (
RoundedRectangle(cornerRadius: 8)
.fill(Color.secondary.opacity(0.1))
)
}
.buttonStyle(.plain)
Button(action: {
showingMailView = true
}) {
HStack(alignment: .center) {
Image(systemName: "envelope.circle")
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading) {
Text("Send Me an Email")
.fontWeight(.bold)
Text("Not a GitHub user? Feel free to submit feedback via email.")
.foregroundStyle(.secondary)
.multilineTextAlignment(.leading)
}
Spacer()
Image(systemName: "chevron.forward")
}
.padding(.all, 6)
.background (
RoundedRectangle(cornerRadius: 8)
.fill(Color.secondary.opacity(0.1))
)
}
.buttonStyle(.plain)
.disabled(!MailView.canSendMail())
.sheet(isPresented: $showingMailView) {
MailView(result: $mailResult)
}
Text("Just don't spam my inbox, please and thank you.")
.foregroundStyle(.secondary)
.font(.caption)
}
.frame(maxWidth: .infinity)
.toolbar {
Button(action: {
dismiss()
}) {
Image(systemName: "xmark")
}
}
}
.padding(.horizontal, 16)
}
}
}
#Preview {
FeedbackView()
}

View File

@@ -0,0 +1,56 @@
//
// MailView.swift
// TigerDine
//
// Created by Campbell on 2/16/26.
//
import SwiftUI
import MessageUI
// More gross yucky UIKit code :(
// Unfortunately there's no native SwiftUI MailView.
struct MailView: UIViewControllerRepresentable {
@Environment(\.dismiss) var dismiss
@Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
var parent: MailView
init(_ parent: MailView) {
self.parent = parent
}
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
defer {
parent.dismiss()
}
if let error = error {
parent.result = .failure(error)
} else {
parent.result = .success(result)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
vc.setToRecipients(["campbell@ninjacheetah.dev"])
vc.setSubject("TigerDine Feedback")
//vc.setMessageBody("", isHTML: false)
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {
}
static func canSendMail() -> Bool {
return MFMailComposeViewController.canSendMail()
}
}