Nom du projet : CrystalSpace
Plateforme : macOS
Langage : Swift
UI : SwiftUI + AppKit lorsque nécessaire
Version minimale : macOS 15
Architecture : application macOS 100 % native
Backend principal : API compatible Wallspace
IA : strictement aucune
CrystalSpace est une application native macOS permettant d’utiliser des vidéos comme fonds d’écran animés, de parcourir une bibliothèque distante compatible avec Wallspace, de télécharger les contenus localement et de gérer indépendamment plusieurs écrans.
L’objectif n’est pas de faire un simple clone graphique de Wallspace mais une implémentation propre, modulaire et open-source-friendly capable de consommer les mêmes ressources/API tout en restant indépendante de Wallspace.
CrystalSpace doit privilégier :
- performances natives ;
- consommation CPU minimale ;
- consommation mémoire contrôlée ;
- absence totale de compte obligatoire ;
- absence de télémétrie ;
- absence de publicité ;
- fonctionnement hors ligne pour les wallpapers téléchargés ;
- architecture modulaire ;
- support multi-écrans ;
- support des vidéos locales ;
- compatibilité avec l’API utilisée par Wallspace ;
- aucun composant d’intelligence artificielle.
CrystalSpace ne doit contenir :
- aucun LLM ;
- aucun modèle Core ML servant à la génération ou recherche sémantique ;
- aucune API OpenAI, Gemini, Claude, etc. ;
- aucune génération d'image ;
- aucune recommandation basée sur ML ;
- aucun moteur de recherche sémantique ;
- aucun tracking utilisateur ;
- aucun identifiant publicitaire.
La recherche repose exclusivement sur des informations déterministes :
- titre ;
- tags ;
- catégorie ;
- auteur ;
- résolution ;
- popularité ;
- date ;
- identifiant.
Écran d'accueil présentant des collections provenant de l'API.
Sections possibles :
- Featured
- Popular
- New
- Recently Added
- Trending
- Categories
- Continue Watching / Recently Viewed
- Favorites
- Downloaded
Le contenu des sections doit être fourni par le backend lorsque disponible.
L'interface ne doit pas dépendre de noms de catégories codés en dur.
Vue permettant de parcourir toute la bibliothèque Wallspace.
Fonctionnalités :
- grille adaptative ;
- chargement paginé ;
- infinite scrolling ;
- aperçu animé au survol ;
- filtre par catégorie ;
- filtre par résolution ;
- filtre par ratio ;
- filtre vidéo/statique si l'API en expose ;
- tri ;
- recherche texte classique.
Tri possible :
Popular
Most liked
Newest
Oldest
Recently updated
Name
Recherche locale/distante classique.
Exemple :
space
peut correspondre à :
title CONTAINS "space"
tags CONTAINS "space"
category == "space"
La recherche doit utiliser l'endpoint Wallspace lorsqu'il existe.
Sinon CrystalSpace peut effectuer un filtrage local des métadonnées déjà récupérées.
Aucune interprétation sémantique du texte.
Chaque wallpaper dispose d'une fiche complète.
Afficher :
Preview
Title
Description
Author
Category
Tags
Mood
Resolution
Aspect ratio
Duration
FPS
File size
Popularity
Likes
Download status
Actions :
Apply
Download
Remove Download
Favorite
Unfavorite
Preview
Open Fullscreen
Apply to Display...
Apply to All Displays
Add to Playlist
Open Source
Toutes les propriétés doivent être optionnelles au niveau du modèle Swift pour supporter les différences de versions de l'API.
Formats minimum acceptés :
MP4
MOV
M4V
Décodage :
AVFoundation
AVPlayer
AVPlayerItem
AVPlayerLayer
Priorité au décodage matériel via VideoToolbox lorsque disponible.
CrystalSpace ne doit pas réencoder une vidéo distante si elle peut être lue directement.
Lecture :
loop = true
audio = muted
playback rate = 1.0
Audio toujours désactivé pour un wallpaper.
Créer un moteur indépendant :
protocol WallpaperRenderer {
func start()
func pause()
func resume()
func stop()
func load(_ wallpaper: LocalWallpaper) async throws
}Implémentation principale :
AVPlayerWallpaperRenderer
Chaque écran peut posséder son propre renderer.
Architecture :
WallpaperEngine
├── DisplaySession
│ ├── NSScreen
│ ├── WallpaperWindow
│ └── WallpaperRenderer
│
├── DisplaySession
│ └── ...
│
└── PowerController
Chaque écran possède une fenêtre spéciale AppKit.
final class WallpaperWindow: NSWindowPropriétés attendues :
borderless
non-activating
no shadow
transparent
non movable
ignores mouse events
Elle doit rester derrière les fenêtres normales et ne jamais apparaître dans Cmd+Tab.
CrystalSpace doit gérer :
- changement de résolution ;
- branchement écran ;
- débranchement écran ;
- changement de Spaces ;
- changement d'écran principal ;
- sortie de veille.
Utiliser :
NSScreen.screensChaque écran possède un identifiant persistant indépendant de son ordre dans NSScreen.screens.
Modèle :
struct DisplayConfiguration: Codable {
let displayID: String
var wallpaperID: String?
var playlistID: UUID?
var playbackMode: PlaybackMode
}Modes :
Independent
Mirror Primary
Same Wallpaper
Playlist
Disabled
CrystalSpace doit continuer de fonctionner avec au moins 7 écrans.
Ne jamais utiliser uniquement :
NSScreen.localizedNamecomme identifiant.
Créer un DisplayManager utilisant les informations CoreGraphics disponibles pour générer une identité persistante.
Exemple :
vendor
product
serial
CGDirectDisplayID
CrystalSpace doit fonctionner sur plusieurs Spaces.
Lorsqu'un utilisateur change de Space :
- le wallpaper ne doit pas disparaître ;
- la vidéo ne doit pas redémarrer inutilement ;
- la lecture doit rester synchronisée.
Prévoir une couche :
SpaceCoordinator
responsable de la relation entre les fenêtres wallpaper et Mission Control.
Éviter autant que possible l'automatisation via System Events/AppleScript.
Fonctionnalité distincte du moteur Desktop.
LockScreenService
Support complet ciblé :
macOS 26+
Sur macOS 15 :
Desktop live wallpaper : oui
Lock Screen live wallpaper : non ou fallback documenté
Ne jamais faire échouer l'application entière lorsque la fonction n'est pas disponible.
API :
protocol LockScreenProvider {
var isSupported: Bool { get }
func apply(_ wallpaper: LocalWallpaper) async throws
func remove() async throws
}Section :
Library → Local
Import par :
File Picker
Drag & Drop
Open With
CrystalSpace conserve seulement un bookmark de sécurité lorsque possible.
Utiliser :
security-scoped bookmarks
Informations extraites avec AVFoundation :
duration
resolution
fps
codec
bitrate
hasAudio
fileSize
L'audio n'est jamais lu.
CrystalSpace doit pouvoir utiliser la bibliothèque fournie par Wallspace à travers son API actuelle.
L'API étant non documentée publiquement, elle doit être considérée comme :
External
Unstable
Versionless until proven otherwise
Toute interaction Wallspace doit passer par un module indépendant.
CrystalSpace
│
▼
WallpaperRepository
│
▼
WallpaperProvider
│
┌────┴────────┐
│ │
▼ ▼
WallSpaceAPI LocalProvider
protocol WallpaperProvider: Sendable {
func home() async throws -> HomeFeed
func wallpapers(
page: Int,
filters: WallpaperFilters
) async throws -> WallpaperPage
func wallpaper(id: String) async throws -> Wallpaper
func search(
query: String,
page: Int
) async throws -> WallpaperPage
func categories() async throws -> [WallpaperCategory]
func downloadURL(
for wallpaper: Wallpaper,
quality: WallpaperQuality
) async throws -> URL
}Wallspace devient donc simplement :
final actor WallSpaceAPIProvider: WallpaperProviderLe client HTTP doit pouvoir reproduire les requêtes nécessaires à :
Home feed
Wallpaper listing
Wallpaper metadata
Categories
Search
Popular
Newest
Wallpaper details
Preview URLs
Thumbnail URLs
Video URLs
Download URLs
Pagination
Si l'API expose également :
likes
community
upload
collections
leur prise en charge peut être ajoutée séparément.
Ne jamais disperser les URLs Wallspace dans le projet.
Centraliser :
struct WallSpaceEndpointsLe client doit être tolérant aux changements du backend.
Utiliser des structures Decodable séparées des modèles applicatifs.
Exemple :
WallSpaceWallpaperDTO
│
▼
WallpaperMapper
│
▼
Wallpaper
Jamais :
API JSON
↓
UI directement
Ainsi un changement de clé Wallspace n'affecte que le module WallSpaceAPI.
struct Wallpaper: Identifiable, Hashable, Sendable {
let id: String
let title: String
let description: String?
let thumbnailURL: URL?
let previewURL: URL?
let author: WallpaperAuthor?
let tags: [String]
let categories: [WallpaperCategory]
let mood: String?
let variants: [WallpaperVariant]
let createdAt: Date?
let updatedAt: Date?
let popularity: Int?
let likes: Int?
}struct WallpaperVariant: Hashable, Sendable {
let quality: WallpaperQuality
let width: Int
let height: Int
let fps: Double?
let codec: String?
let size: Int64?
let url: URL?
}Qualités :
enum WallpaperQuality {
case hd
case qhd
case uhd4K
case original
case unknown(String)
}Il faut accepter de nouvelles qualités sans casser le décodage.
Foundation uniquement.
URLSessionArchitecture :
HTTPClient
│
├── request()
├── download()
├── retry()
└── cancellation
Utiliser :
async/await
actors
Sendable
URLSession
Codable
Éviter les bibliothèques HTTP tierces sauf nécessité réelle.
Deux niveaux.
NSCache
Pour :
- thumbnails ;
- previews ;
- réponses API fréquemment utilisées.
Pour :
- metadata JSON ;
- thumbnails ;
- wallpapers téléchargés.
Créer :
actor DownloadManagerResponsabilités :
queue
pause
resume
cancel
retry
progress
deduplication
storage
verification
Support de :
URLSessionDownloadTask
Un téléchargement interrompu doit pouvoir reprendre lorsque le serveur supporte HTTP Range.
Structure proposée :
~/Library/Application Support/CrystalSpace/
Database/
CrystalSpace.sqlite
Cache/
Metadata/
Thumbnails/
Previews/
Wallpapers/
<wallpaper-id>/
metadata.json
wallpaper.mp4
thumbnail.webp
Local/
Playlists/
Utiliser SQLite.
Option recommandée :
GRDB.swift
ou wrapper SQLite minimal maison.
Tables :
wallpapers
downloads
favorites
playlists
playlist_items
display_configs
local_wallpapers
history
settings
Ne pas stocker inutilement une copie intégrale de toutes les métadonnées du serveur.
Favoris locaux par défaut.
FavoriteStore
Aucun compte nécessaire.
Structure :
wallpaperID
provider
createdAt
Cela permet à terme :
provider = wallspace
provider = local
provider = crystalspace
Création de playlists locales.
struct WallpaperPlaylist {
let id: UUID
var name: String
var wallpapers: [WallpaperReference]
var configuration: PlaylistConfiguration
}Modes :
Sequential
Shuffle
Déclencheurs :
Every X minutes
Every X hours
On wake
On login
Manual
Aucune sélection intelligente.
CrystalSpace doit pouvoir fonctionner principalement depuis la barre de menu.
Menu :
CrystalSpace
─────────────
Current Wallpaper
Change Wallpaper
Next
Previous
Pause
Resume
Displays >
MacBook Display
External Display
Playlists >
Open CrystalSpace
─────────────
Performance Mode >
Automatic
Quality
Balanced
Battery Saver
─────────────
Clear Memory Cache
Clear Disk Cache
Settings
Quit CrystalSpace
Objectifs indicatifs :
CPU : ≈ 0 %
Objectif :
< 2 % CPU moyen
sur Apple Silicon dans des conditions normales.
Le GPU/VideoToolbox doit faire l'essentiel du décodage.
Mémoire cible :
< 150 MB
hors caches vidéo système importants.
Ne jamais décoder une vidéo lorsque :
- l'écran est éteint ;
- le Mac est verrouillé et aucun lock-screen animé n'est actif ;
- un écran n'affiche pas le wallpaper ;
- le système est en veille.
Option utilisateur :
Pause on battery
Option :
Pause when Low Power Mode is enabled
Option :
Pause while fullscreen application is running
Créer :
PowerManager
Observer :
battery state
AC power
Low Power Mode
sleep
wake
display sleep
display wake
Le moteur reçoit ensuite :
.pause(reason:)
.resume(reason:)
Maximum resolution
Maximum FPS
Native screen resolution
Normal FPS
Lower resolution variant
30 FPS maximum when applicable
pause on battery option
Choix déterministe selon :
power source
screen resolution
thermal state
Aucune IA.
Observer :
ProcessInfo.processInfo.thermalStateRéactions :
.nominal
→ normal
.fair
→ normal/balanced
.serious
→ reduce workload
.critical
→ pause animated wallpaper
Style :
native macOS
modern
glass
dark/light
large thumbnails
minimal chrome
Utiliser autant que possible les matériaux macOS :
Material
VisualEffect
Glass
Ne pas recréer artificiellement l'apparence de macOS avec des composants web.
Sidebar proposée :
Home
Discover
├── Explore
├── Popular
└── New
Library
├── Favorites
├── Downloads
├── Local
└── Playlists
Settings
Au survol d'une carte :
Image → short silent video preview
Ne pas charger immédiatement les previews de toute la grille.
Pipeline :
thumbnail
↓ hover ~300 ms
request preview
↓
play
↓ mouse exit
stop
release
Limiter le nombre de previews simultanées.
CrystalSpace doit posséder son propre scheme :
crystalspace://
Exemples :
crystalspace://wallpaper/123
crystalspace://search?q=space
crystalspace://playlist/UUID
Prévoir également un parseur pouvant reconnaître les liens publics Wallspace.
Exemple conceptuel :
https://wallspace.app/wallpaper/...
Lorsqu'un identifiant Wallspace peut être extrait :
Wallspace URL
↓
WallSpaceLinkParser
↓
WallpaperReference
↓
WallSpaceAPIProvider
CrystalSpace doit fonctionner sans connexion pour :
wallpapers téléchargés
local wallpapers
favorites locaux
playlists locales
display configuration
Si Wallspace est inaccessible :
No Connection
mais le moteur wallpaper continue de fonctionner.
Une panne API ne doit jamais retirer le wallpaper courant.
Pas de :
eval
embedded browser authentication
remote code execution
downloaded executable
dynamic plugin from server
Les fichiers distants sont considérés comme non fiables.
Vérifier :
MIME
extension
container
file size
dimensions
video duration
avant utilisation.
Minimiser les permissions macOS.
Ne demander une autorisation que lorsqu'une fonction l'exige réellement.
Éviter notamment une dépendance obligatoire à :
System Events
AppleScript
Accessibility
Screen Recording
CrystalSpace doit continuer de fonctionner sans ces permissions.
CrystalSpace ne génère aucun :
device ID
advertising ID
user fingerprint
analytics ID
Pas de :
Google Analytics
Firebase Analytics
Sentry telemetry
Mixpanel
PostHog
Amplitude
Les requêtes strictement nécessaires au téléchargement de contenu restent évidemment visibles par le serveur Wallspace.
Launch at Login
Show Menu Bar Icon
Check for Updates
Default Display
Performance Mode
Pause on Battery
Pause in Low Power Mode
Pause when Display Sleeps
Pause during Fullscreen Apps
Default Quality
Storage Location
Maximum Cache Size
Configuration indépendante pour chaque écran.
Wallspace Provider Enabled
API Base URL
Clear API Cache
L'URL personnalisable est utile pour le développement et pour une future API CrystalSpace compatible.
Simple écran rappelant :
No analytics
No account
No AI
CrystalSpace/
│
├── App/
│ ├── CrystalSpaceApp.swift
│ ├── AppDelegate.swift
│ └── AppState.swift
│
├── Core/
│ ├── Models/
│ ├── Networking/
│ ├── Storage/
│ ├── Logging/
│ └── Extensions/
│
├── Providers/
│ ├── WallpaperProvider.swift
│ │
│ ├── WallSpace/
│ │ ├── WallSpaceAPIProvider.swift
│ │ ├── WallSpaceEndpoints.swift
│ │ ├── WallSpaceDTO.swift
│ │ ├── WallSpaceMapper.swift
│ │ └── WallSpaceLinkParser.swift
│ │
│ └── Local/
│ └── LocalWallpaperProvider.swift
│
├── WallpaperEngine/
│ ├── WallpaperEngine.swift
│ ├── WallpaperWindow.swift
│ ├── DisplayManager.swift
│ ├── DisplaySession.swift
│ ├── SpaceCoordinator.swift
│ ├── PowerManager.swift
│ └── Renderers/
│ └── AVPlayerWallpaperRenderer.swift
│
├── LockScreen/
│ └── LockScreenService.swift
│
├── Downloads/
│ └── DownloadManager.swift
│
├── Database/
│ ├── DatabaseManager.swift
│ ├── Migrations/
│ └── Records/
│
├── Features/
│ ├── Home/
│ ├── Explore/
│ ├── Search/
│ ├── WallpaperDetails/
│ ├── Favorites/
│ ├── Downloads/
│ ├── Local/
│ ├── Playlists/
│ └── Settings/
│
└── MenuBar/
└── MenuBarController.swift
Utiliser Swift Concurrency partout où cela a du sens.
async/await
Task
TaskGroup
Actor
@MainActor
Sendable
Exemples d'actors :
WallSpaceAPIProvider
DownloadManager
WallpaperCache
DatabaseManager
L'UI reste :
@MainActor
Utiliser :
import OSLogCatégories :
API
Downloads
Playback
Displays
Database
Cache
LockScreen
Power
Ne jamais logger :
tokens
signed URLs complets sensibles
identifiants privés
chemins privés inutiles
Créer :
enum CrystalSpaceError: Error {
case network(NetworkError)
case provider(ProviderError)
case playback(PlaybackError)
case download(DownloadError)
case storage(StorageError)
case unsupportedFeature
}Les erreurs réseau doivent être dissociées des erreurs du moteur wallpaper.
L'application ne doit jamais supposer que Wallspace sera son unique source.
Grâce à :
WallpaperProvider
on doit pouvoir ajouter ultérieurement :
CrystalSpace API
Local folder
Another public API
Community server
sans modifier le moteur de rendu.
SwiftUI shell
AppKit wallpaper window
AVPlayer renderer
single display
local MP4
DisplayManager
persistent display IDs
independent wallpaper sessions
hotplug handling
capture/document actual API
WallSpaceAPIProvider
DTO
pagination
home
explore
search
wallpaper details
download manager
disk cache
offline mode
favorites
playlist storage
scheduler
shuffle
per-display playlists
menu bar
launch at login
power management
thermal management
fullscreen detection
Lock Screen integration
system-specific wallpaper features
Le MVP est considéré comme fonctionnel lorsque CrystalSpace peut :
- lancer l'application ;
- afficher la bibliothèque Wallspace ;
- ouvrir la fiche d'un wallpaper ;
- lire son preview ;
- télécharger sa vidéo ;
- appliquer la vidéo comme wallpaper ;
- utiliser une vidéo MP4 locale ;
- gérer plusieurs écrans ;
- mémoriser les wallpapers configurés ;
- restaurer la configuration au démarrage ;
- fonctionner hors ligne avec les fichiers déjà téléchargés ;
- utiliser moins de ressources qu'une solution basée sur Electron.
CrystalSpace n'a pas vocation à intégrer :
AI search
AI recommendations
AI generation
image generation
video generation
chatbot
accounts sociaux
ads
tracking
Electron
WebView-based UI
Aucune partie du moteur CrystalSpace ne doit dépendre directement du format interne de l'API Wallspace.
Toujours respecter :
Wallspace JSON
↓
WallSpaceDTO
↓
WallSpaceMapper
↓
CrystalSpace Model
↓
Repository
↓
UI / Wallpaper Engine
Cette séparation est obligatoire.
Ainsi, si Wallspace change son API :
CrystalSpace Core → inchangé
UI → inchangée
Wallpaper Engine → inchangé
Database → presque inchangée
WallSpace adapter → mis à jour
CrystalSpace doit suivre cette idée :
Un moteur de wallpapers macOS doit être une couche système légère, pas une plateforme publicitaire ou un moteur de recommandation.
L'application doit rester :
Native.
Fast.
Local-first.
Private.
Deterministic.
No AI.