Posts may have images embedded in HTML (via html/markdown cards) without
a feature_image or explicit image card. Fall back to parsing <img src>
from HTML content to display these images.
- Initialize _tagsEnabled from preferences (was hardcoded true)
- Add refreshTagsEnabled() called when navigating to Home tab
- Immediately clears popularTags and activeTagFilter when disabled
- Add TagsPreferences with per-account toggle (enabled by default)
- Tags toggle in Settings → Features section with "Manage Tags" button
- When tags disabled: hide tag filter chips, tag section in Composer,
tag click handlers become no-ops in Feed
- New Newsletter bottom tab (Home, Newsletter, Stats, Settings)
- NewsletterScreen shows enable toggle, subscriber count, newsletters list
- Remove newsletter section from Settings (moved to dedicated tab)
- Parallelize StatsViewModel fetches (posts, members, tags via async/await)
- Collapse MobiledocBuilder overloads into single function with defaults
- Extract shared FileTypeColor composable from duplicated color mappings
- Remove redundant state in FeedViewModel and FeedScreen
- Unify formatFileSize usage, remove inline duplication
- Fix minor issues in MediaPlayers, PostUploadWorker, Pages, Settings
Phase 4b.3: Sent status in Feed + PostUploadWorker handling.
- PostUploadWorker: handle QUEUED_EMAIL_ONLY with email_only=true on GhostPost,
pass newsletter slug to repository.createPost()
- FeedViewModel: map GhostPost email_only/sent status to FeedPost.emailOnly
- FeedScreen FilterChipsBar: add "Sent" chip (magenta, only when newsletter enabled)
- FeedScreen PostCardContent: show envelope icon + "Sent" in magenta for sent posts,
replace "Share" with "Copy content" for email-only posts
- FeedScreen StatusBadge: handle sent/emailOnly status
- DetailScreen: show email-only info card with errorContainer color when post is
sent via email only, noting it's not visible on the blog
Phase 4b.2: Email-only option in Composer.
- Add "Send via Email Only" dropdown menu item (visible when newsletter enabled)
- Add showEmailOnlyConfirmation state to ComposerUiState
- Add sendEmailOnly(), confirmEmailOnly(), cancelEmailOnly() to ViewModel
- submitEmailOnlyPost() saves with emailOnly=true, QUEUED_EMAIL_ONLY status
- Add EmailOnlyConfirmationDialog with warning icon, post preview,
newsletter picker (if multiple), bold warning about irreversibility,
and error-colored confirm button
Phase 4b.1: Add data model support for email-only posts.
- PostStatus: add SENT enum value
- QueueStatus: add QUEUED_EMAIL_ONLY enum value
- PostFilter: add SENT filter with "status:sent" ghost filter
- GhostPost: add email_only Boolean field
- FeedPost: add emailOnly Boolean field
- LocalPostDao: include QUEUED_EMAIL_ONLY in queued posts query
- OverallStats: handle SENT status in stats calculation
- FeedScreen: show "Pending email send" for QUEUED_EMAIL_ONLY queue status
- Update tests for new enum values and fields
- Add Media3 ExoPlayer dependencies (media3-exoplayer, media3-ui 1.2.1)
- Extend FeedPost with videoUrl and audioUrl fields
- Parse video/audio card URLs from mobiledoc JSON in FeedViewModel
- Map LocalPost video/audio URIs to FeedPost in toFeedPost()
- Create VideoPlayer composable: ExoPlayer in AndroidView, play button overlay, tap to play
- Create AudioPlayer composable: play/pause button, progress slider, duration text
- Integrate compact VideoPlayer and AudioPlayer in FeedScreen post cards
- Integrate full-size VideoPlayer and AudioPlayer in DetailScreen with reveal animations
- Load video/audio URIs when editing a post in ComposerViewModel
- Add newsletter fields to ComposerUiState (enabled, newsletters list,
selected newsletter, sendAsNewsletter, emailSegment, subscriber count,
confirmation dialog state)
- Load newsletter data on init when newsletter features are enabled
- Add newsletter options in publish dropdown: send-as-newsletter switch,
newsletter picker (radio buttons), email segment picker (All/Free/Paid)
- Show warning about irreversible email send with subscriber count
- Change publish button to tertiaryContainer color and email icon when
newsletter sending is active
- Add NewsletterConfirmationDialog requiring "WYSLIJ" typed input to
confirm, with summary of newsletter name, segment, count, and title
- Pass newsletter slug and email segment through to PostRepository
- Store newsletter slug in LocalPost for offline queue support
- Extend FeedPost with fileUrl and fileName fields
- Parse mobiledoc file cards in FeedViewModel.extractFileCardFromMobiledoc()
- Map LocalPost file fields to FeedPost in toFeedPost()
- Create FileAttachmentCard composable with file type icon colors and tap-to-download
- Integrate file card into PostCardContent (FeedScreen) and DetailScreen
- Add videoUri, audioUri, uploadedVideoUrl, uploadedAudioUrl, isUploadingMedia to ComposerUiState
- Add setVideo/removeVideo/setAudio/removeAudio methods to ComposerViewModel
- Update submitPost to upload video/audio via uploadMediaFile and pass URLs to MobiledocBuilder
- Save videoUri/audioUri to LocalPost for offline queue
- Add Video and Audio picker buttons to composer toolbar
- Add MediaPreviewCard composable showing filename, file size, and remove button
- Update PostUploadWorker to upload video/audio before building mobiledoc
- Add file card support to MobiledocBuilder with Ghost's native file card format
- Add file card tests to MobiledocBuilderTest
- Add file state fields to ComposerUiState (fileUri, fileName, fileSize, fileMimeType, uploadedFileUrl)
- Add addFile()/removeFile() methods to ComposerViewModel with 50MB size validation
- Add file picker button and FileAttachmentComposerCard in ComposerScreen
- Update PostUploadWorker to upload files via repository.uploadFile() and include in mobiledoc
- Add "Newsletter" section after Content/Tags section
- Switch to enable/disable newsletter features per account
- Info text explaining the toggle's effect on composer
- Best-effort API validation when toggling ON (fetches newsletters count)
- Animated validation status display
- Add 8-param build() overload accepting videoUrl and audioUrl
- Video card: ["video",{"src":"url","loop":false}]
- Audio card: ["audio",{"src":"url"}]
- Card order: images -> video -> audio -> bookmark
- Add tests for video only, audio only, and all media types combined
- Create NewsletterModels.kt with GhostNewsletter and NewslettersResponse
- Add getNewsletters() endpoint to GhostApiService
- Add optional newsletter/emailSegment query params to createPost/updatePost
- Create NewsletterPreferences for per-account newsletter toggle
- Add fetchNewsletters() and fetchSubscriberCount() to PostRepository
- Pass newsletter params through PostRepository to API service
- Add Robolectric tests for NewsletterPreferences
- Create FileModels.kt with FileUploadResponse and UploadedFile data classes
- Add uploadFile() multipart endpoint to GhostApiService
- Add uploadFile(uri) method to PostRepository following the same pattern as uploadImage()
- Create MediaModels.kt with MediaUploadResponse and UploadedMedia data classes
- Add uploadMedia() and uploadMediaThumbnail() to GhostApiService
- Add uploadMediaFile(uri) to PostRepository with MIME detection
StatsViewModel fetches tags from TagRepository, computes most used tag
and posts-without-tags count. StatsScreen shows "Tags" section with
horizontal progress bars (LinearProgressIndicator per tag, colored by
accent_color), most used tag, total tags, and posts without tags count.
MemberDetailScreen shows scrollable profile: large avatar header with name
and email, 3 quick stat tiles (status, open rate, emails sent), subscription
details for paid members (tier, price, renewal date, cancellation status),
activity section (joined date, last seen, geolocation), newsletters list
with read-only checkboxes, labels as FlowRow of AssistChips, email activity
with open rate progress bar, and member notes.
FeedViewModel fetches tags on refresh(), takes top 10 by post count.
FeedScreen shows LazyRow of FilterChip below status filter: "All tags"
first, then popular tags with post counts. Tapping filters posts by tag.
Post cards now show tags in compact labelSmall format joined by dots.
When more than one account is configured, display an informational
AssistChip at the top of the Composer showing the active blog name
and site icon. Uses SiteMetadataCache for the blog title, falls back
to account name. Non-clickable, only shown for disambiguation.
MembersViewModel manages members list state with loading, pagination,
filter (All/Free/Paid), and debounced search. MembersScreen shows
TopAppBar with total count, search field, segmented filter buttons,
and LazyColumn with member rows (avatar via Coil or colored initial,
name, email, open rate progress bar, relative time, PAID/NEW badges).
Add Routes.MEMBERS and Routes.MEMBER_DETAIL to NavGraph (not in
bottomBarRoutes). Wire "See all members" button from Stats screen.
Replace account name with blog title from SiteMetadataCache in the Feed
TopAppBar. Show site icon (24dp, circular) before the title. Truncate
blog name to 20 characters with ellipsis. Falls back to account name
or "Swoosh" if no cached site data exists.
TagsViewModel manages tag CRUD state. TagsScreen shows searchable list
of OutlinedCards with accent dot, name, count, description. Edit mode
supports name, slug (read-only), description, accent_color hex,
visibility radio. Wired into NavGraph via Routes.TAGS and accessible
from Settings screen via "Tags" row.
Display cached Ghost site metadata (logo/icon, title, description, URL,
version, locale) in a card above the Current Account section. Add "Open
Ghost Admin" button that launches the blog's admin panel in browser.
Show version warning banner if Ghost version is older than v5.
Add PagesViewModel with CRUD operations and edit/create state management.
Add PagesScreen with dual-mode UI (list with long-press context menu and
editor with title/content/slug/status fields). Wire navigation from
Settings via "Static Pages" row. Pages use slide-in-horizontal transition
consistent with other detail screens.
StatsViewModel now fetches members via MemberRepository and computes
MemberStats. StatsScreen shows a 2x3 grid of ElevatedCard tiles when
memberStats is available: Total, New this week, Open rate, Free, Paid,
MRR. Includes animated counters and a "See all members" navigation button.
Member fetch failure is non-fatal (tiles simply hidden).
After successful connection test, fetch Ghost /site/ endpoint to get
blog name, description, icon, and version. Show a confirmation card
with site details before completing setup. Warn if Ghost version < 5.
Cache site metadata per account via SiteMetadataCache. Falls back to
existing behavior if site fetch fails.
ComposerViewModel fetches available tags from TagRepository on init,
filters suggestions as user types, supports addTag/removeTag. ComposerScreen
shows tag input field with dropdown suggestions (name + post count),
"Create new" option, and FlowRow of InputChip tags with close icons.
MemberRepository follows the same pattern as PostRepository: Context constructor,
AccountManager, ApiClient. Includes fetchMembers (paged), fetchMember (single),
fetchAllMembers (all pages, max 20), and getMemberStats (pure function computing
total/free/paid/newThisWeek/avgOpenRate/MRR). Comprehensive tests for getMemberStats.
Follows PostRepository pattern with AccountManager-based auth,
Dispatchers.IO coroutine context, and Result<T> return types.
Exposes fetchPages, createPage, updatePage, deletePage methods
plus getBlogUrl for constructing page URLs in the UI.
Introduce GhostPage, PagesResponse, PageWrapper data classes for
Ghost CMS static pages. Add CRUD endpoints (getPages, createPage,
updatePage, deletePage) to GhostApiService. Include comprehensive
unit tests for serialization and default values.
SharedPreferences-based cache for GhostSite metadata keyed by account ID.
Supports save/get/getVersion/remove operations with Gson serialization.
Includes Robolectric tests for round-trip, overwrite, multi-account
isolation, and removal.
Add MemberModels.kt with GhostMember, MemberLabel, MemberNewsletter,
MemberSubscription, SubscriptionPrice, and SubscriptionTier data classes.
Add getMembers() and getMember() endpoints to GhostApiService.
Add comprehensive JSON parsing tests for all member model types.
Add TagModels.kt with GhostTagFull, TagsResponse, TagWrapper, TagCount
data classes for full Ghost tag management. Add getTags, getTag,
createTag, updateTag, deleteTag endpoints to GhostApiService.
Add GhostSite data class for Ghost CMS site metadata (title, description,
logo, icon, accent color, URL, version, locale). Add getSite() endpoint
to GhostApiService. Include unit tests for Gson deserialization and
version parsing.