Skip to content
Closed
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
28 changes: 28 additions & 0 deletions PayForMe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
48E7F75B2403FD6B000CE4E6 /* FancyLoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E7F75A2403FD6B000CE4E6 /* FancyLoadingButton.swift */; };
56E7DB46FDCDC5B46C475020 /* BalanceCalculationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 143F41EE78329905894C8A29 /* BalanceCalculationTests.swift */; };
57BC8D25B91CE40F4669650C /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0865D2DAF541B24078D1DC41 /* TestHelpers.swift */; };
57C3F60411D3F3343CB70831 /* CategoryClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF8E2876A59F196BE1C24F0 /* CategoryClassifier.swift */; };
62475CF1301F718C0097B7C9 /* CospendTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62475CF0301F718C0097B7C9 /* CospendTag.swift */; };
62A13D412FA3AB760036EE34 /* ShareProjectQRCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A13D402FA3AB700036EE34 /* ShareProjectQRCodeViewModel.swift */; };
62C73597878BA5AD4572FE90 /* BillSortingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92ABB02BEA28BEA7C8C64841 /* BillSortingTests.swift */; };
Expand All @@ -28,6 +29,7 @@
6523FC512558153600BCD843 /* AddPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6523FC502558153600BCD843 /* AddPasswordView.swift */; };
6524D60225385E0800B2AA7B /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = 6524D60125385E0800B2AA7B /* GRDB */; };
6533A02B23D888B100FF822B /* BillCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6533A02A23D888B100FF822B /* BillCell.swift */; };
AF3C149C69191129E15F7CA7 /* CategoryChip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D1989BA96A4911A60FD3F3 /* CategoryChip.swift */; };
654754B82528736F00A82EB6 /* Views+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654754B72528736F00A82EB6 /* Views+Extensions.swift */; };
654754C0252889B200A82EB6 /* ProjectQRPermissionCheckerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654754BF252889B200A82EB6 /* ProjectQRPermissionCheckerView.swift */; };
654754C8252899FF00A82EB6 /* FancyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654754C7252899FF00A82EB6 /* FancyButton.swift */; };
Expand All @@ -45,6 +47,7 @@
65662A1D23D7353400303207 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65662A1C23D7353400303207 /* Server.swift */; };
65662A2223D75C5F00303207 /* AddProjectManualView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65662A2123D75C5F00303207 /* AddProjectManualView.swift */; };
65662A2423D83ED100303207 /* StorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65662A2323D83ED100303207 /* StorageService.swift */; };
F210835FF66EAB36C4EF24E5 /* LocalCategoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16973AEB3337A370B125D368 /* LocalCategoryStore.swift */; };
65662A2623D8546900303207 /* ProjectList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65662A2523D8546900303207 /* ProjectList.swift */; };
656A2AD7241BB4F300786A1E /* PayForMeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A2AD6241BB4F300786A1E /* PayForMeUITests.swift */; };
656A2ADF241BBAF200786A1E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656A2ADE241BBAF200786A1E /* SnapshotHelper.swift */; };
Expand All @@ -67,8 +70,12 @@
65C9E2D023E1B96B00814B37 /* BalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C9E2CF23E1B96B00814B37 /* BalanceViewModel.swift */; };
65D225F32428038500C3F1EC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 65D225F52428038500C3F1EC /* Localizable.strings */; };
65D9094025581BE100351F4B /* AddFromURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D9093F25581BE100351F4B /* AddFromURLView.swift */; };
A7089D0A97757AB90C9CB97E /* CategorySupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A391473E9D9DE0002A4D8F46 /* CategorySupportTests.swift */; };
27BA4EFA758D236CE23AEDA7 /* LocalCategoryStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFD0B11814133F77ACBA4A3 /* LocalCategoryStoreTests.swift */; };
A9A3E941FF5A941B353C8928 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5CE07E061AEA59F79C0E31 /* AppSettings.swift */; };
BF4D7AF265FC4A90B1AC5AEB /* JSONDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F4D55C3EF5EFCD925428041 /* JSONDecodingTests.swift */; };
D279295CFA25D9CC69C6F27C /* BillTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EED3194A2342518B0E74079 /* BillTests.swift */; };
D2B9E84A59C861734B317D95 /* TextMatchUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DE3B9BA48181BBB1E6664F /* TextMatchUtils.swift */; };
DBBD86EF399BCF232C021935 /* NetworkRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316FBA7C0CE4ABB51EC2A121 /* NetworkRequestTests.swift */; };
/* End PBXBuildFile section */

