Skip to content
This repository was archived by the owner on Sep 7, 2026. It is now read-only.
Merged
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
24 changes: 16 additions & 8 deletions docs/AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,25 @@ The items below are the ones worth looking at first — either because they're c

## 4. UX & design consistency

### "Favorite" control — 5 different schemes across content types
### "Favorite" control — 5 different schemes across content types — **Fixed (2026-08-31)**

The single most visible inconsistency in the app, directly contradicting the ROADMAP's own stated principle. **Severity: High.**
Five content types each rendered the same boolean "like" their own way: two pill variants (Manga inline, Audiobooks via a private `_LikeButton`), a bare `IconButton` (Podcasts), and two list-row hearts (Books, Music), in three different reds. The colours were unified on 2026-08-29; the *controls* were not.

- **Movies/Series** (`details_page.dart:996`): bookmark icon (`bookmark_add_outlined`/`bookmark_added_rounded`), purple `0xFF7C5CFF`, labeled "Add to Library"/"In Library" — a *library* concept, not a heart.
- **Anime** (`anime_details_page.dart:748-800`): not a toggle at all — a `PopupMenuButton` 4-state status picker (Watching/Plan to Watch/Completed/Dropped), green `0xFF00D294`.
- **Manga** (`manga_details_page.dart:935-969`) / **Audiobooks** (`audiobook_detail_page.dart:587-649`): heart pill button, labeled "Like"/"Liked", red `0xFFE50914` fill when liked — but the **icon itself turns white**, not red.
- **Podcasts** (`podcast_details_page.dart:70-76`): plain heart `IconButton` in the app bar, no label, and the icon color **is** red when liked — opposite color logic from the visually-similar Manga/Audiobook pill.
- **Books** (`books_page.dart:376-380`) / **Music** (`music_page.dart:2150,4386`): heart icon in list rows, red `0xFFE50914` (Books) vs. pink `0xFFFF4B72` (Music) — a third color for the same red-heart concept. **Fixed** — Music's liked-heart color (and the universal play bar's purple `0xFF7C5CFF`) unified to red `0xFFE50914`, matching Books/Podcasts/Manga/Audiobooks.
All of them now use `LikeButton` (`lib/widgets/common/like_button.dart`), in one of two styles.

Empty-state copy tone is similarly uneven: `type_catalog_page.dart:285` gives an actionable "No content found. Install more addons in Settings." while `manga_page.dart:461` and `podcast_details_page.dart:83` give a flat "No X found" with no next step, despite Manga being equally addon/scrape-dependent. **Severity: low-medium.**
The two styles looked like they contradicted each other — the pill fills red and turns its icon **white**, the bare icon turns **red**. They do not, and the rule is now written down in the widget rather than rediscovered per page: a filled pill needs a white icon to stay legible against its own red fill; a bare icon has no fill and so must carry the colour itself. Both are `kLikedColor`.

Deliberately **not** folded in, because they are not the same concept:

- **Movies/Series** "Add to Library" — library membership, and a bookmark rather than a heart because that is what it means.
- **Anime** status — a four-state picker (Watching / Plan to Watch / Completed / Dropped). Collapsing it to a heart would delete the feature.
- **The fullscreen music player's like** — routes through the studio's user-configurable hover physics, which `LikeButton` would drop. It uses `kLikedColor` and carries the same semantics, so it follows the rules without being the widget.

`LikeButton` also carries `Semantics(button, toggled, label)`, which none of the five had — the bare-icon style has no visible text, so a screen reader previously announced an unlabelled button.

`test/widgets/like_button_test.dart` fails on any new filled/outline heart ternary outside those exceptions.

Empty-state copy tone is still uneven: `type_catalog_page.dart` gives an actionable "No content found. Install more addons in Settings." while `manga_page.dart` and `podcast_details_page.dart` give a flat "No X found" with no next step, despite Manga being equally addon/scrape-dependent. **Still open, severity low-medium.**

### Error/empty/loading state coverage

Expand Down
7 changes: 7 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to PlayTorrio V3 will be documented in this file.

## [unreleased] — 2026-08-22

