/// Nextcloud share types (subset the app uses). const int kShareTypeUser = 0; const int kShareTypeGroup = 1; const int kShareTypePublicLink = 3; const int kShareTypeEmail = 4; /// A Talk conversation ("room") the file is linked into. const int kShareTypeRoom = 10; /// A single share as returned by the OCS `files_sharing` API. /// /// Parsed by hand rather than via code generation: OCS is inconsistent about /// types across versions (e.g. `id`/`share_type` may arrive as either strings /// or numbers) and omits optional fields entirely, so defensive parsing is /// safer than generated `as int` casts. class Share { final int id; final int shareType; final int permissions; /// Server path of the shared item (e.g. `/Documents/x.pdf`). final String? path; /// `'file'` or `'folder'`. final String? itemType; /// Recipient id (user/group id); empty for public links. final String? shareWith; final String? shareWithDisplayname; /// Public link URL (only set for [kShareTypePublicLink]). final String? url; /// Raw expiration as `"YYYY-MM-DD HH:MM:SS"` (or null when none). final String? expiration; final String? label; /// Redacted password marker: the server returns `null` when no password is /// set and a placeholder (`"redacted"`) when one is — never the real value. final String? password; const Share({ required this.id, required this.shareType, required this.permissions, this.path, this.itemType, this.shareWith, this.shareWithDisplayname, this.url, this.expiration, this.label, this.password, }); bool get isPublicLink => shareType == kShareTypePublicLink; bool get isGroup => shareType == kShareTypeGroup; bool get isEmail => shareType == kShareTypeEmail; bool get isRoom => shareType == kShareTypeRoom; bool get isFolder => itemType == 'folder'; /// Whether a (link) password is currently set. See [password]. bool get hasPassword => password != null && password!.isNotEmpty; /// Best display title for the share row. String get displayTitle { if (isPublicLink) return label?.isNotEmpty == true ? label! : 'Link'; final name = shareWithDisplayname; if (name != null && name.isNotEmpty) return name; return shareWith ?? 'Unbekannt'; } /// Human label for the kind of share (for subtitles/headers). String get kindLabel { if (isPublicLink) return 'Öffentlicher Link'; if (isGroup) return 'Gruppe'; if (isRoom) return 'Talk-Chat'; if (isEmail) return 'E-Mail'; if (shareType == kShareTypeUser) return 'Person'; return 'Freigabe'; } static int _asInt(Object? value, {int fallback = 0}) { if (value is int) return value; if (value is String) return int.tryParse(value) ?? fallback; return fallback; } static String? _asString(Object? value) { if (value == null) return null; final s = value.toString(); return s.isEmpty ? null : s; } factory Share.fromJson(Map json) => Share( id: _asInt(json['id']), shareType: _asInt(json['share_type']), permissions: _asInt(json['permissions']), path: _asString(json['path']), itemType: _asString(json['item_type']), shareWith: _asString(json['share_with']), shareWithDisplayname: _asString(json['share_with_displayname']), url: _asString(json['url']), expiration: _asString(json['expiration']), label: _asString(json['label']), password: _asString(json['password']), ); }