Initial commit: NANAHUI CRM - Flutter Firebase CRM assistant
@@ -0,0 +1,27 @@
|
|||||||
|
# API Keys Configuration
|
||||||
|
# Copy this file to .env and fill in your actual keys
|
||||||
|
|
||||||
|
# OpenAI API (for AI features)
|
||||||
|
# Get from: https://platform.openai.com/api-keys
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
# Anthropic API (alternative AI provider)
|
||||||
|
# Get from: https://console.anthropic.com/
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
|
||||||
|
# Google AI API (Gemini)
|
||||||
|
# Get from: https://makersuite.google.com/app/apikey
|
||||||
|
GOOGLE_AI_API_KEY=your_google_ai_api_key_here
|
||||||
|
|
||||||
|
# Firebase Configuration
|
||||||
|
# These will be in your Firebase console
|
||||||
|
FIREBASE_API_KEY=your_firebase_api_key
|
||||||
|
FIREBASE_APP_ID=your_firebase_app_id
|
||||||
|
FIREBASE_PROJECT_ID=your_firebase_project_id
|
||||||
|
FIREBASE_MESSAGING_SENDER_ID=your_firebase_sender_id
|
||||||
|
FIREBASE_STORAGE_BUCKET=your_firebase_storage_bucket
|
||||||
|
|
||||||
|
# Google Calendar API
|
||||||
|
# Enable in Google Cloud Console
|
||||||
|
GOOGLE_CLOUD_CLIENT_ID=your_google_cloud_client_id
|
||||||
|
GOOGLE_CLOUD_CLIENT_SECRET=your_google_cloud_client_secret
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
google-services.json
|
||||||
|
GoogleService-Info.plist
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# Build
|
||||||
|
build/
|
||||||
|
**/build/
|
||||||
|
**/android/build/
|
||||||
|
**/ios/build/
|
||||||
|
**/web/build/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.packages
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: android
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: linux
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: macos
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: web
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
- platform: windows
|
||||||
|
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 🔑 Как добавить SHA-1 fingerprint
|
||||||
|
|
||||||
|
## Способ 1: Через Android Studio (проще)
|
||||||
|
|
||||||
|
1. Открой проект в Android Studio
|
||||||
|
2. Справа открой **Gradle** панель
|
||||||
|
3. Разверни `:android` → `Tasks` → `android`
|
||||||
|
4. Дважды кликни на **signingReport**
|
||||||
|
5. В консоли найди строку `SHA1:`
|
||||||
|
6. Скопируй значение
|
||||||
|
|
||||||
|
## Способ 2: Через командную строку
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd c:\Users\ormix\NANAHUI\android
|
||||||
|
.\gradlew app:signingReport
|
||||||
|
```
|
||||||
|
|
||||||
|
Ищи строку вида:
|
||||||
|
```
|
||||||
|
SHA1: A1:B2:C3:D4:E5:F6:...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Способ 3: Через keytool (если есть Java)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ➕ Добавить в Firebase Console
|
||||||
|
|
||||||
|
1. Открой https://console.firebase.google.com/project/nanax26-1f33b/settings/general/android:com.nanaxe.nanaxe_crm
|
||||||
|
2. Прокрути вниз до **SHA certificate fingerprints**
|
||||||
|
3. Нажми **Add fingerprint**
|
||||||
|
4. Вставь скопированный SHA-1
|
||||||
|
5. Сохрани
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Проверка
|
||||||
|
|
||||||
|
После добавления SHA-1:
|
||||||
|
1. Перескачай `google-services.json` из Firebase Console
|
||||||
|
2. Замени файл в `android/app/google-services.json`
|
||||||
|
3. Запусти приложение: `flutter run`
|
||||||
|
4. Попробуй войти через Google
|
||||||
|
|
||||||
|
Если работает — всё настроено правильно! 🎉
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# 🏗 Архитектура NANAHUI CRM
|
||||||
|
|
||||||
|
## Общая схема
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Flutter Application │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Presentation Layer (UI) │
|
||||||
|
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
|
||||||
|
│ │ Screens │ Widgets │ Themes │ Navigation │ │
|
||||||
|
│ │ │ │ │ (GoRouter) │ │
|
||||||
|
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ State Management Layer (Riverpod) │
|
||||||
|
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
|
||||||
|
│ │ Providers │ Notifiers │ Streams │ State │ │
|
||||||
|
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Domain Layer (Business Logic) │
|
||||||
|
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
|
||||||
|
│ │ Models │ Repositories │ Services │ Use Cases │ │
|
||||||
|
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Data Layer │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Local Storage (Hive) │ │
|
||||||
|
│ │ ┌──────────┬──────────┬──────────┬──────────┐ │ │
|
||||||
|
│ │ │ Tasks │ Alarms │ MindMaps │ Events │ │ │
|
||||||
|
│ │ └──────────┴──────────┴──────────┴──────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Remote Storage (Firebase Firestore) │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────┼────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Firebase Auth │ │ AI Services │ │ Google Calendar │
|
||||||
|
│ (Google Sign- │ │ (OpenAI, │ │ API │
|
||||||
|
│ In, Email) │ │ Anthropic) │ │ │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поток данных
|
||||||
|
|
||||||
|
### 1. Аутентификация
|
||||||
|
```
|
||||||
|
User → AuthScreen → AuthNotifier (Riverpod) → Firebase Auth
|
||||||
|
↓
|
||||||
|
UserModel → Firestore
|
||||||
|
↓
|
||||||
|
HomeScreen
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Управление задачами
|
||||||
|
```
|
||||||
|
User → TasksScreen → TaskNotifier → FirestoreService → Firestore
|
||||||
|
↓
|
||||||
|
LocalStorageService (Hive)
|
||||||
|
↓
|
||||||
|
Offline Cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. AI генерация
|
||||||
|
```
|
||||||
|
User → TaskScreen → AIService → OpenAI API
|
||||||
|
↓
|
||||||
|
Generate Subtasks
|
||||||
|
↓
|
||||||
|
TaskModel → Firestore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Mind-карта
|
||||||
|
```
|
||||||
|
User → MindMapScreen → MindMapNotifier → FirestoreService
|
||||||
|
↓
|
||||||
|
MindMap Nodes (Tree)
|
||||||
|
↓
|
||||||
|
GraphView Widget (Render)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Синхронизация
|
||||||
|
```
|
||||||
|
┌──────────────┐ Compare ┌──────────────┐
|
||||||
|
│ Local Hive │ ←───────────→ │ Firestore │
|
||||||
|
│ (Offline) │ │ (Remote) │
|
||||||
|
└──────────────┘ └──────────────┘
|
||||||
|
↓ ↓
|
||||||
|
Merge Conflicts Real-time Sync
|
||||||
|
↓ ↓
|
||||||
|
Local Priority Remote Priority
|
||||||
|
└──────────────┬──────────────┘
|
||||||
|
↓
|
||||||
|
Merged Data → UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Модульная структура
|
||||||
|
|
||||||
|
### Core Module
|
||||||
|
```
|
||||||
|
core/
|
||||||
|
├── firebase_config.dart # Инициализация Firebase
|
||||||
|
├── firestore_service.dart # CRUD операции Firestore
|
||||||
|
├── local_storage_service.dart # Hive кэш
|
||||||
|
├── ai_service.dart # AI интеграции
|
||||||
|
├── router.dart # GoRouter маршруты
|
||||||
|
└── theme.dart # Material темы
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features Modules
|
||||||
|
```
|
||||||
|
features/
|
||||||
|
├── auth/
|
||||||
|
│ ├── auth_screen.dart
|
||||||
|
│ ├── auth_provider.dart
|
||||||
|
│ └── auth_service.dart
|
||||||
|
├── tasks/
|
||||||
|
│ ├── task_list_screen.dart
|
||||||
|
│ ├── task_detail_screen.dart
|
||||||
|
│ ├── task_provider.dart
|
||||||
|
│ └── task_service.dart
|
||||||
|
├── mindmap/
|
||||||
|
│ ├── mindmap_screen.dart
|
||||||
|
│ ├── mindmap_widget.dart
|
||||||
|
│ ├── mindmap_provider.dart
|
||||||
|
│ └── mindmap_service.dart
|
||||||
|
├── calendar/
|
||||||
|
│ ├── calendar_screen.dart
|
||||||
|
│ ├── event_provider.dart
|
||||||
|
│ └── google_calendar_service.dart
|
||||||
|
└── alarms/
|
||||||
|
├── alarms_screen.dart
|
||||||
|
├── alarm_provider.dart
|
||||||
|
└── notification_service.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ролевая модель
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ User Roles │
|
||||||
|
├──────────────┬──────────────┬──────────────┤
|
||||||
|
│ Admin │ Member │ Viewer │
|
||||||
|
├──────────────┼──────────────┼──────────────┤
|
||||||
|
│ ✓ Все права │ ✓ Создание │ ✓ Просмотр │
|
||||||
|
│ ✓ Редактиро- │ задач │ задач │
|
||||||
|
│ вание mind-│ ✓ Редактиро- │ ✓ Просмотр │
|
||||||
|
│ карт │ вание │ mind-карт │
|
||||||
|
│ ✓ Управление │ своих задач│ ✗ Редактиро- │
|
||||||
|
│ командой │ ✓ Просмотр │ вание │
|
||||||
|
│ ✓ Удаление │ общих задач│ ✗ Удаление │
|
||||||
|
│ любых задач│ ✗ Редактиро- │ │
|
||||||
|
│ │ вание mind-│ │
|
||||||
|
│ │ карт │ │
|
||||||
|
└──────────────┴──────────────┴──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Модель данных
|
||||||
|
|
||||||
|
### Task Model
|
||||||
|
```dart
|
||||||
|
TaskModel {
|
||||||
|
String id;
|
||||||
|
String title;
|
||||||
|
String? description;
|
||||||
|
String projectId;
|
||||||
|
String? parentTaskId; // Для иерархии
|
||||||
|
TaskStatus status; // todo, inProgress, review, done
|
||||||
|
TaskPriority priority; // low, medium, high, urgent
|
||||||
|
String? assigneeId;
|
||||||
|
String? creatorId;
|
||||||
|
List<String> tags;
|
||||||
|
DateTime? dueDate;
|
||||||
|
int estimatedHours;
|
||||||
|
Map<String, dynamic> aiMetadata; // AI данные
|
||||||
|
DateTime createdAt;
|
||||||
|
DateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MindMapNode Model
|
||||||
|
```dart
|
||||||
|
MindMapNode {
|
||||||
|
String id;
|
||||||
|
String projectId;
|
||||||
|
String? parentId; // Для дерева
|
||||||
|
int level; // Уровень вложенности
|
||||||
|
String title;
|
||||||
|
String? taskId; // Связь с задачей
|
||||||
|
String color;
|
||||||
|
double x, y; // Координаты
|
||||||
|
DateTime createdAt;
|
||||||
|
DateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Синхронизация
|
||||||
|
|
||||||
|
### Стратегия
|
||||||
|
1. **Offline-first**: Все данные сначала сохраняются локально
|
||||||
|
2. **Background sync**: Периодическая синхронизация с Firestore
|
||||||
|
3. **Conflict resolution**:
|
||||||
|
- Last write wins для простых полей
|
||||||
|
- Merge для коллекций
|
||||||
|
- User prompt для критических конфликтов
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
```
|
||||||
|
┌─────────┐ ┌─────────────┐ ┌──────────┐
|
||||||
|
│ UI │────→│ Repository │────→│ Firestore│
|
||||||
|
└─────────┘ └─────────────┘ └──────────┘
|
||||||
|
↓ ↓
|
||||||
|
┌─────────┐ ┌─────────────┐
|
||||||
|
│ Cache │←────│ Hive │
|
||||||
|
└─────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Firebase Security Rules
|
||||||
|
```javascript
|
||||||
|
// Проверка прав доступа
|
||||||
|
function hasRole(allowedRoles) {
|
||||||
|
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role in allowedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка владения
|
||||||
|
function isOwner(resource) {
|
||||||
|
return resource.ownerId == request.auth.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка членства в команде
|
||||||
|
function isTeamMember(teamId) {
|
||||||
|
let team = get(/databases/$(database)/documents/teams/$(teamId)).data;
|
||||||
|
return team.memberIds.hasAny([request.auth.uid]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Производительность
|
||||||
|
|
||||||
|
### Оптимизация
|
||||||
|
1. **Pagination**: Загрузка задач порциями по 50
|
||||||
|
2. **Lazy loading**: Mind-карта загружается по частям
|
||||||
|
3. **Caching**: Hive для офлайн-доступа
|
||||||
|
4. **Indexes**: Firestore индексы для частых запросов
|
||||||
|
5. **Batching**: Пакетная запись в Firestore
|
||||||
|
|
||||||
|
### Firestore Indexes
|
||||||
|
```
|
||||||
|
tasks: [projectId, createdAt] [assigneeId, status] [dueDate]
|
||||||
|
events: [startTime, endTime] [attendeeIds]
|
||||||
|
mindmaps: [projectId, level]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Масштабирование
|
||||||
|
|
||||||
|
### Этапы роста
|
||||||
|
|
||||||
|
**Phase 1: MVP (сейчас)**
|
||||||
|
- Базовые CRUD операции
|
||||||
|
- Простая аутентификация
|
||||||
|
- Локальный кэш
|
||||||
|
|
||||||
|
**Phase 2: Team Features**
|
||||||
|
- Командная работа
|
||||||
|
- Роли и разрешения
|
||||||
|
- Real-time collaboration
|
||||||
|
|
||||||
|
**Phase 3: AI Integration**
|
||||||
|
- Умное планирование
|
||||||
|
- Авто-приоритизация
|
||||||
|
- Генерация отчётов
|
||||||
|
|
||||||
|
**Phase 4: Enterprise**
|
||||||
|
- SSO интеграция
|
||||||
|
- Audit logs
|
||||||
|
- Advanced analytics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Документ создан для команды NANAHUI** 🚀
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# ✅ Настройка Firebase завершена!
|
||||||
|
|
||||||
|
## 📦 Что сделано:
|
||||||
|
|
||||||
|
### 1. Gradle настроен правильно ✅
|
||||||
|
- **Kotlin DSL** (`build.gradle.kts`) — современный подход
|
||||||
|
- **Google Services Plugin** версии 4.4.4 добавлен
|
||||||
|
- **Firebase BoM** версии 34.11.0 подключён
|
||||||
|
- Все Firebase зависимости добавлены:
|
||||||
|
- `firebase-analytics`
|
||||||
|
- `firebase-auth`
|
||||||
|
- `firebase-firestore`
|
||||||
|
- `firebase-messaging`
|
||||||
|
|
||||||
|
### 2. Package name ✅
|
||||||
|
- Application ID: `com.nanaxe.nanaxe_crm`
|
||||||
|
- Соответствует `google-services.json`
|
||||||
|
|
||||||
|
### 3. AndroidManifest обновлён ✅
|
||||||
|
- Добавлены разрешения для Firebase и уведомлений
|
||||||
|
- Название приложения: "NANAHUI CRM"
|
||||||
|
|
||||||
|
### 4. Правила безопасности Firestore ✅
|
||||||
|
- Файл `firestore.rules` создан
|
||||||
|
- Доступ только для аутентифицированных пользователей
|
||||||
|
- Поддержка ролей (admin, member, viewer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 СЛЕДУЮЩИЙ ШАГ: Загрузить правила безопасности
|
||||||
|
|
||||||
|
### Открой Firebase Console:
|
||||||
|
1. Перейди в https://console.firebase.google.com
|
||||||
|
2. Выбери проект `nanax26-1f33b`
|
||||||
|
3. Слева: **Build** → **Firestore Database**
|
||||||
|
4. Вверху выбери вкладку **Rules**
|
||||||
|
5. Скопируй содержимое файла `firestore.rules` и вставь в редактор
|
||||||
|
6. Нажми **Publish**
|
||||||
|
|
||||||
|
**Или используй Firebase CLI:**
|
||||||
|
```bash
|
||||||
|
npm install -g firebase-tools
|
||||||
|
firebase login
|
||||||
|
firebase use nanax26-1f33b
|
||||||
|
firebase deploy --only firestore:rules
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Запуск приложения
|
||||||
|
|
||||||
|
### 1. Подключи устройство или запусти эмулятор
|
||||||
|
```bash
|
||||||
|
# Проверка подключенных устройств
|
||||||
|
flutter devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Запусти приложение
|
||||||
|
```bash
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Если ошибка с SHA-1 ключом
|
||||||
|
Для Google Sign-In нужен SHA-1 fingerprint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Для debug ключа (Windows)
|
||||||
|
cd android
|
||||||
|
gradlew app:signingReport
|
||||||
|
```
|
||||||
|
|
||||||
|
Скопируй SHA-1 из отчёта и добавь в Firebase Console:
|
||||||
|
1. Project Settings → Your apps → Android app
|
||||||
|
2. Add fingerprint
|
||||||
|
3. Вставь SHA-1
|
||||||
|
4. Save
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Проверка работы
|
||||||
|
|
||||||
|
### Чек-лист:
|
||||||
|
- [ ] Firebase проект создан
|
||||||
|
- [ ] Authentication включён (Google provider)
|
||||||
|
- [ ] Firestore база создана (region: eur3)
|
||||||
|
- [ ] Android приложение зарегистрировано
|
||||||
|
- [ ] `google-services.json` лежит в `android/app/`
|
||||||
|
- [ ] Gradle файлы обновлены
|
||||||
|
- [ ] Правила безопасности загружены в Firebase
|
||||||
|
- [ ] SHA-1 fingerprint добавлен (для Google Sign-In)
|
||||||
|
|
||||||
|
### Тест аутентификации:
|
||||||
|
1. Запусти приложение
|
||||||
|
2. Нажми "Войти через Google"
|
||||||
|
3. Выбери аккаунт
|
||||||
|
4. Если успешно — переход на главный экран ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Структура Firestore (автоматически создаётся)
|
||||||
|
|
||||||
|
```
|
||||||
|
users/
|
||||||
|
└─ {userId}/
|
||||||
|
├─ email
|
||||||
|
├─ displayName
|
||||||
|
├─ role: "member"
|
||||||
|
├─ teamIds: []
|
||||||
|
└─ settings: {}
|
||||||
|
|
||||||
|
teams/
|
||||||
|
└─ {teamId}/
|
||||||
|
├─ name
|
||||||
|
├─ memberIds: []
|
||||||
|
└─ projects: subcollection
|
||||||
|
|
||||||
|
projects/
|
||||||
|
└─ {projectId}/
|
||||||
|
├─ name
|
||||||
|
├─ ownerId
|
||||||
|
└─ tasks: subcollection
|
||||||
|
|
||||||
|
tasks/
|
||||||
|
└─ {taskId}/
|
||||||
|
├─ title
|
||||||
|
├─ status: "todo"
|
||||||
|
├─ priority: "medium"
|
||||||
|
└─ assigneeId
|
||||||
|
|
||||||
|
mindmaps/
|
||||||
|
└─ {projectId}/
|
||||||
|
└─ nodes/
|
||||||
|
└─ {nodeId}/
|
||||||
|
├─ title
|
||||||
|
├─ level
|
||||||
|
└─ parentId
|
||||||
|
|
||||||
|
events/
|
||||||
|
└─ {eventId}/
|
||||||
|
├─ title
|
||||||
|
├─ startTime
|
||||||
|
└─ attendeeIds
|
||||||
|
|
||||||
|
alarms/
|
||||||
|
└─ {alarmId}/
|
||||||
|
├─ title
|
||||||
|
├─ dateTime
|
||||||
|
└─ isRepeating
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Возможные ошибки
|
||||||
|
|
||||||
|
### "No Firebase App"
|
||||||
|
```
|
||||||
|
Проверь что google-services.json лежит в android/app/
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Sign-in failed"
|
||||||
|
```
|
||||||
|
1. Добавь SHA-1 fingerprint в Firebase Console
|
||||||
|
2. Перескачай google-services.json
|
||||||
|
3. Пересобери приложение: flutter clean && flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Permission denied"
|
||||||
|
```
|
||||||
|
1. Проверь что правила безопасности загружены
|
||||||
|
2. Убедись что пользователь аутентифицирован
|
||||||
|
```
|
||||||
|
|
||||||
|
### "App not found"
|
||||||
|
```
|
||||||
|
1. Проверь что package name совпадает:
|
||||||
|
- В google-services.json: "com.nanaxe.nanaxe_crm"
|
||||||
|
- В build.gradle.kts: applicationId = "com.nanaxe.nanaxe_crm"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Документация
|
||||||
|
|
||||||
|
- [Firebase Flutter Setup](https://firebase.flutter.dev/docs/overview)
|
||||||
|
- [Firestore Rules](https://firebase.google.com/docs/firestore/security/get-started)
|
||||||
|
- [Google Sign-In Flutter](https://pub.dev/packages/google_sign_in)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово! Запускай `flutter run`** 🚀
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Firebase configuration file
|
||||||
|
# Download this from Firebase Console:
|
||||||
|
# Project Settings > Your apps > Add app
|
||||||
|
|
||||||
|
# For Android:
|
||||||
|
# 1. Add your app with package name: com.nanahui.nanahui_crm
|
||||||
|
# 2. Download google-services.json
|
||||||
|
# 3. Place it in: android/app/google-services.json
|
||||||
|
|
||||||
|
# For iOS:
|
||||||
|
# 1. Add your app with bundle ID: com.nanahui.nanahuiCrm
|
||||||
|
# 2. Download GoogleService-Info.plist
|
||||||
|
# 3. Place it in: ios/Runner/GoogleService-Info.plist
|
||||||
|
|
||||||
|
# For Web:
|
||||||
|
# 1. Add web app
|
||||||
|
# 2. Copy firebaseConfig object
|
||||||
|
# 3. Update lib/core/firebase_options.dart with your config
|
||||||
|
|
||||||
|
# Required Firebase APIs:
|
||||||
|
# - Authentication (Google Sign-In)
|
||||||
|
# - Firestore Database
|
||||||
|
# - Cloud Messaging (FCM)
|
||||||
|
# - Cloud Functions (optional, for AI processing)
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# 📁 Структура проекта NANAHUI CRM
|
||||||
|
|
||||||
|
```
|
||||||
|
NANAHUI/
|
||||||
|
│
|
||||||
|
├── 📄 README.md # Общая информация о проекте
|
||||||
|
├── 📄 QUICKSTART.md # Быстрый старт и настройка
|
||||||
|
├── 📄 ARCHITECTURE.md # Архитектурное описание
|
||||||
|
├── 📄 FIREBASE_SETUP.md # Инструкция по настройке Firebase
|
||||||
|
├── 📄 .env.example # Шаблон переменных окружения
|
||||||
|
├── 📄 .gitignore # Git ignore файл
|
||||||
|
├── 📄 pubspec.yaml # Зависимости Flutter
|
||||||
|
│
|
||||||
|
├── 📂 lib/ # Исходный код приложения
|
||||||
|
│ │
|
||||||
|
│ ├── 📄 main.dart # Точка входа
|
||||||
|
│ │
|
||||||
|
│ ├── 📂 core/ # Ядро приложения
|
||||||
|
│ │ ├── firebase_config.dart # Конфигурация Firebase
|
||||||
|
│ │ ├── firestore_service.dart # Сервис для работы с Firestore
|
||||||
|
│ │ ├── local_storage_service.dart # Локальное хранилище (Hive)
|
||||||
|
│ │ ├── ai_service.dart # AI сервис (OpenAI, Anthropic)
|
||||||
|
│ │ ├── router.dart # Маршрутизация (GoRouter)
|
||||||
|
│ │ └── theme.dart # Темы оформления
|
||||||
|
│ │
|
||||||
|
│ ├── 📂 models/ # Модели данных
|
||||||
|
│ │ ├── user_model.dart # Пользователь
|
||||||
|
│ │ ├── task_model.dart # Задача
|
||||||
|
│ │ ├── project_model.dart # Проект
|
||||||
|
│ │ ├── mind_map_model.dart # Mind-карта
|
||||||
|
│ │ ├── calendar_event_model.dart # Событие календаря
|
||||||
|
│ │ └── alarm_model.dart # Будильник/Напоминание
|
||||||
|
│ │
|
||||||
|
│ ├── 📂 providers/ # State Management (Riverpod)
|
||||||
|
│ │ └── auth_provider.dart # Провайдер аутентификации
|
||||||
|
│ │
|
||||||
|
│ ├── 📂 screens/ # Экраны приложения
|
||||||
|
│ │ ├── auth_screen.dart # Экран входа
|
||||||
|
│ │ ├── home_screen.dart # Главная навигация
|
||||||
|
│ │ ├── dashboard_screen.dart # Дашборд
|
||||||
|
│ │ ├── tasks_screen.dart # Задачи
|
||||||
|
│ │ ├── mind_map_screen.dart # Mind-карта
|
||||||
|
│ │ ├── calendar_screen.dart # Календарь
|
||||||
|
│ │ └── alarms_screen.dart # Будильники
|
||||||
|
│ │
|
||||||
|
│ ├── 📂 widgets/ # Переиспользуемые виджеты
|
||||||
|
│ │ └── (создаётся по мере необходимости)
|
||||||
|
│ │
|
||||||
|
│ └── 📂 features/ # Функциональные модули
|
||||||
|
│ └── (для будущего расширения)
|
||||||
|
│
|
||||||
|
├── 📂 assets/ # Ресурсы приложения
|
||||||
|
│ ├── 📂 images/ # Изображения
|
||||||
|
│ │ └── .gitkeep
|
||||||
|
│ └── 📂 icons/ # Иконки
|
||||||
|
│ └── .gitkeep
|
||||||
|
│
|
||||||
|
├── 📂 test/ # Тесты
|
||||||
|
│ └── widget_test.dart # Widget тесты
|
||||||
|
│
|
||||||
|
├── 📂 android/ # Android платформа
|
||||||
|
│ ├── 📂 app/
|
||||||
|
│ │ ├── src/main/
|
||||||
|
│ │ │ ├── AndroidManifest.xml
|
||||||
|
│ │ │ ├── kotlin/ # Kotlin код
|
||||||
|
│ │ │ ├── res/ # Ресурсы Android
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── build.gradle # Android build config
|
||||||
|
│ └── google-services.json # ⚠️ Firebase config (нужно добавить)
|
||||||
|
│
|
||||||
|
├── 📂 ios/ # iOS платформа
|
||||||
|
│ ├── 📂 Runner/
|
||||||
|
│ │ ├── AppDelegate.swift
|
||||||
|
│ │ ├── Info.plist
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── GoogleService-Info.plist # ⚠️ Firebase config (нужно добавить)
|
||||||
|
│
|
||||||
|
├── 📂 web/ # Web платформа
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── manifest.json
|
||||||
|
│
|
||||||
|
├── 📂 windows/ # Windows платформа
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── 📂 linux/ # Linux платформа
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── 📂 macos/ # macOS платформа
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── 📂 build/ # ⚠️ Build artifacts (не в git)
|
||||||
|
└── (генерируется при сборке)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 Ключевые файлы
|
||||||
|
|
||||||
|
### Конфигурация
|
||||||
|
| Файл | Описание |
|
||||||
|
|------|----------|
|
||||||
|
| `pubspec.yaml` | Зависимости и настройки Flutter |
|
||||||
|
| `.gitignore` | Исключения для Git |
|
||||||
|
| `.env.example` | Шаблон переменных окружения |
|
||||||
|
|
||||||
|
### Firebase (нужно добавить)
|
||||||
|
| Файл | Где взять | Куда положить |
|
||||||
|
|------|-----------|---------------|
|
||||||
|
| `google-services.json` | Firebase Console → Android | `android/app/` |
|
||||||
|
| `GoogleService-Info.plist` | Firebase Console → iOS | `ios/Runner/` |
|
||||||
|
|
||||||
|
### Основные модули
|
||||||
|
| Модуль | Файл | Описание |
|
||||||
|
|--------|------|----------|
|
||||||
|
| **Entry Point** | `lib/main.dart` | Инициализация и запуск |
|
||||||
|
| **Firebase** | `lib/core/firebase_config.dart` | Настройка Firebase |
|
||||||
|
| **Auth** | `lib/providers/auth_provider.dart` | Аутентификация через Google |
|
||||||
|
| **Firestore** | `lib/core/firestore_service.dart` | CRUD операции с БД |
|
||||||
|
| **AI** | `lib/core/ai_service.dart` | Интеграция с OpenAI |
|
||||||
|
| **Storage** | `lib/core/local_storage_service.dart` | Офлайн кэш на Hive |
|
||||||
|
| **Router** | `lib/core/router.dart` | Навигация между экранами |
|
||||||
|
| **Theme** | `lib/core/theme.dart` | Светлая/тёмная темы |
|
||||||
|
|
||||||
|
## 📦 Основные зависимости
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- `flutter_riverpod: ^2.5.0` - Реактивное управление состоянием
|
||||||
|
- `riverpod_annotation: ^2.3.5` - Code generation для Riverpod
|
||||||
|
|
||||||
|
### Firebase
|
||||||
|
- `firebase_core: ^2.32.0` - Ядро Firebase
|
||||||
|
- `firebase_auth: ^4.19.0` - Аутентификация
|
||||||
|
- `cloud_firestore: ^4.17.0` - Firestore база данных
|
||||||
|
- `firebase_messaging: ^14.9.0` - Push уведомления
|
||||||
|
|
||||||
|
### Local Storage
|
||||||
|
- `hive_flutter: ^1.1.0` - Быстрое локальное хранилище
|
||||||
|
- `hive: ^2.2.3` - Hive ядро
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- `go_router: ^13.1.0` - Декларативная маршрутизация
|
||||||
|
|
||||||
|
### AI
|
||||||
|
- `http: ^1.2.0` - HTTP клиент для API запросов
|
||||||
|
- `dio: ^5.4.0` - Продвинутый HTTP клиент
|
||||||
|
|
||||||
|
### UI
|
||||||
|
- `graphview: ^1.2.0` - Визуализация графов (для mind-карт)
|
||||||
|
- `flutter_mind_map: ^1.0.23` - Готовый mind-map виджет
|
||||||
|
- `flutter_staggered_grid_view: ^0.7.0` - Сетка для карточек
|
||||||
|
|
||||||
|
### Google Integration
|
||||||
|
- `google_sign_in: ^6.2.1` - Вход через Google
|
||||||
|
- `googleapis: ^13.2.0` - Google API (Calendar и др.)
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- `flutter_local_notifications: ^17.0.0` - Локальные уведомления
|
||||||
|
- `workmanager: ^0.5.2` - Фоновые задачи
|
||||||
|
|
||||||
|
## 🎯 Точки расширения
|
||||||
|
|
||||||
|
### Для добавления новой фичи:
|
||||||
|
|
||||||
|
1. **Создать модель** в `lib/models/`
|
||||||
|
2. **Создать сервис** в `lib/core/` или `lib/features/`
|
||||||
|
3. **Создать провайдер** в `lib/providers/`
|
||||||
|
4. **Создать экран** в `lib/screens/`
|
||||||
|
5. **Добавить маршрут** в `lib/core/router.dart`
|
||||||
|
6. **Добавить виджеты** в `lib/widgets/`
|
||||||
|
|
||||||
|
### Пример: Добавление заметок
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── models/
|
||||||
|
│ └── note_model.dart # ✨ Новая модель
|
||||||
|
├── features/
|
||||||
|
│ └── notes/
|
||||||
|
│ ├── note_service.dart # ✨ Сервис
|
||||||
|
│ └── note_provider.dart # ✨ Провайдер
|
||||||
|
├── screens/
|
||||||
|
│ └── notes_screen.dart # ✨ Экран
|
||||||
|
└── widgets/
|
||||||
|
└── note_card.dart # ✨ Виджет
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Метрики проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
Строки кода: ~2000+
|
||||||
|
Файлов: ~20
|
||||||
|
Модулей: 6 (Auth, Tasks, MindMap, Calendar, Alarms, Dashboard)
|
||||||
|
Моделей данных: 6
|
||||||
|
Экранов: 6
|
||||||
|
Сервисов: 4 (Firebase, Firestore, AI, LocalStorage)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 CI/CD (будущее)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
.github/workflows/
|
||||||
|
├── build.yml # Сборка APK
|
||||||
|
├── test.yml # Запуск тестов
|
||||||
|
└── deploy.yml # Деплой в Store
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Структура создана для команды NANAHUI** 🚀
|
||||||
|
Последнее обновление: Март 2026
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
# 🚀 Быстрый старт - NANAHUI CRM
|
||||||
|
|
||||||
|
## ✅ Что уже создано
|
||||||
|
|
||||||
|
### Структура проекта
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── core/ # Ядро приложения
|
||||||
|
│ ├── ai_service.dart # AI сервис (OpenAI, Anthropic)
|
||||||
|
│ ├── firebase_config.dart # Firebase конфигурация
|
||||||
|
│ ├── firestore_service.dart # Firestore сервис (CRUD операции)
|
||||||
|
│ ├── local_storage_service.dart # Локальное хранилище (Hive)
|
||||||
|
│ ├── router.dart # Маршрутизация (GoRouter)
|
||||||
|
│ └── theme.dart # Темы оформления
|
||||||
|
├── features/ # Функциональные модули (будущие)
|
||||||
|
├── models/ # Модели данных
|
||||||
|
│ ├── user_model.dart # Пользователь
|
||||||
|
│ ├── task_model.dart # Задача
|
||||||
|
│ ├── project_model.dart # Проект
|
||||||
|
│ ├── mind_map_model.dart # Mind-карта
|
||||||
|
│ ├── calendar_event_model.dart # Событие календаря
|
||||||
|
│ └── alarm_model.dart # Будильник
|
||||||
|
├── providers/ # State management (Riverpod)
|
||||||
|
│ └── auth_provider.dart # Аутентификация
|
||||||
|
├── screens/ # Экраны
|
||||||
|
│ ├── auth_screen.dart # Вход
|
||||||
|
│ ├── home_screen.dart # Главная навигация
|
||||||
|
│ ├── dashboard_screen.dart # Дашборд
|
||||||
|
│ ├── tasks_screen.dart # Задачи
|
||||||
|
│ ├── mind_map_screen.dart # Mind-карта
|
||||||
|
│ ├── calendar_screen.dart # Календарь
|
||||||
|
│ └── alarms_screen.dart # Будильники
|
||||||
|
└── widgets/ # Виджеты (будущие)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Настройка Firebase
|
||||||
|
|
||||||
|
### 1. Создайте проект в Firebase Console
|
||||||
|
|
||||||
|
1. Перейдите на https://console.firebase.google.com
|
||||||
|
2. Нажмите "Add project"
|
||||||
|
3. Введите имя: "NANAHUI CRM"
|
||||||
|
4. Отключите Google Analytics (не нужно для начала)
|
||||||
|
5. Создайте проект
|
||||||
|
|
||||||
|
### 2. Включите Authentication
|
||||||
|
|
||||||
|
1. В меню слева: **Build** → **Authentication**
|
||||||
|
2. Нажмите **Get started**
|
||||||
|
3. Включите **Google** провайдер:
|
||||||
|
- Click **Google** → **Enable**
|
||||||
|
- Enter support email: ваш email
|
||||||
|
- **Save**
|
||||||
|
|
||||||
|
### 3. Создайте Firestore базу
|
||||||
|
|
||||||
|
1. В меню: **Build** → **Firestore Database**
|
||||||
|
2. Нажмите **Create database**
|
||||||
|
3. Выберите **Start in test mode** (потом настроим правила)
|
||||||
|
4. Выберите локацию (например, us-central)
|
||||||
|
5. **Enable**
|
||||||
|
|
||||||
|
### 4. Добавьте Android приложение
|
||||||
|
|
||||||
|
1. В Firebase Console: **Project Settings** (шестерёнка)
|
||||||
|
2. **Your apps** → **Add app** → **Android**
|
||||||
|
3. Введите package name: `com.nanahui.nanahui_crm`
|
||||||
|
4. Скачайте `google-services.json`
|
||||||
|
5. Положите файл в: `android/app/google-services.json`
|
||||||
|
|
||||||
|
### 5. Добавьте iOS приложение (если нужно)
|
||||||
|
|
||||||
|
1. **Add app** → **iOS**
|
||||||
|
2. Bundle ID: `com.nanahui.nanahuiCrm`
|
||||||
|
3. Скачайте `GoogleService-Info.plist`
|
||||||
|
4. Положите в: `ios/Runner/GoogleService-Info.plist`
|
||||||
|
|
||||||
|
### 6. Настройте android/app/build.gradle
|
||||||
|
|
||||||
|
В файле `android/app/build.gradle` добавьте:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
dependencies {
|
||||||
|
// ... другие зависимости
|
||||||
|
|
||||||
|
// Firebase
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||||
|
implementation 'com.google.firebase:firebase-analytics'
|
||||||
|
implementation 'com.google.firebase:firebase-auth'
|
||||||
|
implementation 'com.google.firebase:firebase-firestore'
|
||||||
|
implementation 'com.google.firebase:firebase-messaging'
|
||||||
|
}
|
||||||
|
|
||||||
|
// В конце файла:
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 Настройка API ключей
|
||||||
|
|
||||||
|
### OpenAI API (для AI функций)
|
||||||
|
|
||||||
|
1. Перейдите на https://platform.openai.com/api-keys
|
||||||
|
2. Создайте новый API ключ
|
||||||
|
3. Скопируйте ключ
|
||||||
|
|
||||||
|
### Google Calendar API
|
||||||
|
|
||||||
|
1. Перейдите на https://console.cloud.google.com
|
||||||
|
2. Создайте новый проект или выберите существующий
|
||||||
|
3. Включите **Google Calendar API**
|
||||||
|
4. Создайте OAuth 2.0 credentials
|
||||||
|
5. Получите Client ID и Client Secret
|
||||||
|
|
||||||
|
## 📝 Настройка окружения
|
||||||
|
|
||||||
|
1. Скопируйте `.env.example` в `.env`:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Заполните `.env` своими ключами:
|
||||||
|
```
|
||||||
|
OPENAI_API_KEY=sk-your-actual-openai-key
|
||||||
|
FIREBASE_API_KEY=your-firebase-key
|
||||||
|
# и т.д.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏃 Запуск приложения
|
||||||
|
|
||||||
|
### 1. Установите зависимости
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Запуск на эмуляторе/устройстве
|
||||||
|
```bash
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Сборка APK (Android)
|
||||||
|
```bash
|
||||||
|
flutter build apk --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Сборка для iOS
|
||||||
|
```bash
|
||||||
|
flutter build ios --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Тестирование
|
||||||
|
|
||||||
|
### Проверка аутентификации
|
||||||
|
1. Запустите приложение
|
||||||
|
2. Нажмите "Войти через Google"
|
||||||
|
3. Выберите аккаунт
|
||||||
|
4. Должна произойти авторизация и переход на главный экран
|
||||||
|
|
||||||
|
### Проверка навигации
|
||||||
|
1. После входа используйте нижнюю навигационную панель
|
||||||
|
2. Переключайтесь между: Главная, Задачи, Mind-карта, Календарь, Будильники
|
||||||
|
|
||||||
|
## 🔥 Firestore правила безопасности
|
||||||
|
|
||||||
|
Создайте в Firebase Console → Firestore Database → Rules:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
rules_version = '2';
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
// Helper function
|
||||||
|
function isAuthenticated() {
|
||||||
|
return request.auth != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOwner(userId) {
|
||||||
|
return request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users
|
||||||
|
match /users/{userId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow write: if isOwner(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projects
|
||||||
|
match /projects/{projectId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update, delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
match /tasks/{taskId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create, update, delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
match /events/{eventId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create, update, delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alarms
|
||||||
|
match /alarms/{alarmId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create, update, delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mind maps
|
||||||
|
match /mindmaps/{projectId}/nodes/{nodeId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow write: if isAuthenticated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Структура Firestore
|
||||||
|
|
||||||
|
```
|
||||||
|
users/
|
||||||
|
└─ {userId}/
|
||||||
|
├─ email: string
|
||||||
|
├─ displayName: string
|
||||||
|
├─ photoUrl: string
|
||||||
|
├─ role: "admin" | "member" | "viewer"
|
||||||
|
├─ teamIds: string[]
|
||||||
|
└─ settings: map
|
||||||
|
|
||||||
|
projects/
|
||||||
|
└─ {projectId}/
|
||||||
|
├─ name: string
|
||||||
|
├─ description: string
|
||||||
|
├─ teamId: string
|
||||||
|
├─ ownerId: string
|
||||||
|
├─ memberIds: string[]
|
||||||
|
├─ status: string
|
||||||
|
├─ startDate: timestamp
|
||||||
|
├─ endDate: timestamp
|
||||||
|
└─ color: string
|
||||||
|
|
||||||
|
tasks/
|
||||||
|
└─ {taskId}/
|
||||||
|
├─ title: string
|
||||||
|
├─ description: string
|
||||||
|
├─ projectId: string
|
||||||
|
├─ parentTaskId: string?
|
||||||
|
├─ status: "todo" | "inProgress" | "review" | "done"
|
||||||
|
├─ priority: "low" | "medium" | "high" | "urgent"
|
||||||
|
├─ assigneeId: string?
|
||||||
|
├─ creatorId: string
|
||||||
|
├─ dueDate: timestamp?
|
||||||
|
└─ aiMetadata: map
|
||||||
|
|
||||||
|
mindmaps/
|
||||||
|
└─ {projectId}/
|
||||||
|
└─ nodes/
|
||||||
|
└─ {nodeId}/
|
||||||
|
├─ parentId: string?
|
||||||
|
├─ level: number
|
||||||
|
├─ title: string
|
||||||
|
├─ taskId: string?
|
||||||
|
├─ x: number
|
||||||
|
├─ y: number
|
||||||
|
└─ color: string
|
||||||
|
|
||||||
|
events/
|
||||||
|
└─ {eventId}/
|
||||||
|
├─ title: string
|
||||||
|
├─ startTime: timestamp
|
||||||
|
├─ endTime: timestamp
|
||||||
|
├─ googleEventId: string?
|
||||||
|
└─ attendeeIds: string[]
|
||||||
|
|
||||||
|
alarms/
|
||||||
|
└─ {alarmId}/
|
||||||
|
├─ title: string
|
||||||
|
├─ dateTime: timestamp
|
||||||
|
├─ isRepeating: boolean
|
||||||
|
└─ taskId: string?
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 Следующие шаги
|
||||||
|
|
||||||
|
### Первоочередные задачи:
|
||||||
|
|
||||||
|
1. ✅ ~~Создать базовую структуру~~
|
||||||
|
2. ✅ ~~Настроить модели данных~~
|
||||||
|
3. ✅ ~~Создать экраны навигации~~
|
||||||
|
4. 🔲 Настроить Firebase проект
|
||||||
|
5. 🔲 Реализовать реальную аутентификацию
|
||||||
|
6. 🔲 Добавить CRUD операции с Firestore
|
||||||
|
7. 🔲 Реализовать синхронизацию офлайн-данных
|
||||||
|
8. 🔲 Добавить AI функции (OpenAI API)
|
||||||
|
9. 🔲 Интеграция с Google Calendar
|
||||||
|
10. 🔲 Создать mind-map виджет с drag-drop
|
||||||
|
|
||||||
|
### Для тестирования AI:
|
||||||
|
|
||||||
|
1. Добавьте OpenAI API ключ в `lib/core/ai_service.dart`:
|
||||||
|
```dart
|
||||||
|
final aiService = AIService(
|
||||||
|
openaiApiKey: 'sk-your-api-key-here',
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Протестируйте генерацию подзадач:
|
||||||
|
```dart
|
||||||
|
final subtasks = await aiService.generateSubtasks(
|
||||||
|
title: 'Создать CRM систему',
|
||||||
|
description: 'Разработка CRM для управления задачами команды',
|
||||||
|
);
|
||||||
|
print(subtasks);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
### Ошибка: "No Firebase App"
|
||||||
|
- Убедитесь, что `google-services.json` лежит в `android/app/`
|
||||||
|
- Проверьте, что добавлен `apply plugin: 'com.google.gms.google-services'`
|
||||||
|
|
||||||
|
### Ошибка: "Sign-in failed"
|
||||||
|
- Проверьте SHA-1 fingerprint для Android
|
||||||
|
- Убедитесь, что Google Sign-In включён в Firebase Console
|
||||||
|
|
||||||
|
### Ошибка: "Permission denied"
|
||||||
|
- Проверьте правила безопасности Firestore
|
||||||
|
- Убедитесь, что пользователь аутентифицирован
|
||||||
|
|
||||||
|
## 📚 Ресурсы
|
||||||
|
|
||||||
|
- [Flutter документация](https://docs.flutter.dev)
|
||||||
|
- [Riverpod документация](https://riverpod.dev)
|
||||||
|
- [Firebase Flutter](https://firebase.flutter.dev)
|
||||||
|
- [OpenAI API](https://platform.openai.com/docs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Создано для команды NANAHUI** 🚀
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# NANAHUI CRM - CRM-ассистент для команд
|
||||||
|
|
||||||
|
CRM-система с AI-планированием, mind-картами задач и интеграцией с Google Calendar.
|
||||||
|
|
||||||
|
## 🚀 Возможности
|
||||||
|
|
||||||
|
### Для команд и фрилансеров:
|
||||||
|
- ✅ **Управление задачами** - создание, назначение, приоритеты, статусы
|
||||||
|
- ✅ **Mind-карты проектов** - визуальное дерево задач (drag-drop для админов)
|
||||||
|
- ✅ **Календарь** - синхронизация с Google Calendar
|
||||||
|
- ✅ **Будильники/Напоминания** - локальные уведомления
|
||||||
|
- ✅ **AI-ассистент** - умное планирование и генерация подзадач
|
||||||
|
- ✅ **Ролевая модель** - admin, member, viewer
|
||||||
|
- ✅ **Офлайн-режим** - кэширование данных на устройстве
|
||||||
|
|
||||||
|
## 📦 Технологический стек
|
||||||
|
|
||||||
|
| Компонент | Технология |
|
||||||
|
|-----------|------------|
|
||||||
|
| Framework | Flutter |
|
||||||
|
| State Management | Riverpod |
|
||||||
|
| Backend | Firebase (Firestore, Auth, FCM) |
|
||||||
|
| Local Storage | Hive |
|
||||||
|
| Navigation | GoRouter |
|
||||||
|
| Mind Maps | flutter_mind_map, graphview |
|
||||||
|
| AI | OpenAI API, Anthropic API (планируется) |
|
||||||
|
|
||||||
|
## 🏗 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── core/
|
||||||
|
│ ├── firebase_config.dart
|
||||||
|
│ ├── local_storage_service.dart
|
||||||
|
│ ├── router.dart
|
||||||
|
│ └── theme.dart
|
||||||
|
├── features/
|
||||||
|
│ ├── tasks/
|
||||||
|
│ ├── mindmap/
|
||||||
|
│ ├── calendar/
|
||||||
|
│ └── alarms/
|
||||||
|
├── models/
|
||||||
|
│ ├── user_model.dart
|
||||||
|
│ ├── task_model.dart
|
||||||
|
│ ├── project_model.dart
|
||||||
|
│ ├── mind_map_model.dart
|
||||||
|
│ ├── calendar_event_model.dart
|
||||||
|
│ └── alarm_model.dart
|
||||||
|
├── providers/
|
||||||
|
│ └── auth_provider.dart
|
||||||
|
├── screens/
|
||||||
|
│ ├── auth_screen.dart
|
||||||
|
│ ├── home_screen.dart
|
||||||
|
│ ├── tasks_screen.dart
|
||||||
|
│ ├── mind_map_screen.dart
|
||||||
|
│ ├── calendar_screen.dart
|
||||||
|
│ └── alarms_screen.dart
|
||||||
|
└── widgets/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Настройка
|
||||||
|
|
||||||
|
### 1. Firebase Console
|
||||||
|
|
||||||
|
1. Создайте проект в [Firebase Console](https://console.firebase.google.com)
|
||||||
|
2. Включите Authentication (Google Sign-In)
|
||||||
|
3. Создайте Firestore базу данных
|
||||||
|
4. Скачайте конфиги:
|
||||||
|
- `google-services.json` для Android
|
||||||
|
- `GoogleService-Info.plist` для iOS
|
||||||
|
|
||||||
|
### 2. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Роли пользователей
|
||||||
|
|
||||||
|
| Роль | Права |
|
||||||
|
|------|-------|
|
||||||
|
| **Admin** | Создание/редактирование mind-карт, управление командой, все CRUD операции |
|
||||||
|
| **Member** | Создание задач, редактирование своих задач, просмотр общих |
|
||||||
|
| **Viewer** | Только просмотр |
|
||||||
|
|
||||||
|
## 🗄 Структура Firestore
|
||||||
|
|
||||||
|
```
|
||||||
|
users/{userId}
|
||||||
|
- email, displayName, photoUrl
|
||||||
|
- role: admin|member|viewer
|
||||||
|
- teamIds: []
|
||||||
|
- settings: {}
|
||||||
|
|
||||||
|
teams/{teamId}
|
||||||
|
- name, description
|
||||||
|
- memberIds: []
|
||||||
|
- projects: subcollection
|
||||||
|
|
||||||
|
projects/{projectId}
|
||||||
|
- name, description, color
|
||||||
|
- ownerId, memberIds
|
||||||
|
- startDate, endDate
|
||||||
|
|
||||||
|
tasks/{taskId}
|
||||||
|
- title, description
|
||||||
|
- projectId, parentTaskId
|
||||||
|
- status, priority
|
||||||
|
- assigneeId, creatorId
|
||||||
|
- dueDate, estimatedHours
|
||||||
|
- aiMetadata: {}
|
||||||
|
|
||||||
|
mindmaps/{mindMapId}
|
||||||
|
- projectId, title
|
||||||
|
- rootNodeId
|
||||||
|
- nodes: subcollection
|
||||||
|
|
||||||
|
events/{eventId}
|
||||||
|
- title, description
|
||||||
|
- startTime, endTime
|
||||||
|
- googleEventId
|
||||||
|
- attendeeIds: []
|
||||||
|
|
||||||
|
alarms/{alarmId}
|
||||||
|
- title, dateTime
|
||||||
|
- taskId, eventId
|
||||||
|
- isRepeating, repeatPattern
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤖 AI Функции (планируется)
|
||||||
|
|
||||||
|
- **Генерация подзадач** - разбивка больших задач на подзадачи
|
||||||
|
- **Приоритизация** - AI анализ важности задач
|
||||||
|
- **Планирование** - оптимальное распределение задач по времени
|
||||||
|
- **Саммари** - краткое описание проекта/задач
|
||||||
|
- **Анализ тональности** - анализ переписки с клиентами
|
||||||
|
|
||||||
|
## 📝 Следующие шаги
|
||||||
|
|
||||||
|
- [ ] Настроить Firebase проект
|
||||||
|
- [ ] Добавить реальную интеграцию с Firestore
|
||||||
|
- [ ] Реализовать AI сервисы (OpenAI, Anthropic)
|
||||||
|
- [ ] Добавить Google Calendar API
|
||||||
|
- [ ] Реализовать mind-map виджет с drag-drop
|
||||||
|
- [ ] Добавить локальные уведомления
|
||||||
|
- [ ] Реализовать синхронизацию офлайн-данных
|
||||||
|
|
||||||
|
## 📄 Лицензия
|
||||||
|
|
||||||
|
Internal use only (для команды NANAHUI)
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# ✅ НАСТРОЙКА ЗАВЕРШЕНА!
|
||||||
|
|
||||||
|
## 📊 Что сделано:
|
||||||
|
|
||||||
|
### Firebase
|
||||||
|
- ✅ Проект создан: `nanax26-1f33b`
|
||||||
|
- ✅ Firestore база в регионе `eur3`
|
||||||
|
- ✅ Authentication включён (Google Provider)
|
||||||
|
- ✅ Android приложение зарегистрировано
|
||||||
|
- ✅ `google-services.json` добавлен
|
||||||
|
- ✅ Firebase BoM 34.11.0 подключён
|
||||||
|
- ✅ Все Firebase зависимости добавлены
|
||||||
|
|
||||||
|
### Android
|
||||||
|
- ✅ Kotlin DSL (`build.gradle.kts`)
|
||||||
|
- ✅ Package: `com.nanaxe.nanaxe_crm`
|
||||||
|
- ✅ Google Services Plugin 4.4.4
|
||||||
|
- ✅ SHA-1 получен: `29:A6:36:C1:74:29:43:CE:2E:59:B8:28:46:FF:56:40:2B:9B:92:45`
|
||||||
|
|
||||||
|
### Flutter
|
||||||
|
- ✅ Зависимости установлены
|
||||||
|
- ✅ Код готов (3 info warning только о print())
|
||||||
|
- ✅ Структура проекта создана
|
||||||
|
- ✅ 6 экранов готовы
|
||||||
|
- ✅ Модели данных созданы
|
||||||
|
- ✅ Сервисы (Firebase, AI, Storage) готовы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 СЛЕДУЮЩИЕ ШАГИ:
|
||||||
|
|
||||||
|
### 1. Добавь SHA-1 в Firebase Console
|
||||||
|
|
||||||
|
**Сейчас же сделай это:**
|
||||||
|
|
||||||
|
1. Открой: https://console.firebase.google.com/project/nanax26-1f33b/settings/general/android:com.nanaxe.nanaxe_crm
|
||||||
|
2. Нажми **Add fingerprint** в разделе "SHA certificate fingerprints"
|
||||||
|
3. Вставь: `29:A6:36:C1:74:29:43:CE:2E:59:B8:28:46:FF:56:40:2B:9B:92:45`
|
||||||
|
4. Сохрани
|
||||||
|
5. Перескачай `google-services.json`
|
||||||
|
6. Замени файл в `c:\Users\ormix\NANAHUI\google-services.json`
|
||||||
|
|
||||||
|
### 2. Загрузи правила безопасности Firestore
|
||||||
|
|
||||||
|
1. Открой: https://console.firebase.google.com/project/nanax26-1f33b/firestore/rules
|
||||||
|
2. Открой файл `c:\Users\ormix\NANAHUI\firestore.rules`
|
||||||
|
3. Скопируй всё содержимое
|
||||||
|
4. Вставь в редактор на сайте (замени текущие правила `allow read, write: if false;`)
|
||||||
|
5. Нажми **Publish**
|
||||||
|
|
||||||
|
### 3. Запусти приложение
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd c:\Users\ormix\NANAHUI
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Проверка работы
|
||||||
|
|
||||||
|
### Тест 1: Аутентификация
|
||||||
|
1. Запусти приложение
|
||||||
|
2. Нажми "Войти через Google"
|
||||||
|
3. Выбери аккаунт
|
||||||
|
4. ✅ Должна произойти авторизация и переход на главный экран
|
||||||
|
|
||||||
|
### Тест 2: Навигация
|
||||||
|
1. После входа используй нижнюю панель
|
||||||
|
2. Переключайся между: Главная, Задачи, Mind-карта, Календарь, Будильники
|
||||||
|
3. ✅ Все экраны должны открываться
|
||||||
|
|
||||||
|
### Тест 3: Firestore (после загрузки правил)
|
||||||
|
1. Создай задачу через консоль отладки
|
||||||
|
2. Проверь что она появилась в Firestore Console
|
||||||
|
3. ✅ Данные должны сохраняться
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
NANAHUI/
|
||||||
|
├── 📄 README.md # Общая информация
|
||||||
|
├── 📄 QUICKSTART.md # Быстрый старт
|
||||||
|
├── 📄 ARCHITECTURE.md # Архитектура
|
||||||
|
├── 📄 FIREBASE_SETUP.md # Настройка Firebase
|
||||||
|
├── 📄 FIREBASE_DONE.md # Итоги настройки
|
||||||
|
├── 📄 ADD_SHA1.md # Как добавить SHA-1
|
||||||
|
├── 📄 PROJECT_STRUCTURE.md # Структура проекта
|
||||||
|
├── 📄 SETUP_COMPLETE.md # Этот файл
|
||||||
|
│
|
||||||
|
├── 📂 lib/
|
||||||
|
│ ├── 📄 main.dart # Точка входа
|
||||||
|
│ ├── 📂 core/ # Сервисы
|
||||||
|
│ │ ├── firebase_config.dart
|
||||||
|
│ │ ├── firestore_service.dart
|
||||||
|
│ │ ├── local_storage_service.dart
|
||||||
|
│ │ ├── ai_service.dart
|
||||||
|
│ │ ├── router.dart
|
||||||
|
│ │ └── theme.dart
|
||||||
|
│ ├── 📂 models/ # Модели данных
|
||||||
|
│ │ ├── user_model.dart
|
||||||
|
│ │ ├── task_model.dart
|
||||||
|
│ │ ├── project_model.dart
|
||||||
|
│ │ ├── mind_map_model.dart
|
||||||
|
│ │ ├── calendar_event_model.dart
|
||||||
|
│ │ └── alarm_model.dart
|
||||||
|
│ ├── 📂 providers/ # State management
|
||||||
|
│ │ └── auth_provider.dart
|
||||||
|
│ └── 📂 screens/ # Экраны
|
||||||
|
│ ├── auth_screen.dart
|
||||||
|
│ ├── home_screen.dart
|
||||||
|
│ ├── dashboard_screen.dart
|
||||||
|
│ ├── tasks_screen.dart
|
||||||
|
│ ├── mind_map_screen.dart
|
||||||
|
│ ├── calendar_screen.dart
|
||||||
|
│ └── alarms_screen.dart
|
||||||
|
│
|
||||||
|
├── 📂 android/
|
||||||
|
│ ├── 📂 app/
|
||||||
|
│ │ ├── build.gradle.kts # ✅ Настроен
|
||||||
|
│ │ └── src/main/AndroidManifest.xml # ✅ Разрешения
|
||||||
|
│ └── google-services.json # ✅ Firebase config
|
||||||
|
│
|
||||||
|
└── firestore.rules # ✅ Правила безопасности
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
### Firebase Project
|
||||||
|
- **Project ID**: `nanax26-1f33b`
|
||||||
|
- **Package**: `com.nanaxe.nanaxe_crm`
|
||||||
|
- **Region**: `eur3` (Europe)
|
||||||
|
|
||||||
|
### SHA-1 Fingerprint
|
||||||
|
```
|
||||||
|
29:A6:36:C1:74:29:43:CE:2E:59:B8:28:46:FF:56:40:2B:9B:92:45
|
||||||
|
```
|
||||||
|
|
||||||
|
### Firebase Dependencies
|
||||||
|
```kotlin
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:34.11.0"))
|
||||||
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
|
implementation("com.google.firebase:firebase-auth")
|
||||||
|
implementation("com.google.firebase:firebase-firestore")
|
||||||
|
implementation("com.google.firebase:firebase-messaging")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Если что-то не работает
|
||||||
|
|
||||||
|
### Ошибка: "Sign-in failed"
|
||||||
|
```
|
||||||
|
1. Проверь что SHA-1 добавлен в Firebase Console
|
||||||
|
2. Перескачай google-services.json
|
||||||
|
3. flutter clean && flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка: "Permission denied"
|
||||||
|
```
|
||||||
|
1. Загрузи правила безопасности из firestore.rules
|
||||||
|
2. Проверь что пользователь аутентифицирован
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка: "No Firebase App"
|
||||||
|
```
|
||||||
|
1. Проверь что google-services.json лежит в android/app/
|
||||||
|
2. Проверь что package name совпадает
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибка: "App not found"
|
||||||
|
```
|
||||||
|
1. Проверь что package name в google-services.json совпадает с build.gradle.kts
|
||||||
|
2. Должно быть: com.nanaxe.nanaxe_crm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Документация
|
||||||
|
|
||||||
|
- [Firebase Flutter](https://firebase.flutter.dev)
|
||||||
|
- [Firestore Rules](https://firebase.google.com/docs/firestore/security/get-started)
|
||||||
|
- [Google Sign-In](https://pub.dev/packages/google_sign_in)
|
||||||
|
- [Flutter Docs](https://docs.flutter.dev)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Что дальше?
|
||||||
|
|
||||||
|
### Ближайшие задачи:
|
||||||
|
1. ✅ Настроить Firebase (ГОТОВО)
|
||||||
|
2. ✅ Получить SHA-1 (ГОТОВО)
|
||||||
|
3. ⏳ Добавить SHA-1 в Firebase Console
|
||||||
|
4. ⏳ Загрузить правила безопасности
|
||||||
|
5. ⏳ Протестировать аутентификацию
|
||||||
|
6. ⏳ Реализовать CRUD задач
|
||||||
|
7. ⏳ Добавить AI функции
|
||||||
|
8. ⏳ Интеграция с Google Calendar
|
||||||
|
|
||||||
|
### Для разработки:
|
||||||
|
```bash
|
||||||
|
# Запуск с отладкой
|
||||||
|
flutter run --debug
|
||||||
|
|
||||||
|
# Сборка APK
|
||||||
|
flutter build apk --release
|
||||||
|
|
||||||
|
# Тесты
|
||||||
|
flutter test
|
||||||
|
|
||||||
|
# Анализ кода
|
||||||
|
flutter analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово! Добавь SHA-1 и загружай правила! 🚀**
|
||||||
|
|
||||||
|
Создано для команды NANAHUI
|
||||||
|
Март 2026
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.nanahui.nanahui_crm"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.nanaxe.nanaxe_crm"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Import the Firebase BoM
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:34.11.0"))
|
||||||
|
|
||||||
|
// Firebase Analytics
|
||||||
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
|
|
||||||
|
// Firebase Auth
|
||||||
|
implementation("com.google.firebase:firebase-auth")
|
||||||
|
|
||||||
|
// Firebase Firestore
|
||||||
|
implementation("com.google.firebase:firebase-firestore")
|
||||||
|
|
||||||
|
// Firebase Messaging
|
||||||
|
implementation("com.google.firebase:firebase-messaging")
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Firebase & Google Sign-In permissions -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="NANAHUI CRM"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.nanahui.nanahui_crm
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.google.gms.google-services") version "4.4.4" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.gitkeep
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.gitkeep
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function isAuthenticated() {
|
||||||
|
return request.auth != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOwner(userId) {
|
||||||
|
return request.auth.uid == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTeamMember(teamId) {
|
||||||
|
let team = get(/databases/$(database)/documents/teams/$(teamId)).data;
|
||||||
|
return team.memberIds.hasAny([request.auth.uid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users - пользователи могут читать и обновлять свой профиль
|
||||||
|
match /users/{userId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isOwner(userId);
|
||||||
|
allow update, delete: if isOwner(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teams - команды
|
||||||
|
match /teams/{teamId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update: if isAuthenticated() && isTeamMember(teamId);
|
||||||
|
allow delete: if isAuthenticated() && isTeamMember(teamId);
|
||||||
|
|
||||||
|
// Подколлекции команд
|
||||||
|
match /projects/{projectId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update, delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projects - проекты
|
||||||
|
match /projects/{projectId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update, delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks - задачи
|
||||||
|
match /tasks/{taskId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update: if isAuthenticated();
|
||||||
|
allow delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mind Maps - карты проектов
|
||||||
|
match /mindmaps/{projectId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update: if isAuthenticated();
|
||||||
|
allow delete: if isAuthenticated();
|
||||||
|
|
||||||
|
match /nodes/{nodeId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update: if isAuthenticated();
|
||||||
|
allow delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events - события календаря
|
||||||
|
match /events/{eventId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update: if isAuthenticated();
|
||||||
|
allow delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alarms - будильники
|
||||||
|
match /alarms/{alarmId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow create: if isAuthenticated();
|
||||||
|
allow update: if isAuthenticated();
|
||||||
|
allow delete: if isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings - настройки
|
||||||
|
match /settings/{settingId} {
|
||||||
|
allow read: if isAuthenticated();
|
||||||
|
allow write: if isAuthenticated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.nanahui.nanahuiCrm;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.nanahui.nanahuiCrm.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.nanahui.nanahuiCrm.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.nanahui.nanahuiCrm.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.nanahui.nanahuiCrm;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.nanahui.nanahuiCrm;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Nanahui Crm</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>nanahui_crm</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import '../models/task_model.dart';
|
||||||
|
|
||||||
|
class AIService {
|
||||||
|
final String? openaiApiKey;
|
||||||
|
final String? anthropicApiKey;
|
||||||
|
final String? googleAiApiKey;
|
||||||
|
|
||||||
|
AIService({
|
||||||
|
this.openaiApiKey,
|
||||||
|
this.anthropicApiKey,
|
||||||
|
this.googleAiApiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Генерация подзадач из основной задачи
|
||||||
|
Future<List<Map<String, dynamic>>> generateSubtasks({
|
||||||
|
required String title,
|
||||||
|
required String description,
|
||||||
|
}) async {
|
||||||
|
if (openaiApiKey == null) {
|
||||||
|
return _mockGenerateSubtasks(title, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('https://api.openai.com/v1/chat/completions'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $openaiApiKey',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'model': 'gpt-4o-mini',
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content': 'You are a task planning assistant. Break down tasks into logical subtasks.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': 'Break down this task into 3-7 subtasks:\n\nTitle: $title\nDescription: $description\n\nReturn ONLY a JSON array of subtasks with format: [{"title": "...", "estimatedHours": 0}]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'temperature': 0.7,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
final content = data['choices'][0]['message']['content'];
|
||||||
|
final List<dynamic> subtasks = jsonDecode(content);
|
||||||
|
return subtasks.cast<Map<String, dynamic>>();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('AI Service Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _mockGenerateSubtasks(title, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI приоритизация задач
|
||||||
|
Future<TaskPriority> prioritizeTask({
|
||||||
|
required String title,
|
||||||
|
required String description,
|
||||||
|
DateTime? dueDate,
|
||||||
|
}) async {
|
||||||
|
if (openaiApiKey == null) {
|
||||||
|
return _mockPrioritizeTask(dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('https://api.openai.com/v1/chat/completions'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $openaiApiKey',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'model': 'gpt-4o-mini',
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content': 'You are a task prioritization assistant. Determine task priority: low, medium, high, or urgent.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': 'What priority should this task have?\n\nTitle: $title\nDescription: $description\nDue: ${dueDate ?? "Not specified"}\n\nReply with ONE word: low, medium, high, or urgent',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'temperature': 0.3,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
final content = data['choices'][0]['message']['content'].trim().toLowerCase();
|
||||||
|
|
||||||
|
switch (content) {
|
||||||
|
case 'urgent': return TaskPriority.urgent;
|
||||||
|
case 'high': return TaskPriority.high;
|
||||||
|
case 'low': return TaskPriority.low;
|
||||||
|
default: return TaskPriority.medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('AI Service Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _mockPrioritizeTask(dueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Умное планирование - распределение задач по времени
|
||||||
|
Future<List<Map<String, dynamic>>> smartSchedule({
|
||||||
|
required List<TaskModel> tasks,
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime endDate,
|
||||||
|
required int availableHoursPerDay,
|
||||||
|
}) async {
|
||||||
|
// Mock implementation для начала
|
||||||
|
final schedule = <Map<String, dynamic>>[];
|
||||||
|
var currentDate = startDate;
|
||||||
|
var remainingHours = availableHoursPerDay;
|
||||||
|
|
||||||
|
for (final task in tasks) {
|
||||||
|
if (currentDate.isAfter(endDate)) break;
|
||||||
|
|
||||||
|
if (task.estimatedHours <= remainingHours) {
|
||||||
|
schedule.add({
|
||||||
|
'taskId': task.id,
|
||||||
|
'date': currentDate,
|
||||||
|
'hours': task.estimatedHours,
|
||||||
|
});
|
||||||
|
remainingHours -= task.estimatedHours;
|
||||||
|
} else {
|
||||||
|
// Переходим на следующий день
|
||||||
|
currentDate = currentDate.add(const Duration(days: 1));
|
||||||
|
remainingHours = availableHoursPerDay;
|
||||||
|
|
||||||
|
if (currentDate.isAfter(endDate)) break;
|
||||||
|
|
||||||
|
schedule.add({
|
||||||
|
'taskId': task.id,
|
||||||
|
'date': currentDate,
|
||||||
|
'hours': task.estimatedHours > remainingHours
|
||||||
|
? remainingHours
|
||||||
|
: task.estimatedHours,
|
||||||
|
});
|
||||||
|
remainingHours -= task.estimatedHours > remainingHours
|
||||||
|
? remainingHours
|
||||||
|
: task.estimatedHours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерация описания задачи
|
||||||
|
Future<String> generateTaskDescription({
|
||||||
|
required String title,
|
||||||
|
String? context,
|
||||||
|
}) async {
|
||||||
|
if (openaiApiKey == null) {
|
||||||
|
return 'Описание для задачи: $title';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('https://api.openai.com/v1/chat/completions'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $openaiApiKey',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'model': 'gpt-4o-mini',
|
||||||
|
'messages': [
|
||||||
|
{
|
||||||
|
'role': 'system',
|
||||||
|
'content': 'You are a task description writer. Write clear, concise task descriptions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': 'Write a brief description for this task:\n\nTitle: $title\nContext: ${context ?? "No additional context"}\n\nKeep it under 100 words.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'temperature': 0.5,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
return data['choices'][0]['message']['content'];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('AI Service Error: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Описание для задачи: $title';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock данные для тестирования
|
||||||
|
List<Map<String, dynamic>> _mockGenerateSubtasks(String title, String description) {
|
||||||
|
return [
|
||||||
|
{'title': 'Анализ требований', 'estimatedHours': 2},
|
||||||
|
{'title': 'Проектирование', 'estimatedHours': 3},
|
||||||
|
{'title': 'Реализация', 'estimatedHours': 8},
|
||||||
|
{'title': 'Тестирование', 'estimatedHours': 3},
|
||||||
|
{'title': 'Документирование', 'estimatedHours': 2},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskPriority _mockPrioritizeTask(DateTime? dueDate) {
|
||||||
|
if (dueDate == null) return TaskPriority.medium;
|
||||||
|
|
||||||
|
final daysUntilDue = dueDate.difference(DateTime.now()).inDays;
|
||||||
|
|
||||||
|
if (daysUntilDue <= 1) return TaskPriority.urgent;
|
||||||
|
if (daysUntilDue <= 3) return TaskPriority.high;
|
||||||
|
if (daysUntilDue <= 7) return TaskPriority.medium;
|
||||||
|
return TaskPriority.low;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final aiService = AIService(
|
||||||
|
// API ключи лучше хранить в .env файле или Firebase Remote Config
|
||||||
|
openaiApiKey: null, // Добавить ключ здесь или через env
|
||||||
|
anthropicApiKey: null,
|
||||||
|
googleAiApiKey: null,
|
||||||
|
);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class FirebaseConfig {
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
debugPrint('Firebase initialized successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setupFirestore() async {
|
||||||
|
// Настройки Firestore для лучшей производительности
|
||||||
|
// Будут применены после инициализации
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import '../models/task_model.dart';
|
||||||
|
import '../models/project_model.dart';
|
||||||
|
import '../models/mind_map_model.dart';
|
||||||
|
import '../models/calendar_event_model.dart';
|
||||||
|
import '../models/alarm_model.dart';
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
class FirestoreService {
|
||||||
|
final FirebaseFirestore _db = FirebaseFirestore.instance;
|
||||||
|
|
||||||
|
// === USERS ===
|
||||||
|
Future<void> createUser(UserModel user) async {
|
||||||
|
await _db.collection('users').doc(user.id).set(user.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserModel?> getUser(String userId) async {
|
||||||
|
final doc = await _db.collection('users').doc(userId).get();
|
||||||
|
return doc.exists ? UserModel.fromFirestore(doc) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateUser(UserModel user) async {
|
||||||
|
await _db.collection('users').doc(user.id).update(user.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
// === PROJECTS ===
|
||||||
|
Future<void> createProject(ProjectModel project) async {
|
||||||
|
await _db.collection('projects').doc(project.id).set(project.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ProjectModel>> getProjectsByTeam(String teamId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('projects')
|
||||||
|
.where('teamId', isEqualTo: teamId)
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ProjectModel>> getProjectsByUser(String userId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('projects')
|
||||||
|
.where('memberIds', arrayContains: userId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => ProjectModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === TASKS ===
|
||||||
|
Future<void> createTask(TaskModel task) async {
|
||||||
|
await _db.collection('tasks').doc(task.id).set(task.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TaskModel>> getTasksByProject(String projectId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('tasks')
|
||||||
|
.where('projectId', isEqualTo: projectId)
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TaskModel>> getTasksByAssignee(String assigneeId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('tasks')
|
||||||
|
.where('assigneeId', isEqualTo: assigneeId)
|
||||||
|
.where('status', whereIn: ['todo', 'inProgress', 'review'])
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateTask(TaskModel task) async {
|
||||||
|
await _db.collection('tasks').doc(task.id).update(task.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTask(String taskId) async {
|
||||||
|
await _db.collection('tasks').doc(taskId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Задачи с поддержкой иерархии (для mind-map)
|
||||||
|
Future<List<TaskModel>> getTaskHierarchy(String projectId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('tasks')
|
||||||
|
.where('projectId', isEqualTo: projectId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MIND MAPS ===
|
||||||
|
Future<void> createMindMapNode(MindMapNode node) async {
|
||||||
|
await _db.collection('mindmaps').doc(node.projectId).collection('nodes').doc(node.id).set(node.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<MindMapNode>> getMindMapNodes(String projectId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('mindmaps')
|
||||||
|
.doc(projectId)
|
||||||
|
.collection('nodes')
|
||||||
|
.orderBy('level')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => MindMapNode.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateMindMapNode(MindMapNode node) async {
|
||||||
|
await _db
|
||||||
|
.collection('mindmaps')
|
||||||
|
.doc(node.projectId)
|
||||||
|
.collection('nodes')
|
||||||
|
.doc(node.id)
|
||||||
|
.update(node.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CALENDAR EVENTS ===
|
||||||
|
Future<void> createEvent(CalendarEventModel event) async {
|
||||||
|
await _db.collection('events').doc(event.id).set(event.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<CalendarEventModel>> getEventsByDateRange({
|
||||||
|
required DateTime start,
|
||||||
|
required DateTime end,
|
||||||
|
String? userId,
|
||||||
|
}) async {
|
||||||
|
Query query = _db
|
||||||
|
.collection('events')
|
||||||
|
.where('startTime', isLessThanOrEqualTo: Timestamp.fromDate(end))
|
||||||
|
.where('endTime', isGreaterThanOrEqualTo: Timestamp.fromDate(start));
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
query = query.where('attendeeIds', arrayContains: userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final snapshot = await query.get();
|
||||||
|
return snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateEvent(CalendarEventModel event) async {
|
||||||
|
await _db.collection('events').doc(event.id).update(event.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteEvent(String eventId) async {
|
||||||
|
await _db.collection('events').doc(eventId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ALARMS ===
|
||||||
|
Future<void> createAlarm(AlarmModel alarm) async {
|
||||||
|
await _db.collection('alarms').doc(alarm.id).set(alarm.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AlarmModel>> getAlarmsByUser(String userId) async {
|
||||||
|
final snapshot = await _db
|
||||||
|
.collection('alarms')
|
||||||
|
.where('isEnabled', isEqualTo: true)
|
||||||
|
.orderBy('dateTime')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => AlarmModel.fromFirestore(doc)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAlarm(AlarmModel alarm) async {
|
||||||
|
await _db.collection('alarms').doc(alarm.id).update(alarm.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlarm(String alarmId) async {
|
||||||
|
await _db.collection('alarms').doc(alarmId).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === BATCH OPERATIONS ===
|
||||||
|
Future<void> syncTasks(List<TaskModel> tasks) async {
|
||||||
|
final batch = _db.batch();
|
||||||
|
|
||||||
|
for (final task in tasks) {
|
||||||
|
batch.set(_db.collection('tasks').doc(task.id), task.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === STREAMS (для реального времени) ===
|
||||||
|
Stream<List<TaskModel>> watchTasks(String projectId) {
|
||||||
|
return _db
|
||||||
|
.collection('tasks')
|
||||||
|
.where('projectId', isEqualTo: projectId)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) => snapshot.docs.map((doc) => TaskModel.fromFirestore(doc)).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<CalendarEventModel>> watchEvents(String userId) {
|
||||||
|
return _db
|
||||||
|
.collection('events')
|
||||||
|
.where('attendeeIds', arrayContains: userId)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) => snapshot.docs.map((doc) => CalendarEventModel.fromFirestore(doc)).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final firestoreService = FirestoreService();
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import '../models/task_model.dart';
|
||||||
|
import '../models/alarm_model.dart';
|
||||||
|
import '../models/mind_map_model.dart';
|
||||||
|
import '../models/calendar_event_model.dart';
|
||||||
|
|
||||||
|
class LocalStorageService {
|
||||||
|
static const String _tasksBoxName = 'tasks';
|
||||||
|
static const String _alarmsBoxName = 'alarms';
|
||||||
|
static const String _mindMapsBoxName = 'mindMaps';
|
||||||
|
static const String _eventsBoxName = 'events';
|
||||||
|
static const String _settingsBoxName = 'settings';
|
||||||
|
|
||||||
|
late Box<Map> _tasksBox;
|
||||||
|
late Box<Map> _alarmsBox;
|
||||||
|
late Box<Map> _mindMapsBox;
|
||||||
|
late Box<Map> _eventsBox;
|
||||||
|
late Box<Object?> _settingsBox;
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
await Hive.initFlutter();
|
||||||
|
|
||||||
|
_tasksBox = await Hive.openBox<Map>(_tasksBoxName);
|
||||||
|
_alarmsBox = await Hive.openBox<Map>(_alarmsBoxName);
|
||||||
|
_mindMapsBox = await Hive.openBox<Map>(_mindMapsBoxName);
|
||||||
|
_eventsBox = await Hive.openBox<Map>(_eventsBoxName);
|
||||||
|
_settingsBox = await Hive.openBox<Object?>(_settingsBoxName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
Future<void> saveTask(TaskModel task) async {
|
||||||
|
await _tasksBox.put(task.id, task.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveTasks(List<TaskModel> tasks) async {
|
||||||
|
for (final task in tasks) {
|
||||||
|
await _tasksBox.put(task.id, task.toFirestore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TaskModel> getTasks() {
|
||||||
|
return _tasksBox.values.map((data) {
|
||||||
|
return TaskModel.fromFirestore(
|
||||||
|
_createMockDocument(data as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTask(String taskId) async {
|
||||||
|
await _tasksBox.delete(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alarms
|
||||||
|
Future<void> saveAlarm(AlarmModel alarm) async {
|
||||||
|
await _alarmsBox.put(alarm.id, alarm.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AlarmModel> getAlarms() {
|
||||||
|
return _alarmsBox.values.map((data) {
|
||||||
|
return AlarmModel.fromFirestore(
|
||||||
|
_createMockDocument(data as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAlarm(String alarmId) async {
|
||||||
|
await _alarmsBox.delete(alarmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mind Maps
|
||||||
|
Future<void> saveMindMapNodes(List<MindMapNode> nodes) async {
|
||||||
|
for (final node in nodes) {
|
||||||
|
await _mindMapsBox.put(node.id, node.toFirestore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MindMapNode> getMindMapNodes() {
|
||||||
|
return _mindMapsBox.values.map((data) {
|
||||||
|
return MindMapNode.fromFirestore(
|
||||||
|
_createMockDocument(data as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
Future<void> saveEvent(CalendarEventModel event) async {
|
||||||
|
await _eventsBox.put(event.id, event.toFirestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CalendarEventModel> getEvents() {
|
||||||
|
return _eventsBox.values.map((data) {
|
||||||
|
return CalendarEventModel.fromFirestore(
|
||||||
|
_createMockDocument(data as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
Future<void> saveSetting(String key, Object? value) async {
|
||||||
|
await _settingsBox.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
T? getSetting<T>(String key, {T? defaultValue}) {
|
||||||
|
return _settingsBox.get(key, defaultValue: defaultValue) as T?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all local data
|
||||||
|
Future<void> clearAll() async {
|
||||||
|
await _tasksBox.clear();
|
||||||
|
await _alarmsBox.clear();
|
||||||
|
await _mindMapsBox.clear();
|
||||||
|
await _eventsBox.clear();
|
||||||
|
await _settingsBox.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync with Firebase (merge remote and local)
|
||||||
|
Future<void> syncWithRemote(List<TaskModel> remoteTasks) async {
|
||||||
|
final localTasks = getTasks();
|
||||||
|
final localMap = {for (var t in localTasks) t.id: t};
|
||||||
|
final remoteMap = {for (var t in remoteTasks) t.id: t};
|
||||||
|
|
||||||
|
// Merge: remote takes precedence
|
||||||
|
final merged = {...localMap, ...remoteMap};
|
||||||
|
|
||||||
|
await _tasksBox.clear();
|
||||||
|
for (final task in merged.values) {
|
||||||
|
await _tasksBox.put(task.id, task.toFirestore());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to create mock DocumentSnapshot
|
||||||
|
dynamic _createMockDocument(Map<String, dynamic> data) {
|
||||||
|
// Простая эмуляция для конвертации
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final localStorageService = LocalStorageService();
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../screens/auth_screen.dart';
|
||||||
|
import '../screens/home_screen.dart';
|
||||||
|
import '../screens/tasks_screen.dart';
|
||||||
|
import '../screens/mind_map_screen.dart';
|
||||||
|
import '../screens/calendar_screen.dart';
|
||||||
|
import '../screens/alarms_screen.dart';
|
||||||
|
|
||||||
|
class AppRouter {
|
||||||
|
static final GoRouter router = GoRouter(
|
||||||
|
initialLocation: '/auth',
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/auth',
|
||||||
|
name: 'auth',
|
||||||
|
builder: (context, state) => const AuthScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/home',
|
||||||
|
name: 'home',
|
||||||
|
builder: (context, state) => const HomeScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'tasks',
|
||||||
|
name: 'tasks',
|
||||||
|
builder: (context, state) => const TasksScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'mindmap',
|
||||||
|
name: 'mindmap',
|
||||||
|
builder: (context, state) => const MindMapScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'calendar',
|
||||||
|
name: 'calendar',
|
||||||
|
builder: (context, state) => const CalendarScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'alarms',
|
||||||
|
name: 'alarms',
|
||||||
|
builder: (context, state) => const AlarmsScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppTheme {
|
||||||
|
static ThemeData get lightTheme {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.blue,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ThemeData get darkTheme {
|
||||||
|
return ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.blue,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||||
|
elevation: 4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'core/firebase_config.dart';
|
||||||
|
import 'core/local_storage_service.dart';
|
||||||
|
import 'core/theme.dart';
|
||||||
|
import 'core/router.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// Инициализация Firebase
|
||||||
|
await FirebaseConfig.initialize();
|
||||||
|
|
||||||
|
// Инициализация локального хранилища
|
||||||
|
await localStorageService.initialize();
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
const ProviderScope(
|
||||||
|
child: NanahuiCrmApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NanahuiCrmApp extends StatelessWidget {
|
||||||
|
const NanahuiCrmApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp.router(
|
||||||
|
title: 'NANAHUI CRM',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: AppTheme.lightTheme,
|
||||||
|
darkTheme: AppTheme.darkTheme,
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
|
routerConfig: AppRouter.router,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class AlarmModel {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String? taskId;
|
||||||
|
final String? eventId;
|
||||||
|
final DateTime dateTime;
|
||||||
|
final bool isRepeating;
|
||||||
|
final String repeatPattern; // daily, weekly, monthly, custom
|
||||||
|
final List<int> repeatDays; // For custom (e.g., [1, 3, 5] for Mon, Wed, Fri)
|
||||||
|
final bool isEnabled;
|
||||||
|
final String sound;
|
||||||
|
final int snoozeMinutes;
|
||||||
|
final Map<String, dynamic> metadata;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
AlarmModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
this.taskId,
|
||||||
|
this.eventId,
|
||||||
|
required this.dateTime,
|
||||||
|
this.isRepeating = false,
|
||||||
|
this.repeatPattern = 'daily',
|
||||||
|
this.repeatDays = const [],
|
||||||
|
this.isEnabled = true,
|
||||||
|
this.sound = 'default',
|
||||||
|
this.snoozeMinutes = 5,
|
||||||
|
this.metadata = const {},
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory AlarmModel.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
return AlarmModel(
|
||||||
|
id: doc.id,
|
||||||
|
title: data['title'] ?? '',
|
||||||
|
taskId: data['taskId'],
|
||||||
|
eventId: data['eventId'],
|
||||||
|
dateTime: (data['dateTime'] as Timestamp).toDate(),
|
||||||
|
isRepeating: data['isRepeating'] ?? false,
|
||||||
|
repeatPattern: data['repeatPattern'] ?? 'daily',
|
||||||
|
repeatDays: List<int>.from(data['repeatDays'] ?? []),
|
||||||
|
isEnabled: data['isEnabled'] ?? true,
|
||||||
|
sound: data['sound'] ?? 'default',
|
||||||
|
snoozeMinutes: data['snoozeMinutes'] ?? 5,
|
||||||
|
metadata: data['metadata'] ?? {},
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'taskId': taskId,
|
||||||
|
'eventId': eventId,
|
||||||
|
'dateTime': Timestamp.fromDate(dateTime),
|
||||||
|
'isRepeating': isRepeating,
|
||||||
|
'repeatPattern': repeatPattern,
|
||||||
|
'repeatDays': repeatDays,
|
||||||
|
'isEnabled': isEnabled,
|
||||||
|
'sound': sound,
|
||||||
|
'snoozeMinutes': snoozeMinutes,
|
||||||
|
'metadata': metadata,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AlarmModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? taskId,
|
||||||
|
String? eventId,
|
||||||
|
DateTime? dateTime,
|
||||||
|
bool? isRepeating,
|
||||||
|
String? repeatPattern,
|
||||||
|
List<int>? repeatDays,
|
||||||
|
bool? isEnabled,
|
||||||
|
String? sound,
|
||||||
|
int? snoozeMinutes,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return AlarmModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
taskId: taskId ?? this.taskId,
|
||||||
|
eventId: eventId ?? this.eventId,
|
||||||
|
dateTime: dateTime ?? this.dateTime,
|
||||||
|
isRepeating: isRepeating ?? this.isRepeating,
|
||||||
|
repeatPattern: repeatPattern ?? this.repeatPattern,
|
||||||
|
repeatDays: repeatDays ?? this.repeatDays,
|
||||||
|
isEnabled: isEnabled ?? this.isEnabled,
|
||||||
|
sound: sound ?? this.sound,
|
||||||
|
snoozeMinutes: snoozeMinutes ?? this.snoozeMinutes,
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime get nextOccurrence {
|
||||||
|
if (!isRepeating) return dateTime;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (dateTime.isAfter(now)) return dateTime;
|
||||||
|
|
||||||
|
switch (repeatPattern) {
|
||||||
|
case 'daily':
|
||||||
|
return dateTime.add(const Duration(days: 1));
|
||||||
|
case 'weekly':
|
||||||
|
return dateTime.add(const Duration(days: 7));
|
||||||
|
case 'monthly':
|
||||||
|
return DateTime(dateTime.year, dateTime.month + 1, dateTime.day);
|
||||||
|
case 'custom':
|
||||||
|
// Find next weekday in repeatDays
|
||||||
|
var next = dateTime.add(const Duration(days: 1));
|
||||||
|
while (!repeatDays.contains(next.weekday)) {
|
||||||
|
next = next.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
return DateTime(next.year, next.month, next.day, dateTime.hour, dateTime.minute);
|
||||||
|
default:
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class CalendarEventModel {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final String location;
|
||||||
|
final DateTime startTime;
|
||||||
|
final DateTime endTime;
|
||||||
|
final bool isAllDay;
|
||||||
|
final String? googleEventId;
|
||||||
|
final String? taskId;
|
||||||
|
final List<String> attendeeIds;
|
||||||
|
final List<String> attendeeEmails;
|
||||||
|
final String color;
|
||||||
|
final String? recurringRuleId;
|
||||||
|
final Map<String, dynamic> reminders;
|
||||||
|
final String status; // confirmed, tentative, cancelled
|
||||||
|
final String createdBy;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
CalendarEventModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.location = '',
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
this.isAllDay = false,
|
||||||
|
this.googleEventId,
|
||||||
|
this.taskId,
|
||||||
|
this.attendeeIds = const [],
|
||||||
|
this.attendeeEmails = const [],
|
||||||
|
this.color = '#2196F3',
|
||||||
|
this.recurringRuleId,
|
||||||
|
this.reminders = const {},
|
||||||
|
this.status = 'confirmed',
|
||||||
|
required this.createdBy,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CalendarEventModel.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
return CalendarEventModel(
|
||||||
|
id: doc.id,
|
||||||
|
title: data['title'] ?? '',
|
||||||
|
description: data['description'],
|
||||||
|
location: data['location'] ?? '',
|
||||||
|
startTime: (data['startTime'] as Timestamp).toDate(),
|
||||||
|
endTime: (data['endTime'] as Timestamp).toDate(),
|
||||||
|
isAllDay: data['isAllDay'] ?? false,
|
||||||
|
googleEventId: data['googleEventId'],
|
||||||
|
taskId: data['taskId'],
|
||||||
|
attendeeIds: List<String>.from(data['attendeeIds'] ?? []),
|
||||||
|
attendeeEmails: List<String>.from(data['attendeeEmails'] ?? []),
|
||||||
|
color: data['color'] ?? '#2196F3',
|
||||||
|
recurringRuleId: data['recurringRuleId'],
|
||||||
|
reminders: data['reminders'] ?? {},
|
||||||
|
status: data['status'] ?? 'confirmed',
|
||||||
|
createdBy: data['createdBy'] ?? '',
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'location': location,
|
||||||
|
'startTime': Timestamp.fromDate(startTime),
|
||||||
|
'endTime': Timestamp.fromDate(endTime),
|
||||||
|
'isAllDay': isAllDay,
|
||||||
|
'googleEventId': googleEventId,
|
||||||
|
'taskId': taskId,
|
||||||
|
'attendeeIds': attendeeIds,
|
||||||
|
'attendeeEmails': attendeeEmails,
|
||||||
|
'color': color,
|
||||||
|
'recurringRuleId': recurringRuleId,
|
||||||
|
'reminders': reminders,
|
||||||
|
'status': status,
|
||||||
|
'createdBy': createdBy,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CalendarEventModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? location,
|
||||||
|
DateTime? startTime,
|
||||||
|
DateTime? endTime,
|
||||||
|
bool? isAllDay,
|
||||||
|
String? googleEventId,
|
||||||
|
String? taskId,
|
||||||
|
List<String>? attendeeIds,
|
||||||
|
List<String>? attendeeEmails,
|
||||||
|
String? color,
|
||||||
|
String? recurringRuleId,
|
||||||
|
Map<String, dynamic>? reminders,
|
||||||
|
String? status,
|
||||||
|
String? createdBy,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return CalendarEventModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
location: location ?? this.location,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
endTime: endTime ?? this.endTime,
|
||||||
|
isAllDay: isAllDay ?? this.isAllDay,
|
||||||
|
googleEventId: googleEventId ?? this.googleEventId,
|
||||||
|
taskId: taskId ?? this.taskId,
|
||||||
|
attendeeIds: attendeeIds ?? this.attendeeIds,
|
||||||
|
attendeeEmails: attendeeEmails ?? this.attendeeEmails,
|
||||||
|
color: color ?? this.color,
|
||||||
|
recurringRuleId: recurringRuleId ?? this.recurringRuleId,
|
||||||
|
reminders: reminders ?? this.reminders,
|
||||||
|
status: status ?? this.status,
|
||||||
|
createdBy: createdBy ?? this.createdBy,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration get duration => endTime.difference(startTime);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class MindMapNode {
|
||||||
|
final String id;
|
||||||
|
final String projectId;
|
||||||
|
final String? parentId;
|
||||||
|
final int level;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final String? taskId; // Link to task
|
||||||
|
final String color;
|
||||||
|
final String icon;
|
||||||
|
final Map<String, dynamic> metadata;
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
MindMapNode({
|
||||||
|
required this.id,
|
||||||
|
required this.projectId,
|
||||||
|
this.parentId,
|
||||||
|
required this.level,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.taskId,
|
||||||
|
this.color = '#2196F3',
|
||||||
|
this.icon = 'folder',
|
||||||
|
this.metadata = const {},
|
||||||
|
this.x = 0,
|
||||||
|
this.y = 0,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MindMapNode.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
return MindMapNode(
|
||||||
|
id: doc.id,
|
||||||
|
projectId: data['projectId'] ?? '',
|
||||||
|
parentId: data['parentId'],
|
||||||
|
level: data['level'] ?? 0,
|
||||||
|
title: data['title'] ?? '',
|
||||||
|
description: data['description'],
|
||||||
|
taskId: data['taskId'],
|
||||||
|
color: data['color'] ?? '#2196F3',
|
||||||
|
icon: data['icon'] ?? 'folder',
|
||||||
|
metadata: data['metadata'] ?? {},
|
||||||
|
x: (data['x'] ?? 0).toDouble(),
|
||||||
|
y: (data['y'] ?? 0).toDouble(),
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'projectId': projectId,
|
||||||
|
'parentId': parentId,
|
||||||
|
'level': level,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'taskId': taskId,
|
||||||
|
'color': color,
|
||||||
|
'icon': icon,
|
||||||
|
'metadata': metadata,
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
MindMapNode copyWith({
|
||||||
|
String? id,
|
||||||
|
String? projectId,
|
||||||
|
String? parentId,
|
||||||
|
int? level,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? taskId,
|
||||||
|
String? color,
|
||||||
|
String? icon,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
double? x,
|
||||||
|
double? y,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return MindMapNode(
|
||||||
|
id: id ?? this.id,
|
||||||
|
projectId: projectId ?? this.projectId,
|
||||||
|
parentId: parentId ?? this.parentId,
|
||||||
|
level: level ?? this.level,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
taskId: taskId ?? this.taskId,
|
||||||
|
color: color ?? this.color,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
x: x ?? this.x,
|
||||||
|
y: y ?? this.y,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MindMapModel {
|
||||||
|
final String id;
|
||||||
|
final String projectId;
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final String rootNodeId;
|
||||||
|
final List<MindMapNode> nodes;
|
||||||
|
final Map<String, dynamic> layout;
|
||||||
|
final String createdBy;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
MindMapModel({
|
||||||
|
required this.id,
|
||||||
|
required this.projectId,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.rootNodeId,
|
||||||
|
required this.nodes,
|
||||||
|
required this.layout,
|
||||||
|
required this.createdBy,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory MindMapModel.fromFirestore({
|
||||||
|
required String id,
|
||||||
|
required Map<String, dynamic> data,
|
||||||
|
required List<MindMapNode> nodes,
|
||||||
|
}) {
|
||||||
|
return MindMapModel(
|
||||||
|
id: id,
|
||||||
|
projectId: data['projectId'] ?? '',
|
||||||
|
title: data['title'] ?? '',
|
||||||
|
description: data['description'] ?? '',
|
||||||
|
rootNodeId: data['rootNodeId'] ?? '',
|
||||||
|
nodes: nodes,
|
||||||
|
layout: data['layout'] ?? {},
|
||||||
|
createdBy: data['createdBy'] ?? '',
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'projectId': projectId,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'rootNodeId': rootNodeId,
|
||||||
|
'layout': layout,
|
||||||
|
'createdBy': createdBy,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class ProjectModel {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final String teamId;
|
||||||
|
final String ownerId;
|
||||||
|
final List<String> memberIds;
|
||||||
|
final String status;
|
||||||
|
final DateTime startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
final String color;
|
||||||
|
final String icon;
|
||||||
|
final Map<String, dynamic> settings;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
ProjectModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.teamId,
|
||||||
|
required this.ownerId,
|
||||||
|
this.memberIds = const [],
|
||||||
|
this.status = 'active',
|
||||||
|
required this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.color = '#1976D2',
|
||||||
|
this.icon = 'project',
|
||||||
|
this.settings = const {},
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ProjectModel.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
return ProjectModel(
|
||||||
|
id: doc.id,
|
||||||
|
name: data['name'] ?? '',
|
||||||
|
description: data['description'] ?? '',
|
||||||
|
teamId: data['teamId'] ?? '',
|
||||||
|
ownerId: data['ownerId'] ?? '',
|
||||||
|
memberIds: List<String>.from(data['memberIds'] ?? []),
|
||||||
|
status: data['status'] ?? 'active',
|
||||||
|
startDate: (data['startDate'] as Timestamp).toDate(),
|
||||||
|
endDate: data['endDate'] != null
|
||||||
|
? (data['endDate'] as Timestamp).toDate()
|
||||||
|
: null,
|
||||||
|
color: data['color'] ?? '#1976D2',
|
||||||
|
icon: data['icon'] ?? 'project',
|
||||||
|
settings: data['settings'] ?? {},
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'description': description,
|
||||||
|
'teamId': teamId,
|
||||||
|
'ownerId': ownerId,
|
||||||
|
'memberIds': memberIds,
|
||||||
|
'status': status,
|
||||||
|
'startDate': Timestamp.fromDate(startDate),
|
||||||
|
'endDate': endDate != null ? Timestamp.fromDate(endDate!) : null,
|
||||||
|
'color': color,
|
||||||
|
'icon': icon,
|
||||||
|
'settings': settings,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
String? teamId,
|
||||||
|
String? ownerId,
|
||||||
|
List<String>? memberIds,
|
||||||
|
String? status,
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
String? color,
|
||||||
|
String? icon,
|
||||||
|
Map<String, dynamic>? settings,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return ProjectModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
description: description ?? this.description,
|
||||||
|
teamId: teamId ?? this.teamId,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
memberIds: memberIds ?? this.memberIds,
|
||||||
|
status: status ?? this.status,
|
||||||
|
startDate: startDate ?? this.startDate,
|
||||||
|
endDate: endDate ?? this.endDate,
|
||||||
|
color: color ?? this.color,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
settings: settings ?? this.settings,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isMember(String userId) {
|
||||||
|
return ownerId == userId || memberIds.contains(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOwner(String userId) {
|
||||||
|
return ownerId == userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
enum TaskStatus {
|
||||||
|
todo,
|
||||||
|
inProgress,
|
||||||
|
review,
|
||||||
|
done,
|
||||||
|
archived,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TaskPriority {
|
||||||
|
low,
|
||||||
|
medium,
|
||||||
|
high,
|
||||||
|
urgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskModel {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final String projectId;
|
||||||
|
final String? parentTaskId; // For mind-map hierarchy
|
||||||
|
final TaskStatus status;
|
||||||
|
final TaskPriority priority;
|
||||||
|
final String? assigneeId;
|
||||||
|
final String? creatorId;
|
||||||
|
final List<String> tags;
|
||||||
|
final DateTime? dueDate;
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? completedDate;
|
||||||
|
final int estimatedHours;
|
||||||
|
final int spentHours;
|
||||||
|
final Map<String, dynamic> aiMetadata; // AI-generated suggestions
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
TaskModel({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
required this.projectId,
|
||||||
|
this.parentTaskId,
|
||||||
|
this.status = TaskStatus.todo,
|
||||||
|
this.priority = TaskPriority.medium,
|
||||||
|
this.assigneeId,
|
||||||
|
this.creatorId,
|
||||||
|
this.tags = const [],
|
||||||
|
this.dueDate,
|
||||||
|
this.startDate,
|
||||||
|
this.completedDate,
|
||||||
|
this.estimatedHours = 0,
|
||||||
|
this.spentHours = 0,
|
||||||
|
this.aiMetadata = const {},
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TaskModel.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
return TaskModel(
|
||||||
|
id: doc.id,
|
||||||
|
title: data['title'] ?? '',
|
||||||
|
description: data['description'],
|
||||||
|
projectId: data['projectId'] ?? '',
|
||||||
|
parentTaskId: data['parentTaskId'],
|
||||||
|
status: TaskStatus.values.firstWhere(
|
||||||
|
(e) => e.name == data['status'],
|
||||||
|
orElse: () => TaskStatus.todo,
|
||||||
|
),
|
||||||
|
priority: TaskPriority.values.firstWhere(
|
||||||
|
(e) => e.name == data['priority'],
|
||||||
|
orElse: () => TaskPriority.medium,
|
||||||
|
),
|
||||||
|
assigneeId: data['assigneeId'],
|
||||||
|
creatorId: data['creatorId'],
|
||||||
|
tags: List<String>.from(data['tags'] ?? []),
|
||||||
|
dueDate: data['dueDate'] != null
|
||||||
|
? (data['dueDate'] as Timestamp).toDate()
|
||||||
|
: null,
|
||||||
|
startDate: data['startDate'] != null
|
||||||
|
? (data['startDate'] as Timestamp).toDate()
|
||||||
|
: null,
|
||||||
|
completedDate: data['completedDate'] != null
|
||||||
|
? (data['completedDate'] as Timestamp).toDate()
|
||||||
|
: null,
|
||||||
|
estimatedHours: data['estimatedHours'] ?? 0,
|
||||||
|
spentHours: data['spentHours'] ?? 0,
|
||||||
|
aiMetadata: data['aiMetadata'] ?? {},
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'projectId': projectId,
|
||||||
|
'parentTaskId': parentTaskId,
|
||||||
|
'status': status.name,
|
||||||
|
'priority': priority.name,
|
||||||
|
'assigneeId': assigneeId,
|
||||||
|
'creatorId': creatorId,
|
||||||
|
'tags': tags,
|
||||||
|
'dueDate': dueDate != null ? Timestamp.fromDate(dueDate!) : null,
|
||||||
|
'startDate': startDate != null ? Timestamp.fromDate(startDate!) : null,
|
||||||
|
'completedDate': completedDate != null
|
||||||
|
? Timestamp.fromDate(completedDate!)
|
||||||
|
: null,
|
||||||
|
'estimatedHours': estimatedHours,
|
||||||
|
'spentHours': spentHours,
|
||||||
|
'aiMetadata': aiMetadata,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? projectId,
|
||||||
|
String? parentTaskId,
|
||||||
|
TaskStatus? status,
|
||||||
|
TaskPriority? priority,
|
||||||
|
String? assigneeId,
|
||||||
|
String? creatorId,
|
||||||
|
List<String>? tags,
|
||||||
|
DateTime? dueDate,
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? completedDate,
|
||||||
|
int? estimatedHours,
|
||||||
|
int? spentHours,
|
||||||
|
Map<String, dynamic>? aiMetadata,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
return TaskModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
projectId: projectId ?? this.projectId,
|
||||||
|
parentTaskId: parentTaskId ?? this.parentTaskId,
|
||||||
|
status: status ?? this.status,
|
||||||
|
priority: priority ?? this.priority,
|
||||||
|
assigneeId: assigneeId ?? this.assigneeId,
|
||||||
|
creatorId: creatorId ?? this.creatorId,
|
||||||
|
tags: tags ?? this.tags,
|
||||||
|
dueDate: dueDate ?? this.dueDate,
|
||||||
|
startDate: startDate ?? this.startDate,
|
||||||
|
completedDate: completedDate ?? this.completedDate,
|
||||||
|
estimatedHours: estimatedHours ?? this.estimatedHours,
|
||||||
|
spentHours: spentHours ?? this.spentHours,
|
||||||
|
aiMetadata: aiMetadata ?? this.aiMetadata,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isOverdue {
|
||||||
|
if (dueDate == null) return false;
|
||||||
|
return DateTime.now().isAfter(dueDate!) && status != TaskStatus.done;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
enum UserRole {
|
||||||
|
admin,
|
||||||
|
member,
|
||||||
|
viewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserModel {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String? displayName;
|
||||||
|
final String? photoUrl;
|
||||||
|
final UserRole role;
|
||||||
|
final List<String> teamIds;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final Map<String, dynamic> settings;
|
||||||
|
|
||||||
|
UserModel({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
this.displayName,
|
||||||
|
this.photoUrl,
|
||||||
|
this.role = UserRole.member,
|
||||||
|
this.teamIds = const [],
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.settings = const {},
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UserModel.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
return UserModel(
|
||||||
|
id: doc.id,
|
||||||
|
email: data['email'] ?? '',
|
||||||
|
displayName: data['displayName'],
|
||||||
|
photoUrl: data['photoUrl'],
|
||||||
|
role: UserRole.values.firstWhere(
|
||||||
|
(e) => e.name == data['role'],
|
||||||
|
orElse: () => UserRole.member,
|
||||||
|
),
|
||||||
|
teamIds: List<String>.from(data['teamIds'] ?? []),
|
||||||
|
createdAt: (data['createdAt'] as Timestamp).toDate(),
|
||||||
|
updatedAt: (data['updatedAt'] as Timestamp).toDate(),
|
||||||
|
settings: data['settings'] ?? {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toFirestore() {
|
||||||
|
return {
|
||||||
|
'email': email,
|
||||||
|
'displayName': displayName,
|
||||||
|
'photoUrl': photoUrl,
|
||||||
|
'role': role.name,
|
||||||
|
'teamIds': teamIds,
|
||||||
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
'updatedAt': Timestamp.fromDate(updatedAt),
|
||||||
|
'settings': settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? email,
|
||||||
|
String? displayName,
|
||||||
|
String? photoUrl,
|
||||||
|
UserRole? role,
|
||||||
|
List<String>? teamIds,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
Map<String, dynamic>? settings,
|
||||||
|
}) {
|
||||||
|
return UserModel(
|
||||||
|
id: id ?? this.id,
|
||||||
|
email: email ?? this.email,
|
||||||
|
displayName: displayName ?? this.displayName,
|
||||||
|
photoUrl: photoUrl ?? this.photoUrl,
|
||||||
|
role: role ?? this.role,
|
||||||
|
teamIds: teamIds ?? this.teamIds,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
settings: settings ?? this.settings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isAdmin => role == UserRole.admin;
|
||||||
|
bool get isMember => role == UserRole.member;
|
||||||
|
bool get isViewer => role == UserRole.viewer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:google_sign_in/google_sign_in.dart';
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
class AuthNotifier extends StateNotifier<AsyncValue<UserModel?>> {
|
||||||
|
final FirebaseAuth _auth;
|
||||||
|
final GoogleSignIn _googleSignIn;
|
||||||
|
|
||||||
|
AuthNotifier(this._auth, this._googleSignIn) : super(const AsyncValue.data(null)) {
|
||||||
|
// Слушаем изменения состояния аутентификации
|
||||||
|
_auth.authStateChanges().listen((user) {
|
||||||
|
if (user != null) {
|
||||||
|
state = AsyncValue.data(UserModel(
|
||||||
|
id: user.uid,
|
||||||
|
email: user.email ?? '',
|
||||||
|
displayName: user.displayName,
|
||||||
|
photoUrl: user.photoURL,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
state = const AsyncValue.data(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signInWithGoogle() async {
|
||||||
|
try {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
|
||||||
|
final googleUser = await _googleSignIn.signIn();
|
||||||
|
if (googleUser == null) {
|
||||||
|
state = const AsyncValue.data(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final googleAuth = await googleUser.authentication;
|
||||||
|
final credential = GoogleAuthProvider.credential(
|
||||||
|
accessToken: googleAuth.accessToken,
|
||||||
|
idToken: googleAuth.idToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
final userCredential = await _auth.signInWithCredential(credential);
|
||||||
|
|
||||||
|
// Здесь можно создать/обновить пользователя в Firestore
|
||||||
|
state = AsyncValue.data(UserModel(
|
||||||
|
id: userCredential.user!.uid,
|
||||||
|
email: userCredential.user!.email ?? '',
|
||||||
|
displayName: userCredential.user!.displayName,
|
||||||
|
photoUrl: userCredential.user!.photoURL,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
));
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncValue.error(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signOut() async {
|
||||||
|
try {
|
||||||
|
await _googleSignIn.signOut();
|
||||||
|
await _auth.signOut();
|
||||||
|
state = const AsyncValue.data(null);
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncValue.error(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
User? get currentUser => _auth.currentUser;
|
||||||
|
bool get isLoggedIn => _auth.currentUser != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final authProvider = StateNotifierProvider<AuthNotifier, AsyncValue<UserModel?>>((ref) {
|
||||||
|
return AuthNotifier(FirebaseAuth.instance, GoogleSignIn());
|
||||||
|
});
|
||||||
|
|
||||||
|
final authStateProvider = Provider<AuthNotifier>((ref) {
|
||||||
|
return ref.read(authProvider.notifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
final isAuthenticatedProvider = Provider<bool>((ref) {
|
||||||
|
return ref.watch(authProvider).value != null;
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AlarmsScreen extends StatelessWidget {
|
||||||
|
const AlarmsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Будильники'),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: 5,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _AlarmCard(index: index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Новый будильник'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlarmCard extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const _AlarmCard({required this.index});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: index % 2 == 0 ? Colors.blue : Colors.grey,
|
||||||
|
child: const Icon(Icons.alarm, color: Colors.white),
|
||||||
|
),
|
||||||
|
title: Text('Будильник #${index + 1}'),
|
||||||
|
subtitle: Text('${8 + index}:00'),
|
||||||
|
trailing: Switch(
|
||||||
|
value: index % 2 == 0,
|
||||||
|
onChanged: (value) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../providers/auth_provider.dart';
|
||||||
|
|
||||||
|
class AuthScreen extends ConsumerWidget {
|
||||||
|
const AuthScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final authState = ref.watch(authProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.business_center,
|
||||||
|
size: 120,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
const Text(
|
||||||
|
'NANAHUI CRM',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'CRM-ассистент для команд',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
authState.when(
|
||||||
|
data: (user) {
|
||||||
|
if (user != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.go('/home');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
error: (error, stack) => Text(
|
||||||
|
'Error: $error',
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(authProvider.notifier).signInWithGoogle();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.login),
|
||||||
|
label: const Text('Войти через Google'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CalendarScreen extends StatelessWidget {
|
||||||
|
const CalendarScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Календарь'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.sync),
|
||||||
|
onPressed: () {
|
||||||
|
// Синхронизация с Google Calendar
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_buildCalendarHeader(),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: 5,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _CalendarEventCard(index: index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCalendarHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_left),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Март 2026',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_right),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalendarEventCard extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const _CalendarEventCard({required this.index});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Container(
|
||||||
|
width: 4,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getEventColor(index),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text('Событие #${index + 1}'),
|
||||||
|
subtitle: Text('10:00 - 11:00'),
|
||||||
|
trailing: const Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getEventColor(int index) {
|
||||||
|
final colors = [Colors.blue, Colors.green, Colors.orange, Colors.purple];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class DashboardScreen extends StatelessWidget {
|
||||||
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('NANAHUI CRM'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildWelcomeCard(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildQuickStats(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildQuickActions(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWelcomeCard() {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Добро пожаловать!',
|
||||||
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Управляйте задачами команды и планируйте работу с помощью AI',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Новая задача'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.auto_awesome),
|
||||||
|
label: const Text('AI План'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuickStats() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Быстрая статистика',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _StatCard(
|
||||||
|
icon: Icons.task,
|
||||||
|
label: 'Задачи',
|
||||||
|
value: '12',
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _StatCard(
|
||||||
|
icon: Icons.pending_actions,
|
||||||
|
label: 'В работе',
|
||||||
|
value: '5',
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _StatCard(
|
||||||
|
icon: Icons.check_circle,
|
||||||
|
label: 'Готово',
|
||||||
|
value: '48',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuickActions(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Быстрые действия',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
_ActionChip(
|
||||||
|
icon: Icons.calendar_today,
|
||||||
|
label: 'Планирование',
|
||||||
|
onTap: () => context.go('/home/calendar'),
|
||||||
|
),
|
||||||
|
_ActionChip(
|
||||||
|
icon: Icons.account_tree,
|
||||||
|
label: 'Mind-карта',
|
||||||
|
onTap: () => context.go('/home/mindmap'),
|
||||||
|
),
|
||||||
|
_ActionChip(
|
||||||
|
icon: Icons.insights,
|
||||||
|
label: 'Аналитика',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
_ActionChip(
|
||||||
|
icon: Icons.people,
|
||||||
|
label: 'Команда',
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StatCard extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const _StatCard({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 32),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionChip extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ActionChip({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dashboard_screen.dart';
|
||||||
|
import 'tasks_screen.dart';
|
||||||
|
import 'mind_map_screen.dart';
|
||||||
|
import 'calendar_screen.dart';
|
||||||
|
import 'alarms_screen.dart';
|
||||||
|
|
||||||
|
class HomeScreen extends StatefulWidget {
|
||||||
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomeScreen> createState() => _HomeScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
|
final List<Widget> _screens = [
|
||||||
|
const DashboardScreen(),
|
||||||
|
const TasksScreen(),
|
||||||
|
const MindMapScreen(),
|
||||||
|
const CalendarScreen(),
|
||||||
|
const AlarmsScreen(),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: _screens[_selectedIndex],
|
||||||
|
bottomNavigationBar: NavigationBar(
|
||||||
|
selectedIndex: _selectedIndex,
|
||||||
|
onDestinationSelected: (index) {
|
||||||
|
setState(() {
|
||||||
|
_selectedIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
|
selectedIcon: Icon(Icons.dashboard),
|
||||||
|
label: 'Главная',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.task_outlined),
|
||||||
|
selectedIcon: Icon(Icons.task),
|
||||||
|
label: 'Задачи',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.account_tree_outlined),
|
||||||
|
selectedIcon: Icon(Icons.account_tree),
|
||||||
|
label: 'Mind-карта',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.calendar_today_outlined),
|
||||||
|
selectedIcon: Icon(Icons.calendar_today),
|
||||||
|
label: 'Календарь',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.alarm_outlined),
|
||||||
|
selectedIcon: Icon(Icons.alarm),
|
||||||
|
label: 'Будильники',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MindMapScreen extends StatelessWidget {
|
||||||
|
const MindMapScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Mind-карта'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
onPressed: () {
|
||||||
|
// Edit mode - только для админов
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.account_tree,
|
||||||
|
size: 100,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'Mind-карта проекта',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Создайте структуру задач в виде дерева',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Создать mind-карту'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
// AI генерация mind-карты
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.auto_awesome),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TasksScreen extends StatelessWidget {
|
||||||
|
const TasksScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Задачи'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.filter_list),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: 10,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _TaskCard(index: index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Новая задача'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TaskCard extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const _TaskCard({required this.index});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: _getPriorityColor(index),
|
||||||
|
child: Text('${index + 1}'),
|
||||||
|
),
|
||||||
|
title: Text('Задача #${index + 1}'),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Описание задачи...'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.calendar_today, size: 12, color: Colors.grey),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'До: ${DateTime.now().add(Duration(days: index)).toString().split(' ')[0]}',
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem(value: 'edit', child: Text('Редактировать')),
|
||||||
|
const PopupMenuItem(value: 'delete', child: Text('Удалить')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isThreeLine: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getPriorityColor(int index) {
|
||||||
|
final colors = [Colors.red, Colors.orange, Colors.blue, Colors.green];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "nanahui_crm")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.nanahui.nanahui_crm")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||||
|
|
||||||
|
# Root filesystem for cross-building.
|
||||||
|
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||||
|
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration options.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
|
# directory.
|
||||||
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Start with a clean build bundle directory every time.
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||||
|
# which isn't available in 3.10.
|
||||||
|
function(list_prepend LIST_NAME PREFIX)
|
||||||
|
set(NEW_LIST "")
|
||||||
|
foreach(element ${${LIST_NAME}})
|
||||||
|
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||||
|
endforeach(element)
|
||||||
|
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||