### One like control instead of five (AUDIT: favorite schemes, High) — 2026-08-31
- **Five content types each rendered the same boolean "like" their own way**: two pill variants (Manga inline, Audiobooks via a private `_LikeButton`), a bare `IconButton` (Podcasts), and two list-row hearts (Books, Music). The colours were unified on 2026-08-29; the controls were not.
- All now use `LikeButton`, in one of two styles.
- **The two styles only looked contradictory.** The pill fills red and turns its icon *white*; the bare icon turns *red*. That is correct, and the reason is now in the widget rather than rediscovered per page: a filled pill needs a white icon to stay legible against its own fill, while a bare icon has no fill and must carry the colour itself. Both use `kLikedColor`.
- **Not folded in, because they are different concepts**: Movies/Series' "Add to Library" (library membership, hence a bookmark); Anime's four-state status picker (collapsing it to a heart would delete the feature); and the fullscreen music player's like, which routes through the studio's configurable hover physics — it uses `kLikedColor` and the same semantics without being the widget.
- **Gained accessibility none of the five had**: `Semantics(button, toggled, label)`. The bare-icon style has no visible text, so a screen reader previously announced an unlabelled button.
- A test fails on any new filled/outline heart ternary outside those exceptions. Its first draft flagged a sub-tab icon and an empty-state glyph — matching the *ternary* rather than the icon name is what distinguishes a toggle from decoration.
### Remove the Music Player Studio's Mini Bar half — 2026-08-31
- **It configured a widget that does not exist.** The studio's "Mini Bar" target let you pick a preset, drag components into an order and watch a live preview, then told you it was "set as active bottom bar". This fork replaced upstream's per-page mini players with the shared `UniversalPlayBar`, which reads none of `MusicSettings` — so none of it ever reached the screen.
- Wiring it up instead was the other option and was rejected: the bar is shared across music, video, audiobooks and podcasts, so a music-only component order would make it rearrange itself whenever you switched source type. The one-shared-bar architecture is the fork's design; a music-only mini-player customiser contradicts it.
Expand Down
76 changes: 2 additions & 74 deletions lib/pages/audiobooks/audiobook_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import '../../services/audiobook/audiobook_scraper_service.dart';
import '../../utils/fullscreen_navigator.dart';
import 'audiobook_player_screen.dart';
import 'audiobook_route_transitions.dart';
import '../../widgets/common/like_button.dart';

