后端接口从 6 个扩到 13 个,全部走真数据:
- 新增 api:playback_api(/app/episodes/{id}/play)、watch_api(/app/watch/*)、
library_api(/app/favorites*);票据从 CatalogApi 搬到 PlaybackApi
- 新增 repository:PlaybackRepository(含票据 TTL 缓存,默认 10 分钟,时钟可注入)、
FavoriteRepository、WatchHistoryRepository 换成服务端模型(每剧一条续看记录,删掉 demoProfileId)
- PlayerLogic:起播前取票(拿不到给三态 UI,失败可重试);accessType 驱动锁定文案,
不在前端猜;签名地址原样交给播放器(只补 host,不动 path/query);
进度上报节流 5s → 10s,切集/退出/切后台各补一次,播完带 finished
- 续看定位:服务端记录里的那一集才 seek(其它集从头播),续看位置进详情/播放页时读一次
- FavoriteController:收藏以服务端为准,乐观更新 + 失败回滚;退出登录清空
- UI 三态:详情页(加载/失败可重试/空剧集 + 续看进度条 + 收藏按钮)、
播放页(票据加载中/失败可重试/后端判定不可播)、我的页(续看失败可重试、收藏三态、最近观看)
- l10n 新增 8 个键(en/zh/id 三份):网络/进度/收藏失败、锁定提示、续看文案等
测试:flutter test 188 全过(存量 27 个测试文件未跑红);新增票据过期重取、进度节流、
续看定位、收藏回滚、签名 query 原样保留、真实后端端到端联调(live_watch_flow)
584 lines
12 KiB
Dart
584 lines
12 KiB
Dart
// ignore: unused_import
|
|
import 'package:intl/intl.dart' as intl;
|
|
import 'app_localizations.dart';
|
|
|
|
// ignore_for_file: type=lint
|
|
|
|
/// The translations for Indonesian (`id`).
|
|
class AppLocalizationsId extends AppLocalizations {
|
|
AppLocalizationsId([String locale = 'id']) : super(locale);
|
|
|
|
@override
|
|
String get appName => 'FruitShorts';
|
|
|
|
@override
|
|
String get demo => 'Demo';
|
|
|
|
@override
|
|
String get homeHeadline => 'Tonton sekarang';
|
|
|
|
@override
|
|
String get homeSubtitle => 'Pratinjau drama pendek gratis.';
|
|
|
|
@override
|
|
String get mediaNotice => 'Video demo lokal dari pengguna · Pratinjau gratis';
|
|
|
|
@override
|
|
String get featuredDemo => 'UNGGULAN · DEMO TEKNIS';
|
|
|
|
@override
|
|
String get demoTitle => 'Sinyal';
|
|
|
|
@override
|
|
String get demoSynopsis =>
|
|
'Pratinjau video lokal singkat. Episode lain bisa ditambahkan saat medianya tersedia.';
|
|
|
|
@override
|
|
String get homeTab => 'Beranda';
|
|
|
|
@override
|
|
String get openQuickMenu => 'Buka menu cepat';
|
|
|
|
@override
|
|
String get quickAccess => 'Akses cepat';
|
|
|
|
@override
|
|
String get currentSeries => 'Serial saat ini';
|
|
|
|
@override
|
|
String get discoverTab => 'Jelajahi';
|
|
|
|
@override
|
|
String get searchDiscover => 'Cari drama, aktor, genre...';
|
|
|
|
@override
|
|
String get featured => 'Unggulan';
|
|
|
|
@override
|
|
String get suspense => 'Thriller';
|
|
|
|
@override
|
|
String get romance => 'Romantis';
|
|
|
|
@override
|
|
String get actionGenre => 'Aksi';
|
|
|
|
@override
|
|
String get filter => 'Filter';
|
|
|
|
@override
|
|
String get topCharts => 'Peringkat Teratas';
|
|
|
|
@override
|
|
String get newReleases => 'Rilisan Baru';
|
|
|
|
@override
|
|
String get trendingMicroDramas => 'Drama Mini Populer';
|
|
|
|
@override
|
|
String get seeAll => 'Lihat Semua';
|
|
|
|
@override
|
|
String get artworkOnly => 'Gambar';
|
|
|
|
@override
|
|
String get artworkOnlyNotice =>
|
|
'Ini hanya gambar katalog. Videonya belum tersedia.';
|
|
|
|
@override
|
|
String get videoPending => 'Video belum siap';
|
|
|
|
@override
|
|
String get watchNow => 'Tonton gratis';
|
|
|
|
@override
|
|
String get noSearchResults => 'Tidak ada drama yang cocok.';
|
|
|
|
@override
|
|
String get demoCatalogNotice =>
|
|
'Tiga drama demo memakai video lokal yang sama.';
|
|
|
|
@override
|
|
String get myTab => 'Saya';
|
|
|
|
@override
|
|
String get localViewer => 'Penonton Lokal';
|
|
|
|
@override
|
|
String get localDemoProfile => 'Profil demo lokal';
|
|
|
|
@override
|
|
String get continueWatching => 'Lanjut Tonton';
|
|
|
|
@override
|
|
String watchedPercent(int percent) {
|
|
return 'Sudah ditonton $percent%';
|
|
}
|
|
|
|
@override
|
|
String get resume => 'Lanjutkan';
|
|
|
|
@override
|
|
String get history => 'Riwayat';
|
|
|
|
@override
|
|
String get watchlist => 'Daftar Tonton';
|
|
|
|
@override
|
|
String get likedTab => 'Disukai';
|
|
|
|
@override
|
|
String get all => 'Semua';
|
|
|
|
@override
|
|
String get inProgress => 'Sedang Berjalan';
|
|
|
|
@override
|
|
String get completed => 'Selesai';
|
|
|
|
@override
|
|
String get noSavedShows => 'Belum ada tontonan di sini.';
|
|
|
|
@override
|
|
String get preferences => 'Preferensi';
|
|
|
|
@override
|
|
String get appLanguage => 'Bahasa Aplikasi';
|
|
|
|
@override
|
|
String get followingTab => 'Mengikuti';
|
|
|
|
@override
|
|
String get animatedTab => 'Animasi';
|
|
|
|
@override
|
|
String get recommendedTab => 'Rekomendasi';
|
|
|
|
@override
|
|
String get noCategoryDramas => 'Belum ada drama di kategori ini.';
|
|
|
|
@override
|
|
String get trendingTab => 'Sedang Tren';
|
|
|
|
@override
|
|
String get liveActionTab => 'Live Action';
|
|
|
|
@override
|
|
String get search => 'Cari';
|
|
|
|
@override
|
|
String get free => 'Gratis';
|
|
|
|
@override
|
|
String get mute => 'Bisukan';
|
|
|
|
@override
|
|
String get unmute => 'Nyalakan suara';
|
|
|
|
@override
|
|
String get like => 'Suka';
|
|
|
|
@override
|
|
String get save => 'Simpan';
|
|
|
|
@override
|
|
String get comments => 'Komentar';
|
|
|
|
@override
|
|
String get share => 'Bagikan';
|
|
|
|
@override
|
|
String get localOnlyComments => 'Hanya terlihat di perangkat ini';
|
|
|
|
@override
|
|
String get noComments => 'Belum ada komentar.';
|
|
|
|
@override
|
|
String get writeComment => 'Tulis komentar';
|
|
|
|
@override
|
|
String get postComment => 'Kirim';
|
|
|
|
@override
|
|
String get shareFailed => 'Tidak bisa membuka menu berbagi.';
|
|
|
|
@override
|
|
String get nowPlaying => 'Sedang diputar';
|
|
|
|
@override
|
|
String get watchFullscreen => 'Tonton mode lanskap';
|
|
|
|
@override
|
|
String get exitFullscreen => 'Keluar dari layar penuh';
|
|
|
|
@override
|
|
String get pictureInPicture => 'Gambar dalam gambar';
|
|
|
|
@override
|
|
String get pipUnavailable =>
|
|
'Gambar dalam gambar tidak tersedia di perangkat ini';
|
|
|
|
@override
|
|
String get originalTag => 'Original';
|
|
|
|
@override
|
|
String get dramaTag => 'Drama pendek';
|
|
|
|
@override
|
|
String episodeLabel(int number) {
|
|
return 'Episode $number';
|
|
}
|
|
|
|
@override
|
|
String get exploreSeries => 'Lihat serial';
|
|
|
|
@override
|
|
String episodeCount(int count) {
|
|
return '$count EPISODE';
|
|
}
|
|
|
|
@override
|
|
String technicalDemoCount(int count) {
|
|
return 'PRATINJAU GRATIS · $count EPISODE';
|
|
}
|
|
|
|
@override
|
|
String get episodes => 'Episode';
|
|
|
|
@override
|
|
String get watchEpisodeOne => 'Tonton episode 1';
|
|
|
|
@override
|
|
String continueEpisode(int number) {
|
|
return 'Lanjut episode $number';
|
|
}
|
|
|
|
@override
|
|
String get freeTestMedia => 'Klip demo sama · gratis';
|
|
|
|
@override
|
|
String get lockedStoreUnavailable => 'Terkunci · toko belum tersedia';
|
|
|
|
@override
|
|
String get freeTestClip => 'Klip uji gratis';
|
|
|
|
@override
|
|
String get locked => 'Terkunci';
|
|
|
|
@override
|
|
String get freeSamplesNotice =>
|
|
'Dua sampel gratis · pengaturan toko belum selesai';
|
|
|
|
@override
|
|
String get demoTestMedia => 'GRATIS · KLIP DEMO SAMA';
|
|
|
|
@override
|
|
String episodeProgress(int number, int total) {
|
|
return 'EPISODE $number / $total';
|
|
}
|
|
|
|
@override
|
|
String get testVideoNotice => 'Video uji teknis buatan sendiri';
|
|
|
|
@override
|
|
String lockedTitle(int number) {
|
|
return 'Episode $number terkunci';
|
|
}
|
|
|
|
@override
|
|
String get lockedBody =>
|
|
'Pembukaan kunci tersedia setelah toko dan verifikasi disiapkan. Demo ini tidak memproses pembayaran.';
|
|
|
|
@override
|
|
String get playbackError => 'Klip ini tidak bisa diputar.';
|
|
|
|
@override
|
|
String get catalogError => 'Katalog gagal dimuat.';
|
|
|
|
@override
|
|
String get detailError => 'Daftar episode gagal dimuat.';
|
|
|
|
@override
|
|
String get noMediaNotice =>
|
|
'Episode ini belum punya media yang bisa diputar.';
|
|
|
|
@override
|
|
String get noStories => 'Belum ada cerita.';
|
|
|
|
@override
|
|
String get retry => 'Coba lagi';
|
|
|
|
@override
|
|
String get backToSeries => 'Kembali ke serial';
|
|
|
|
@override
|
|
String get nextEpisode => 'Episode berikutnya';
|
|
|
|
@override
|
|
String get pause => 'Jeda';
|
|
|
|
@override
|
|
String get play => 'Putar';
|
|
|
|
@override
|
|
String get seekVideo => 'Geser posisi video';
|
|
|
|
@override
|
|
String get seekFailed => 'Gagal mengubah posisi putar.';
|
|
|
|
@override
|
|
String get captions => 'Subtitle';
|
|
|
|
@override
|
|
String get captionsOff => 'Subtitle nonaktif';
|
|
|
|
@override
|
|
String get englishCaptions => 'Subtitle bahasa Inggris';
|
|
|
|
@override
|
|
String get chineseCaptions => 'Subtitle bahasa Mandarin';
|
|
|
|
@override
|
|
String get language => 'Bahasa';
|
|
|
|
@override
|
|
String get systemLanguage => 'Sistem';
|
|
|
|
@override
|
|
String get english => 'English';
|
|
|
|
@override
|
|
String get simplifiedChinese => '简体中文';
|
|
|
|
@override
|
|
String get episodeOneTitle => 'Cahaya Pertama';
|
|
|
|
@override
|
|
String get episodeTwoTitle => 'Frekuensi Baru';
|
|
|
|
@override
|
|
String get episodeThreeTitle => 'Menyeberang';
|
|
|
|
@override
|
|
String get episodeFourTitle => 'Setelah Gema';
|
|
|
|
@override
|
|
String get dramaSignalTitle => 'Sinyal';
|
|
|
|
@override
|
|
String get dramaSignalSynopsis =>
|
|
'Serial demo untuk tontonan vertikal. Setiap episode memakai satu klip lokal yang sama.';
|
|
|
|
@override
|
|
String get dramaCityTitle => 'Cahaya Kota & Malam Beludru';
|
|
|
|
@override
|
|
String get dramaCitySynopsis =>
|
|
'Serial romansa demo. Video lokal yang sama dipakai untuk mencoba pindah episode dan lanjut tonton.';
|
|
|
|
@override
|
|
String get dramaVengeanceTitle => 'Badai Pembalasan';
|
|
|
|
@override
|
|
String get dramaVengeanceSynopsis =>
|
|
'Serial aksi demo. Video lokal yang sama dipakai untuk mencoba pindah episode dan lanjut tonton.';
|
|
|
|
@override
|
|
String get dramaEchoesTitle => 'Gema Takdir';
|
|
|
|
@override
|
|
String get genreSuspenseMystery => 'Thriller · Misteri';
|
|
|
|
@override
|
|
String get genreUrbanRomance => 'Romansa Kota';
|
|
|
|
@override
|
|
String get genreActionThriller => 'Aksi · Thriller';
|
|
|
|
@override
|
|
String get genreDramaMystery => 'Drama · Misteri';
|
|
|
|
@override
|
|
String get continueWatchingHeading => 'LANJUT TONTON';
|
|
|
|
@override
|
|
String get preferencesHeading => 'PREFERENSI';
|
|
|
|
@override
|
|
String get brandMark => 'FRUITSHORTS';
|
|
|
|
@override
|
|
String get shareFooter => 'FruitShorts';
|
|
|
|
@override
|
|
String get shortsTab => 'Drama Pendek';
|
|
|
|
@override
|
|
String get myListTab => 'Daftar Saya';
|
|
|
|
@override
|
|
String get watchFull => 'Tonton Penuh';
|
|
|
|
@override
|
|
String get swipeForNext => 'Geser ke atas untuk lanjut';
|
|
|
|
@override
|
|
String get clearScreen => 'Bersihkan layar';
|
|
|
|
@override
|
|
String get exitClearScreen => 'Keluar dari mode bersih';
|
|
|
|
@override
|
|
String get logIn => 'Masuk';
|
|
|
|
@override
|
|
String get logInHint => 'Simpan koin dan daftar kamu';
|
|
|
|
@override
|
|
String get rewards => 'Hadiah';
|
|
|
|
@override
|
|
String get forYou => 'Untuk Kamu';
|
|
|
|
@override
|
|
String get hot => 'Populer';
|
|
|
|
@override
|
|
String get newest => 'Terbaru';
|
|
|
|
@override
|
|
String get favorite => 'Favorit';
|
|
|
|
@override
|
|
String get myAccount => 'Akun saya';
|
|
|
|
@override
|
|
String get coins => 'Koin';
|
|
|
|
@override
|
|
String get topUpWallet => 'Isi Ulang Dompet';
|
|
|
|
@override
|
|
String get vipMembership => 'Keanggotaan VIP';
|
|
|
|
@override
|
|
String get notActive => 'Tidak Aktif';
|
|
|
|
@override
|
|
String get joinVip => 'Gabung VIP';
|
|
|
|
@override
|
|
String get earnRewards => 'Dapatkan Hadiah';
|
|
|
|
@override
|
|
String get freeBonus => 'Bonus Gratis';
|
|
|
|
@override
|
|
String get watchHistory => 'Riwayat Tontonan';
|
|
|
|
@override
|
|
String get settings => 'Pengaturan';
|
|
|
|
@override
|
|
String get signOut => 'Keluar';
|
|
|
|
@override
|
|
String get mockUserName => 'Vicky sheng';
|
|
|
|
@override
|
|
String get mockUserId => 'ID: M1CZWVYE';
|
|
|
|
@override
|
|
String get mockNotice => 'Hanya data contoh.';
|
|
|
|
@override
|
|
String get topBadge => 'TOP 1';
|
|
|
|
@override
|
|
String get signIn => 'Masuk';
|
|
|
|
@override
|
|
String get signUp => 'Daftar';
|
|
|
|
@override
|
|
String get authSignInTitle => 'Selamat datang kembali';
|
|
|
|
@override
|
|
String get authSignUpTitle => 'Buat akun kamu';
|
|
|
|
@override
|
|
String get authSubtitle => 'Masuk dengan email dan kata sandi.';
|
|
|
|
@override
|
|
String get authEmailLabel => 'Email';
|
|
|
|
@override
|
|
String get authPasswordLabel => 'Kata sandi';
|
|
|
|
@override
|
|
String get authPasswordHint => 'Minimal 8 karakter, berisi huruf dan angka';
|
|
|
|
@override
|
|
String get authEmailRequired => 'Masukkan email kamu';
|
|
|
|
@override
|
|
String get authEmailInvalid => 'Email itu sepertinya tidak benar';
|
|
|
|
@override
|
|
String get authPasswordRequired => 'Masukkan kata sandi kamu';
|
|
|
|
@override
|
|
String get authPasswordLength => 'Kata sandi harus 8-64 karakter';
|
|
|
|
@override
|
|
String get authPasswordWeak => 'Kata sandi perlu huruf dan angka';
|
|
|
|
@override
|
|
String get authNetworkError => 'Jaringan bermasalah, coba lagi';
|
|
|
|
@override
|
|
String get authSwitchToSignUp => 'Baru di sini? Buat akun';
|
|
|
|
@override
|
|
String get authSwitchToSignIn => 'Sudah punya akun? Masuk';
|
|
|
|
@override
|
|
String get authGateTitle => 'Masuk untuk melanjutkan';
|
|
|
|
@override
|
|
String get authGateMessage => 'Favorit dan komentar perlu akun.';
|
|
|
|
@override
|
|
String get authGateAction => 'Masuk';
|
|
|
|
@override
|
|
String get cancel => 'Nanti saja';
|
|
|
|
@override
|
|
String get noEpisodesYet => 'Belum ada episode yang bisa diputar.';
|
|
|
|
@override
|
|
String get networkError =>
|
|
'Jaringan tidak tersedia. Periksa koneksi lalu coba lagi.';
|
|
|
|
@override
|
|
String get progressError => 'Posisi tonton gagal dimuat.';
|
|
|
|
@override
|
|
String get favoritesError => 'Daftar favorit gagal dimuat.';
|
|
|
|
@override
|
|
String get favoriteToggleFailed => 'Gagal memperbarui favorit. Coba lagi.';
|
|
|
|
@override
|
|
String get remove => 'Hapus';
|
|
|
|
@override
|
|
String get unlockComingSoon => 'Pembukaan dengan koin segera hadir.';
|
|
|
|
@override
|
|
String resumeProgress(Object position, Object total) {
|
|
return 'Lanjut di $position / $total';
|
|
}
|
|
}
|