Expand All @@ -92,6 +99,7 @@
/* Begin PBXFileReference section */
0865D2DAF541B24078D1DC41 /* TestHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TestHelpers.swift; sourceTree = "<group>"; };
143F41EE78329905894C8A29 /* BalanceCalculationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BalanceCalculationTests.swift; sourceTree = "<group>"; };
2A5CE07E061AEA59F79C0E31 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
316FBA7C0CE4ABB51EC2A121 /* NetworkRequestTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkRequestTests.swift; sourceTree = "<group>"; };
481FB4FA23E964C8003BD108 /* ProjectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectManager.swift; sourceTree = "<group>"; };
481FB4FC23EAD78F003BD108 /* AddProjectManualViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProjectManualViewModel.swift; sourceTree = "<group>"; };
Expand All @@ -111,6 +119,7 @@
6523FC4B25580EEF00BCD843 /* UrlExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlExtensionsTests.swift; sourceTree = "<group>"; };
6523FC502558153600BCD843 /* AddPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPasswordView.swift; sourceTree = "<group>"; };
6533A02A23D888B100FF822B /* BillCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillCell.swift; sourceTree = "<group>"; };
08D1989BA96A4911A60FD3F3 /* CategoryChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryChip.swift; sourceTree = "<group>"; };
654754B72528736F00A82EB6 /* Views+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Views+Extensions.swift"; sourceTree = "<group>"; };
654754BF252889B200A82EB6 /* ProjectQRPermissionCheckerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectQRPermissionCheckerView.swift; sourceTree = "<group>"; };
654754C7252899FF00A82EB6 /* FancyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyButton.swift; sourceTree = "<group>"; };
Expand All @@ -132,6 +141,7 @@
65662A1C23D7353400303207 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
65662A2123D75C5F00303207 /* AddProjectManualView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProjectManualView.swift; sourceTree = "<group>"; };
65662A2323D83ED100303207 /* StorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageService.swift; sourceTree = "<group>"; };
16973AEB3337A370B125D368 /* LocalCategoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCategoryStore.swift; sourceTree = "<group>"; };
65662A2523D8546900303207 /* ProjectList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectList.swift; sourceTree = "<group>"; };
656A2AD4241BB4F300786A1E /* PayForMeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PayForMeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
656A2AD6241BB4F300786A1E /* PayForMeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayForMeUITests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -160,9 +170,13 @@
65D225F7242803C800C3F1EC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
65D9093F25581BE100351F4B /* AddFromURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFromURLView.swift; sourceTree = "<group>"; };
65FE0E8E2544968B002B80CF /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
6FF8E2876A59F196BE1C24F0 /* CategoryClassifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryClassifier.swift; sourceTree = "<group>"; };
7EED3194A2342518B0E74079 /* BillTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BillTests.swift; sourceTree = "<group>"; };
8F4D55C3EF5EFCD925428041 /* JSONDecodingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = JSONDecodingTests.swift; sourceTree = "<group>"; };
92ABB02BEA28BEA7C8C64841 /* BillSortingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BillSortingTests.swift; sourceTree = "<group>"; };
A391473E9D9DE0002A4D8F46 /* CategorySupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategorySupportTests.swift; sourceTree = "<group>"; };
BBFD0B11814133F77ACBA4A3 /* LocalCategoryStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalCategoryStoreTests.swift; sourceTree = "<group>"; };
B0DE3B9BA48181BBB1E6664F /* TextMatchUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMatchUtils.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -236,6 +250,8 @@
6523FC4B25580EEF00BCD843 /* UrlExtensionsTests.swift */,
0865D2DAF541B24078D1DC41 /* TestHelpers.swift */,
7EED3194A2342518B0E74079 /* BillTests.swift */,
A391473E9D9DE0002A4D8F46 /* CategorySupportTests.swift */,
BBFD0B11814133F77ACBA4A3 /* LocalCategoryStoreTests.swift */,
8F4D55C3EF5EFCD925428041 /* JSONDecodingTests.swift */,
143F41EE78329905894C8A29 /* BalanceCalculationTests.swift */,
92ABB02BEA28BEA7C8C64841 /* BillSortingTests.swift */,
Expand Down Expand Up @@ -311,6 +327,7 @@
children = (
65662A1A23D733B700303207 /* NetworkService.swift */,
65662A2323D83ED100303207 /* StorageService.swift */,
16973AEB3337A370B125D368 /* LocalCategoryStore.swift */,
481FB4FA23E964C8003BD108 /* ProjectManager.swift */,
);
path = Services;
Expand Down Expand Up @@ -370,6 +387,7 @@
children = (
65005A0323DE0C5C00D1D599 /* BillList.swift */,
6533A02A23D888B100FF822B /* BillCell.swift */,
08D1989BA96A4911A60FD3F3 /* CategoryChip.swift */,
6591C8E6240542250054BA24 /* LoadingDots.swift */,
6591C8E2240527750054BA24 /* PersonsView.swift */,
656E8CB723D9FDA300B3ED10 /* BillListViewModel.swift */,
Expand All @@ -382,6 +400,9 @@
children = (
489D35F923E081120029F59F /* Util.swift */,
65A65E3E2527529D00C86533 /* Combine.swift */,
2A5CE07E061AEA59F79C0E31 /* AppSettings.swift */,
6FF8E2876A59F196BE1C24F0 /* CategoryClassifier.swift */,
B0DE3B9BA48181BBB1E6664F /* TextMatchUtils.swift */,
654754B72528736F00A82EB6 /* Views+Extensions.swift */,
);
path = Util;
Expand Down Expand Up @@ -553,6 +574,8 @@
6523FC4C25580EEF00BCD843 /* UrlExtensionsTests.swift in Sources */,
57BC8D25B91CE40F4669650C /* TestHelpers.swift in Sources */,
D279295CFA25D9CC69C6F27C /* BillTests.swift in Sources */,
A7089D0A97757AB90C9CB97E /* CategorySupportTests.swift in Sources */,
27BA4EFA758D236CE23AEDA7 /* LocalCategoryStoreTests.swift in Sources */,
BF4D7AF265FC4A90B1AC5AEB /* JSONDecodingTests.swift in Sources */,
56E7DB46FDCDC5B46C475020 /* BalanceCalculationTests.swift in Sources */,
62C73597878BA5AD4572FE90 /* BillSortingTests.swift in Sources */,
Expand Down Expand Up @@ -580,11 +603,15 @@
65662A0323D7320000303207 /* AppDelegate.swift in Sources */,
654754C0252889B200A82EB6 /* ProjectQRPermissionCheckerView.swift in Sources */,
6533A02B23D888B100FF822B /* BillCell.swift in Sources */,
AF3C149C69191129E15F7CA7 /* CategoryChip.swift in Sources */,
48CF709B240FF82000770B97 /* AddMemberView.swift in Sources */,
65D9094025581BE100351F4B /* AddFromURLView.swift in Sources */,
658FC55925273E4D004A49AA /* AddProjectQRViewModel.swift in Sources */,
489D35FA23E081120029F59F /* Util.swift in Sources */,
65A65E3F2527529D00C86533 /* Combine.swift in Sources */,
A9A3E941FF5A941B353C8928 /* AppSettings.swift in Sources */,
57C3F60411D3F3343CB70831 /* CategoryClassifier.swift in Sources */,
D2B9E84A59C861734B317D95 /* TextMatchUtils.swift in Sources */,
62475CF1301F718C0097B7C9 /* CospendTag.swift in Sources */,
489995DA23F6EC6F008B7E38 /* OnboardingView.swift in Sources */,
6591C8E524052C240054BA24 /* CommunicationIndicator.swift in Sources */,
Expand All @@ -604,6 +631,7 @@
658FC54B25272639004A49AA /* AddProjectQRView.swift in Sources */,
481FB4FD23EAD78F003BD108 /* AddProjectManualViewModel.swift in Sources */,
65662A2423D83ED100303207 /* StorageService.swift in Sources */,
F210835FF66EAB36C4EF24E5 /* LocalCategoryStore.swift in Sources */,
655DD2AE2993DACB00E06675 /* ProjectListEntry.swift in Sources */,
656E8CB823D9FDA300B3ED10 /* BillListViewModel.swift in Sources */,
62A13D412FA3AB760036EE34 /* ShareProjectQRCodeViewModel.swift in Sources */,
Expand Down
11 changes: 10 additions & 1 deletion PayForMe/Model/Bill.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ struct Bill: Codable, Identifiable, Hashable {
var paymentmodeid: Int?
var lastchanged: Int?

func paramsFor(_ backend: ProjectBackend) -> [String: Any] {
/// `features` are the capabilities an iHateMoney server announces; category fields are only
/// sent to servers that know them (stock iHateMoney has none).
func paramsFor(_ backend: ProjectBackend, features: Set<String> = []) -> [String: Any] {
var dict: [String: Any] = [
"date": DateFormatter.cospend.string(from: date),
"what": what,
Expand All @@ -40,6 +42,13 @@ struct Bill: Codable, Identifiable, Hashable {
}
if backend == .iHateMoney {
dict["payed_for"] = owers.map { $0.id.description }
// The fork's form treats "" as "none" and rejects "0".
if features.contains("categoryid") {
dict["categoryid"] = (categoryid ?? 0) == 0 ? "" : categoryid!.description
}
if features.contains("paymentmodes") {
dict["paymentmodeid"] = (paymentmodeid ?? 0) == 0 ? "" : paymentmodeid!.description
}
}

return dict
Expand Down
50 changes: 48 additions & 2 deletions PayForMe/Model/CospendTag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,76 @@ struct CospendTag: Codable, Identifiable, Hashable {

var label: String {
guard let icon = icon, !icon.isEmpty else { return displayName }
return "\(icon) \(displayName)"
return "\(Self.emojiPresentation(icon)) \(displayName)"
}

/// Server colors come as "#rrggbb" or "rrggbb"; empty or malformed values yield nil.
var rgb: (red: Double, green: Double, blue: Double)? {
guard var hex = color?.trimmingCharacters(in: .whitespaces) else { return nil }
if hex.hasPrefix("#") { hex.removeFirst() }
guard hex.count == 6, let value = UInt32(hex, radix: 16) else { return nil }
return (Double((value >> 16) & 0xFF) / 255, Double((value >> 8) & 0xFF) / 255, Double(value & 0xFF) / 255)
}

/// Emoji such as 🌩 or πŸ› default to a small monochrome text glyph unless followed by the
/// emoji variation selector (U+FE0F), which servers usually leave out. Add it where missing.
static func emojiPresentation(_ icon: String) -> String {
var result = ""
for character in icon {
result.append(character)
guard let first = character.unicodeScalars.first,
character.unicodeScalars.count == 1,
first.properties.isEmoji, !first.properties.isEmojiPresentation, first.value > 0xFF
else { continue }
result.unicodeScalars.append("\u{FE0F}")
}
return result
}

/// Cospend's built-in categories (negative ids). Servers do not list them in the project
/// info, but accept them as `categoryid`: Nextcloud Cospend and iHateMoney category forks.
/// -11 (Reimbursement) is not a category.
static let globalCategories: [CospendTag] = [
CospendTag(id: -1, name: "Grocery", color: "#ffaa00", icon: "πŸ›’", order: nil),
CospendTag(id: -2, name: "Bar/Party", color: "#aa55ff", icon: "πŸŽ‰", order: nil),
CospendTag(id: -3, name: "Rent", color: "#da8733", icon: "🏠", order: nil),
CospendTag(id: -4, name: "Bill", color: "#4aa6b0", icon: "🌩", order: nil),
CospendTag(id: -5, name: "Excursion/Culture", color: "#0055ff", icon: "🚸", order: nil),
CospendTag(id: -6, name: "Health", color: "#bf090c", icon: "πŸ’š", order: nil),
CospendTag(id: -10, name: "Shopping", color: "#e167d1", icon: "πŸ›", order: nil),
CospendTag(id: -12, name: "Restaurant", color: "#d0d5e1", icon: "🍴", order: nil),
CospendTag(id: -13, name: "Accommodation", color: "#5de1a3", icon: "πŸ›Œ", order: nil),
CospendTag(id: -14, name: "Transport", color: "#6f2ee1", icon: "🚌", order: nil),
CospendTag(id: -15, name: "Sport", color: "#69e177", icon: "🎾", order: nil),
]
}

struct CospendProjectTags: Decodable {
let categories: [CospendTag]
let paymentModes: [CospendTag]
/// Optional capability list of iHateMoney category forks (`categoryid`, `categories`,
/// `paymentmodes`, ...). Empty for Cospend and for stock iHateMoney.
let features: Set<String>

static let empty = CospendProjectTags(categories: [], paymentModes: [])

init(categories: [CospendTag], paymentModes: [CospendTag]) {
init(categories: [CospendTag], paymentModes: [CospendTag], features: Set<String> = []) {
self.categories = categories
self.paymentModes = paymentModes
self.features = features
}

private enum CodingKeys: String, CodingKey {
case categories
case paymentmodes
case features
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
categories = Self.decodeList(from: container, forKey: .categories)
paymentModes = Self.decodeList(from: container, forKey: .paymentmodes)
features = Set((try? container.decode([String].self, forKey: .features)) ?? [])
}

private static func decodeList(from container: KeyedDecodingContainer<CodingKeys>, forKey key: CodingKeys) -> [CospendTag] {
Expand Down
Loading