full app done

This commit is contained in:
davmar
2026-09-18 00:01:42 +02:00
parent 8475abeddb
commit 78fd23f69f
74 changed files with 8104 additions and 1963 deletions
-130
View File
@@ -1,130 +0,0 @@
/// Domain models for catalog items shown in lists and detail screens.
library;
import 'package:flutter/material.dart';
/// High-level grouping used for filters, icons, and accent colors.
enum ItemCategory {
design,
development,
productivity,
lifestyle,
}
extension ItemCategoryX on ItemCategory {
String get label => switch (this) {
ItemCategory.design => 'Design',
ItemCategory.development => 'Development',
ItemCategory.productivity => 'Productivity',
ItemCategory.lifestyle => 'Lifestyle',
};
IconData get icon => switch (this) {
ItemCategory.design => Icons.palette_outlined,
ItemCategory.development => Icons.code,
ItemCategory.productivity => Icons.bolt_outlined,
ItemCategory.lifestyle => Icons.spa_outlined,
};
/// Stable accent used by [Hero] tiles so the animation has a colored target.
Color get accent => switch (this) {
ItemCategory.design => const Color(0xFF7C3AED),
ItemCategory.development => const Color(0xFF0F766E),
ItemCategory.productivity => const Color(0xFFC2410C),
ItemCategory.lifestyle => const Color(0xFF0369A1),
};
static ItemCategory fromName(String name) {
return ItemCategory.values.firstWhere(
(category) => category.name == name,
orElse: () => ItemCategory.productivity,
);
}
static ItemCategory fromIndex(int index) {
return ItemCategory.values[index.abs() % ItemCategory.values.length];
}
}
/// A catalog entry displayed on Home / Search and opened on the detail route.
class Item {
const Item({
required this.id,
required this.title,
required this.subtitle,
required this.description,
required this.category,
required this.createdAt,
});
final String id;
final String title;
final String subtitle;
final String description;
final ItemCategory category;
final DateTime createdAt;
/// Tag used by the list-to-detail [Hero] animation.
String get heroTag => 'item-hero-$id';
Item copyWith({
String? id,
String? title,
String? subtitle,
String? description,
ItemCategory? category,
DateTime? createdAt,
}) {
return Item(
id: id ?? this.id,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
description: description ?? this.description,
category: category ?? this.category,
createdAt: createdAt ?? this.createdAt,
);
}
/// Maps a JSONPlaceholder `/posts` payload (or any similar JSON object).
factory Item.fromJson(Map<String, dynamic> json) {
final id = json['id']?.toString() ?? '0';
final title = (json['title'] as String? ?? 'Untitled').trim();
final body = (json['body'] as String? ?? '').trim();
final numericId = int.tryParse(id) ?? 0;
return Item(
id: 'remote-$id',
title: _titleCase(title),
subtitle: _firstSentence(body),
description: body.replaceAll('\n', ' '),
category: ItemCategoryX.fromIndex(numericId),
createdAt: DateTime(2026, 1, 1).add(Duration(days: numericId)),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'subtitle': subtitle,
'description': description,
'category': category.name,
'createdAt': createdAt.toIso8601String(),
};
}
static String _titleCase(String value) {
if (value.isEmpty) return value;
return value[0].toUpperCase() + value.substring(1);
}
static String _firstSentence(String value) {
if (value.isEmpty) return 'No summary available.';
final cleaned = value.replaceAll('\n', ' ');
final period = cleaned.indexOf('.');
if (period <= 0) {
return cleaned.length > 80 ? '${cleaned.substring(0, 77)}…' : cleaned;
}
return cleaned.substring(0, period + 1);
}
}
+33
View File
@@ -0,0 +1,33 @@
/// A single values-sort card with title, description, and optional domain.
library;
/// Immutable card drawn from the values deck.
class ValueCard {
const ValueCard({
required this.id,
required this.title,
required this.description,
this.domain = 'Values',
});
final String id;
final String title;
final String description;
final String domain;
factory ValueCard.fromJson(Map<String, dynamic> json) {
return ValueCard(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String? ?? '',
domain: json['domain'] as String? ?? 'Values',
);
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'description': description,
'domain': domain,
};
}
+68
View File
@@ -0,0 +1,68 @@
/// The five reflective piles used during the initial card sort.
library;
import 'package:flutter/material.dart';
/// Pile ranks from most to least important (design labels 5→1).
enum ValuePile {
mostImportant,
veryImportant,
important,
somewhatImportant,
notImportant,
}
extension ValuePileX on ValuePile {
/// Short UI label (e.g. "Most Important").
String get label => switch (this) {
ValuePile.mostImportant => 'Most Important',
ValuePile.veryImportant => 'Very Important',
ValuePile.important => 'Important',
ValuePile.somewhatImportant => 'Somewhat Important',
ValuePile.notImportant => 'Not Important',
};
/// Compact chip label used in filters and progress bars.
String get shortLabel => switch (this) {
ValuePile.mostImportant => 'Most',
ValuePile.veryImportant => 'Very',
ValuePile.important => 'Important',
ValuePile.somewhatImportant => 'Somewhat',
ValuePile.notImportant => 'Not',
};
/// Supporting line under pile buttons on the sort screen.
String get subtitle => switch (this) {
ValuePile.mostImportant => 'Non-negotiable core',
ValuePile.veryImportant => 'Deeply held guide',
ValuePile.important => 'Valued aspect',
ValuePile.somewhatImportant => 'Contextual',
ValuePile.notImportant => 'Not a current compass',
};
/// Numeric rank shown in the Stitch designs (5 = most).
int get rank => switch (this) {
ValuePile.mostImportant => 5,
ValuePile.veryImportant => 4,
ValuePile.important => 3,
ValuePile.somewhatImportant => 2,
ValuePile.notImportant => 1,
};
/// Accent color for this pile.
Color get accent => switch (this) {
ValuePile.mostImportant => const Color(0xFFC45C3E),
ValuePile.veryImportant => const Color(0xFFC87D55),
ValuePile.important => const Color(0xFF6B8168),
ValuePile.somewhatImportant => const Color(0xFF5B7A8C),
ValuePile.notImportant => const Color(0xFF8A847C),
};
static ValuePile? fromName(String? name) {
if (name == null) return null;
for (final pile in ValuePile.values) {
if (pile.name == name) return pile;
}
return null;
}
}