Skip to content
Open
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
21 changes: 21 additions & 0 deletions docs/feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 意見回饋補充資訊

登入後仍只需填寫標題與內容。展開「更多資訊(選填)」可提供回饋類型、相關路線/站牌及聯絡方式,後兩項各最多 200 字。未填的選填欄位不會送出。

「附上 App 與顯示資訊」每次進入頁面預設關閉。開啟後會預覽將附上的 App 版本、建置編號、程式版本、平台、語言、畫面尺寸、文字縮放與顯示主題。送出時只有開啟此選項才會加入 `diagnostics`;取消後不附上。收合選填區塊保留已填內容和開關選擇,送出失敗也保留內容供重試。

API 為 `POST /api/v1/feedback`,新增欄位是 `category`、`route_context`、`contact`、`diagnostics`。回饋類型使用 `bug`、`data`、`feature`、`other`。既有的驗證身分與 User-Agent 請求標頭維持原行為。

需要搭配 `busapiserver` 的 API、資料庫升級與管理後台更新;先部署後端再發布 App。後端啟動時自動補上選填欄位,舊版 App 與舊回饋資料繼續相容;舊後端不接受新欄位。

驗證:

```sh
flutter test test/feedback_service_test.dart test/feedback_screen_test.dart
```

後端專案內執行:

```sh
python -m unittest test_feedback_api test_rate_limit
```
38 changes: 38 additions & 0 deletions lib/core/feedback_models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
enum FeedbackCategory { bug, data, feature, other }

/// Only the app/display fields shown in the feedback consent preview.
class FeedbackDiagnostics {
const FeedbackDiagnostics({
required this.appVersion,
required this.buildNumber,
required this.commitHash,
required this.platform,
required this.locale,
required this.screenWidth,
required this.screenHeight,
required this.textScale,
required this.brightness,
});

final String appVersion;
final String buildNumber;
final String commitHash;
final String platform;
final String locale;
final int screenWidth;
final int screenHeight;
final double textScale;
final String brightness;

Map<String, Object> toJson() => {
'app_version': appVersion,
'build_number': buildNumber,
'commit_hash': commitHash,
'platform': platform,
'locale': locale,
'screen_width': screenWidth,
'screen_height': screenHeight,
'text_scale': textScale,
'brightness': brightness,
};
}
29 changes: 28 additions & 1 deletion lib/core/feedback_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'api_http.dart';
import 'api_config.dart';
import 'api_user_agent.dart';
import 'auth_token_store.dart';
import 'feedback_models.dart';
import 'http_error_utils.dart';