class AudiobookDetailPage extends StatefulWidget {
final Audiobook audiobook;
Expand Down Expand Up @@ -302,7 +303,7 @@ class _AudiobookDetailPageState extends State<AudiobookDetailPage> {
onPressed: () => _playChapter(_chapters!.first),
),
const SizedBox(width: 12),
_LikeButton(
LikeButton(
isLiked: _isLiked,
onTap: _toggleLike,
),
Expand Down Expand Up @@ -584,79 +585,6 @@ class _PlayFirstChapterButtonState extends State<_PlayFirstChapterButton> {
}
}

class _LikeButton extends StatefulWidget {
final bool isLiked;
final VoidCallback onTap;

const _LikeButton({required this.isLiked, required this.onTap});

@override
State<_LikeButton> createState() => _LikeButtonState();
}

class _LikeButtonState extends State<_LikeButton> {
bool _isHovered = false;
bool _isPressed = false;

@override
Widget build(BuildContext context) {
final scale = _isPressed ? 0.94 : (_isHovered ? 1.05 : 1.0);

return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
onTap: widget.onTap,
child: AnimatedScale(
scale: scale,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOutCubic,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: widget.isLiked
? const Color(0xFFE50914)
: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.isLiked
? const Color(0xFFE50914)
: Colors.white.withValues(alpha: 0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
widget.isLiked
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: widget.isLiked ? Colors.white : Colors.white70,
size: 22,
),
const SizedBox(width: 8),
Text(
widget.isLiked ? 'Liked' : 'Like',
style: TextStyle(
color: widget.isLiked ? Colors.white : Colors.white70,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
);
}
}

class _ChapterTile extends StatefulWidget {
final AudiobookChapter chapter;
final AppThemePalette palette;
Expand Down
44 changes: 3 additions & 41 deletions lib/pages/manga/manga_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../../services/theme/app_theme_service.dart';
import '../../services/manga/manga_service.dart';
import '../../utils/fullscreen_navigator.dart';
import 'manga_reader_page.dart';
import '../../widgets/common/like_button.dart';

class MangaDetailsPage extends StatefulWidget {
final Manga manga;
Expand Down Expand Up @@ -932,47 +933,8 @@ class _MangaDetailsPageState extends State<MangaDetailsPage> {
);
}

Widget _buildLikeButton() {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: _toggleLike,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: BoxDecoration(
color: _isLiked
? const Color(0xFFE50914)
: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _isLiked
? const Color(0xFFE50914)
: Colors.white.withValues(alpha: 0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: _isLiked ? Colors.white : Colors.white70,
size: 20,
),
const SizedBox(width: 8),
Text(
_isLiked ? 'Liked' : 'Like',
style: TextStyle(
color: _isLiked ? Colors.white : Colors.white70,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
Widget _buildLikeButton() =>
LikeButton(isLiked: _isLiked, onTap: _toggleLike, size: 20);

Widget _buildAppBar() {
final topPadding = MediaQuery.paddingOf(context).top;
Expand Down
40 changes: 23 additions & 17 deletions lib/pages/music/music_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import '../../services/music/music_service.dart';
import '../../services/music/music_settings.dart';
import '../../widgets/music/music_interactive_physics_button.dart';
import '../../widgets/music/music_waveform_seekbar.dart';
import '../../widgets/common/like_button.dart';

class MusicPage extends StatefulWidget {
const MusicPage({super.key});
Expand Down Expand Up @@ -2227,18 +2228,11 @@ class _MusicHeroBillboard extends StatelessWidget {
),
),
const SizedBox(width: 12),
_MusicHoverable(
scaleFactor: 1.1,
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: Colors.white.withValues(alpha: 0.15),
),
icon: Icon(
isSaved ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: isSaved ? const Color(0xFFE50914) : Colors.white,
),
onPressed: onSaveTap,
),
LikeButton(
isLiked: isSaved,
onTap: onSaveTap,
style: LikeButtonStyle.icon,
size: 24,
),
const SizedBox(width: 8),
_MusicHoverable(
Expand Down Expand Up @@ -4420,17 +4414,29 @@ class _MusicExpandedPlayerState extends State<_MusicExpandedPlayer> with SingleT
],
),
),
// Not a LikeButton: the fullscreen player runs its like through the
// studio's configurable hover physics, which LikeButton would drop.
// It follows the same rules -- kLikedColor, filled when liked, no
// fill behind it -- via the shared constant, so the colour cannot
// drift even though the widget differs.
MusicInteractivePhysicsButton(
effect: MusicSettings.customHoverEffect.value,
glowColor: const Color(0xFFE50914),
glowColor: kLikedColor,
borderRadius: BorderRadius.circular(20),
onTap: widget.onToggleSave,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Icon(
widget.isSaved ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: widget.isSaved ? const Color(0xFFE50914) : Colors.white70,
size: 28,
child: Semantics(
button: true,
toggled: widget.isSaved,
label: widget.isSaved ? 'Remove from liked' : 'Add to liked',
child: Icon(
widget.isSaved
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: widget.isSaved ? kLikedColor : Colors.white70,
size: 28,
),
),
),
),
Expand Down
11 changes: 5 additions & 6 deletions lib/pages/podcast/podcast_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import '../../services/podcast/podcast_library_service.dart';
import '../../services/podcast/podcast_player_controller.dart';
import '../../services/podcast/podcast_service.dart';
import '../../widgets/common/like_button.dart';

/// Episode list for one podcast, fetched live from its RSS feed.
class PodcastDetailsPage extends StatefulWidget {
Expand Down Expand Up @@ -67,12 +68,10 @@ class _PodcastDetailsPageState extends State<PodcastDetailsPage> {
backgroundColor: const Color(0xFF080A0F),
title: Text(widget.podcast.name, maxLines: 1, overflow: TextOverflow.ellipsis),
actions: [
IconButton(
icon: Icon(
_isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: _isLiked ? const Color(0xFFE50914) : Colors.white70,
),
onPressed: _toggleLike,
LikeButton(
isLiked: _isLiked,
onTap: _toggleLike,
style: LikeButtonStyle.icon,
),
],
),
Expand Down
13 changes: 6 additions & 7 deletions lib/pages/read/books_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../../services/books/book_library_service.dart';
import '../../services/books/book_progress_service.dart';
import '../../services/books/books_service.dart';
import 'book_reader_page.dart';
import '../../widgets/common/like_button.dart';

/// Books section in the Read hub: search libgen.li, download an epub, and
/// read it in-app. Ported/restyled from PlayTorrioV2's `books_screen.dart`.
Expand Down Expand Up @@ -371,13 +372,11 @@ class _BookRow extends StatelessWidget {
],
),
),
IconButton(
icon: Icon(
isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
color: isLiked ? const Color(0xFFE50914) : Colors.white38,
size: 20,
),
onPressed: onToggleLike,
LikeButton(
isLiked: isLiked,
onTap: onToggleLike,
style: LikeButtonStyle.icon,
size: 20,
),
const Icon(Icons.download_rounded, color: Colors.white38, size: 20),
],
Expand Down
Loading
Loading