414 lines
12 KiB
Dart
414 lines
12 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
/// Preset avatar options available for all riders without external image hosting.
|
|
class PresetAvatar {
|
|
final String id;
|
|
final String label;
|
|
final int primaryColorHex;
|
|
final int secondaryColorHex;
|
|
|
|
const PresetAvatar({
|
|
required this.id,
|
|
required this.label,
|
|
required this.primaryColorHex,
|
|
required this.secondaryColorHex,
|
|
});
|
|
|
|
static const List<PresetAvatar> availablePresets = [
|
|
PresetAvatar(
|
|
id: 'helmet_cyan',
|
|
label: 'Cyan Apex',
|
|
primaryColorHex: 0xFF00E5FF,
|
|
secondaryColorHex: 0xFF003840,
|
|
),
|
|
PresetAvatar(
|
|
id: 'helmet_amber',
|
|
label: 'Amber Sector',
|
|
primaryColorHex: 0xFFFFB300,
|
|
secondaryColorHex: 0xFF4A3200,
|
|
),
|
|
PresetAvatar(
|
|
id: 'tach_neon',
|
|
label: 'Neon Tach',
|
|
primaryColorHex: 0xFF00E676,
|
|
secondaryColorHex: 0xFF00381B,
|
|
),
|
|
PresetAvatar(
|
|
id: 'speed_violet',
|
|
label: 'Violet Velocity',
|
|
primaryColorHex: 0xFFD500F9,
|
|
secondaryColorHex: 0xFF3E004A,
|
|
),
|
|
PresetAvatar(
|
|
id: 'cruiser_red',
|
|
label: 'Crimson Cruiser',
|
|
primaryColorHex: 0xFFFF1744,
|
|
secondaryColorHex: 0xFF4A0513,
|
|
),
|
|
PresetAvatar(
|
|
id: 'adv_blue',
|
|
label: 'Adventure Azure',
|
|
primaryColorHex: 0xFF2979FF,
|
|
secondaryColorHex: 0xFF0A224A,
|
|
),
|
|
PresetAvatar(
|
|
id: 'cafe_gold',
|
|
label: 'Cafe Gold',
|
|
primaryColorHex: 0xFFFFD700,
|
|
secondaryColorHex: 0xFF4A3E00,
|
|
),
|
|
PresetAvatar(
|
|
id: 'touring_slate',
|
|
label: 'Touring Slate',
|
|
primaryColorHex: 0xFF90A4AE,
|
|
secondaryColorHex: 0xFF263238,
|
|
),
|
|
];
|
|
|
|
static PresetAvatar byId(String? id) {
|
|
if (id == null) return availablePresets.first;
|
|
return availablePresets.firstWhere(
|
|
(p) => p.id == id,
|
|
orElse: () => availablePresets.first,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Privacy controls for a rider profile.
|
|
@immutable
|
|
class ProfilePrivacySettings {
|
|
final bool isSearchableByHandle;
|
|
final bool isProfilePublic;
|
|
final bool allowFriendRequests;
|
|
|
|
const ProfilePrivacySettings({
|
|
this.isSearchableByHandle = true,
|
|
this.isProfilePublic = true,
|
|
this.allowFriendRequests = true,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'isSearchableByHandle': isSearchableByHandle,
|
|
'isProfilePublic': isProfilePublic,
|
|
'allowFriendRequests': allowFriendRequests,
|
|
};
|
|
|
|
factory ProfilePrivacySettings.fromJson(Map<String, dynamic>? json) {
|
|
if (json == null) return const ProfilePrivacySettings();
|
|
return ProfilePrivacySettings(
|
|
isSearchableByHandle: json['isSearchableByHandle'] as bool? ?? true,
|
|
isProfilePublic: json['isProfilePublic'] as bool? ?? true,
|
|
allowFriendRequests: json['allowFriendRequests'] as bool? ?? true,
|
|
);
|
|
}
|
|
|
|
ProfilePrivacySettings copyWith({
|
|
bool? isSearchableByHandle,
|
|
bool? isProfilePublic,
|
|
bool? allowFriendRequests,
|
|
}) {
|
|
return ProfilePrivacySettings(
|
|
isSearchableByHandle: isSearchableByHandle ?? this.isSearchableByHandle,
|
|
isProfilePublic: isProfilePublic ?? this.isProfilePublic,
|
|
allowFriendRequests: allowFriendRequests ?? this.allowFriendRequests,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is ProfilePrivacySettings &&
|
|
runtimeType == other.runtimeType &&
|
|
isSearchableByHandle == other.isSearchableByHandle &&
|
|
isProfilePublic == other.isProfilePublic &&
|
|
allowFriendRequests == other.allowFriendRequests;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
isSearchableByHandle.hashCode ^
|
|
isProfilePublic.hashCode ^
|
|
allowFriendRequests.hashCode;
|
|
}
|
|
|
|
/// Public rider profile decoupled from authentication credentials and cloud ride ownership.
|
|
@immutable
|
|
class RiderProfile {
|
|
final String uid;
|
|
final String handle;
|
|
final String displayName;
|
|
final String bio;
|
|
final String region;
|
|
final String? avatarPreset;
|
|
final String? avatarUrl;
|
|
final ProfilePrivacySettings privacySettings;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
const RiderProfile({
|
|
required this.uid,
|
|
required this.handle,
|
|
required this.displayName,
|
|
this.bio = '',
|
|
this.region = '',
|
|
this.avatarPreset,
|
|
this.avatarUrl,
|
|
this.privacySettings = const ProfilePrivacySettings(),
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
String get displayHandle => '@$handle';
|
|
|
|
PresetAvatar get preset => PresetAvatar.byId(avatarPreset);
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': uid,
|
|
'username': handle,
|
|
'displayName': displayName,
|
|
'bio': bio,
|
|
'location': region,
|
|
'profilePhotoUrl': avatarUrl,
|
|
'avatarPreset': avatarPreset,
|
|
'privacySettings': privacySettings.toJson(),
|
|
'createdAt': createdAt.toIso8601String(),
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
};
|
|
|
|
factory RiderProfile.fromJson(Map<String, dynamic> json) {
|
|
final created = json['createdAt'] != null
|
|
? DateTime.tryParse(json['createdAt'] as String) ?? DateTime.now()
|
|
: DateTime.now();
|
|
final updated = json['updatedAt'] != null
|
|
? DateTime.tryParse(json['updatedAt'] as String) ?? created
|
|
: created;
|
|
|
|
return RiderProfile(
|
|
uid: json['id'] as String? ?? '',
|
|
handle: (json['username'] as String? ?? '').toLowerCase().trim(),
|
|
displayName: json['displayName'] as String? ?? '',
|
|
bio: json['bio'] as String? ?? '',
|
|
region: json['location'] as String? ?? '',
|
|
avatarPreset: json['avatarPreset'] as String?,
|
|
avatarUrl: json['profilePhotoUrl'] as String?,
|
|
privacySettings: ProfilePrivacySettings.fromJson(
|
|
json['privacySettings'] as Map<String, dynamic>?,
|
|
),
|
|
createdAt: created,
|
|
updatedAt: updated,
|
|
);
|
|
}
|
|
|
|
RiderProfile copyWith({
|
|
String? uid,
|
|
String? handle,
|
|
String? displayName,
|
|
String? bio,
|
|
String? region,
|
|
String? avatarPreset,
|
|
String? avatarUrl,
|
|
ProfilePrivacySettings? privacySettings,
|
|
DateTime? createdAt,
|
|
DateTime? updatedAt,
|
|
}) {
|
|
return RiderProfile(
|
|
uid: uid ?? this.uid,
|
|
handle: handle ?? this.handle,
|
|
displayName: displayName ?? this.displayName,
|
|
bio: bio ?? this.bio,
|
|
region: region ?? this.region,
|
|
avatarPreset: avatarPreset ?? this.avatarPreset,
|
|
avatarUrl: avatarUrl ?? this.avatarUrl,
|
|
privacySettings: privacySettings ?? this.privacySettings,
|
|
createdAt: createdAt ?? this.createdAt,
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is RiderProfile &&
|
|
runtimeType == other.runtimeType &&
|
|
uid == other.uid &&
|
|
handle == other.handle &&
|
|
displayName == other.displayName &&
|
|
bio == other.bio &&
|
|
region == other.region &&
|
|
avatarPreset == other.avatarPreset &&
|
|
avatarUrl == other.avatarUrl &&
|
|
privacySettings == other.privacySettings;
|
|
|
|
@override
|
|
int get hashCode =>
|
|
uid.hashCode ^
|
|
handle.hashCode ^
|
|
displayName.hashCode ^
|
|
bio.hashCode ^
|
|
region.hashCode ^
|
|
avatarPreset.hashCode ^
|
|
avatarUrl.hashCode ^
|
|
privacySettings.hashCode;
|
|
}
|
|
|
|
/// Relationship status between the active rider and another rider.
|
|
enum FriendshipStatus {
|
|
self,
|
|
none,
|
|
friend,
|
|
requestIncoming,
|
|
requestOutgoing,
|
|
blocked,
|
|
}
|
|
|
|
/// Mutual friend summary.
|
|
@immutable
|
|
class FriendSummary {
|
|
final String uid;
|
|
final String handle;
|
|
final String displayName;
|
|
final String? avatarPreset;
|
|
final String? avatarUrl;
|
|
final DateTime friendedAt;
|
|
|
|
const FriendSummary({
|
|
required this.uid,
|
|
required this.handle,
|
|
required this.displayName,
|
|
this.avatarPreset,
|
|
this.avatarUrl,
|
|
required this.friendedAt,
|
|
});
|
|
|
|
String get displayHandle => '@$handle';
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'uid': uid,
|
|
'handle': handle,
|
|
'displayName': displayName,
|
|
'avatarPreset': avatarPreset,
|
|
'avatarUrl': avatarUrl,
|
|
'friendedAt': friendedAt.toIso8601String(),
|
|
};
|
|
|
|
factory FriendSummary.fromJson(Map<String, dynamic> json) => FriendSummary(
|
|
uid: json['uid'] as String? ?? '',
|
|
handle: json['handle'] as String? ?? '',
|
|
displayName: json['displayName'] as String? ?? '',
|
|
avatarPreset: json['avatarPreset'] as String?,
|
|
avatarUrl: json['avatarUrl'] as String?,
|
|
friendedAt: json['friendedAt'] != null
|
|
? DateTime.tryParse(json['friendedAt'] as String) ?? DateTime.now()
|
|
: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
/// Incoming or outgoing friend request.
|
|
@immutable
|
|
class FriendRequest {
|
|
final String requestId;
|
|
final String senderUid;
|
|
final String senderHandle;
|
|
final String senderDisplayName;
|
|
final String? senderAvatarPreset;
|
|
final String? senderAvatarUrl;
|
|
final String receiverUid;
|
|
final DateTime sentAt;
|
|
|
|
const FriendRequest({
|
|
required this.requestId,
|
|
required this.senderUid,
|
|
required this.senderHandle,
|
|
required this.senderDisplayName,
|
|
this.senderAvatarPreset,
|
|
this.senderAvatarUrl,
|
|
required this.receiverUid,
|
|
required this.sentAt,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'requestId': requestId,
|
|
'senderUid': senderUid,
|
|
'senderHandle': senderHandle,
|
|
'senderDisplayName': senderDisplayName,
|
|
'senderAvatarPreset': senderAvatarPreset,
|
|
'senderAvatarUrl': senderAvatarUrl,
|
|
'receiverUid': receiverUid,
|
|
'sentAt': sentAt.toIso8601String(),
|
|
};
|
|
|
|
factory FriendRequest.fromJson(Map<String, dynamic> json) => FriendRequest(
|
|
requestId: json['requestId'] as String? ?? '',
|
|
senderUid: json['senderUid'] as String? ?? '',
|
|
senderHandle: json['senderHandle'] as String? ?? '',
|
|
senderDisplayName: json['senderDisplayName'] as String? ?? '',
|
|
senderAvatarPreset: json['senderAvatarPreset'] as String?,
|
|
senderAvatarUrl: json['senderAvatarUrl'] as String?,
|
|
receiverUid: json['receiverUid'] as String? ?? '',
|
|
sentAt: json['sentAt'] != null
|
|
? DateTime.tryParse(json['sentAt'] as String) ?? DateTime.now()
|
|
: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
/// Blocked rider record.
|
|
@immutable
|
|
class BlockedRider {
|
|
final String blockedUid;
|
|
final String? blockedHandle;
|
|
final String? blockedDisplayName;
|
|
final DateTime blockedAt;
|
|
|
|
const BlockedRider({
|
|
required this.blockedUid,
|
|
this.blockedHandle,
|
|
this.blockedDisplayName,
|
|
required this.blockedAt,
|
|
});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'blockedUid': blockedUid,
|
|
'blockedHandle': blockedHandle,
|
|
'blockedDisplayName': blockedDisplayName,
|
|
'blockedAt': blockedAt.toIso8601String(),
|
|
};
|
|
|
|
factory BlockedRider.fromJson(Map<String, dynamic> json) => BlockedRider(
|
|
blockedUid: json['blockedUid'] as String? ?? '',
|
|
blockedHandle: json['blockedHandle'] as String?,
|
|
blockedDisplayName: json['blockedDisplayName'] as String?,
|
|
blockedAt: json['blockedAt'] != null
|
|
? DateTime.tryParse(json['blockedAt'] as String) ?? DateTime.now()
|
|
: DateTime.now(),
|
|
);
|
|
}
|
|
|
|
/// Search result summary for public rider lookup.
|
|
@immutable
|
|
class RiderSearchResult {
|
|
final String uid;
|
|
final String handle;
|
|
final String displayName;
|
|
final String bio;
|
|
final String region;
|
|
final String? avatarPreset;
|
|
final String? avatarUrl;
|
|
final bool isProfilePublic;
|
|
final bool allowFriendRequests;
|
|
final FriendshipStatus relationship;
|
|
|
|
const RiderSearchResult({
|
|
required this.uid,
|
|
required this.handle,
|
|
required this.displayName,
|
|
this.bio = '',
|
|
this.region = '',
|
|
this.avatarPreset,
|
|
this.avatarUrl,
|
|
required this.isProfilePublic,
|
|
required this.allowFriendRequests,
|
|
required this.relationship,
|
|
});
|
|
|
|
String get displayHandle => '@$handle';
|
|
}
|