class FeedbackRateLimitException implements Exception {
Expand Down Expand Up @@ -46,6 +47,10 @@ class FeedbackService {
Future<FeedbackSubmissionResult> submitFeedback({
required String title,
required String content,
FeedbackCategory? category,
String? routeContext,
String? contact,
FeedbackDiagnostics? diagnostics,
}) async {
final cleanedTitle = title.trim();
final cleanedContent = content.trim();
Expand All @@ -62,13 +67,24 @@ class FeedbackService {
throw ArgumentError('內容不能超過 4000 字。');
}

final cleanedRouteContext = _optionalText(routeContext, 'route_context');
final cleanedContact = _optionalText(contact, 'contact');
final payload = <String, Object>{
'title': cleanedTitle,
'content': cleanedContent,
if (category != null) 'category': category.name,
'route_context': ?cleanedRouteContext,
'contact': ?cleanedContact,
if (diagnostics != null) 'diagnostics': diagnostics.toJson(),
};

final http.Response response;
try {
response = await apiPost(
_client,
Uri.parse('${ApiConfig.baseUrl}/api/v1/feedback'),
headers: ApiUserAgent.applyTo(apiJsonContentHeaders),
body: jsonEncode({'title': cleanedTitle, 'content': cleanedContent}),
body: jsonEncode(payload),
);
} on ApiRequestException catch (error) {
if (error.statusCode != 429) rethrow;
Expand Down Expand Up @@ -99,6 +115,17 @@ class FeedbackService {
decoded.map((key, value) => MapEntry(key.toString(), value)),
);
}

void dispose() => _client.close();
}

String? _optionalText(String? value, String field) {
final cleaned = value?.trim();
if (cleaned == null || cleaned.isEmpty) return null;
if (cleaned.length > 200) {
throw ArgumentError.value(null, field, 'Must be at most 200 characters.');
}
return cleaned;
}

int _jsonInt(Object? value) {
Expand Down
23 changes: 23 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,29 @@
"feedbackContentRequired": "Enter some details",
"feedbackContentTooLong": "The details can be at most {max} characters.",
"@feedbackContentTooLong": {"placeholders": {"max": {"type": "int"}}},
"feedbackMoreInfo": "More information (optional)",
"feedbackMoreInfoHint": "Additional context can help us understand the issue. You can also submit without it.",
"feedbackCategoryLabel": "Category (optional)",
"feedbackCategoryNone": "Not specified",
"feedbackCategoryBug": "Bug report",
"feedbackCategoryData": "Transit data issue",
"feedbackCategoryFeature": "Feature suggestion",
"feedbackCategoryOther": "Other",
"feedbackRouteContextLabel": "Related route / stop (optional)",
"feedbackRouteContextHint": "For example: Taipei route 307, toward Banqiao, Taipei Main Station",
"feedbackContactLabel": "Contact details (optional)",
"feedbackContactHint": "For example: email or Discord username for follow-up questions",
"feedbackOptionalTooLong": "Use no more than {max} characters",
"feedbackIncludeDiagnostics": "Include app and display information",
"feedbackDiagnosticsHint": "When enabled, the version, platform, language and display settings shown below will be included to help troubleshoot the issue.",
"feedbackDiagnosticVersion": "App version",
"feedbackDiagnosticCommit": "Code revision",
"feedbackDiagnosticPlatform": "Platform",
"feedbackDiagnosticLocale": "Language",
"feedbackDiagnosticScreen": "Screen size (logical pixels)",
"feedbackDiagnosticTextScale": "Text scale (at 14pt)",
"feedbackDiagnosticTheme": "Theme",
"@feedbackOptionalTooLong": {"placeholders": {"max": {"type": "int"}}},
"feedbackSubmitting": "Sending...",
"feedbackSubmit": "Send feedback",
"feedbackInvalidFormat": "The feedback format is invalid.",
Expand Down
132 changes: 132 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4342,6 +4342,138 @@ abstract class AppLocalizations {
/// **'The details can be at most {max} characters.'**
String feedbackContentTooLong(int max);

/// No description provided for @feedbackMoreInfo.
///
/// In en, this message translates to:
/// **'More information (optional)'**
String get feedbackMoreInfo;

/// No description provided for @feedbackMoreInfoHint.
///
/// In en, this message translates to:
/// **'Additional context can help us understand the issue. You can also submit without it.'**
String get feedbackMoreInfoHint;

/// No description provided for @feedbackCategoryLabel.
///
/// In en, this message translates to:
/// **'Category (optional)'**
String get feedbackCategoryLabel;

/// No description provided for @feedbackCategoryNone.
///
/// In en, this message translates to:
/// **'Not specified'**
String get feedbackCategoryNone;

/// No description provided for @feedbackCategoryBug.
///
/// In en, this message translates to:
/// **'Bug report'**
String get feedbackCategoryBug;

/// No description provided for @feedbackCategoryData.
///
/// In en, this message translates to:
/// **'Transit data issue'**
String get feedbackCategoryData;

/// No description provided for @feedbackCategoryFeature.
///
/// In en, this message translates to:
/// **'Feature suggestion'**
String get feedbackCategoryFeature;

/// No description provided for @feedbackCategoryOther.
///
/// In en, this message translates to:
/// **'Other'**
String get feedbackCategoryOther;

/// No description provided for @feedbackRouteContextLabel.
///
/// In en, this message translates to:
/// **'Related route / stop (optional)'**
String get feedbackRouteContextLabel;

/// No description provided for @feedbackRouteContextHint.
///
/// In en, this message translates to:
/// **'For example: Taipei route 307, toward Banqiao, Taipei Main Station'**
String get feedbackRouteContextHint;

/// No description provided for @feedbackContactLabel.
///
/// In en, this message translates to:
/// **'Contact details (optional)'**
String get feedbackContactLabel;

/// No description provided for @feedbackContactHint.
///
/// In en, this message translates to:
/// **'For example: email or Discord username for follow-up questions'**
String get feedbackContactHint;

/// No description provided for @feedbackOptionalTooLong.
///
/// In en, this message translates to:
/// **'Use no more than {max} characters'**
String feedbackOptionalTooLong(int max);

/// No description provided for @feedbackIncludeDiagnostics.
///
/// In en, this message translates to:
/// **'Include app and display information'**
String get feedbackIncludeDiagnostics;

/// No description provided for @feedbackDiagnosticsHint.
///
/// In en, this message translates to:
/// **'When enabled, the version, platform, language and display settings shown below will be included to help troubleshoot the issue.'**
String get feedbackDiagnosticsHint;

/// No description provided for @feedbackDiagnosticVersion.
///
/// In en, this message translates to:
/// **'App version'**
String get feedbackDiagnosticVersion;

/// No description provided for @feedbackDiagnosticCommit.
///
/// In en, this message translates to:
/// **'Code revision'**
String get feedbackDiagnosticCommit;

/// No description provided for @feedbackDiagnosticPlatform.
///
/// In en, this message translates to:
/// **'Platform'**
String get feedbackDiagnosticPlatform;

/// No description provided for @feedbackDiagnosticLocale.
///
/// In en, this message translates to:
/// **'Language'**
String get feedbackDiagnosticLocale;

/// No description provided for @feedbackDiagnosticScreen.
///
/// In en, this message translates to:
/// **'Screen size (logical pixels)'**
String get feedbackDiagnosticScreen;

/// No description provided for @feedbackDiagnosticTextScale.
///
/// In en, this message translates to:
/// **'Text scale (at 14pt)'**
String get feedbackDiagnosticTextScale;

/// No description provided for @feedbackDiagnosticTheme.
///
/// In en, this message translates to:
/// **'Theme'**
String get feedbackDiagnosticTheme;

/// No description provided for @feedbackSubmitting.
///
/// In en, this message translates to:
Expand Down
73 changes: 73 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2701,6 +2701,79 @@ class AppLocalizationsEn extends AppLocalizations {
return 'The details can be at most $max characters.';
}

@override
String get feedbackMoreInfo => 'More information (optional)';

@override
String get feedbackMoreInfoHint =>
'Additional context can help us understand the issue. You can also submit without it.';

@override
String get feedbackCategoryLabel => 'Category (optional)';

@override
String get feedbackCategoryNone => 'Not specified';

@override
String get feedbackCategoryBug => 'Bug report';

@override
String get feedbackCategoryData => 'Transit data issue';

@override
String get feedbackCategoryFeature => 'Feature suggestion';

@override
String get feedbackCategoryOther => 'Other';

@override
String get feedbackRouteContextLabel => 'Related route / stop (optional)';

@override
String get feedbackRouteContextHint =>
'For example: Taipei route 307, toward Banqiao, Taipei Main Station';

@override
String get feedbackContactLabel => 'Contact details (optional)';

@override
String get feedbackContactHint =>
'For example: email or Discord username for follow-up questions';

@override
String feedbackOptionalTooLong(int max) {
return 'Use no more than $max characters';
}

@override
String get feedbackIncludeDiagnostics =>
'Include app and display information';

@override
String get feedbackDiagnosticsHint =>
'When enabled, the version, platform, language and display settings shown below will be included to help troubleshoot the issue.';

@override
String get feedbackDiagnosticVersion => 'App version';

@override
String get feedbackDiagnosticCommit => 'Code revision';

@override
String get feedbackDiagnosticPlatform => 'Platform';

@override
String get feedbackDiagnosticLocale => 'Language';

@override
String get feedbackDiagnosticScreen => 'Screen size (logical pixels)';

@override
String get feedbackDiagnosticTextScale => 'Text scale (at 14pt)';

@override
String get feedbackDiagnosticTheme => 'Theme';

@override
String get feedbackSubmitting => 'Sending...';

Expand Down
Loading
Loading