Directory Theme class reference
AI Headshot Theme — Class reference
The 23 classes that make _ai behave differently from the shared Directory core. Generated from the theme source.
← Back to AI Headshot Theme features
AssetIndex AssetIndex.php
Search-facet index for gallery images (Ppt_ai). Computes and stores the queryable facets a photo gallery filters on — orientation, dominant colour and resolution (megapixels) — so the search page can add meta_query clauses without re-analysing images on every request. Computed once when a listing is saved (ListingEditor → reindex) and backfillable in bulk. Orientation and resolution come from the primary image's stored width/ height (no image library needed); the dominant colour is a best-effort average via Imagick or GD (skipped silently when neither is available — the admin can hide that filter). Meta keys (all on the listing post): ph_orientation 'landscape' | 'portrait' | 'square' ph_megapixels float, e.g. 24.0 ph_color a palette key from self::colors()
Expiry Expiry.php
PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.
Gallery Gallery.php
Single-listing gallery styles. The admin picks one of four layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default] grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)
Geocoder Geocoder.php
Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.
HowCopy HowCopy.php
How it works — the AI Headshot theme's own voice, per niche. The built-in /how-it-works/ page is a block composition seeded by Frontend\Pages::defaultBlocks('how'), and its copy comes from Pages::howCopy(), which knows three voices: the Directory original, a people voice for the dating and escort forks, and a generic one that swaps the profile's item noun into the Directory sentences. The ai profile's item noun is "Image", so every AI site shipped a page that read "We connect you with the images you're looking for / Browse thousands of images, compare the details that matter and get in touch directly — all in one place, with no accounts or fees standing in your way", offered the Directory's step titles ("Search & discover", "Get in touch") and pointed its buttons at the listings archive and Add Listing. None of that is this product: nobody browses to a seller here, an account and credits ARE required, and the journey is upload a photo → pick a style → download what came back. Worse, ai ships with search off by default, so the "Browse images" button resolved to the home page — a dead link on the page every home-page FAQ and CTA block links to (Pages::url('how') appears in 15+ of the fork's blocks). This fills Pages::howCopy()'s keys through the ppt_how_copy filter, keyed on the ACTIVE DESIGN's niche (Frontend\Preview::activeNiche()) — so a Cartoon Headshots site (Toon / Caper / Cel) talks about cartoons, a Pet Portraits site about your dog, a Photo Restore site about the scan of an old print, and the page matches the design the visitor is actually looking at. An unmapped or missing niche gets the neutral AI voice in fallback(), never the Directory one. What the copy may claim is bounded by what the theme really does (Generation\Models::slots()): every styled job takes ONE photo minimum (up to eight, rotated through a batch), restore and image-to-video take a single photo, and jobs are paid for in credits. No credit counts and no prices appear here — both belong to the owner's packs, not to a page shipped in the box.
Links Links.php
Links — the destinations an AI Headshot home page points at, resolved in one place. Every Classic Headshots block (Frame, Lens, Polish) carries the same handful of calls to action: "Create my headshots", "See pricing", "How it works", "View the gallery". Resolving them here means a block never hard-codes a slug and never ships a link that goes nowhere: - studio() — the member hub's AI Studio panel (/account/#studio), where the selfie upload lives. A signed-out visitor is sent to sign-up instead when the wizard is on: the hub's own guard would bounce them to login and the #studio fragment never reaches the server, so the hash would be lost on the way. Same reasoning as MembersPage::listingsUrl(). - page() — a theme page slot (pricing, how), or '' when the slot is not served on this site. Pages::url() answers home_url('/') for a missing slot, and a button on the home page that reloads the home page is a dead link; callers hide the button on ''. - gallery() — the public browse page, or '' when the profile has search switched off (the ai profile does by default), for the same reason. Callers always let an owner-typed link win over these; this is only the default.
ListingActions ListingActions.php
Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.
ListingCard ListingCard.php
The listing card for the Stock Photo Theme (Ppt_ai). An image-first card: the preview photo dominates, with format + resolution badges riding the image and a price / "Free" chip, and a compact contributor + downloads footer under the title — the look of a stock-photo/asset library (Unsplash / Shutterstock). Forked from Ppt\Directory\ListingCard for the photography profile so the stock-photo grid can diverge from the generic directory card. Every _ai surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a photo asset looks the same everywhere. Its extra classes (.ppt-lc--photo, .ppt-lc-ph-*) live in assets/css/listing-card.css. Normalized shape (adds format/resolution/downloads/author to the base shape): name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id, format, resolution, downloads, author
ListingEditor ListingEditor.php
Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.
ListingFaq ListingFaq.php
Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.
ListingHours ListingHours.php
Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.
ListingLocation ListingLocation.php
Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().
ListingMap ListingMap.php
The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.
ListingSections ListingSections.php
PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.
ListingTiles ListingTiles.php
The tile row at the top of the AI Photo listing form: Category and Media type. Category used to be a stacked chip-and-button field under the title — a row of chips, a "Choose category" button and a line of hint text, three stacked things for one decision. It now reads as the same square tile the Directory theme uses (Ppt\Directory\ListingTiles) and the Coupon, Software and Real Estate themes before it: icon, label, and either the chips that are set or the word "Select". The Category tile opens the SHARED category picker modal (assets/js/category-picker.js) — searchable, with a Clear All / Continue footer — so there is still exactly one picker implementation across every fork. Media type is the fork-specific second tile, in the slot Directory gives to Business hours. It answers "is this listing a photo or a video?" — the choice that decides which uploader the form shows and which side of the Images/Videos search filter the asset lands on (StockMeta::META_ASSET_TYPE, read by SearchPage::mediaMetaQuery()). It was a segmented Photo/Video toggle buried at the top of the Photos & video card, three cards down the form, so the one decision that changes what the rest of the form asks for was the one you met last. The toggle is not duplicated here: mediaModal() renders the SAME radio group, with the same names and values, inside a dialog that lives in the Photos & video card. The card's own script (inc/_ai/views/listing-form.php) still scopes every query to [data-asset-card], so the pane switching and the "remove the photo before you can switch to video" locks keep working untouched — there is one control, in a new place, not a second source of truth for the same meta. Collections are deliberately NOT a third choice: StockMeta::META_COLLECTION is derived on save from how many assets the listing carries (ListingEditor::syncCollection()), so offering it as a manual pick would put the admin in a fight with the save path.
ListingViews ListingViews.php
PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).
Media Media.php
Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.
Reviews Reviews.php
Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.
SearchPage SearchPage.php
Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | newest | oldest | rating | price_low | price_high | title paged pagination
SingleListing SingleListing.php
Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.
StockMeta StockMeta.php
Stock-photo asset facts (Ppt_ai) — the per-listing details a stock-photo card and single page show that a generic directory listing has no concept of: the file format(s), the pixel resolution, the download count and the contributor. Real meta always wins. When a value isn't set AND we're rendering a demo/preview listing (a ppt-demo post under Preview::isPreview()), a deterministic sample value is returned so the demo grid always reads as a real stock library. Outside demo mode an unset value returns empty/0 — nothing is fabricated on a live site. This mirrors the demo-fallback idiom in Ppt_sp\Product. Meta keys (a listing form / import can populate these): ph_format e.g. "JPG", "RAW + JPG" ph_resolution e.g. "6000 × 4000" ph_downloads integer download count ph_dpi e.g. "300" ph_filesize e.g. "24.6 MB" ph_keywords comma-separated tags
TermMeta TermMeta.php
Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.
Uploads Uploads.php
Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.