mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'upstream/main' into export_notify
This commit is contained in:
commit
86150ec6a1
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac
|
||||
flutter_profile: development-mac-x86_64
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -113,7 +113,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
flutter config --enable-macos-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86 appflowy
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy
|
||||
|
||||
- name: Archive macOS app
|
||||
working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}
|
||||
|
35
CHANGELOG.md
35
CHANGELOG.md
@ -1,5 +1,40 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.0.4 - 2022-06-06
|
||||
- Drag to adjust the width of a column
|
||||
- Upgrade to Flutter 3.0
|
||||
- Native support for M1 chip
|
||||
- Date supports time formats
|
||||
- New property: URL
|
||||
- Keyboard shortcuts support for Grid: press Enter to leave the edit mode; control c/v to copy-paste cell values
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed some bugs
|
||||
|
||||
|
||||
## Version 0.0.4 - beta.3 - 2022-05-02
|
||||
- Drag to reorder app/ view/ field
|
||||
- Row record open as a page
|
||||
- Auto resize the height of the row in the grid
|
||||
- Support more number formats
|
||||
- Search column options, supporting Single select, Multi-select, and number format
|
||||
|
||||
![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif)
|
||||
|
||||
|
||||
### Bug Fixes & Improvements
|
||||
- Improved row/cell data cache
|
||||
- Fixed some bugs
|
||||
|
||||
|
||||
## Version 0.0.4 - beta.2 - 2022-04-11
|
||||
|
||||
- Support properties: Text, Number, Date, Checkbox, Select, Multi-select
|
||||
- Insert / delete rows
|
||||
- Add / delete / hide columns
|
||||
- Edit property
|
||||
![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png)
|
||||
|
||||
## Version 0.0.4 - beta.1 - 2022-04-08
|
||||
v0.0.4 - beta.1 is pre-release
|
||||
|
||||
|
@ -45,7 +45,15 @@ APP_ENVIRONMENT = "local"
|
||||
FLUTTER_FLOWY_SDK_PATH="app_flowy/packages/flowy_sdk"
|
||||
PROTOBUF_DERIVE_CACHE="../shared-lib/flowy-derive/src/derive_cache/derive_cache.rs"
|
||||
|
||||
[env.development-mac]
|
||||
[env.development-mac-arm64]
|
||||
RUST_LOG = "info"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
|
||||
BUILD_FLAG = "debug"
|
||||
FLUTTER_OUTPUT_DIR = "Debug"
|
||||
PRODUCT_EXT = "app"
|
||||
|
||||
[env.development-mac-x86_64]
|
||||
RUST_LOG = "info"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
@ -53,21 +61,23 @@ BUILD_FLAG = "debug"
|
||||
FLUTTER_OUTPUT_DIR = "Debug"
|
||||
PRODUCT_EXT = "app"
|
||||
|
||||
[env.production-mac-aarch64]
|
||||
[env.production-mac-arm64]
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "aarch64-apple-darwin"
|
||||
FLUTTER_OUTPUT_DIR = "Release"
|
||||
PRODUCT_EXT = "app"
|
||||
APP_ENVIRONMENT = "production"
|
||||
BUILD_ARCHS = "arm64"
|
||||
|
||||
[env.production-mac-x86]
|
||||
[env.production-mac-x86_64]
|
||||
BUILD_FLAG = "release"
|
||||
TARGET_OS = "macos"
|
||||
RUST_COMPILE_TARGET = "x86_64-apple-darwin"
|
||||
FLUTTER_OUTPUT_DIR = "Release"
|
||||
PRODUCT_EXT = "app"
|
||||
APP_ENVIRONMENT = "production"
|
||||
BUILD_ARCHS = "x86_64"
|
||||
|
||||
[env.development-windows-x86]
|
||||
TARGET_OS = "windows"
|
||||
@ -138,6 +148,7 @@ script = [
|
||||
echo PRODUCT_EXT: ${PRODUCT_EXT}
|
||||
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
|
||||
echo ${platforms}
|
||||
echo ${BUILD_ARCHS}
|
||||
'''
|
||||
]
|
||||
script_runner = "@shell"
|
||||
|
@ -186,7 +186,8 @@
|
||||
"row": {
|
||||
"duplicate": "Duplicate",
|
||||
"delete": "Delete",
|
||||
"textPlaceholder": "Empty"
|
||||
"textPlaceholder": "Empty",
|
||||
"copyProperty": "Copied property to clipboard"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Create",
|
||||
@ -203,6 +204,10 @@
|
||||
"colorPannelTitle": "Colors",
|
||||
"pannelTitle": "Select an option or create one",
|
||||
"searchOption": "Search for an option"
|
||||
},
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "12:00 AM",
|
||||
"timeHintTextInTwentyFourHour": "12:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,11 @@
|
||||
"letsGoButtonText": "Vamos lá",
|
||||
"title": "Título",
|
||||
"signUp": {
|
||||
"buttonText": "Inscreve-se",
|
||||
"title": "Inscrever-se @:appName",
|
||||
"buttonText": "Se inscreva",
|
||||
"title": "Se inscreva no @:appName",
|
||||
"getStartedText": "Começar",
|
||||
"emptyPasswordError": "Senha não pode ser em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
|
||||
"emptyPasswordError": "Senha não pode estar em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As senhas não conferem.",
|
||||
"alreadyHaveAnAccount": "Já possui uma conta?",
|
||||
"emailHint": "Email",
|
||||
@ -19,14 +19,14 @@
|
||||
"repeatPasswordHint": "Confirme a senha"
|
||||
},
|
||||
"signIn": {
|
||||
"loginTitle": "Login to @:appName",
|
||||
"loginTitle": "Entre no @:appName",
|
||||
"loginButtonText": "Login",
|
||||
"buttonText": "Entre",
|
||||
"forgotPassword": "Esqueceu a senha?",
|
||||
"emailHint": "Email",
|
||||
"passwordHint": "Senha",
|
||||
"dontHaveAnAccount": "Não possui uma conta?",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode ser em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a senha não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As senhas não conferem."
|
||||
},
|
||||
"workspace": {
|
||||
@ -67,7 +67,7 @@
|
||||
"whatsNew": "O que há de novo?",
|
||||
"help": "Ajuda & Suporte",
|
||||
"debug": {
|
||||
"name": "Informação de debug",
|
||||
"name": "Informação de depuração",
|
||||
"success": "Copiar informação de debug para o clipboard!",
|
||||
"fail": "Falha em copiar a informação de debug para o clipboard"
|
||||
}
|
||||
@ -104,7 +104,7 @@
|
||||
},
|
||||
"button": {
|
||||
"OK": "OK",
|
||||
"Cancel": "Canelar",
|
||||
"Cancel": "Cancelar",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Sair",
|
||||
"complete": "Completar",
|
||||
@ -143,4 +143,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
146
frontend/app_flowy/assets/translations/pt-PT.json
Normal file
146
frontend/app_flowy/assets/translations/pt-PT.json
Normal file
@ -0,0 +1,146 @@
|
||||
{
|
||||
"appName": "AppFlowy",
|
||||
"defaultUsername": "Me",
|
||||
"welcomeText": "Bem vindo ao @:appName",
|
||||
"githubStarText": "Star on GitHub",
|
||||
"subscribeNewsletterText": "Inscreve-te ao Newsletter",
|
||||
"letsGoButtonText": "Bora",
|
||||
"title": "Título",
|
||||
"signUp": {
|
||||
"buttonText": "Inscreve-te",
|
||||
"title": "Inscreve-te ao @:appName",
|
||||
"getStartedText": "Começar",
|
||||
"emptyPasswordError": "A palavra-passe não pode estar em branco.",
|
||||
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As palavras-passes não coincidem.",
|
||||
"alreadyHaveAnAccount": "Já possuis uma conta?",
|
||||
"emailHint": "Email",
|
||||
"passwordHint": "Password",
|
||||
"repeatPasswordHint": "Confirma a tua password"
|
||||
},
|
||||
"signIn": {
|
||||
"loginTitle": "Entre no @:appName",
|
||||
"loginButtonText": "Login",
|
||||
"buttonText": "Entre",
|
||||
"forgotPassword": "Esqueceste-te da tua palavra-passe?",
|
||||
"emailHint": "Email",
|
||||
"passwordHint": "Palavra-passe",
|
||||
"dontHaveAnAccount": "Não possuis uma conta?",
|
||||
"repeatPasswordEmptyError": "Confirmar a palavra-passe não pode estar em branco.",
|
||||
"unmatchedPasswordError": "As palavras-passes não conferem."
|
||||
},
|
||||
"workspace": {
|
||||
"create": "Cria um ambiente de trabalho",
|
||||
"hint": "ambiente de trabalho",
|
||||
"notFoundError": "Ambiente de trabalho não encontrada"
|
||||
},
|
||||
"shareAction": {
|
||||
"buttonText": "Partilhar",
|
||||
"workInProgress": "Em breve",
|
||||
"markdown": "Markdown",
|
||||
"copyLink": "Copiar o link"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Renomear",
|
||||
"delete": "Apagar",
|
||||
"duplicate": "Duplicar"
|
||||
},
|
||||
"blankPageTitle": "Página em branco",
|
||||
"newPageText": "Nova página",
|
||||
"trash": {
|
||||
"text": "Lixo",
|
||||
"restoreAll": "Restaurar todos",
|
||||
"deleteAll": "Apagar todos",
|
||||
"pageHeader": {
|
||||
"fileName": "Nome do ficheiro",
|
||||
"lastModified": "Última modificação",
|
||||
"created": "Criado"
|
||||
}
|
||||
},
|
||||
"deletePagePrompt": {
|
||||
"text": "Esta página está no lixo",
|
||||
"restore": "Restaurar a página",
|
||||
"deletePermanent": "Apagar permanentemente"
|
||||
},
|
||||
"dialogCreatePageNameHint": "Nome da página",
|
||||
"questionBubble": {
|
||||
"whatsNew": "O que há de novo?",
|
||||
"help": "Ajuda & Suporte",
|
||||
"debug": {
|
||||
"name": "Informação de depuração",
|
||||
"success": "Copiar informação de depuração para o clipboard!",
|
||||
"fail": "Falha em copiar a informação de depuração para o clipboard"
|
||||
}
|
||||
},
|
||||
"menuAppHeader": {
|
||||
"addPageTooltip": "Adiciona uma nova página.",
|
||||
"defaultNewPageName": "Sem título",
|
||||
"renameDialog": "Renomear"
|
||||
},
|
||||
"toolbar": {
|
||||
"undo": "Desfazer",
|
||||
"redo": "Refazer",
|
||||
"bold": "Negrito",
|
||||
"italic": "Itálico",
|
||||
"underline": "Sublinhado",
|
||||
"strike": "Riscado",
|
||||
"numList": "Lista numerada",
|
||||
"bulletList": "Lista com marcadores",
|
||||
"checkList": "Lista de verificação",
|
||||
"inlineCode": "Embutir código",
|
||||
"quote": "Citação em bloco",
|
||||
"header": "Cabeçalho",
|
||||
"highlight": "Realçar"
|
||||
},
|
||||
"tooltip": {
|
||||
"lightMode": "Mudar para o modo Claro.",
|
||||
"darkMode": "Mudar para o modo Escuro."
|
||||
},
|
||||
"contactsPage": {
|
||||
"title": "Conctatos",
|
||||
"whatsHappening": "O que está a acontecer nesta semana?",
|
||||
"addContact": "Adicionar um conctato",
|
||||
"editContact": "Editar um conctato"
|
||||
},
|
||||
"button": {
|
||||
"OK": "OK",
|
||||
"Cancel": "Cancelar",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Sair",
|
||||
"complete": "Completar",
|
||||
"save": "Guardar"
|
||||
},
|
||||
"label": {
|
||||
"welcome": "Bem vindo!",
|
||||
"firstName": "Nome",
|
||||
"middleName": "Nome do Meio",
|
||||
"lastName": "Apelido",
|
||||
"stepX": "Passo {X}"
|
||||
},
|
||||
"oAuth": {
|
||||
"err": {
|
||||
"failedTitle": "Erro ao conectar à sua conta.",
|
||||
"failedMsg": "Verifica se concluiste o processo de login no teu navegador."
|
||||
},
|
||||
"google": {
|
||||
"title": "GOOGLE SIGN-IN",
|
||||
"instruction1": "Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.",
|
||||
"instruction2": "Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:",
|
||||
"instruction3": "Navega até o link a seguir no seu navegador e digite o código acima:",
|
||||
"instruction4": "Clica no botão abaixo ao concluir a inscrição:"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Definições",
|
||||
"menu": {
|
||||
"appearance": "Aparência",
|
||||
"language": "Idioma",
|
||||
"open": "Abrir as Definições"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Modo Claro",
|
||||
"darkLabel": "Modo Escuro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +141,68 @@
|
||||
"lightLabel": "Светлая тема",
|
||||
"darkLabel": "Тёмная тема"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"settings": {
|
||||
"filter": "Фильтр",
|
||||
"sortBy": "Сортировать",
|
||||
"Properties": "Свойства"
|
||||
},
|
||||
"field": {
|
||||
"hide": "Скрыть",
|
||||
"insertLeft": "Вставить слева",
|
||||
"insertRight": "Вставить справа",
|
||||
"duplicate": "Дублировать",
|
||||
"delete": "Удалить",
|
||||
"textFieldName": "Текст",
|
||||
"checkboxFieldName": "Checkbox",
|
||||
"dateFieldName": "Дата",
|
||||
"numberFieldName": "Число",
|
||||
"singleSelectFieldName": "Выбор",
|
||||
"multiSelectFieldName": "Выбор многих",
|
||||
"urlFieldName": "URL",
|
||||
"numberFormat": " Формат числа",
|
||||
"dateFormat": " Формат даты",
|
||||
"includeTime": " Время",
|
||||
"dateFormatFriendly": "День Месяц, Год",
|
||||
"dateFormatISO": "Год-Месяц-День",
|
||||
"dateFormatLocal": "Год/Месяц/День",
|
||||
"dateFormatUS": "Год/Месяц/День",
|
||||
"timeFormat": " Форматировать время",
|
||||
"invalidTimeFormat": "Неверный формат",
|
||||
"timeFormatTwelveHour": "12 часов",
|
||||
"timeFormatTwentyFourHour": "24 часа",
|
||||
"addSelectOption": "Добавить вариант",
|
||||
"optionTitle": "Варианты",
|
||||
"addOption": "Добавить",
|
||||
"editProperty": "Редактировать свойство"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Дублировать",
|
||||
"delete": "Удалить",
|
||||
"textPlaceholder": "Пусто",
|
||||
"copyProperty": "Свойство скопировано"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Создать",
|
||||
"purpleColor": "Фиолетовый",
|
||||
"pinkColor": "Розовый",
|
||||
"lightPinkColor": "Светло-розовый",
|
||||
"orangeColor": "Оранжевый",
|
||||
"yellowColor": "Желтый",
|
||||
"limeColor": "Ярко-зелёный",
|
||||
"greenColor": "Зелёный",
|
||||
"aquaColor": "Морской волны",
|
||||
"blueColor": "Синий",
|
||||
"deleteTag": "Удалить вариант",
|
||||
"colorPannelTitle": "Цвета",
|
||||
"pannelTitle": "Выберите или создайте вариант",
|
||||
"searchOption": "Поиск"
|
||||
},
|
||||
"date": {
|
||||
"timeHintTextInTwelveHour": "12:00 AM",
|
||||
"timeHintTextInTwentyFourHour": "12:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
class DependencyResolver {
|
||||
@ -46,6 +47,8 @@ void _resolveUserDeps(GetIt getIt) {
|
||||
}
|
||||
|
||||
void _resolveHomeDeps(GetIt getIt) {
|
||||
getIt.registerSingleton(FToast());
|
||||
|
||||
getIt.registerSingleton(MenuSharedState());
|
||||
|
||||
getIt.registerFactoryParam<UserListener, UserProfile, void>(
|
||||
|
@ -67,40 +67,42 @@ class ApplicationWidget extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
||||
value: settingModel,
|
||||
builder: (context, _) {
|
||||
const ratio = 1.73;
|
||||
const minWidth = 600.0;
|
||||
setWindowMinSize(const Size(minWidth, minWidth / ratio));
|
||||
settingModel.readLocaleWhenAppLaunch(context);
|
||||
AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
|
||||
(value) => value.theme,
|
||||
);
|
||||
Locale locale = context.select<AppearanceSettingModel, Locale>(
|
||||
(value) => value.locale,
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: settingModel,
|
||||
builder: (context, _) {
|
||||
const ratio = 1.73;
|
||||
const minWidth = 600.0;
|
||||
setWindowMinSize(const Size(minWidth, minWidth / ratio));
|
||||
settingModel.readLocaleWhenAppLaunch(context);
|
||||
AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
|
||||
(value) => value.theme,
|
||||
);
|
||||
Locale locale = context.select<AppearanceSettingModel, Locale>(
|
||||
(value) => value.locale,
|
||||
);
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: theme),
|
||||
Provider.value(value: locale),
|
||||
],
|
||||
builder: (context, _) {
|
||||
return MaterialApp(
|
||||
builder: overlayManagerBuilder(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: theme.themeData,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: locale,
|
||||
navigatorKey: AppGlobals.rootNavKey,
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: theme),
|
||||
Provider.value(value: locale),
|
||||
],
|
||||
builder: (context, _) {
|
||||
return MaterialApp(
|
||||
builder: overlayManagerBuilder(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: theme.themeData,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: locale,
|
||||
navigatorKey: AppGlobals.rootNavKey,
|
||||
home: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppGlobals {
|
||||
|
@ -12,14 +12,14 @@ class DocumentService {
|
||||
await FolderEventSetLatestView(ViewId(value: docId)).send();
|
||||
|
||||
final payload = TextBlockId(value: docId);
|
||||
return BlockEventGetBlockData(payload).send();
|
||||
return TextBlockEventGetBlockData(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<TextBlockDelta, FlowyError>> composeDelta({required String docId, required String data}) {
|
||||
final payload = TextBlockDelta.create()
|
||||
..blockId = docId
|
||||
..deltaStr = data;
|
||||
return BlockEventApplyDelta(payload).send();
|
||||
return TextBlockEventApplyDelta(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> closeDocument({required String docId}) {
|
||||
|
@ -10,7 +10,7 @@ class ShareService {
|
||||
..viewId = docId
|
||||
..exportType = type;
|
||||
|
||||
return BlockEventExportDocument(request).send();
|
||||
return TextBlockEventExportDocument(request).send();
|
||||
}
|
||||
|
||||
Future<Either<ExportData, FlowyError>> exportText(String docId) {
|
||||
|
@ -2,7 +2,7 @@ part of 'cell_service.dart';
|
||||
|
||||
typedef GridCellContext = _GridCellContext<String, String>;
|
||||
typedef GridSelectOptionCellContext = _GridCellContext<SelectOptionCellData, String>;
|
||||
typedef GridDateCellContext = _GridCellContext<DateCellData, DateCalData>;
|
||||
typedef GridDateCellContext = _GridCellContext<DateCellData, CalendarData>;
|
||||
typedef GridURLCellContext = _GridCellContext<URLCellData, String>;
|
||||
|
||||
class GridCellContextBuilder {
|
||||
@ -31,6 +31,7 @@ class GridCellContextBuilder {
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
gridCell: _gridCell,
|
||||
parser: DateCellDataParser(),
|
||||
config: const GridCellDataConfig(reloadOnFieldChanged: true),
|
||||
);
|
||||
|
||||
return GridDateCellContext(
|
||||
@ -105,7 +106,7 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
final FieldService _fieldService;
|
||||
|
||||
late final CellListener _cellListener;
|
||||
late final ValueNotifier<T?> _cellDataNotifier;
|
||||
late final ValueNotifier<T?>? _cellDataNotifier;
|
||||
bool isListening = false;
|
||||
VoidCallback? _onFieldChangedFn;
|
||||
Timer? _loadDataOperation;
|
||||
@ -163,19 +164,19 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
}
|
||||
|
||||
onCellChangedFn() {
|
||||
onCellChanged(_cellDataNotifier.value);
|
||||
onCellChanged(_cellDataNotifier?.value);
|
||||
|
||||
if (cellDataLoader.config.reloadOnCellChanged) {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
_cellDataNotifier.addListener(onCellChangedFn);
|
||||
_cellDataNotifier?.addListener(onCellChangedFn);
|
||||
return onCellChangedFn;
|
||||
}
|
||||
|
||||
void removeListener(VoidCallback fn) {
|
||||
_cellDataNotifier.removeListener(fn);
|
||||
_cellDataNotifier?.removeListener(fn);
|
||||
}
|
||||
|
||||
T? getCellData({bool loadIfNoCache = true}) {
|
||||
@ -211,13 +212,14 @@ class _GridCellContext<T, D> extends Equatable {
|
||||
_loadDataOperation?.cancel();
|
||||
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
cellDataLoader.loadData().then((data) {
|
||||
_cellDataNotifier.value = data;
|
||||
_cellDataNotifier?.value = data;
|
||||
cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_cellListener.stop();
|
||||
_loadDataOperation?.cancel();
|
||||
_saveDataOperation?.cancel();
|
||||
|
||||
|
@ -58,11 +58,7 @@ class GridCellDataLoader<T> extends IGridCellDataLoader<T> {
|
||||
return fut.then(
|
||||
(result) => result.fold((Cell cell) {
|
||||
try {
|
||||
if (cell.data.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
return parser.parserData(cell.data);
|
||||
}
|
||||
return parser.parserData(cell.data);
|
||||
} catch (e, s) {
|
||||
Log.error('$parser parser cellData failed, $e');
|
||||
Log.error('Stack trace \n $s');
|
||||
@ -102,13 +98,17 @@ class SelectOptionCellDataLoader extends IGridCellDataLoader<SelectOptionCellDat
|
||||
class StringCellDataParser implements ICellDataParser<String> {
|
||||
@override
|
||||
String? parserData(List<int> data) {
|
||||
return utf8.decode(data);
|
||||
final s = utf8.decode(data);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
class DateCellDataParser implements ICellDataParser<DateCellData> {
|
||||
@override
|
||||
DateCellData? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateCellData.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
@ -116,6 +116,9 @@ class DateCellDataParser implements ICellDataParser<DateCellData> {
|
||||
class SelectOptionCellDataParser implements ICellDataParser<SelectOptionCellData> {
|
||||
@override
|
||||
SelectOptionCellData? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return SelectOptionCellData.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
@ -123,6 +126,9 @@ class SelectOptionCellDataParser implements ICellDataParser<SelectOptionCellData
|
||||
class URLCellDataParser implements ICellDataParser<URLCellData> {
|
||||
@override
|
||||
URLCellData? parserData(List<int> data) {
|
||||
if (data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return URLCellData.fromBuffer(data);
|
||||
}
|
||||
}
|
||||
|
@ -31,18 +31,18 @@ class CellDataPersistence implements _GridCellDataPersistence<String> {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DateCalData with _$DateCalData {
|
||||
const factory DateCalData({required DateTime date, String? time}) = _DateCellPersistenceData;
|
||||
class CalendarData with _$CalendarData {
|
||||
const factory CalendarData({required DateTime date, String? time}) = _CalendarData;
|
||||
}
|
||||
|
||||
class DateCellDataPersistence implements _GridCellDataPersistence<DateCalData> {
|
||||
class DateCellDataPersistence implements _GridCellDataPersistence<CalendarData> {
|
||||
final GridCell gridCell;
|
||||
DateCellDataPersistence({
|
||||
required this.gridCell,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Option<FlowyError>> save(DateCalData data) {
|
||||
Future<Option<FlowyError>> save(CalendarData data) {
|
||||
var payload = DateChangesetPayload.create()..cellIdentifier = _cellIdentifier(gridCell);
|
||||
|
||||
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
|
@ -38,9 +38,9 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
emit(state.copyWith(focusedDay: focusedDay));
|
||||
},
|
||||
didReceiveCellUpdate: (DateCellData? cellData) {
|
||||
final dateData = dateDataFromCellData(cellData);
|
||||
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
|
||||
emit(state.copyWith(dateData: dateData, time: time));
|
||||
final calData = calDataFromCellData(cellData);
|
||||
final time = calData.foldRight("", (dateData, previous) => dateData.time);
|
||||
emit(state.copyWith(calData: calData, time: time));
|
||||
},
|
||||
setIncludeTime: (includeTime) async {
|
||||
await _updateTypeOption(emit, includeTime: includeTime);
|
||||
@ -52,7 +52,12 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
||||
},
|
||||
setTime: (time) async {
|
||||
await _updateDateData(emit, time: time);
|
||||
if (state.calData.isSome()) {
|
||||
await _updateDateData(emit, time: time);
|
||||
}
|
||||
},
|
||||
didUpdateCalData: (Option<CalendarData> data, Option<String> timeFormatError) {
|
||||
emit(state.copyWith(calData: data, timeFormatError: timeFormatError));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -60,8 +65,8 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
}
|
||||
|
||||
Future<void> _updateDateData(Emitter<DateCalState> emit, {DateTime? date, String? time}) {
|
||||
final DateCalData newDateData = state.dateData.fold(
|
||||
() => DateCalData(date: date ?? DateTime.now(), time: time),
|
||||
final CalendarData newDateData = state.calData.fold(
|
||||
() => CalendarData(date: date ?? DateTime.now(), time: time),
|
||||
(dateData) {
|
||||
var newDateData = dateData;
|
||||
if (date != null && !isSameDay(newDateData.date, date)) {
|
||||
@ -78,24 +83,22 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
return _saveDateData(emit, newDateData);
|
||||
}
|
||||
|
||||
Future<void> _saveDateData(Emitter<DateCalState> emit, DateCalData newDateData) async {
|
||||
if (state.dateData == Some(newDateData)) {
|
||||
Future<void> _saveDateData(Emitter<DateCalState> emit, CalendarData newCalData) async {
|
||||
if (state.calData == Some(newCalData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cellContext.saveCellData(newDateData, resultCallback: (result) {
|
||||
updateCalData(Option<CalendarData> calData, Option<String> timeFormatError) {
|
||||
if (!isClosed) add(DateCalEvent.didUpdateCalData(calData, timeFormatError));
|
||||
}
|
||||
|
||||
cellContext.saveCellData(newCalData, resultCallback: (result) {
|
||||
result.fold(
|
||||
() => emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: none(),
|
||||
)),
|
||||
() => updateCalData(Some(newCalData), none()),
|
||||
(err) {
|
||||
switch (ErrorCode.valueOf(err.code)!) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
emit(state.copyWith(
|
||||
dateData: Some(newDateData),
|
||||
timeFormatError: Some(timeFormatPrompt(err)),
|
||||
));
|
||||
updateCalData(none(), Some(timeFormatPrompt(err)));
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
@ -168,7 +171,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(l) => emit(state.copyWith(dateTypeOption: newDateTypeOption)),
|
||||
(l) => emit(state.copyWith(dateTypeOption: newDateTypeOption, timeHintText: _timeHintText(newDateTypeOption))),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
@ -185,6 +188,8 @@ class DateCalEvent with _$DateCalEvent {
|
||||
const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime;
|
||||
const factory DateCalEvent.setTime(String time) = _Time;
|
||||
const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate;
|
||||
const factory DateCalEvent.didUpdateCalData(Option<CalendarData> data, Option<String> timeFormatError) =
|
||||
_DidUpdateCalData;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -194,36 +199,48 @@ class DateCalState with _$DateCalState {
|
||||
required CalendarFormat format,
|
||||
required DateTime focusedDay,
|
||||
required Option<String> timeFormatError,
|
||||
required Option<DateCalData> dateData,
|
||||
required Option<CalendarData> calData,
|
||||
required String? time,
|
||||
required String timeHintText,
|
||||
}) = _DateCalState;
|
||||
|
||||
factory DateCalState.initial(
|
||||
DateTypeOption dateTypeOption,
|
||||
DateCellData? cellData,
|
||||
) {
|
||||
Option<DateCalData> dateData = dateDataFromCellData(cellData);
|
||||
final time = dateData.foldRight("", (dateData, previous) => dateData.time);
|
||||
Option<CalendarData> calData = calDataFromCellData(cellData);
|
||||
final time = calData.foldRight("", (dateData, previous) => dateData.time);
|
||||
return DateCalState(
|
||||
dateTypeOption: dateTypeOption,
|
||||
format: CalendarFormat.month,
|
||||
focusedDay: DateTime.now(),
|
||||
time: time,
|
||||
dateData: dateData,
|
||||
calData: calData,
|
||||
timeFormatError: none(),
|
||||
timeHintText: _timeHintText(dateTypeOption),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Option<DateCalData> dateDataFromCellData(DateCellData? cellData) {
|
||||
String _timeHintText(DateTypeOption typeOption) {
|
||||
switch (typeOption.timeFormat) {
|
||||
case TimeFormat.TwelveHour:
|
||||
return LocaleKeys.grid_date_timeHintTextInTwelveHour.tr();
|
||||
case TimeFormat.TwentyFourHour:
|
||||
return LocaleKeys.grid_date_timeHintTextInTwentyFourHour.tr();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Option<CalendarData> calDataFromCellData(DateCellData? cellData) {
|
||||
String? time = timeFromCellData(cellData);
|
||||
Option<DateCalData> dateData = none();
|
||||
Option<CalendarData> calData = none();
|
||||
if (cellData != null) {
|
||||
final timestamp = cellData.timestamp * 1000;
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||
dateData = Some(DateCalData(date: date, time: time));
|
||||
calData = Some(CalendarData(date: date, time: time));
|
||||
}
|
||||
return dateData;
|
||||
return calData;
|
||||
}
|
||||
|
||||
$fixnum.Int64 timestampFromDateTime(DateTime dateTime) {
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'cell_service/cell_service.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
part 'date_cell_bloc.freezed.dart';
|
||||
|
||||
class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
@ -17,11 +16,7 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
|
||||
event.when(
|
||||
initial: () => _startListening(),
|
||||
didReceiveCellUpdate: (DateCellData? cellData) {
|
||||
if (cellData != null) {
|
||||
emit(state.copyWith(data: Some(cellData)));
|
||||
} else {
|
||||
emit(state.copyWith(data: none()));
|
||||
}
|
||||
emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData)));
|
||||
},
|
||||
didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)),
|
||||
);
|
||||
@ -60,21 +55,26 @@ class DateCellEvent with _$DateCellEvent {
|
||||
@freezed
|
||||
class DateCellState with _$DateCellState {
|
||||
const factory DateCellState({
|
||||
required Option<DateCellData> data,
|
||||
required DateCellData? data,
|
||||
required String dateStr,
|
||||
required Field field,
|
||||
}) = _DateCellState;
|
||||
|
||||
factory DateCellState.initial(GridDateCellContext context) {
|
||||
final cellData = context.getCellData();
|
||||
Option<DateCellData> data = none();
|
||||
|
||||
if (cellData != null) {
|
||||
data = Some(cellData);
|
||||
}
|
||||
|
||||
return DateCellState(
|
||||
field: context.field,
|
||||
data: data,
|
||||
data: cellData,
|
||||
dateStr: _dateStrFromCellData(cellData),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _dateStrFromCellData(DateCellData? cellData) {
|
||||
String dateStr = "";
|
||||
if (cellData != null) {
|
||||
dateStr = cellData.date + " " + cellData.time;
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
part 'number_cell_bloc.freezed.dart';
|
||||
@ -14,25 +16,28 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
}) : super(NumberCellState.initial(cellContext)) {
|
||||
on<NumberCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_Initial value) async {
|
||||
event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveCellUpdate: (_DidReceiveCellUpdate value) {
|
||||
emit(state.copyWith(content: value.cellContent ?? ""));
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
},
|
||||
updateCell: (_UpdateCell value) async {
|
||||
await _updateCellValue(value, emit);
|
||||
updateCell: (text) {
|
||||
cellContext.saveCellData(text, resultCallback: (result) {
|
||||
result.fold(
|
||||
() => null,
|
||||
(err) {
|
||||
if (!isClosed) add(NumberCellEvent.didReceiveCellUpdate(right(err)));
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateCellValue(_UpdateCell value, Emitter<NumberCellState> emit) async {
|
||||
cellContext.saveCellData(value.text);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
@ -47,7 +52,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
_onCellChangedFn = cellContext.startListening(
|
||||
onCellChanged: ((cellContent) {
|
||||
if (!isClosed) {
|
||||
add(NumberCellEvent.didReceiveCellUpdate(cellContent));
|
||||
add(NumberCellEvent.didReceiveCellUpdate(left(cellContent ?? "")));
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -58,17 +63,19 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
|
||||
class NumberCellEvent with _$NumberCellEvent {
|
||||
const factory NumberCellEvent.initial() = _Initial;
|
||||
const factory NumberCellEvent.updateCell(String text) = _UpdateCell;
|
||||
const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) = _DidReceiveCellUpdate;
|
||||
const factory NumberCellEvent.didReceiveCellUpdate(Either<String, FlowyError> cellContent) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class NumberCellState with _$NumberCellState {
|
||||
const factory NumberCellState({
|
||||
required String content,
|
||||
required Either<String, FlowyError> content,
|
||||
}) = _NumberCellState;
|
||||
|
||||
factory NumberCellState.initial(GridCellContext context) {
|
||||
final cellContent = context.getCellData() ?? "";
|
||||
return NumberCellState(content: cellContent);
|
||||
return NumberCellState(
|
||||
content: left(cellContent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,9 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
url: cellData?.url ?? "",
|
||||
));
|
||||
},
|
||||
updateURL: (String url) {
|
||||
cellContext.saveCellData(url, deduplicate: true);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -53,6 +56,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
|
||||
@freezed
|
||||
class URLCellEvent with _$URLCellEvent {
|
||||
const factory URLCellEvent.initial() = _InitialCell;
|
||||
const factory URLCellEvent.updateURL(String url) = _UpdateURL;
|
||||
const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
|
@ -24,13 +24,15 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
_startListening();
|
||||
},
|
||||
didReceiveFieldUpdate: (field) {
|
||||
emit(state.copyWith(field: field));
|
||||
emit(state.copyWith(field: cellContext.field));
|
||||
},
|
||||
updateWidth: (offset) {
|
||||
final defaultWidth = state.field.width.toDouble();
|
||||
final width = defaultWidth + offset;
|
||||
if (width > defaultWidth && width < 300) {
|
||||
_fieldService.updateField(width: width);
|
||||
startUpdateWidth: (offset) {
|
||||
final width = state.width + offset;
|
||||
emit(state.copyWith(width: width));
|
||||
},
|
||||
endUpdateWidth: () {
|
||||
if (state.width != state.field.width.toDouble()) {
|
||||
_fieldService.updateField(width: state.width);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -61,7 +63,8 @@ class FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {
|
||||
class FieldCellEvent with _$FieldCellEvent {
|
||||
const factory FieldCellEvent.initial() = _InitialCell;
|
||||
const factory FieldCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
|
||||
const factory FieldCellEvent.updateWidth(double offset) = _UpdateWidth;
|
||||
const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth;
|
||||
const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -69,10 +72,12 @@ class FieldCellState with _$FieldCellState {
|
||||
const factory FieldCellState({
|
||||
required String gridId,
|
||||
required Field field,
|
||||
required double width,
|
||||
}) = _FieldCellState;
|
||||
|
||||
factory FieldCellState.initial(GridFieldCellContext cellContext) => FieldCellState(
|
||||
gridId: cellContext.gridId,
|
||||
field: cellContext.field,
|
||||
width: cellContext.field.width.toDouble(),
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
|
||||
@ -8,6 +9,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'cell/cell_service/cell_service.dart';
|
||||
import 'grid_service.dart';
|
||||
import 'row/row_service.dart';
|
||||
import 'dart:collection';
|
||||
|
||||
part 'grid_bloc.freezed.dart';
|
||||
|
||||
@ -33,19 +35,19 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
on<GridEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (InitialGrid value) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
await _loadGrid(emit);
|
||||
},
|
||||
createRow: (_CreateRow value) {
|
||||
createRow: () {
|
||||
_gridService.createRow();
|
||||
},
|
||||
didReceiveRowUpdate: (_DidReceiveRowUpdate value) {
|
||||
emit(state.copyWith(rows: value.rows, listState: value.listState));
|
||||
didReceiveRowUpdate: (rows, listState) {
|
||||
emit(state.copyWith(rows: rows, listState: listState));
|
||||
},
|
||||
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
|
||||
emit(state.copyWith(rows: rowCache.clonedRows, fields: value.fields));
|
||||
didReceiveFieldUpdate: (fields) {
|
||||
emit(state.copyWith(rows: rowCache.clonedRows, fields: GridFieldEquatable(fields)));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -93,7 +95,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
|
||||
emit(state.copyWith(
|
||||
grid: Some(grid),
|
||||
fields: fieldCache.fields,
|
||||
fields: GridFieldEquatable(fieldCache.fields),
|
||||
rows: rowCache.clonedRows,
|
||||
loadingState: GridLoadingState.finish(left(unit)),
|
||||
));
|
||||
@ -117,14 +119,14 @@ class GridState with _$GridState {
|
||||
const factory GridState({
|
||||
required String gridId,
|
||||
required Option<Grid> grid,
|
||||
required List<Field> fields,
|
||||
required GridFieldEquatable fields,
|
||||
required List<GridRow> rows,
|
||||
required GridLoadingState loadingState,
|
||||
required GridRowChangeReason listState,
|
||||
}) = _GridState;
|
||||
|
||||
factory GridState.initial(String gridId) => GridState(
|
||||
fields: [],
|
||||
fields: const GridFieldEquatable([]),
|
||||
rows: [],
|
||||
grid: none(),
|
||||
gridId: gridId,
|
||||
@ -138,3 +140,19 @@ class GridLoadingState with _$GridLoadingState {
|
||||
const factory GridLoadingState.loading() = _Loading;
|
||||
const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
|
||||
}
|
||||
|
||||
class GridFieldEquatable extends Equatable {
|
||||
final List<Field> _fields;
|
||||
|
||||
const GridFieldEquatable(List<Field> fields) : _fields = fields;
|
||||
|
||||
@override
|
||||
List<Object?> get props {
|
||||
return [
|
||||
_fields.length,
|
||||
_fields.map((field) => field.width).reduce((value, element) => value + element),
|
||||
];
|
||||
}
|
||||
|
||||
UnmodifiableListView<Field> get value => UnmodifiableListView(_fields);
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
_rowService.createRow();
|
||||
},
|
||||
didReceiveCellDatas: (_DidReceiveCellDatas value) async {
|
||||
final fields = value.gridCellMap.values.map((e) => CellSnapshot(e.field)).toList();
|
||||
final fields = value.gridCellMap.values.map((e) => GridCellEquatable(e.field)).toList();
|
||||
final snapshots = UnmodifiableListView(fields);
|
||||
emit(state.copyWith(
|
||||
gridCellMap: value.gridCellMap,
|
||||
@ -74,26 +74,27 @@ class RowState with _$RowState {
|
||||
const factory RowState({
|
||||
required GridRow rowData,
|
||||
required GridCellMap gridCellMap,
|
||||
required UnmodifiableListView<CellSnapshot> snapshots,
|
||||
required UnmodifiableListView<GridCellEquatable> snapshots,
|
||||
GridRowChangeReason? changeReason,
|
||||
}) = _RowState;
|
||||
|
||||
factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState(
|
||||
rowData: rowData,
|
||||
gridCellMap: cellDataMap,
|
||||
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => CellSnapshot(e.field)).toList()),
|
||||
snapshots: UnmodifiableListView(cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()),
|
||||
);
|
||||
}
|
||||
|
||||
class CellSnapshot extends Equatable {
|
||||
class GridCellEquatable extends Equatable {
|
||||
final Field _field;
|
||||
|
||||
const CellSnapshot(Field field) : _field = field;
|
||||
const GridCellEquatable(Field field) : _field = field;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
_field.id,
|
||||
_field.fieldType,
|
||||
_field.visibility,
|
||||
_field.width,
|
||||
];
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import 'home_stack.dart';
|
||||
import 'menu/menu.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
final UserProfile user;
|
||||
final CurrentWorkspaceSetting workspaceSetting;
|
||||
const HomeScreen(this.user, this.workspaceSetting, {Key? key}) : super(key: key);
|
||||
@ -52,7 +51,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
key: HomeScreen.scaffoldKey,
|
||||
body: BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.unauthorized != c.unauthorized,
|
||||
listener: (context, state) {
|
||||
|
@ -2,15 +2,13 @@ import 'dart:io' show Platform;
|
||||
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/home/home_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_screen.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:time/time.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
import 'package:app_flowy/plugin/plugin.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
|
||||
@ -22,11 +20,7 @@ import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef NavigationCallback = void Function(String id);
|
||||
|
||||
late FToast fToast;
|
||||
|
||||
class HomeStack extends StatelessWidget {
|
||||
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
// final Size size;
|
||||
const HomeStack({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -74,8 +68,7 @@ class _FadingIndexedStackState extends State<FadingIndexedStack> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fToast = FToast();
|
||||
fToast.init(HomeScreen.scaffoldKey.currentState!.context);
|
||||
initToastWithContext(context);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -28,7 +28,7 @@ class AddButton extends StatelessWidget {
|
||||
onSelected: onSelected,
|
||||
).show(context);
|
||||
},
|
||||
icon: svgWidget("home/add").padding(horizontal: 3, vertical: 3),
|
||||
icon: svgWidget("home/add", color: theme.iconColor).padding(horizontal: 3, vertical: 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -46,8 +46,8 @@ class ActionList {
|
||||
return CreateItem(
|
||||
pluginBuilder: pluginBuilder,
|
||||
onSelected: (builder) {
|
||||
FlowyOverlay.of(buildContext).remove(_identifier);
|
||||
onSelected(builder);
|
||||
FlowyOverlay.of(buildContext).remove(_identifier);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -0,0 +1,37 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
class FlowyMessageToast extends StatelessWidget {
|
||||
final String message;
|
||||
const FlowyMessageToast({required this.message, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: FlowyText.medium(message, color: Colors.white),
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void initToastWithContext(BuildContext context) {
|
||||
getIt<FToast>().init(context);
|
||||
}
|
||||
|
||||
void showMessageToast(String message) {
|
||||
final child = FlowyMessageToast(message: message);
|
||||
|
||||
getIt<FToast>().showToast(
|
||||
child: child,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastDuration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
@ -29,9 +29,9 @@ class ToolbarIconButton extends StatelessWidget {
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
|
||||
onPressed: onPressed,
|
||||
width: width,
|
||||
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName),
|
||||
icon: isToggled == true ? svgWidget(iconName, color: Colors.white) : svgWidget(iconName, color: theme.iconColor),
|
||||
fillColor: isToggled == true ? theme.main1 : theme.shader6,
|
||||
hoverColor: isToggled == true ? theme.main1 : theme.shader5,
|
||||
hoverColor: isToggled == true ? theme.main1 : theme.hover,
|
||||
tooltipText: tooltipText,
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import 'layout/sizes.dart';
|
||||
import 'widgets/row/grid_row.dart';
|
||||
import 'widgets/footer/grid_footer.dart';
|
||||
import 'widgets/header/grid_header.dart';
|
||||
import 'widgets/shortcuts.dart';
|
||||
import 'widgets/toolbar/grid_toolbar.dart';
|
||||
|
||||
class GridPage extends StatefulWidget {
|
||||
@ -40,7 +41,7 @@ class _GridPageState extends State<GridPage> {
|
||||
return state.loadingState.map(
|
||||
loading: (_) => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => const FlowyGrid(),
|
||||
(_) => const GridShortcuts(child: FlowyGrid()),
|
||||
(err) => FlowyErrorPage(err.toString()),
|
||||
),
|
||||
);
|
||||
@ -91,9 +92,9 @@ class _FlowyGridState extends State<FlowyGrid> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridBloc, GridState>(
|
||||
buildWhen: (previous, current) => previous.fields.length != current.fields.length,
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final contentWidth = GridLayout.headerWidth(state.fields);
|
||||
final contentWidth = GridLayout.headerWidth(state.fields.value);
|
||||
final child = _wrapScrollView(
|
||||
contentWidth,
|
||||
[
|
||||
|
@ -0,0 +1,198 @@
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class GridCellAccessoryBuildContext {
|
||||
final BuildContext anchorContext;
|
||||
final bool isCellEditing;
|
||||
|
||||
GridCellAccessoryBuildContext({
|
||||
required this.anchorContext,
|
||||
required this.isCellEditing,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class GridCellAccessory implements Widget {
|
||||
void onTap();
|
||||
|
||||
// The accessory will be hidden if enable() return false;
|
||||
bool enable() => true;
|
||||
}
|
||||
|
||||
class PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
|
||||
final VoidCallback onTapCallback;
|
||||
final bool isCellEditing;
|
||||
const PrimaryCellAccessory({
|
||||
required this.onTapCallback,
|
||||
required this.isCellEditing,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isCellEditing) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return svgWidget("grid/expander", color: theme.main1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() => onTapCallback();
|
||||
|
||||
@override
|
||||
bool enable() => !isCellEditing;
|
||||
}
|
||||
|
||||
typedef AccessoryBuilder = List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext);
|
||||
|
||||
abstract class CellAccessory extends Widget {
|
||||
const CellAccessory({Key? key}) : super(key: key);
|
||||
|
||||
// The hover will show if the isHover's value is true
|
||||
ValueNotifier<bool>? get onAccessoryHover;
|
||||
|
||||
AccessoryBuilder? get accessoryBuilder;
|
||||
}
|
||||
|
||||
class AccessoryHover extends StatefulWidget {
|
||||
final CellAccessory child;
|
||||
final EdgeInsets contentPadding;
|
||||
const AccessoryHover({
|
||||
required this.child,
|
||||
this.contentPadding = EdgeInsets.zero,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AccessoryHover> createState() => _AccessoryHoverState();
|
||||
}
|
||||
|
||||
class _AccessoryHoverState extends State<AccessoryHover> {
|
||||
late AccessoryHoverState _hoverState;
|
||||
VoidCallback? _listenerFn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_hoverState = AccessoryHoverState();
|
||||
_listenerFn = () => _hoverState.onHover = widget.child.onAccessoryHover?.value ?? false;
|
||||
widget.child.onAccessoryHover?.addListener(_listenerFn!);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverState.dispose();
|
||||
|
||||
if (_listenerFn != null) {
|
||||
widget.child.onAccessoryHover?.removeListener(_listenerFn!);
|
||||
_listenerFn = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [
|
||||
const _Background(),
|
||||
Padding(padding: widget.contentPadding, child: widget.child),
|
||||
];
|
||||
|
||||
final accessoryBuilder = widget.child.accessoryBuilder;
|
||||
if (accessoryBuilder != null) {
|
||||
final accessories = accessoryBuilder((GridCellAccessoryBuildContext(
|
||||
anchorContext: context,
|
||||
isCellEditing: false,
|
||||
)));
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: CellAccessoryContainer(accessories: accessories),
|
||||
).positioned(right: 0),
|
||||
);
|
||||
}
|
||||
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _hoverState,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) => setState(() => _hoverState.onHover = true),
|
||||
onExit: (p) => setState(() => _hoverState.onHover = false),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccessoryHoverState extends ChangeNotifier {
|
||||
bool _onHover = false;
|
||||
|
||||
set onHover(bool value) {
|
||||
if (_onHover != value) {
|
||||
_onHover = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onHover => _onHover;
|
||||
}
|
||||
|
||||
class _Background extends StatelessWidget {
|
||||
const _Background({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Consumer<AccessoryHoverState>(
|
||||
builder: (context, state, child) {
|
||||
if (state.onHover) {
|
||||
return FlowyHoverContainer(
|
||||
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellAccessoryContainer extends StatelessWidget {
|
||||
final List<GridCellAccessory> accessories;
|
||||
const CellAccessoryContainer({required this.accessories, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final children = accessories.where((accessory) => accessory.enable()).map((accessory) {
|
||||
final hover = FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.bg3, backgroundColor: theme.surface),
|
||||
builder: (_, onHover) => Container(
|
||||
width: 26,
|
||||
height: 26,
|
||||
padding: const EdgeInsets.all(3),
|
||||
child: accessory,
|
||||
),
|
||||
);
|
||||
return GestureDetector(
|
||||
child: hover,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => accessory.onTap(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Wrap(children: children, spacing: 6);
|
||||
}
|
||||
}
|
@ -1,13 +1,10 @@
|
||||
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'cell_accessory.dart';
|
||||
import 'cell_shortcuts.dart';
|
||||
import 'checkbox_cell.dart';
|
||||
import 'date_cell/date_cell.dart';
|
||||
import 'number_cell.dart';
|
||||
@ -48,24 +45,132 @@ class BlankCell extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GridCellWidget implements FlowyHoverWidget {
|
||||
@override
|
||||
final ValueNotifier<bool> onFocus = ValueNotifier<bool>(false);
|
||||
abstract class CellEditable {
|
||||
GridCellFocusListener get beginFocus;
|
||||
|
||||
final GridCellRequestFocusNotifier requestFocus = GridCellRequestFocusNotifier();
|
||||
ValueNotifier<bool> get onCellFocus;
|
||||
|
||||
ValueNotifier<bool> get onCellEditing;
|
||||
}
|
||||
|
||||
class GridCellRequestFocusNotifier extends ChangeNotifier {
|
||||
VoidCallback? _listener;
|
||||
abstract class GridCellWidget extends StatefulWidget implements CellAccessory, CellEditable, CellShortcuts {
|
||||
GridCellWidget({Key? key}) : super(key: key) {
|
||||
onCellEditing.addListener(() {
|
||||
onCellFocus.value = onCellEditing.value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
final ValueNotifier<bool> onCellFocus = ValueNotifier<bool>(false);
|
||||
|
||||
// When the cell is focused, we assume that the accessory alse be hovered.
|
||||
@override
|
||||
ValueNotifier<bool> get onAccessoryHover => onCellFocus;
|
||||
|
||||
@override
|
||||
final ValueNotifier<bool> onCellEditing = ValueNotifier<bool>(false);
|
||||
|
||||
@override
|
||||
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
|
||||
|
||||
@override
|
||||
final GridCellFocusListener beginFocus = GridCellFocusListener();
|
||||
|
||||
@override
|
||||
final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};
|
||||
}
|
||||
|
||||
abstract class GridCellState<T extends GridCellWidget> extends State<T> {
|
||||
@override
|
||||
void initState() {
|
||||
widget.beginFocus.setListener(() => requestBeginFocus());
|
||||
widget.shortcutHandlers[CellKeyboardKey.onCopy] = () => onCopy();
|
||||
widget.shortcutHandlers[CellKeyboardKey.onInsert] = () {
|
||||
Clipboard.getData("text/plain").then((data) {
|
||||
final s = data?.text;
|
||||
if (s is String) {
|
||||
onInsert(s);
|
||||
}
|
||||
});
|
||||
};
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
if (oldWidget != this) {
|
||||
widget.beginFocus.setListener(() => requestBeginFocus());
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.beginFocus.removeAllListener();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void requestBeginFocus();
|
||||
|
||||
String? onCopy() => null;
|
||||
|
||||
void onInsert(String value) {}
|
||||
}
|
||||
|
||||
abstract class GridFocusNodeCellState<T extends GridCellWidget> extends GridCellState<T> {
|
||||
SingleListenrFocusNode focusNode = SingleListenrFocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.shortcutHandlers[CellKeyboardKey.onEnter] = () => focusNode.unfocus();
|
||||
_listenOnFocusNodeChanged();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant T oldWidget) {
|
||||
if (oldWidget != this) {
|
||||
_listenOnFocusNodeChanged();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.shortcutHandlers.clear();
|
||||
focusNode.removeAllListener();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
if (focusNode.hasFocus == false && focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(focusNode);
|
||||
}
|
||||
}
|
||||
|
||||
void _listenOnFocusNodeChanged() {
|
||||
widget.onCellEditing.value = focusNode.hasFocus;
|
||||
focusNode.setListener(() {
|
||||
widget.onCellEditing.value = focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {}
|
||||
}
|
||||
|
||||
class GridCellFocusListener extends ChangeNotifier {
|
||||
VoidCallback? _listener;
|
||||
|
||||
void setListener(VoidCallback listener) {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
|
||||
_listener = listener;
|
||||
super.addListener(listener);
|
||||
addListener(listener);
|
||||
}
|
||||
|
||||
void removeAllListener() {
|
||||
@ -81,10 +186,10 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
|
||||
|
||||
abstract class GridCellStyle {}
|
||||
|
||||
class CellSingleFocusNode extends FocusNode {
|
||||
class SingleListenrFocusNode extends FocusNode {
|
||||
VoidCallback? _listener;
|
||||
|
||||
void setSingleListener(VoidCallback listener) {
|
||||
void setListener(VoidCallback listener) {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
@ -93,120 +198,9 @@ class CellSingleFocusNode extends FocusNode {
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
void removeSingleListener() {
|
||||
void removeAllListener() {
|
||||
if (_listener != null) {
|
||||
removeListener(_listener!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellStateNotifier extends ChangeNotifier {
|
||||
bool _isFocus = false;
|
||||
bool _onEnter = false;
|
||||
|
||||
set isFocus(bool value) {
|
||||
if (_isFocus != value) {
|
||||
_isFocus = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get isFocus => _isFocus;
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
||||
|
||||
class CellContainer extends StatelessWidget {
|
||||
final GridCellWidget child;
|
||||
final Widget? expander;
|
||||
final double width;
|
||||
final RegionStateNotifier rowStateNotifier;
|
||||
const CellContainer({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.width,
|
||||
required this.rowStateNotifier,
|
||||
this.expander,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
|
||||
create: (_) => CellStateNotifier(),
|
||||
update: (_, row, cell) => cell!..onEnter = row.onEnter,
|
||||
child: Selector<CellStateNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.isFocus,
|
||||
builder: (context, isFocus, _) {
|
||||
Widget container = Center(child: child);
|
||||
child.onFocus.addListener(() {
|
||||
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
|
||||
});
|
||||
|
||||
if (expander != null) {
|
||||
container = CellEnterRegion(child: container, expander: expander!);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => child.requestFocus.notify(),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isFocus),
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
if (isFocus) {
|
||||
final borderSide = BorderSide(color: theme.main1, width: 1.0);
|
||||
return BoxDecoration(border: Border.fromBorderSide(borderSide));
|
||||
} else {
|
||||
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellEnterRegion extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Widget expander;
|
||||
const CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<CellStateNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.onEnter,
|
||||
builder: (context, onEnter, _) {
|
||||
List<Widget> children = [child];
|
||||
if (onEnter) {
|
||||
children.add(expander.positioned(right: 0));
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
|
||||
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
// alignment: AlignmentDirectional.centerEnd,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,140 @@
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import 'cell_accessory.dart';
|
||||
import 'cell_builder.dart';
|
||||
import 'cell_shortcuts.dart';
|
||||
|
||||
class CellContainer extends StatelessWidget {
|
||||
final GridCellWidget child;
|
||||
final AccessoryBuilder? accessoryBuilder;
|
||||
final double width;
|
||||
final RegionStateNotifier rowStateNotifier;
|
||||
const CellContainer({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.width,
|
||||
required this.rowStateNotifier,
|
||||
this.accessoryBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<RegionStateNotifier, CellContainerNotifier>(
|
||||
create: (_) => CellContainerNotifier(child),
|
||||
update: (_, rowStateNotifier, cellStateNotifier) => cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
|
||||
child: Selector<CellContainerNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.isFocus,
|
||||
builder: (context, isFocus, _) {
|
||||
Widget container = Center(child: GridCellShortcuts(child: child));
|
||||
|
||||
if (accessoryBuilder != null) {
|
||||
final accessories = accessoryBuilder!(GridCellAccessoryBuildContext(
|
||||
anchorContext: context,
|
||||
isCellEditing: isFocus,
|
||||
));
|
||||
|
||||
if (accessories.isNotEmpty) {
|
||||
container = CellEnterRegion(child: container, accessories: accessories);
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => child.beginFocus.notify(),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: width, minHeight: 46),
|
||||
decoration: _makeBoxDecoration(context, isFocus),
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: container,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
if (isFocus) {
|
||||
final borderSide = BorderSide(color: theme.main1, width: 1.0);
|
||||
return BoxDecoration(border: Border.fromBorderSide(borderSide));
|
||||
} else {
|
||||
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellEnterRegion extends StatelessWidget {
|
||||
final Widget child;
|
||||
final List<GridCellAccessory> accessories;
|
||||
const CellEnterRegion({required this.child, required this.accessories, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<CellContainerNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.onEnter,
|
||||
builder: (context, onEnter, _) {
|
||||
List<Widget> children = [child];
|
||||
if (onEnter) {
|
||||
children.add(CellAccessoryContainer(accessories: accessories).positioned(right: 0));
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (p) => Provider.of<CellContainerNotifier>(context, listen: false).onEnter = true,
|
||||
onExit: (p) => Provider.of<CellContainerNotifier>(context, listen: false).onEnter = false,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CellContainerNotifier extends ChangeNotifier {
|
||||
final CellEditable cellEditable;
|
||||
bool mouted = false;
|
||||
VoidCallback? _onCellFocusListener;
|
||||
bool _isFocus = false;
|
||||
bool _onEnter = false;
|
||||
|
||||
CellContainerNotifier(this.cellEditable) {
|
||||
_onCellFocusListener = () => isFocus = cellEditable.onCellFocus.value;
|
||||
cellEditable.onCellFocus.addListener(_onCellFocusListener!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_onCellFocusListener != null) {
|
||||
cellEditable.onCellFocus.removeListener(_onCellFocusListener!);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
set isFocus(bool value) {
|
||||
if (_isFocus != value) {
|
||||
_isFocus = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
set onEnter(bool value) {
|
||||
if (_onEnter != value) {
|
||||
_onEnter = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get isFocus => _isFocus;
|
||||
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef CellKeyboardAction = dynamic Function();
|
||||
|
||||
enum CellKeyboardKey {
|
||||
onEnter,
|
||||
onCopy,
|
||||
onInsert,
|
||||
}
|
||||
|
||||
abstract class CellShortcuts extends Widget {
|
||||
const CellShortcuts({Key? key}) : super(key: key);
|
||||
|
||||
Map<CellKeyboardKey, CellKeyboardAction> get shortcutHandlers;
|
||||
}
|
||||
|
||||
class GridCellShortcuts extends StatelessWidget {
|
||||
final CellShortcuts child;
|
||||
const GridCellShortcuts({required this.child, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyC): const GridCellCopyIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyV): const GridCellInsertIntent(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: {
|
||||
GridCellEnterIdent: GridCellEnterAction(child: child),
|
||||
GridCellCopyIntent: GridCellCopyAction(child: child),
|
||||
GridCellInsertIntent: GridCellInsertAction(child: child),
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellEnterIdent extends Intent {
|
||||
const GridCellEnterIdent();
|
||||
}
|
||||
|
||||
class GridCellEnterAction extends Action<GridCellEnterIdent> {
|
||||
final CellShortcuts child;
|
||||
GridCellEnterAction({required this.child});
|
||||
|
||||
@override
|
||||
void invoke(covariant GridCellEnterIdent intent) {
|
||||
final callback = child.shortcutHandlers[CellKeyboardKey.onEnter];
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellCopyIntent extends Intent {
|
||||
const GridCellCopyIntent();
|
||||
}
|
||||
|
||||
class GridCellCopyAction extends Action<GridCellCopyIntent> {
|
||||
final CellShortcuts child;
|
||||
GridCellCopyAction({required this.child});
|
||||
|
||||
@override
|
||||
void invoke(covariant GridCellCopyIntent intent) {
|
||||
final callback = child.shortcutHandlers[CellKeyboardKey.onCopy];
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final s = callback();
|
||||
if (s is String) {
|
||||
Clipboard.setData(ClipboardData(text: s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GridCellInsertIntent extends Intent {
|
||||
const GridCellInsertIntent();
|
||||
}
|
||||
|
||||
class GridCellInsertAction extends Action<GridCellInsertIntent> {
|
||||
final CellShortcuts child;
|
||||
GridCellInsertAction({required this.child});
|
||||
|
||||
@override
|
||||
void invoke(covariant GridCellInsertIntent intent) {
|
||||
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'cell_builder.dart';
|
||||
|
||||
class CheckboxCell extends StatefulWidget with GridCellWidget {
|
||||
class CheckboxCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
CheckboxCell({
|
||||
required this.cellContextBuilder,
|
||||
@ -14,17 +14,16 @@ class CheckboxCell extends StatefulWidget with GridCellWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CheckboxCell> createState() => _CheckboxCellState();
|
||||
GridCellState<CheckboxCell> createState() => _CheckboxCellState();
|
||||
}
|
||||
|
||||
class _CheckboxCellState extends State<CheckboxCell> {
|
||||
class _CheckboxCellState extends GridCellState<CheckboxCell> {
|
||||
late CheckboxCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build();
|
||||
_cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
|
||||
_listenCellRequestFocus();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -49,22 +48,23 @@ class _CheckboxCellState extends State<CheckboxCell> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CheckboxCell oldWidget) {
|
||||
_listenCellRequestFocus();
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _listenCellRequestFocus() {
|
||||
widget.requestFocus.addListener(() {
|
||||
_cellBloc.add(const CheckboxCellEvent.select());
|
||||
});
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
_cellBloc.add(const CheckboxCellEvent.select());
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() {
|
||||
if (_cellBloc.state.isSelected) {
|
||||
return "Yes";
|
||||
} else {
|
||||
return "No";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ abstract class GridCellDelegate {
|
||||
GridCellDelegate get delegate;
|
||||
}
|
||||
|
||||
class DateCell extends StatefulWidget with GridCellWidget {
|
||||
class DateCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final DateCellStyle? cellStyle;
|
||||
|
||||
@ -35,10 +35,10 @@ class DateCell extends StatefulWidget with GridCellWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
State<DateCell> createState() => _DateCellState();
|
||||
GridCellState<DateCell> createState() => _DateCellState();
|
||||
}
|
||||
|
||||
class _DateCellState extends State<DateCell> {
|
||||
class _DateCellState extends GridCellState<DateCell> {
|
||||
late DateCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
@ -64,7 +64,7 @@ class _DateCellState extends State<DateCell> {
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: FlowyText.medium(state.data.foldRight("", (data, _) => data.date), fontSize: 12),
|
||||
child: FlowyText.medium(state.dateStr, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -76,8 +76,8 @@ class _DateCellState extends State<DateCell> {
|
||||
|
||||
void _showCalendar(BuildContext context) {
|
||||
final bloc = context.read<DateCellBloc>();
|
||||
widget.onFocus.value = true;
|
||||
final calendar = DateCellEditor(onDismissed: () => widget.onFocus.value = false);
|
||||
widget.onCellEditing.value = true;
|
||||
final calendar = DateCellEditor(onDismissed: () => widget.onCellEditing.value = false);
|
||||
calendar.show(
|
||||
context,
|
||||
cellContext: bloc.cellContext.clone(),
|
||||
@ -89,4 +89,10 @@ class _DateCellState extends State<DateCell> {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() {}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.dateStr;
|
||||
}
|
||||
|
@ -160,18 +160,21 @@ class _CellCalendarWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
selectedDayPredicate: (day) {
|
||||
return state.dateData.fold(
|
||||
return state.calData.fold(
|
||||
() => false,
|
||||
(dateData) => isSameDay(dateData.date, day),
|
||||
);
|
||||
},
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
context.read<DateCalBloc>().add(DateCalEvent.selectDay(selectedDay));
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
context.read<DateCalBloc>().add(DateCalEvent.setCalFormat(format));
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
context.read<DateCalBloc>().add(DateCalEvent.setFocusedDay(focusedDay));
|
||||
},
|
||||
);
|
||||
@ -234,6 +237,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
if (widget.bloc.state.dateTypeOption.includeTime) {
|
||||
_focusNode.addListener(() {
|
||||
if (mounted) {
|
||||
_CalDateTimeSetting.hide(context);
|
||||
widget.bloc.add(DateCalEvent.setTime(_controller.text));
|
||||
}
|
||||
});
|
||||
@ -257,6 +261,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
child: RoundedInputField(
|
||||
height: 40,
|
||||
focusNode: _focusNode,
|
||||
hintText: state.timeHintText,
|
||||
controller: _controller,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
normalBorderColor: theme.shader4,
|
||||
@ -326,6 +331,7 @@ class _CalDateTimeSetting extends StatefulWidget {
|
||||
}
|
||||
|
||||
void show(BuildContext context) {
|
||||
hide(context);
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
child: this,
|
||||
@ -337,6 +343,10 @@ class _CalDateTimeSetting extends StatefulWidget {
|
||||
anchorOffset: const Offset(20, 0),
|
||||
);
|
||||
}
|
||||
|
||||
static void hide(BuildContext context) {
|
||||
FlowyOverlay.of(context).remove(identifier());
|
||||
}
|
||||
}
|
||||
|
||||
class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -7,7 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'cell_builder.dart';
|
||||
|
||||
class NumberCell extends StatefulWidget with GridCellWidget {
|
||||
class NumberCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
|
||||
NumberCell({
|
||||
@ -16,101 +15,79 @@ class NumberCell extends StatefulWidget with GridCellWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NumberCell> createState() => _NumberCellState();
|
||||
GridFocusNodeCellState<NumberCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCellState extends State<NumberCell> {
|
||||
class _NumberCellState extends GridFocusNodeCellState<NumberCell> {
|
||||
late NumberCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellContext = widget.cellContextBuilder.build();
|
||||
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = CellSingleFocusNode();
|
||||
_listenFocusNode();
|
||||
_controller = TextEditingController(text: contentFromState(_cellBloc.state));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_listenCellRequestFocus(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocConsumer<NumberCellBloc, NumberCellState>(
|
||||
listener: (context, state) {
|
||||
if (_controller.text != state.content) {
|
||||
_controller.text = state.content;
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<NumberCellBloc, NumberCellState>(
|
||||
listenWhen: (p, c) => p.content != c.content,
|
||||
listener: (context, state) => _controller.text = contentFromState(state),
|
||||
),
|
||||
],
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant NumberCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 300), () {
|
||||
if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
|
||||
final number = num.tryParse(_controller.text);
|
||||
if (number != null) {
|
||||
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
|
||||
} else {
|
||||
_controller.text = "";
|
||||
}
|
||||
if (_cellBloc.isClosed == false && _controller.text != contentFromState(_cellBloc.state)) {
|
||||
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _listenFocusNode() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
_focusNode.setSingleListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
String contentFromState(NumberCellState state) {
|
||||
return state.content.fold((l) => l, (r) => "");
|
||||
}
|
||||
|
||||
void _listenCellRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
});
|
||||
@override
|
||||
String? onCopy() {
|
||||
return _cellBloc.state.content.fold((content) => content, (r) => null);
|
||||
}
|
||||
|
||||
@override
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(NumberCellEvent.updateCell(value));
|
||||
}
|
||||
}
|
||||
|
@ -64,9 +64,11 @@ class SelectOptionTag extends StatelessWidget {
|
||||
final String name;
|
||||
final Color color;
|
||||
final bool isSelected;
|
||||
final VoidCallback? onSelected;
|
||||
const SelectOptionTag({
|
||||
required this.name,
|
||||
required this.color,
|
||||
this.onSelected,
|
||||
this.isSelected = false,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@ -74,12 +76,14 @@ class SelectOptionTag extends StatelessWidget {
|
||||
factory SelectOptionTag.fromSelectOption({
|
||||
required BuildContext context,
|
||||
required SelectOption option,
|
||||
VoidCallback? onSelected,
|
||||
bool isSelected = false,
|
||||
}) {
|
||||
return SelectOptionTag(
|
||||
name: option.name,
|
||||
color: option.color.make(context),
|
||||
isSelected: isSelected,
|
||||
onSelected: onSelected,
|
||||
);
|
||||
}
|
||||
|
||||
@ -92,19 +96,12 @@ class SelectOptionTag extends StatelessWidget {
|
||||
backgroundColor: color,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
selected: true,
|
||||
onSelected: (_) {},
|
||||
onSelected: (_) {
|
||||
if (onSelected != null) {
|
||||
onSelected!();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// return Container(
|
||||
// decoration: BoxDecoration(
|
||||
// color: option.color.make(context),
|
||||
// shape: BoxShape.rectangle,
|
||||
// borderRadius: BorderRadius.circular(8.0),
|
||||
// ),
|
||||
// child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
|
||||
// margin: const EdgeInsets.symmetric(horizontal: 3.0),
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +133,11 @@ class SelectOptionTagCell extends StatelessWidget {
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
flex: 2,
|
||||
child: SelectOptionTag.fromSelectOption(context: context, option: option),
|
||||
child: SelectOptionTag.fromSelectOption(
|
||||
context: context,
|
||||
option: option,
|
||||
onSelected: () => onSelected(option),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
...children,
|
||||
|
@ -20,7 +20,7 @@ class SelectOptionCellStyle extends GridCellStyle {
|
||||
});
|
||||
}
|
||||
|
||||
class SingleSelectCell extends StatefulWidget with GridCellWidget {
|
||||
class SingleSelectCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final SelectOptionCellStyle? cellStyle;
|
||||
|
||||
@ -59,7 +59,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||
return _SelectOptionCell(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onFocus: (value) => widget.onFocus.value = value,
|
||||
onFocus: (value) => widget.onCellEditing.value = value,
|
||||
cellContextBuilder: widget.cellContextBuilder);
|
||||
},
|
||||
),
|
||||
@ -74,7 +74,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------
|
||||
class MultiSelectCell extends StatefulWidget with GridCellWidget {
|
||||
class MultiSelectCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final SelectOptionCellStyle? cellStyle;
|
||||
|
||||
@ -113,7 +113,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||
return _SelectOptionCell(
|
||||
selectOptions: state.selectedOptions,
|
||||
cellStyle: widget.cellStyle,
|
||||
onFocus: (value) => widget.onFocus.value = value,
|
||||
onFocus: (value) => widget.onCellEditing.value = value,
|
||||
cellContextBuilder: widget.cellContextBuilder);
|
||||
},
|
||||
),
|
||||
|
@ -156,6 +156,7 @@ class _TextField extends StatelessWidget {
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPannelWidth * 0.7,
|
||||
tagController: _tagController,
|
||||
onClick: () => FlowyOverlay.of(context).remove(SelectOptionTypeOptionEditor.identifier),
|
||||
newText: (text) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
@ -207,6 +208,7 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
SelectOptionTag(
|
||||
name: name,
|
||||
color: theme.shader6,
|
||||
onSelected: () => context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.newOption(name)),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -233,7 +235,11 @@ class _SelectOptionCell extends StatelessWidget {
|
||||
context.read<SelectOptionCellEditorBloc>().add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
},
|
||||
children: [
|
||||
if (isSelected) svgWidget("grid/checkmark"),
|
||||
if (isSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: svgWidget("grid/checkmark"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -22,6 +22,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
|
||||
final Function(String) onNewTag;
|
||||
final Function(String) newText;
|
||||
final VoidCallback? onClick;
|
||||
|
||||
SelectOptionTextField({
|
||||
required this.options,
|
||||
@ -30,6 +31,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
required this.tagController,
|
||||
required this.onNewTag,
|
||||
required this.newText,
|
||||
this.onClick,
|
||||
TextEditingController? controller,
|
||||
FocusNode? focusNode,
|
||||
Key? key,
|
||||
@ -53,6 +55,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
autofocus: true,
|
||||
controller: editController,
|
||||
focusNode: focusNode,
|
||||
onTap: onClick,
|
||||
onChanged: (text) {
|
||||
if (onChanged != null) {
|
||||
onChanged(text);
|
||||
|
@ -13,7 +13,7 @@ class GridTextCellStyle extends GridCellStyle {
|
||||
});
|
||||
}
|
||||
|
||||
class GridTextCell extends StatefulWidget with GridCellWidget {
|
||||
class GridTextCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final GridTextCellStyle? cellStyle;
|
||||
GridTextCell({
|
||||
@ -29,13 +29,12 @@ class GridTextCell extends StatefulWidget with GridCellWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
State<GridTextCell> createState() => _GridTextCellState();
|
||||
GridFocusNodeCellState<GridTextCell> createState() => _GridTextCellState();
|
||||
}
|
||||
|
||||
class _GridTextCellState extends State<GridTextCell> {
|
||||
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
late TextCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
late CellSingleFocusNode _focusNode;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
@ -44,10 +43,6 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
_cellBloc = getIt<TextCellBloc>(param1: cellContext);
|
||||
_cellBloc.add(const TextCellEvent.initial());
|
||||
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||
_focusNode = CellSingleFocusNode();
|
||||
|
||||
_listenFocusNode();
|
||||
_listenRequestFocus(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -63,9 +58,9 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
focusNode: focusNode,
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => _focusNode.unfocus(),
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
@ -81,39 +76,12 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_delayOperation?.cancel();
|
||||
_cellBloc.close();
|
||||
_focusNode.removeSingleListener();
|
||||
_focusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant GridTextCell oldWidget) {
|
||||
if (oldWidget != widget) {
|
||||
_listenFocusNode();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
void _listenFocusNode() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
_focusNode.setSingleListener(() {
|
||||
widget.onFocus.value = _focusNode.hasFocus;
|
||||
focusChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void _listenRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
@ -124,4 +92,12 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.content;
|
||||
|
||||
@override
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(TextCellEvent.updateText(value));
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class URLCellEditor extends StatefulWidget {
|
||||
class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
|
||||
final GridURLCellContext cellContext;
|
||||
const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
|
||||
final VoidCallback completed;
|
||||
const URLCellEditor({required this.cellContext, required this.completed, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<URLCellEditor> createState() => _URLCellEditorState();
|
||||
@ -16,27 +17,43 @@ class URLCellEditor extends StatefulWidget {
|
||||
static void show(
|
||||
BuildContext context,
|
||||
GridURLCellContext cellContext,
|
||||
VoidCallback completed,
|
||||
) {
|
||||
FlowyOverlay.of(context).remove(identifier());
|
||||
final editor = URLCellEditor(
|
||||
cellContext: cellContext,
|
||||
completed: completed,
|
||||
);
|
||||
|
||||
//
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
child: SizedBox(width: 200, child: editor),
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: Padding(padding: const EdgeInsets.all(6), child: editor),
|
||||
),
|
||||
constraints: BoxConstraints.loose(const Size(300, 160)),
|
||||
),
|
||||
identifier: URLCellEditor.identifier(),
|
||||
anchorContext: context,
|
||||
anchorDirection: AnchorDirection.bottomWithCenterAligned,
|
||||
delegate: editor,
|
||||
);
|
||||
}
|
||||
|
||||
static String identifier() {
|
||||
return (URLCellEditor).toString();
|
||||
}
|
||||
|
||||
@override
|
||||
bool asBarrier() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didRemove() {
|
||||
completed();
|
||||
}
|
||||
}
|
||||
|
||||
class _URLCellEditorState extends State<URLCellEditor> {
|
||||
|
@ -1,10 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@ -14,12 +17,20 @@ import 'cell_editor.dart';
|
||||
class GridURLCellStyle extends GridCellStyle {
|
||||
String? placeholder;
|
||||
|
||||
List<GridURLCellAccessoryType> accessoryTypes;
|
||||
|
||||
GridURLCellStyle({
|
||||
this.placeholder,
|
||||
this.accessoryTypes = const [],
|
||||
});
|
||||
}
|
||||
|
||||
class GridURLCell extends StatefulWidget with GridCellWidget {
|
||||
enum GridURLCellAccessoryType {
|
||||
edit,
|
||||
copyURL,
|
||||
}
|
||||
|
||||
class GridURLCell extends GridCellWidget {
|
||||
final GridCellContextBuilder cellContextBuilder;
|
||||
late final GridURLCellStyle? cellStyle;
|
||||
GridURLCell({
|
||||
@ -35,10 +46,39 @@ class GridURLCell extends StatefulWidget with GridCellWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
State<GridURLCell> createState() => _GridURLCellState();
|
||||
GridCellState<GridURLCell> createState() => _GridURLCellState();
|
||||
|
||||
GridCellAccessory accessoryFromType(GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
|
||||
switch (ty) {
|
||||
case GridURLCellAccessoryType.edit:
|
||||
final cellContext = cellContextBuilder.build() as GridURLCellContext;
|
||||
return _EditURLAccessory(cellContext: cellContext, anchorContext: buildContext.anchorContext);
|
||||
|
||||
case GridURLCellAccessoryType.copyURL:
|
||||
final cellContext = cellContextBuilder.build() as GridURLCellContext;
|
||||
return _CopyURLAccessory(cellContext: cellContext);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext) get accessoryBuilder => (buildContext) {
|
||||
final List<GridCellAccessory> accessories = [];
|
||||
if (cellStyle != null) {
|
||||
accessories.addAll(cellStyle!.accessoryTypes.map((ty) {
|
||||
return accessoryFromType(ty, buildContext);
|
||||
}));
|
||||
}
|
||||
|
||||
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
|
||||
if (accessories.isEmpty) {
|
||||
accessories.add(accessoryFromType(GridURLCellAccessoryType.edit, buildContext));
|
||||
}
|
||||
|
||||
return accessories;
|
||||
};
|
||||
}
|
||||
|
||||
class _GridURLCellState extends State<GridURLCell> {
|
||||
class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
late URLCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
@ -46,7 +86,6 @@ class _GridURLCellState extends State<GridURLCell> {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
_cellBloc = URLCellBloc(cellContext: cellContext);
|
||||
_cellBloc.add(const URLCellEvent.initial());
|
||||
_listenRequestFocus(context);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -66,14 +105,17 @@ class _GridURLCellState extends State<GridURLCell> {
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: _tapGesture(context),
|
||||
),
|
||||
);
|
||||
|
||||
return CellEnterRegion(
|
||||
return SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
child: Align(alignment: Alignment.centerLeft, child: richText),
|
||||
expander: _EditCellIndicator(onTap: () {}),
|
||||
);
|
||||
onTap: () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -81,51 +123,72 @@ class _GridURLCellState extends State<GridURLCell> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
widget.requestFocus.removeAllListener();
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
TapGestureRecognizer _tapGesture(BuildContext context) {
|
||||
final gesture = TapGestureRecognizer();
|
||||
gesture.onTap = () async {
|
||||
final url = context.read<URLCellBloc>().state.url;
|
||||
await _openUrlOrEdit(url);
|
||||
};
|
||||
return gesture;
|
||||
}
|
||||
|
||||
Future<void> _openUrlOrEdit(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (url.isNotEmpty && await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
|
||||
URLCellEditor.show(context, cellContext);
|
||||
widget.onCellEditing.value = true;
|
||||
URLCellEditor.show(context, cellContext, () {
|
||||
widget.onCellEditing.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _listenRequestFocus(BuildContext context) {
|
||||
widget.requestFocus.addListener(() {
|
||||
_openUrlOrEdit(_cellBloc.state.url);
|
||||
});
|
||||
@override
|
||||
void requestBeginFocus() {
|
||||
_openUrlOrEdit(_cellBloc.state.url);
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.content;
|
||||
|
||||
@override
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(URLCellEvent.updateURL(value));
|
||||
}
|
||||
}
|
||||
|
||||
class _EditCellIndicator extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const _EditCellIndicator({required this.onTap, Key? key}) : super(key: key);
|
||||
class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
|
||||
final GridURLCellContext cellContext;
|
||||
final BuildContext anchorContext;
|
||||
const _EditURLAccessory({
|
||||
required this.cellContext,
|
||||
required this.anchorContext,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FlowyIconButton(
|
||||
width: 26,
|
||||
onPressed: onTap,
|
||||
hoverColor: theme.hover,
|
||||
radius: BorderRadius.circular(4),
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
icon: svgWidget("editor/edit", color: theme.iconColor),
|
||||
);
|
||||
return svgWidget("editor/edit", color: theme.iconColor);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
URLCellEditor.show(anchorContext, cellContext, () {});
|
||||
}
|
||||
}
|
||||
|
||||
class _CopyURLAccessory extends StatelessWidget with GridCellAccessory {
|
||||
final GridURLCellContext cellContext;
|
||||
const _CopyURLAccessory({required this.cellContext, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return svgWidget("editor/copy", color: theme.iconColor);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
final content = cellContext.getCellData(loadIfNoCache: false)?.content ?? "";
|
||||
Clipboard.setData(ClipboardData(text: content));
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show Field;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -24,6 +23,7 @@ class GridFieldCell extends StatelessWidget {
|
||||
return BlocProvider(
|
||||
create: (context) => FieldCellBloc(cellContext: cellContext)..add(const FieldCellEvent.initial()),
|
||||
child: BlocBuilder<FieldCellBloc, FieldCellState>(
|
||||
// buildWhen: (p, c) => p.field != c.field,
|
||||
builder: (context, state) {
|
||||
final button = FieldCellButton(
|
||||
field: state.field,
|
||||
@ -38,7 +38,7 @@ class GridFieldCell extends StatelessWidget {
|
||||
);
|
||||
|
||||
return _GridHeaderCellContainer(
|
||||
width: state.field.width.toDouble(),
|
||||
width: state.width,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
fit: StackFit.expand,
|
||||
@ -60,13 +60,14 @@ class GridFieldCell extends StatelessWidget {
|
||||
|
||||
void _showFieldEditor(BuildContext context) {
|
||||
final state = context.read<FieldCellBloc>().state;
|
||||
final field = state.field;
|
||||
|
||||
FieldEditor(
|
||||
gridId: state.gridId,
|
||||
fieldName: state.field.name,
|
||||
fieldName: field.name,
|
||||
contextLoader: FieldContextLoader(
|
||||
gridId: state.gridId,
|
||||
field: state.field,
|
||||
field: field,
|
||||
),
|
||||
).show(context);
|
||||
}
|
||||
@ -84,7 +85,7 @@ class _GridHeaderCellContainer extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
|
||||
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||
final decoration = BoxDecoration(
|
||||
border: Border(
|
||||
top: borderSide,
|
||||
@ -113,21 +114,19 @@ class _DragToExpandLine extends StatelessWidget {
|
||||
onTap: () {},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragCancel: () {},
|
||||
onHorizontalDragUpdate: (value) {
|
||||
// context.read<FieldCellBloc>().add(FieldCellEvent.updateWidth(value.delta.dx));
|
||||
Log.info(value);
|
||||
context.read<FieldCellBloc>().add(FieldCellEvent.startUpdateWidth(value.delta.dx));
|
||||
},
|
||||
onHorizontalDragEnd: (end) {
|
||||
Log.info(end);
|
||||
context.read<FieldCellBloc>().add(const FieldCellEvent.endUpdateWidth());
|
||||
},
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(
|
||||
hoverColor: theme.main1,
|
||||
borderRadius: BorderRadius.zero,
|
||||
contentMargin: const EdgeInsets.only(left: 5),
|
||||
contentMargin: const EdgeInsets.only(left: 6),
|
||||
),
|
||||
child: const SizedBox(width: 2),
|
||||
child: const SizedBox(width: 4),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/format.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide NumberFormat;
|
||||
|
@ -25,6 +25,8 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
static String get identifier => (SelectOptionTypeOptionEditor).toString();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_cotainer.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
@ -170,16 +172,29 @@ class _RowCells extends StatelessWidget {
|
||||
List<Widget> _makeCells(BuildContext context, GridCellMap gridCellMap) {
|
||||
return gridCellMap.values.map(
|
||||
(gridCell) {
|
||||
Widget? expander;
|
||||
if (gridCell.field.isPrimary) {
|
||||
expander = _CellExpander(onExpand: onExpand);
|
||||
final GridCellWidget child = buildGridCellWidget(gridCell, cellCache);
|
||||
|
||||
accessoryBuilder(GridCellAccessoryBuildContext buildContext) {
|
||||
final builder = child.accessoryBuilder;
|
||||
List<GridCellAccessory> accessories = [];
|
||||
if (gridCell.field.isPrimary) {
|
||||
accessories.add(PrimaryCellAccessory(
|
||||
onTapCallback: onExpand,
|
||||
isCellEditing: buildContext.isCellEditing,
|
||||
));
|
||||
}
|
||||
|
||||
if (builder != null) {
|
||||
accessories.addAll(builder(buildContext));
|
||||
}
|
||||
return accessories;
|
||||
}
|
||||
|
||||
return CellContainer(
|
||||
width: gridCell.field.width.toDouble(),
|
||||
child: buildGridCellWidget(gridCell, cellCache),
|
||||
child: child,
|
||||
rowStateNotifier: Provider.of<RegionStateNotifier>(context, listen: false),
|
||||
expander: expander,
|
||||
accessoryBuilder: accessoryBuilder,
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
@ -199,26 +214,6 @@ class RegionStateNotifier extends ChangeNotifier {
|
||||
bool get onEnter => _onEnter;
|
||||
}
|
||||
|
||||
class _CellExpander extends StatelessWidget {
|
||||
final VoidCallback onExpand;
|
||||
const _CellExpander({required this.onExpand, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: FlowyIconButton(
|
||||
width: 26,
|
||||
onPressed: onExpand,
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
radius: BorderRadius.circular(4),
|
||||
icon: svgWidget("grid/expander", color: theme.main1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RowEnterRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
const _RowEnterRegion({required this.child, Key? key}) : super(key: key);
|
||||
|
@ -1,44 +0,0 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/prelude.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class NumberCell extends StatefulWidget {
|
||||
final GridCell cellData;
|
||||
|
||||
const NumberCell({
|
||||
required this.cellData,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<NumberCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCellState extends State<NumberCell> {
|
||||
late NumberCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_cellBloc = getIt<NumberCellBloc>(param1: widget.cellData);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<NumberCellBloc, NumberCellState>(
|
||||
builder: (context, state) {
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ class GridRowActionSheet extends StatelessWidget {
|
||||
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
|
||||
builder: (context, state) {
|
||||
final cells = _RowAction.values
|
||||
.where((value) => value.enable())
|
||||
.map(
|
||||
(action) => _RowActionCell(
|
||||
action: action,
|
||||
|
@ -3,6 +3,7 @@ import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart';
|
||||
@ -10,7 +11,6 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -149,12 +149,18 @@ class _RowDetailCell extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final style = _customCellStyle(theme, gridCell.field.fieldType);
|
||||
final cell = buildGridCellWidget(gridCell, cellCache, style: style);
|
||||
|
||||
final cell = buildGridCellWidget(
|
||||
gridCell,
|
||||
cellCache,
|
||||
style: _buildCellStyle(theme, gridCell.field.fieldType),
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => cell.beginFocus.notify(),
|
||||
child: AccessoryHover(
|
||||
child: cell,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
),
|
||||
);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 40),
|
||||
child: IntrinsicHeight(
|
||||
@ -167,12 +173,7 @@ class _RowDetailCell extends StatelessWidget {
|
||||
child: FieldCellButton(field: gridCell.field, onTap: () => _showFieldEditor(context)),
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(
|
||||
child: FlowyHover2(
|
||||
child: cell,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
),
|
||||
),
|
||||
Expanded(child: gesture),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -191,7 +192,7 @@ class _RowDetailCell extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
|
||||
GridCellStyle? _customCellStyle(AppTheme theme, FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return null;
|
||||
@ -217,7 +218,11 @@ GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
|
||||
case FieldType.URL:
|
||||
return GridURLCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
accessoryTypes: [
|
||||
GridURLCellAccessoryType.edit,
|
||||
GridURLCellAccessoryType.copyURL,
|
||||
],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class GridShortcuts extends StatelessWidget {
|
||||
final Widget child;
|
||||
const GridShortcuts({required this.child, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shortcuts(
|
||||
shortcuts: bindKeys([]),
|
||||
child: Actions(
|
||||
dispatcher: LoggingActionDispatcher(),
|
||||
actions: const {},
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {
|
||||
return {for (var key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
|
||||
}
|
||||
|
||||
Map<Type, Action<Intent>> bindActions() {
|
||||
return {
|
||||
KeyboardKeyIdent: KeyboardBindingAction(),
|
||||
};
|
||||
}
|
||||
|
||||
class KeyboardKeyIdent extends Intent {
|
||||
final KeyboardKey key;
|
||||
|
||||
const KeyboardKeyIdent(this.key);
|
||||
}
|
||||
|
||||
class KeyboardBindingAction extends Action<KeyboardKeyIdent> {
|
||||
KeyboardBindingAction();
|
||||
|
||||
@override
|
||||
void invoke(covariant KeyboardKeyIdent intent) {
|
||||
// print(intent);
|
||||
}
|
||||
}
|
||||
|
||||
class LoggingActionDispatcher extends ActionDispatcher {
|
||||
@override
|
||||
Object? invokeAction(
|
||||
covariant Action<Intent> action,
|
||||
covariant Intent intent, [
|
||||
BuildContext? context,
|
||||
]) {
|
||||
// print('Action invoked: $action($intent) from $context');
|
||||
super.invokeAction(action, intent, context);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ class GridSettingList extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _renderList() {
|
||||
final cells = GridSettingAction.values.map((action) {
|
||||
final cells = GridSettingAction.values.where((value) => value.enable()).map((action) {
|
||||
return _SettingItem(action: action);
|
||||
}).toList();
|
||||
|
||||
|
@ -27,7 +27,7 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
||||
bool _isToggled = false;
|
||||
// Style get _selectionStyle => widget.controller.getSelectionStyle();
|
||||
final GlobalKey emojiButtonKey = GlobalKey();
|
||||
OverlayEntry _entry = OverlayEntry(builder: (context) => Container());
|
||||
OverlayEntry? _entry;
|
||||
// final FocusNode _keyFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
@ -52,6 +52,12 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_entry?.remove();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// @override
|
||||
// void didUpdateWidget(covariant FlowyEmojiStyleButton oldWidget) {
|
||||
// super.didUpdateWidget(oldWidget);
|
||||
@ -77,8 +83,9 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
||||
// }
|
||||
|
||||
void _toggleAttribute() {
|
||||
if (_entry.mounted) {
|
||||
_entry.remove();
|
||||
if (_entry?.mounted ?? false) {
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
setState(() => _isToggled = false);
|
||||
} else {
|
||||
RenderBox box = emojiButtonKey.currentContext?.findRenderObject() as RenderBox;
|
||||
@ -93,7 +100,7 @@ class _EmojiStyleButtonState extends State<FlowyEmojiStyleButton> {
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context)!.insert(_entry);
|
||||
Overlay.of(context)!.insert(_entry!);
|
||||
setState(() => _isToggled = true);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:app_flowy/startup/tasks/rust_sdk.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
@ -16,7 +16,6 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
class QuestionBubble extends StatelessWidget {
|
||||
const QuestionBubble({Key? key}) : super(key: key);
|
||||
@ -46,7 +45,7 @@ class QuestionBubble extends StatelessWidget {
|
||||
_launchURL("https://discord.gg/9Q2xaN37tV");
|
||||
break;
|
||||
case BubbleAction.debug:
|
||||
const _DebugToast().show();
|
||||
_DebugToast().show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
@ -71,55 +70,14 @@ class QuestionBubble extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugToast extends StatelessWidget {
|
||||
const _DebugToast({Key? key}) : super(key: key);
|
||||
class _DebugToast {
|
||||
void show() async {
|
||||
var debugInfo = "";
|
||||
debugInfo += await _getDeviceInfo();
|
||||
debugInfo += await _getDocumentPath();
|
||||
Clipboard.setData(ClipboardData(text: debugInfo));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: Future(() async {
|
||||
var debugInfo = "";
|
||||
debugInfo += await _getDeviceInfo();
|
||||
debugInfo += await _getDocumentPath();
|
||||
|
||||
Clipboard.setData(ClipboardData(text: debugInfo));
|
||||
}),
|
||||
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
return _done(context, Text("Error: ${snapshot.error}"));
|
||||
} else {
|
||||
return _done(context, null);
|
||||
}
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _done(BuildContext context, Widget? error) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(25.0), color: theme.main1),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.check),
|
||||
const SizedBox(width: 12.0),
|
||||
(error == null) ? Text(LocaleKeys.questionBubble_debug_success.tr()) : error
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void show() {
|
||||
fToast.showToast(
|
||||
child: this,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastDuration: const Duration(seconds: 3),
|
||||
);
|
||||
showMessageToast(LocaleKeys.questionBubble_debug_success.tr());
|
||||
}
|
||||
|
||||
Future<String> _getDeviceInfo() async {
|
||||
|
@ -26,6 +26,23 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
def build_specify_archs_only
|
||||
if ENV.has_key?('BUILD_ARCHS')
|
||||
xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj'
|
||||
project = Xcodeproj::Project.open(xcodeproj_path)
|
||||
project.targets.each do |target|
|
||||
if target.name == 'Runner'
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['ARCHS'] = ENV['BUILD_ARCHS']
|
||||
end
|
||||
end
|
||||
end
|
||||
project.save()
|
||||
end
|
||||
end
|
||||
|
||||
build_specify_archs_only()
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
@ -421,7 +421,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
EXCLUDED_ARCHS = arm64;
|
||||
EXCLUDED_ARCHS = "";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -553,7 +553,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
EXCLUDED_ARCHS = arm64;
|
||||
EXCLUDED_ARCHS = "";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -577,7 +577,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
EXCLUDED_ARCHS = arm64;
|
||||
EXCLUDED_ARCHS = "";
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -71,7 +71,7 @@ class TextStyles {
|
||||
static TextStyle get CalloutFocus => Callout.bold;
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
static TextStyle get Btn => quicksand.bold.size(FontSizes.s14).letterSpace(1.75);
|
||||
static TextStyle get Btn => quicksand.bold.size(FontSizes.s16).letterSpace(1.75);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
static TextStyle get BtnSelected => quicksand.size(FontSizes.s14).letterSpace(1.75);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -28,7 +27,7 @@ class FlowyButton extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: FlowyHover(
|
||||
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: hoverColor),
|
||||
style: HoverStyle(borderRadius: BorderRadius.zero, hoverColor: hoverColor),
|
||||
setSelected: () => isSelected,
|
||||
builder: (context, onHover) => _render(),
|
||||
),
|
||||
|
@ -1,9 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
typedef HoverBuilder = Widget Function(BuildContext context, bool onHover);
|
||||
|
||||
@ -52,7 +49,7 @@ class _FlowyHoverState extends State<FlowyHover> {
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return child;
|
||||
return Container(child: child, color: widget.style.backgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,12 +60,14 @@ class HoverStyle {
|
||||
final Color hoverColor;
|
||||
final BorderRadius borderRadius;
|
||||
final EdgeInsets contentMargin;
|
||||
final Color backgroundColor;
|
||||
|
||||
const HoverStyle(
|
||||
{this.borderColor = Colors.transparent,
|
||||
this.borderWidth = 0,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(6)),
|
||||
this.contentMargin = EdgeInsets.zero,
|
||||
this.backgroundColor = Colors.transparent,
|
||||
required this.hoverColor});
|
||||
}
|
||||
|
||||
@ -100,120 +99,3 @@ class FlowyHoverContainer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
abstract class FlowyHoverWidget extends Widget {
|
||||
const FlowyHoverWidget({Key? key}) : super(key: key);
|
||||
|
||||
ValueNotifier<bool>? get onFocus;
|
||||
}
|
||||
|
||||
class FlowyHover2 extends StatefulWidget {
|
||||
final FlowyHoverWidget child;
|
||||
final EdgeInsets contentPadding;
|
||||
const FlowyHover2({
|
||||
required this.child,
|
||||
this.contentPadding = EdgeInsets.zero,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FlowyHover2> createState() => _FlowyHover2State();
|
||||
}
|
||||
|
||||
class _FlowyHover2State extends State<FlowyHover2> {
|
||||
late FlowyHoverState _hoverState;
|
||||
VoidCallback? _listenerFn;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_hoverState = FlowyHoverState();
|
||||
|
||||
listener() {
|
||||
_hoverState.onFocus = widget.child.onFocus?.value ?? false;
|
||||
}
|
||||
|
||||
_listenerFn = listener;
|
||||
widget.child.onFocus?.addListener(listener);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverState.dispose();
|
||||
|
||||
if (_listenerFn != null) {
|
||||
widget.child.onFocus?.removeListener(_listenerFn!);
|
||||
_listenerFn = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: _hoverState,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
opaque: false,
|
||||
onEnter: (p) => setState(() => _hoverState.onHover = true),
|
||||
onExit: (p) => setState(() => _hoverState.onHover = false),
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
const _HoverBackground(),
|
||||
Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HoverBackground extends StatelessWidget {
|
||||
const _HoverBackground({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return Consumer<FlowyHoverState>(
|
||||
builder: (context, state, child) {
|
||||
if (state.onHover || state.onFocus) {
|
||||
return FlowyHoverContainer(
|
||||
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlowyHoverState extends ChangeNotifier {
|
||||
bool _onHover = false;
|
||||
bool _onFocus = false;
|
||||
|
||||
set onHover(bool value) {
|
||||
if (_onHover != value) {
|
||||
_onHover = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onHover => _onHover;
|
||||
|
||||
set onFocus(bool value) {
|
||||
if (_onFocus != value) {
|
||||
_onFocus = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool get onFocus => _onFocus;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class StyledSingleChildScrollView extends StatefulWidget {
|
||||
this.handleColor,
|
||||
this.controller,
|
||||
this.scrollbarPadding,
|
||||
this.barSize = 6,
|
||||
this.barSize = 12,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -15,7 +15,7 @@ class PrimaryTextButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle txtStyle = TextStyles.Footnote.textColor(Colors.white);
|
||||
TextStyle txtStyle = TextStyles.Btn.textColor(Colors.white);
|
||||
return PrimaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class SecondaryTextButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
TextStyle txtStyle = TextStyles.Footnote.textColor(theme.main1);
|
||||
TextStyle txtStyle = TextStyles.Btn.textColor(theme.main1);
|
||||
return SecondaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
|
||||
/// Auto generate. Do not edit
|
||||
part of '../../dispatch.dart';
|
||||
class BlockEventGetBlockData {
|
||||
class TextBlockEventGetBlockData {
|
||||
TextBlockId request;
|
||||
BlockEventGetBlockData(this.request);
|
||||
TextBlockEventGetBlockData(this.request);
|
||||
|
||||
Future<Either<TextBlockDelta, FlowyError>> send() {
|
||||
final request = FFIRequest.create()
|
||||
..event = BlockEvent.GetBlockData.toString()
|
||||
..event = TextBlockEvent.GetBlockData.toString()
|
||||
..payload = requestToBytes(this.request);
|
||||
|
||||
return Dispatch.asyncRequest(request)
|
||||
@ -18,13 +18,13 @@ class BlockEventGetBlockData {
|
||||
}
|
||||
}
|
||||
|
||||
class BlockEventApplyDelta {
|
||||
class TextBlockEventApplyDelta {
|
||||
TextBlockDelta request;
|
||||
BlockEventApplyDelta(this.request);
|
||||
TextBlockEventApplyDelta(this.request);
|
||||
|
||||
Future<Either<TextBlockDelta, FlowyError>> send() {
|
||||
final request = FFIRequest.create()
|
||||
..event = BlockEvent.ApplyDelta.toString()
|
||||
..event = TextBlockEvent.ApplyDelta.toString()
|
||||
..payload = requestToBytes(this.request);
|
||||
|
||||
return Dispatch.asyncRequest(request)
|
||||
@ -35,13 +35,13 @@ class BlockEventApplyDelta {
|
||||
}
|
||||
}
|
||||
|
||||
class BlockEventExportDocument {
|
||||
class TextBlockEventExportDocument {
|
||||
ExportPayload request;
|
||||
BlockEventExportDocument(this.request);
|
||||
TextBlockEventExportDocument(this.request);
|
||||
|
||||
Future<Either<ExportData, FlowyError>> send() {
|
||||
final request = FFIRequest.create()
|
||||
..event = BlockEvent.ExportDocument.toString()
|
||||
..event = TextBlockEvent.ExportDocument.toString()
|
||||
..payload = requestToBytes(this.request);
|
||||
|
||||
return Dispatch.asyncRequest(request)
|
||||
|
@ -0,0 +1,11 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: format.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
export 'format.pbenum.dart';
|
||||
|
@ -0,0 +1,94 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: format.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
|
||||
// ignore_for_file: UNDEFINED_SHOWN_NAME
|
||||
import 'dart:core' as $core;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class NumberFormat extends $pb.ProtobufEnum {
|
||||
static const NumberFormat Number = NumberFormat._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Number');
|
||||
static const NumberFormat USD = NumberFormat._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'USD');
|
||||
static const NumberFormat CanadianDollar = NumberFormat._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CanadianDollar');
|
||||
static const NumberFormat EUR = NumberFormat._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'EUR');
|
||||
static const NumberFormat Pound = NumberFormat._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Pound');
|
||||
static const NumberFormat Yen = NumberFormat._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yen');
|
||||
static const NumberFormat Ruble = NumberFormat._(7, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ruble');
|
||||
static const NumberFormat Rupee = NumberFormat._(8, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupee');
|
||||
static const NumberFormat Won = NumberFormat._(9, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Won');
|
||||
static const NumberFormat Yuan = NumberFormat._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yuan');
|
||||
static const NumberFormat Real = NumberFormat._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Real');
|
||||
static const NumberFormat Lira = NumberFormat._(12, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Lira');
|
||||
static const NumberFormat Rupiah = NumberFormat._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupiah');
|
||||
static const NumberFormat Franc = NumberFormat._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Franc');
|
||||
static const NumberFormat HongKongDollar = NumberFormat._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'HongKongDollar');
|
||||
static const NumberFormat NewZealandDollar = NumberFormat._(16, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewZealandDollar');
|
||||
static const NumberFormat Krona = NumberFormat._(17, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Krona');
|
||||
static const NumberFormat NorwegianKrone = NumberFormat._(18, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NorwegianKrone');
|
||||
static const NumberFormat MexicanPeso = NumberFormat._(19, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MexicanPeso');
|
||||
static const NumberFormat Rand = NumberFormat._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rand');
|
||||
static const NumberFormat NewTaiwanDollar = NumberFormat._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewTaiwanDollar');
|
||||
static const NumberFormat DanishKrone = NumberFormat._(22, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DanishKrone');
|
||||
static const NumberFormat Baht = NumberFormat._(23, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Baht');
|
||||
static const NumberFormat Forint = NumberFormat._(24, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Forint');
|
||||
static const NumberFormat Koruna = NumberFormat._(25, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Koruna');
|
||||
static const NumberFormat Shekel = NumberFormat._(26, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Shekel');
|
||||
static const NumberFormat ChileanPeso = NumberFormat._(27, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ChileanPeso');
|
||||
static const NumberFormat PhilippinePeso = NumberFormat._(28, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PhilippinePeso');
|
||||
static const NumberFormat Dirham = NumberFormat._(29, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Dirham');
|
||||
static const NumberFormat ColombianPeso = NumberFormat._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ColombianPeso');
|
||||
static const NumberFormat Riyal = NumberFormat._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Riyal');
|
||||
static const NumberFormat Ringgit = NumberFormat._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ringgit');
|
||||
static const NumberFormat Leu = NumberFormat._(33, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Leu');
|
||||
static const NumberFormat ArgentinePeso = NumberFormat._(34, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ArgentinePeso');
|
||||
static const NumberFormat UruguayanPeso = NumberFormat._(35, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UruguayanPeso');
|
||||
static const NumberFormat Percent = NumberFormat._(36, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Percent');
|
||||
|
||||
static const $core.List<NumberFormat> values = <NumberFormat> [
|
||||
Number,
|
||||
USD,
|
||||
CanadianDollar,
|
||||
EUR,
|
||||
Pound,
|
||||
Yen,
|
||||
Ruble,
|
||||
Rupee,
|
||||
Won,
|
||||
Yuan,
|
||||
Real,
|
||||
Lira,
|
||||
Rupiah,
|
||||
Franc,
|
||||
HongKongDollar,
|
||||
NewZealandDollar,
|
||||
Krona,
|
||||
NorwegianKrone,
|
||||
MexicanPeso,
|
||||
Rand,
|
||||
NewTaiwanDollar,
|
||||
DanishKrone,
|
||||
Baht,
|
||||
Forint,
|
||||
Koruna,
|
||||
Shekel,
|
||||
ChileanPeso,
|
||||
PhilippinePeso,
|
||||
Dirham,
|
||||
ColombianPeso,
|
||||
Riyal,
|
||||
Ringgit,
|
||||
Leu,
|
||||
ArgentinePeso,
|
||||
UruguayanPeso,
|
||||
Percent,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, NumberFormat> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static NumberFormat? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const NumberFormat._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: format.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
@$core.Deprecated('Use numberFormatDescriptor instead')
|
||||
const NumberFormat$json = const {
|
||||
'1': 'NumberFormat',
|
||||
'2': const [
|
||||
const {'1': 'Number', '2': 0},
|
||||
const {'1': 'USD', '2': 1},
|
||||
const {'1': 'CanadianDollar', '2': 2},
|
||||
const {'1': 'EUR', '2': 4},
|
||||
const {'1': 'Pound', '2': 5},
|
||||
const {'1': 'Yen', '2': 6},
|
||||
const {'1': 'Ruble', '2': 7},
|
||||
const {'1': 'Rupee', '2': 8},
|
||||
const {'1': 'Won', '2': 9},
|
||||
const {'1': 'Yuan', '2': 10},
|
||||
const {'1': 'Real', '2': 11},
|
||||
const {'1': 'Lira', '2': 12},
|
||||
const {'1': 'Rupiah', '2': 13},
|
||||
const {'1': 'Franc', '2': 14},
|
||||
const {'1': 'HongKongDollar', '2': 15},
|
||||
const {'1': 'NewZealandDollar', '2': 16},
|
||||
const {'1': 'Krona', '2': 17},
|
||||
const {'1': 'NorwegianKrone', '2': 18},
|
||||
const {'1': 'MexicanPeso', '2': 19},
|
||||
const {'1': 'Rand', '2': 20},
|
||||
const {'1': 'NewTaiwanDollar', '2': 21},
|
||||
const {'1': 'DanishKrone', '2': 22},
|
||||
const {'1': 'Baht', '2': 23},
|
||||
const {'1': 'Forint', '2': 24},
|
||||
const {'1': 'Koruna', '2': 25},
|
||||
const {'1': 'Shekel', '2': 26},
|
||||
const {'1': 'ChileanPeso', '2': 27},
|
||||
const {'1': 'PhilippinePeso', '2': 28},
|
||||
const {'1': 'Dirham', '2': 29},
|
||||
const {'1': 'ColombianPeso', '2': 30},
|
||||
const {'1': 'Riyal', '2': 31},
|
||||
const {'1': 'Ringgit', '2': 32},
|
||||
const {'1': 'Leu', '2': 33},
|
||||
const {'1': 'ArgentinePeso', '2': 34},
|
||||
const {'1': 'UruguayanPeso', '2': 35},
|
||||
const {'1': 'Percent', '2': 36},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `NumberFormat`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List numberFormatDescriptor = $convert.base64Decode('CgxOdW1iZXJGb3JtYXQSCgoGTnVtYmVyEAASBwoDVVNEEAESEgoOQ2FuYWRpYW5Eb2xsYXIQAhIHCgNFVVIQBBIJCgVQb3VuZBAFEgcKA1llbhAGEgkKBVJ1YmxlEAcSCQoFUnVwZWUQCBIHCgNXb24QCRIICgRZdWFuEAoSCAoEUmVhbBALEggKBExpcmEQDBIKCgZSdXBpYWgQDRIJCgVGcmFuYxAOEhIKDkhvbmdLb25nRG9sbGFyEA8SFAoQTmV3WmVhbGFuZERvbGxhchAQEgkKBUtyb25hEBESEgoOTm9yd2VnaWFuS3JvbmUQEhIPCgtNZXhpY2FuUGVzbxATEggKBFJhbmQQFBITCg9OZXdUYWl3YW5Eb2xsYXIQFRIPCgtEYW5pc2hLcm9uZRAWEggKBEJhaHQQFxIKCgZGb3JpbnQQGBIKCgZLb3J1bmEQGRIKCgZTaGVrZWwQGhIPCgtDaGlsZWFuUGVzbxAbEhIKDlBoaWxpcHBpbmVQZXNvEBwSCgoGRGlyaGFtEB0SEQoNQ29sb21iaWFuUGVzbxAeEgkKBVJpeWFsEB8SCwoHUmluZ2dpdBAgEgcKA0xldRAhEhEKDUFyZ2VudGluZVBlc28QIhIRCg1VcnVndWF5YW5QZXNvECMSCwoHUGVyY2VudBAk');
|
@ -0,0 +1,9 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: format.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
|
||||
|
||||
export 'format.pb.dart';
|
||||
|
@ -9,13 +9,11 @@ import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
import 'number_type_option.pbenum.dart';
|
||||
|
||||
export 'number_type_option.pbenum.dart';
|
||||
import 'format.pbenum.dart' as $0;
|
||||
|
||||
class NumberTypeOption extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'NumberTypeOption', createEmptyInstance: create)
|
||||
..e<NumberFormat>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'format', $pb.PbFieldType.OE, defaultOrMaker: NumberFormat.Number, valueOf: NumberFormat.valueOf, enumValues: NumberFormat.values)
|
||||
..e<$0.NumberFormat>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'format', $pb.PbFieldType.OE, defaultOrMaker: $0.NumberFormat.Number, valueOf: $0.NumberFormat.valueOf, enumValues: $0.NumberFormat.values)
|
||||
..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'scale', $pb.PbFieldType.OU3)
|
||||
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'symbol')
|
||||
..aOB(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'signPositive')
|
||||
@ -25,7 +23,7 @@ class NumberTypeOption extends $pb.GeneratedMessage {
|
||||
|
||||
NumberTypeOption._() : super();
|
||||
factory NumberTypeOption({
|
||||
NumberFormat? format,
|
||||
$0.NumberFormat? format,
|
||||
$core.int? scale,
|
||||
$core.String? symbol,
|
||||
$core.bool? signPositive,
|
||||
@ -71,9 +69,9 @@ class NumberTypeOption extends $pb.GeneratedMessage {
|
||||
static NumberTypeOption? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
NumberFormat get format => $_getN(0);
|
||||
$0.NumberFormat get format => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set format(NumberFormat v) { setField(1, v); }
|
||||
set format($0.NumberFormat v) { setField(1, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasFormat() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
|
@ -5,90 +5,3 @@
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
|
||||
|
||||
// ignore_for_file: UNDEFINED_SHOWN_NAME
|
||||
import 'dart:core' as $core;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class NumberFormat extends $pb.ProtobufEnum {
|
||||
static const NumberFormat Number = NumberFormat._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Number');
|
||||
static const NumberFormat USD = NumberFormat._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'USD');
|
||||
static const NumberFormat CanadianDollar = NumberFormat._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CanadianDollar');
|
||||
static const NumberFormat EUR = NumberFormat._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'EUR');
|
||||
static const NumberFormat Pound = NumberFormat._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Pound');
|
||||
static const NumberFormat Yen = NumberFormat._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yen');
|
||||
static const NumberFormat Ruble = NumberFormat._(7, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ruble');
|
||||
static const NumberFormat Rupee = NumberFormat._(8, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupee');
|
||||
static const NumberFormat Won = NumberFormat._(9, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Won');
|
||||
static const NumberFormat Yuan = NumberFormat._(10, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Yuan');
|
||||
static const NumberFormat Real = NumberFormat._(11, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Real');
|
||||
static const NumberFormat Lira = NumberFormat._(12, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Lira');
|
||||
static const NumberFormat Rupiah = NumberFormat._(13, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rupiah');
|
||||
static const NumberFormat Franc = NumberFormat._(14, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Franc');
|
||||
static const NumberFormat HongKongDollar = NumberFormat._(15, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'HongKongDollar');
|
||||
static const NumberFormat NewZealandDollar = NumberFormat._(16, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewZealandDollar');
|
||||
static const NumberFormat Krona = NumberFormat._(17, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Krona');
|
||||
static const NumberFormat NorwegianKrone = NumberFormat._(18, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NorwegianKrone');
|
||||
static const NumberFormat MexicanPeso = NumberFormat._(19, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MexicanPeso');
|
||||
static const NumberFormat Rand = NumberFormat._(20, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Rand');
|
||||
static const NumberFormat NewTaiwanDollar = NumberFormat._(21, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'NewTaiwanDollar');
|
||||
static const NumberFormat DanishKrone = NumberFormat._(22, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DanishKrone');
|
||||
static const NumberFormat Baht = NumberFormat._(23, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Baht');
|
||||
static const NumberFormat Forint = NumberFormat._(24, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Forint');
|
||||
static const NumberFormat Koruna = NumberFormat._(25, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Koruna');
|
||||
static const NumberFormat Shekel = NumberFormat._(26, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Shekel');
|
||||
static const NumberFormat ChileanPeso = NumberFormat._(27, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ChileanPeso');
|
||||
static const NumberFormat PhilippinePeso = NumberFormat._(28, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PhilippinePeso');
|
||||
static const NumberFormat Dirham = NumberFormat._(29, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Dirham');
|
||||
static const NumberFormat ColombianPeso = NumberFormat._(30, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ColombianPeso');
|
||||
static const NumberFormat Riyal = NumberFormat._(31, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Riyal');
|
||||
static const NumberFormat Ringgit = NumberFormat._(32, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Ringgit');
|
||||
static const NumberFormat Leu = NumberFormat._(33, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Leu');
|
||||
static const NumberFormat ArgentinePeso = NumberFormat._(34, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ArgentinePeso');
|
||||
static const NumberFormat UruguayanPeso = NumberFormat._(35, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'UruguayanPeso');
|
||||
static const NumberFormat Percent = NumberFormat._(36, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Percent');
|
||||
|
||||
static const $core.List<NumberFormat> values = <NumberFormat> [
|
||||
Number,
|
||||
USD,
|
||||
CanadianDollar,
|
||||
EUR,
|
||||
Pound,
|
||||
Yen,
|
||||
Ruble,
|
||||
Rupee,
|
||||
Won,
|
||||
Yuan,
|
||||
Real,
|
||||
Lira,
|
||||
Rupiah,
|
||||
Franc,
|
||||
HongKongDollar,
|
||||
NewZealandDollar,
|
||||
Krona,
|
||||
NorwegianKrone,
|
||||
MexicanPeso,
|
||||
Rand,
|
||||
NewTaiwanDollar,
|
||||
DanishKrone,
|
||||
Baht,
|
||||
Forint,
|
||||
Koruna,
|
||||
Shekel,
|
||||
ChileanPeso,
|
||||
PhilippinePeso,
|
||||
Dirham,
|
||||
ColombianPeso,
|
||||
Riyal,
|
||||
Ringgit,
|
||||
Leu,
|
||||
ArgentinePeso,
|
||||
UruguayanPeso,
|
||||
Percent,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, NumberFormat> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static NumberFormat? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const NumberFormat._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
|
@ -8,51 +8,6 @@
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
@$core.Deprecated('Use numberFormatDescriptor instead')
|
||||
const NumberFormat$json = const {
|
||||
'1': 'NumberFormat',
|
||||
'2': const [
|
||||
const {'1': 'Number', '2': 0},
|
||||
const {'1': 'USD', '2': 1},
|
||||
const {'1': 'CanadianDollar', '2': 2},
|
||||
const {'1': 'EUR', '2': 4},
|
||||
const {'1': 'Pound', '2': 5},
|
||||
const {'1': 'Yen', '2': 6},
|
||||
const {'1': 'Ruble', '2': 7},
|
||||
const {'1': 'Rupee', '2': 8},
|
||||
const {'1': 'Won', '2': 9},
|
||||
const {'1': 'Yuan', '2': 10},
|
||||
const {'1': 'Real', '2': 11},
|
||||
const {'1': 'Lira', '2': 12},
|
||||
const {'1': 'Rupiah', '2': 13},
|
||||
const {'1': 'Franc', '2': 14},
|
||||
const {'1': 'HongKongDollar', '2': 15},
|
||||
const {'1': 'NewZealandDollar', '2': 16},
|
||||
const {'1': 'Krona', '2': 17},
|
||||
const {'1': 'NorwegianKrone', '2': 18},
|
||||
const {'1': 'MexicanPeso', '2': 19},
|
||||
const {'1': 'Rand', '2': 20},
|
||||
const {'1': 'NewTaiwanDollar', '2': 21},
|
||||
const {'1': 'DanishKrone', '2': 22},
|
||||
const {'1': 'Baht', '2': 23},
|
||||
const {'1': 'Forint', '2': 24},
|
||||
const {'1': 'Koruna', '2': 25},
|
||||
const {'1': 'Shekel', '2': 26},
|
||||
const {'1': 'ChileanPeso', '2': 27},
|
||||
const {'1': 'PhilippinePeso', '2': 28},
|
||||
const {'1': 'Dirham', '2': 29},
|
||||
const {'1': 'ColombianPeso', '2': 30},
|
||||
const {'1': 'Riyal', '2': 31},
|
||||
const {'1': 'Ringgit', '2': 32},
|
||||
const {'1': 'Leu', '2': 33},
|
||||
const {'1': 'ArgentinePeso', '2': 34},
|
||||
const {'1': 'UruguayanPeso', '2': 35},
|
||||
const {'1': 'Percent', '2': 36},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `NumberFormat`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List numberFormatDescriptor = $convert.base64Decode('CgxOdW1iZXJGb3JtYXQSCgoGTnVtYmVyEAASBwoDVVNEEAESEgoOQ2FuYWRpYW5Eb2xsYXIQAhIHCgNFVVIQBBIJCgVQb3VuZBAFEgcKA1llbhAGEgkKBVJ1YmxlEAcSCQoFUnVwZWUQCBIHCgNXb24QCRIICgRZdWFuEAoSCAoEUmVhbBALEggKBExpcmEQDBIKCgZSdXBpYWgQDRIJCgVGcmFuYxAOEhIKDkhvbmdLb25nRG9sbGFyEA8SFAoQTmV3WmVhbGFuZERvbGxhchAQEgkKBUtyb25hEBESEgoOTm9yd2VnaWFuS3JvbmUQEhIPCgtNZXhpY2FuUGVzbxATEggKBFJhbmQQFBITCg9OZXdUYWl3YW5Eb2xsYXIQFRIPCgtEYW5pc2hLcm9uZRAWEggKBEJhaHQQFxIKCgZGb3JpbnQQGBIKCgZLb3J1bmEQGRIKCgZTaGVrZWwQGhIPCgtDaGlsZWFuUGVzbxAbEhIKDlBoaWxpcHBpbmVQZXNvEBwSCgoGRGlyaGFtEB0SEQoNQ29sb21iaWFuUGVzbxAeEgkKBVJpeWFsEB8SCwoHUmluZ2dpdBAgEgcKA0xldRAhEhEKDUFyZ2VudGluZVBlc28QIhIRCg1VcnVndWF5YW5QZXNvECMSCwoHUGVyY2VudBAk');
|
||||
@$core.Deprecated('Use numberTypeOptionDescriptor instead')
|
||||
const NumberTypeOption$json = const {
|
||||
'1': 'NumberTypeOption',
|
||||
|
@ -7,6 +7,7 @@ export './row_entities.pb.dart';
|
||||
export './cell_entities.pb.dart';
|
||||
export './url_type_option.pb.dart';
|
||||
export './checkbox_type_option.pb.dart';
|
||||
export './format.pb.dart';
|
||||
export './event_map.pb.dart';
|
||||
export './text_type_option.pb.dart';
|
||||
export './date_type_option.pb.dart';
|
||||
|
@ -9,20 +9,20 @@
|
||||
import 'dart:core' as $core;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class BlockEvent extends $pb.ProtobufEnum {
|
||||
static const BlockEvent GetBlockData = BlockEvent._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetBlockData');
|
||||
static const BlockEvent ApplyDelta = BlockEvent._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplyDelta');
|
||||
static const BlockEvent ExportDocument = BlockEvent._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ExportDocument');
|
||||
class TextBlockEvent extends $pb.ProtobufEnum {
|
||||
static const TextBlockEvent GetBlockData = TextBlockEvent._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'GetBlockData');
|
||||
static const TextBlockEvent ApplyDelta = TextBlockEvent._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ApplyDelta');
|
||||
static const TextBlockEvent ExportDocument = TextBlockEvent._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ExportDocument');
|
||||
|
||||
static const $core.List<BlockEvent> values = <BlockEvent> [
|
||||
static const $core.List<TextBlockEvent> values = <TextBlockEvent> [
|
||||
GetBlockData,
|
||||
ApplyDelta,
|
||||
ExportDocument,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, BlockEvent> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static BlockEvent? valueOf($core.int value) => _byValue[value];
|
||||
static final $core.Map<$core.int, TextBlockEvent> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
static TextBlockEvent? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const BlockEvent._($core.int v, $core.String n) : super(v, n);
|
||||
const TextBlockEvent._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
|
@ -8,9 +8,9 @@
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
@$core.Deprecated('Use blockEventDescriptor instead')
|
||||
const BlockEvent$json = const {
|
||||
'1': 'BlockEvent',
|
||||
@$core.Deprecated('Use textBlockEventDescriptor instead')
|
||||
const TextBlockEvent$json = const {
|
||||
'1': 'TextBlockEvent',
|
||||
'2': const [
|
||||
const {'1': 'GetBlockData', '2': 0},
|
||||
const {'1': 'ApplyDelta', '2': 1},
|
||||
@ -18,5 +18,5 @@ const BlockEvent$json = const {
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `BlockEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List blockEventDescriptor = $convert.base64Decode('CgpCbG9ja0V2ZW50EhAKDEdldEJsb2NrRGF0YRAAEg4KCkFwcGx5RGVsdGEQARISCg5FeHBvcnREb2N1bWVudBAC');
|
||||
/// Descriptor for `TextBlockEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||
final $typed_data.Uint8List textBlockEventDescriptor = $convert.base64Decode('Cg5UZXh0QmxvY2tFdmVudBIQCgxHZXRCbG9ja0RhdGEQABIOCgpBcHBseURlbHRhEAESEgoORXhwb3J0RG9jdW1lbnQQAg==');
|
||||
|
@ -72,7 +72,7 @@ dependencies:
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
device_info_plus: ^3.2.1
|
||||
fluttertoast: ^8.0.8
|
||||
fluttertoast: ^8.0.9
|
||||
table_calendar: ^3.0.5
|
||||
reorderables: ^0.5.0
|
||||
linked_scroll_controller: ^0.2.0
|
||||
|
1
frontend/rust-lib/Cargo.lock
generated
1
frontend/rust-lib/Cargo.lock
generated
@ -937,6 +937,7 @@ dependencies = [
|
||||
"flowy-revision",
|
||||
"flowy-sync",
|
||||
"flowy-test",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"lib-dispatch",
|
||||
|
@ -241,11 +241,11 @@ pub trait ViewDataProcessor {
|
||||
|
||||
fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn delta_bytes(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
|
||||
fn view_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
|
||||
|
||||
fn create_default_view(&self, user_id: &str, view_id: &str) -> FutureResult<Bytes, FlowyError>;
|
||||
|
||||
fn process_create_view_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError>;
|
||||
fn process_view_delta_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError>;
|
||||
|
||||
fn data_type(&self) -> ViewDataType;
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ impl ViewController {
|
||||
params.data = view_data.to_vec();
|
||||
} else {
|
||||
let delta_data = processor
|
||||
.process_create_view_data(&user_id, ¶ms.view_id, params.data.clone())
|
||||
.process_view_delta_data(&user_id, ¶ms.view_id, params.data.clone())
|
||||
.await?;
|
||||
let _ = self
|
||||
.create_view(¶ms.view_id, params.data_type.clone(), delta_data)
|
||||
@ -176,7 +176,7 @@ impl ViewController {
|
||||
.await?;
|
||||
|
||||
let processor = self.get_data_processor(&view.data_type)?;
|
||||
let delta_bytes = processor.delta_bytes(view_id).await?;
|
||||
let delta_bytes = processor.view_delta_data(view_id).await?;
|
||||
let duplicate_params = CreateViewParams {
|
||||
belong_to_id: view.belong_to_id.clone(),
|
||||
name: format!("{} (copy)", &view.name),
|
||||
@ -238,7 +238,7 @@ impl ViewController {
|
||||
}
|
||||
|
||||
impl ViewController {
|
||||
#[tracing::instrument(level = "debug", skip(self), err)]
|
||||
#[tracing::instrument(level = "debug", skip(self, params), err)]
|
||||
async fn create_view_on_server(&self, params: CreateViewParams) -> Result<View, FlowyError> {
|
||||
let token = self.user.token()?;
|
||||
let view = self.cloud_service.create_view(&token, params).await?;
|
||||
|
@ -37,6 +37,7 @@ serde_repr = "0.1"
|
||||
indexmap = {version = "1.8.1", features = ["serde"]}
|
||||
fancy-regex = "0.10.0"
|
||||
url = { version = "2"}
|
||||
futures = "0.3.15"
|
||||
|
||||
[dev-dependencies]
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
|
@ -154,11 +154,10 @@ pub async fn make_grid_view_data(
|
||||
grid_manager: Arc<GridManager>,
|
||||
build_context: BuildGridContext,
|
||||
) -> FlowyResult<Bytes> {
|
||||
let block_id = build_context.block_meta.block_id.clone();
|
||||
let grid_meta = GridMeta {
|
||||
grid_id: view_id.to_string(),
|
||||
fields: build_context.field_metas,
|
||||
blocks: vec![build_context.block_meta],
|
||||
blocks: build_context.blocks,
|
||||
};
|
||||
|
||||
// Create grid
|
||||
@ -168,19 +167,23 @@ pub async fn make_grid_view_data(
|
||||
Revision::initial_revision(user_id, view_id, grid_delta_data.clone()).into();
|
||||
let _ = grid_manager.create_grid(view_id, repeated_revision).await?;
|
||||
|
||||
// Indexing the block's rows
|
||||
build_context.block_meta_data.rows.iter().for_each(|row| {
|
||||
let _ = grid_manager.block_index_cache.insert(&row.block_id, &row.id);
|
||||
});
|
||||
for block_meta_data in build_context.blocks_meta_data {
|
||||
let block_id = block_meta_data.block_id.clone();
|
||||
|
||||
// Create grid's block
|
||||
let grid_block_meta_delta = make_block_meta_delta(&build_context.block_meta_data);
|
||||
let block_meta_delta_data = grid_block_meta_delta.to_delta_bytes();
|
||||
let repeated_revision: RepeatedRevision =
|
||||
Revision::initial_revision(user_id, &block_id, block_meta_delta_data).into();
|
||||
let _ = grid_manager
|
||||
.create_grid_block_meta(&block_id, repeated_revision)
|
||||
.await?;
|
||||
// Indexing the block's rows
|
||||
block_meta_data.rows.iter().for_each(|row| {
|
||||
let _ = grid_manager.block_index_cache.insert(&row.block_id, &row.id);
|
||||
});
|
||||
|
||||
// Create grid's block
|
||||
let grid_block_meta_delta = make_block_meta_delta(&block_meta_data);
|
||||
let block_meta_delta_data = grid_block_meta_delta.to_delta_bytes();
|
||||
let repeated_revision: RepeatedRevision =
|
||||
Revision::initial_revision(user_id, &block_id, block_meta_delta_data).into();
|
||||
let _ = grid_manager
|
||||
.create_grid_block_meta(&block_id, repeated_revision)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(grid_delta_data)
|
||||
}
|
||||
|
207
frontend/rust-lib/flowy-grid/src/protobuf/model/format.rs
Normal file
207
frontend/rust-lib/flowy-grid/src/protobuf/model/format.rs
Normal file
@ -0,0 +1,207 @@
|
||||
// This file is generated by rust-protobuf 2.25.2. Do not edit
|
||||
// @generated
|
||||
|
||||
// https://github.com/rust-lang/rust-clippy/issues/702
|
||||
#![allow(unknown_lints)]
|
||||
#![allow(clippy::all)]
|
||||
|
||||
#![allow(unused_attributes)]
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
|
||||
#![allow(box_pointers)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(trivial_casts)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(unused_results)]
|
||||
//! Generated file from `format.proto`
|
||||
|
||||
/// Generated files are compatible only with the same version
|
||||
/// of protobuf runtime.
|
||||
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2;
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
|
||||
pub enum NumberFormat {
|
||||
Number = 0,
|
||||
USD = 1,
|
||||
CanadianDollar = 2,
|
||||
EUR = 4,
|
||||
Pound = 5,
|
||||
Yen = 6,
|
||||
Ruble = 7,
|
||||
Rupee = 8,
|
||||
Won = 9,
|
||||
Yuan = 10,
|
||||
Real = 11,
|
||||
Lira = 12,
|
||||
Rupiah = 13,
|
||||
Franc = 14,
|
||||
HongKongDollar = 15,
|
||||
NewZealandDollar = 16,
|
||||
Krona = 17,
|
||||
NorwegianKrone = 18,
|
||||
MexicanPeso = 19,
|
||||
Rand = 20,
|
||||
NewTaiwanDollar = 21,
|
||||
DanishKrone = 22,
|
||||
Baht = 23,
|
||||
Forint = 24,
|
||||
Koruna = 25,
|
||||
Shekel = 26,
|
||||
ChileanPeso = 27,
|
||||
PhilippinePeso = 28,
|
||||
Dirham = 29,
|
||||
ColombianPeso = 30,
|
||||
Riyal = 31,
|
||||
Ringgit = 32,
|
||||
Leu = 33,
|
||||
ArgentinePeso = 34,
|
||||
UruguayanPeso = 35,
|
||||
Percent = 36,
|
||||
}
|
||||
|
||||
impl ::protobuf::ProtobufEnum for NumberFormat {
|
||||
fn value(&self) -> i32 {
|
||||
*self as i32
|
||||
}
|
||||
|
||||
fn from_i32(value: i32) -> ::std::option::Option<NumberFormat> {
|
||||
match value {
|
||||
0 => ::std::option::Option::Some(NumberFormat::Number),
|
||||
1 => ::std::option::Option::Some(NumberFormat::USD),
|
||||
2 => ::std::option::Option::Some(NumberFormat::CanadianDollar),
|
||||
4 => ::std::option::Option::Some(NumberFormat::EUR),
|
||||
5 => ::std::option::Option::Some(NumberFormat::Pound),
|
||||
6 => ::std::option::Option::Some(NumberFormat::Yen),
|
||||
7 => ::std::option::Option::Some(NumberFormat::Ruble),
|
||||
8 => ::std::option::Option::Some(NumberFormat::Rupee),
|
||||
9 => ::std::option::Option::Some(NumberFormat::Won),
|
||||
10 => ::std::option::Option::Some(NumberFormat::Yuan),
|
||||
11 => ::std::option::Option::Some(NumberFormat::Real),
|
||||
12 => ::std::option::Option::Some(NumberFormat::Lira),
|
||||
13 => ::std::option::Option::Some(NumberFormat::Rupiah),
|
||||
14 => ::std::option::Option::Some(NumberFormat::Franc),
|
||||
15 => ::std::option::Option::Some(NumberFormat::HongKongDollar),
|
||||
16 => ::std::option::Option::Some(NumberFormat::NewZealandDollar),
|
||||
17 => ::std::option::Option::Some(NumberFormat::Krona),
|
||||
18 => ::std::option::Option::Some(NumberFormat::NorwegianKrone),
|
||||
19 => ::std::option::Option::Some(NumberFormat::MexicanPeso),
|
||||
20 => ::std::option::Option::Some(NumberFormat::Rand),
|
||||
21 => ::std::option::Option::Some(NumberFormat::NewTaiwanDollar),
|
||||
22 => ::std::option::Option::Some(NumberFormat::DanishKrone),
|
||||
23 => ::std::option::Option::Some(NumberFormat::Baht),
|
||||
24 => ::std::option::Option::Some(NumberFormat::Forint),
|
||||
25 => ::std::option::Option::Some(NumberFormat::Koruna),
|
||||
26 => ::std::option::Option::Some(NumberFormat::Shekel),
|
||||
27 => ::std::option::Option::Some(NumberFormat::ChileanPeso),
|
||||
28 => ::std::option::Option::Some(NumberFormat::PhilippinePeso),
|
||||
29 => ::std::option::Option::Some(NumberFormat::Dirham),
|
||||
30 => ::std::option::Option::Some(NumberFormat::ColombianPeso),
|
||||
31 => ::std::option::Option::Some(NumberFormat::Riyal),
|
||||
32 => ::std::option::Option::Some(NumberFormat::Ringgit),
|
||||
33 => ::std::option::Option::Some(NumberFormat::Leu),
|
||||
34 => ::std::option::Option::Some(NumberFormat::ArgentinePeso),
|
||||
35 => ::std::option::Option::Some(NumberFormat::UruguayanPeso),
|
||||
36 => ::std::option::Option::Some(NumberFormat::Percent),
|
||||
_ => ::std::option::Option::None
|
||||
}
|
||||
}
|
||||
|
||||
fn values() -> &'static [Self] {
|
||||
static values: &'static [NumberFormat] = &[
|
||||
NumberFormat::Number,
|
||||
NumberFormat::USD,
|
||||
NumberFormat::CanadianDollar,
|
||||
NumberFormat::EUR,
|
||||
NumberFormat::Pound,
|
||||
NumberFormat::Yen,
|
||||
NumberFormat::Ruble,
|
||||
NumberFormat::Rupee,
|
||||
NumberFormat::Won,
|
||||
NumberFormat::Yuan,
|
||||
NumberFormat::Real,
|
||||
NumberFormat::Lira,
|
||||
NumberFormat::Rupiah,
|
||||
NumberFormat::Franc,
|
||||
NumberFormat::HongKongDollar,
|
||||
NumberFormat::NewZealandDollar,
|
||||
NumberFormat::Krona,
|
||||
NumberFormat::NorwegianKrone,
|
||||
NumberFormat::MexicanPeso,
|
||||
NumberFormat::Rand,
|
||||
NumberFormat::NewTaiwanDollar,
|
||||
NumberFormat::DanishKrone,
|
||||
NumberFormat::Baht,
|
||||
NumberFormat::Forint,
|
||||
NumberFormat::Koruna,
|
||||
NumberFormat::Shekel,
|
||||
NumberFormat::ChileanPeso,
|
||||
NumberFormat::PhilippinePeso,
|
||||
NumberFormat::Dirham,
|
||||
NumberFormat::ColombianPeso,
|
||||
NumberFormat::Riyal,
|
||||
NumberFormat::Ringgit,
|
||||
NumberFormat::Leu,
|
||||
NumberFormat::ArgentinePeso,
|
||||
NumberFormat::UruguayanPeso,
|
||||
NumberFormat::Percent,
|
||||
];
|
||||
values
|
||||
}
|
||||
|
||||
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
|
||||
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
|
||||
descriptor.get(|| {
|
||||
::protobuf::reflect::EnumDescriptor::new_pb_name::<NumberFormat>("NumberFormat", file_descriptor_proto())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::marker::Copy for NumberFormat {
|
||||
}
|
||||
|
||||
impl ::std::default::Default for NumberFormat {
|
||||
fn default() -> Self {
|
||||
NumberFormat::Number
|
||||
}
|
||||
}
|
||||
|
||||
impl ::protobuf::reflect::ProtobufValue for NumberFormat {
|
||||
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
|
||||
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
|
||||
}
|
||||
}
|
||||
|
||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\n\x0cformat.proto*\xf8\x03\n\x0cNumberFormat\x12\n\n\x06Number\x10\0\
|
||||
\x12\x07\n\x03USD\x10\x01\x12\x12\n\x0eCanadianDollar\x10\x02\x12\x07\n\
|
||||
\x03EUR\x10\x04\x12\t\n\x05Pound\x10\x05\x12\x07\n\x03Yen\x10\x06\x12\t\
|
||||
\n\x05Ruble\x10\x07\x12\t\n\x05Rupee\x10\x08\x12\x07\n\x03Won\x10\t\x12\
|
||||
\x08\n\x04Yuan\x10\n\x12\x08\n\x04Real\x10\x0b\x12\x08\n\x04Lira\x10\x0c\
|
||||
\x12\n\n\x06Rupiah\x10\r\x12\t\n\x05Franc\x10\x0e\x12\x12\n\x0eHongKongD\
|
||||
ollar\x10\x0f\x12\x14\n\x10NewZealandDollar\x10\x10\x12\t\n\x05Krona\x10\
|
||||
\x11\x12\x12\n\x0eNorwegianKrone\x10\x12\x12\x0f\n\x0bMexicanPeso\x10\
|
||||
\x13\x12\x08\n\x04Rand\x10\x14\x12\x13\n\x0fNewTaiwanDollar\x10\x15\x12\
|
||||
\x0f\n\x0bDanishKrone\x10\x16\x12\x08\n\x04Baht\x10\x17\x12\n\n\x06Forin\
|
||||
t\x10\x18\x12\n\n\x06Koruna\x10\x19\x12\n\n\x06Shekel\x10\x1a\x12\x0f\n\
|
||||
\x0bChileanPeso\x10\x1b\x12\x12\n\x0ePhilippinePeso\x10\x1c\x12\n\n\x06D\
|
||||
irham\x10\x1d\x12\x11\n\rColombianPeso\x10\x1e\x12\t\n\x05Riyal\x10\x1f\
|
||||
\x12\x0b\n\x07Ringgit\x10\x20\x12\x07\n\x03Leu\x10!\x12\x11\n\rArgentine\
|
||||
Peso\x10\"\x12\x11\n\rUruguayanPeso\x10#\x12\x0b\n\x07Percent\x10$b\x06p\
|
||||
roto3\
|
||||
";
|
||||
|
||||
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
||||
|
||||
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
|
||||
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
|
||||
}
|
||||
|
||||
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
|
||||
file_descriptor_proto_lazy.get(|| {
|
||||
parse_descriptor_proto()
|
||||
})
|
||||
}
|
@ -25,6 +25,9 @@ pub use url_type_option::*;
|
||||
mod checkbox_type_option;
|
||||
pub use checkbox_type_option::*;
|
||||
|
||||
mod format;
|
||||
pub use format::*;
|
||||
|
||||
mod event_map;
|
||||
pub use event_map::*;
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
#[derive(PartialEq,Clone,Default)]
|
||||
pub struct NumberTypeOption {
|
||||
// message fields
|
||||
pub format: NumberFormat,
|
||||
pub format: super::format::NumberFormat,
|
||||
pub scale: u32,
|
||||
pub symbol: ::std::string::String,
|
||||
pub sign_positive: bool,
|
||||
@ -50,15 +50,15 @@ impl NumberTypeOption {
|
||||
// .NumberFormat format = 1;
|
||||
|
||||
|
||||
pub fn get_format(&self) -> NumberFormat {
|
||||
pub fn get_format(&self) -> super::format::NumberFormat {
|
||||
self.format
|
||||
}
|
||||
pub fn clear_format(&mut self) {
|
||||
self.format = NumberFormat::Number;
|
||||
self.format = super::format::NumberFormat::Number;
|
||||
}
|
||||
|
||||
// Param is passed by value, moved
|
||||
pub fn set_format(&mut self, v: NumberFormat) {
|
||||
pub fn set_format(&mut self, v: super::format::NumberFormat) {
|
||||
self.format = v;
|
||||
}
|
||||
|
||||
@ -189,7 +189,7 @@ impl ::protobuf::Message for NumberTypeOption {
|
||||
#[allow(unused_variables)]
|
||||
fn compute_size(&self) -> u32 {
|
||||
let mut my_size = 0;
|
||||
if self.format != NumberFormat::Number {
|
||||
if self.format != super::format::NumberFormat::Number {
|
||||
my_size += ::protobuf::rt::enum_size(1, self.format);
|
||||
}
|
||||
if self.scale != 0 {
|
||||
@ -210,7 +210,7 @@ impl ::protobuf::Message for NumberTypeOption {
|
||||
}
|
||||
|
||||
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
|
||||
if self.format != NumberFormat::Number {
|
||||
if self.format != super::format::NumberFormat::Number {
|
||||
os.write_enum(1, ::protobuf::ProtobufEnum::value(&self.format))?;
|
||||
}
|
||||
if self.scale != 0 {
|
||||
@ -263,7 +263,7 @@ impl ::protobuf::Message for NumberTypeOption {
|
||||
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
|
||||
descriptor.get(|| {
|
||||
let mut fields = ::std::vec::Vec::new();
|
||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<NumberFormat>>(
|
||||
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum<super::format::NumberFormat>>(
|
||||
"format",
|
||||
|m: &NumberTypeOption| { &m.format },
|
||||
|m: &mut NumberTypeOption| { &mut m.format },
|
||||
@ -304,7 +304,7 @@ impl ::protobuf::Message for NumberTypeOption {
|
||||
|
||||
impl ::protobuf::Clear for NumberTypeOption {
|
||||
fn clear(&mut self) {
|
||||
self.format = NumberFormat::Number;
|
||||
self.format = super::format::NumberFormat::Number;
|
||||
self.scale = 0;
|
||||
self.symbol.clear();
|
||||
self.sign_positive = false;
|
||||
@ -325,179 +325,13 @@ impl ::protobuf::reflect::ProtobufValue for NumberTypeOption {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
|
||||
pub enum NumberFormat {
|
||||
Number = 0,
|
||||
USD = 1,
|
||||
CanadianDollar = 2,
|
||||
EUR = 4,
|
||||
Pound = 5,
|
||||
Yen = 6,
|
||||
Ruble = 7,
|
||||
Rupee = 8,
|
||||
Won = 9,
|
||||
Yuan = 10,
|
||||
Real = 11,
|
||||
Lira = 12,
|
||||
Rupiah = 13,
|
||||
Franc = 14,
|
||||
HongKongDollar = 15,
|
||||
NewZealandDollar = 16,
|
||||
Krona = 17,
|
||||
NorwegianKrone = 18,
|
||||
MexicanPeso = 19,
|
||||
Rand = 20,
|
||||
NewTaiwanDollar = 21,
|
||||
DanishKrone = 22,
|
||||
Baht = 23,
|
||||
Forint = 24,
|
||||
Koruna = 25,
|
||||
Shekel = 26,
|
||||
ChileanPeso = 27,
|
||||
PhilippinePeso = 28,
|
||||
Dirham = 29,
|
||||
ColombianPeso = 30,
|
||||
Riyal = 31,
|
||||
Ringgit = 32,
|
||||
Leu = 33,
|
||||
ArgentinePeso = 34,
|
||||
UruguayanPeso = 35,
|
||||
Percent = 36,
|
||||
}
|
||||
|
||||
impl ::protobuf::ProtobufEnum for NumberFormat {
|
||||
fn value(&self) -> i32 {
|
||||
*self as i32
|
||||
}
|
||||
|
||||
fn from_i32(value: i32) -> ::std::option::Option<NumberFormat> {
|
||||
match value {
|
||||
0 => ::std::option::Option::Some(NumberFormat::Number),
|
||||
1 => ::std::option::Option::Some(NumberFormat::USD),
|
||||
2 => ::std::option::Option::Some(NumberFormat::CanadianDollar),
|
||||
4 => ::std::option::Option::Some(NumberFormat::EUR),
|
||||
5 => ::std::option::Option::Some(NumberFormat::Pound),
|
||||
6 => ::std::option::Option::Some(NumberFormat::Yen),
|
||||
7 => ::std::option::Option::Some(NumberFormat::Ruble),
|
||||
8 => ::std::option::Option::Some(NumberFormat::Rupee),
|
||||
9 => ::std::option::Option::Some(NumberFormat::Won),
|
||||
10 => ::std::option::Option::Some(NumberFormat::Yuan),
|
||||
11 => ::std::option::Option::Some(NumberFormat::Real),
|
||||
12 => ::std::option::Option::Some(NumberFormat::Lira),
|
||||
13 => ::std::option::Option::Some(NumberFormat::Rupiah),
|
||||
14 => ::std::option::Option::Some(NumberFormat::Franc),
|
||||
15 => ::std::option::Option::Some(NumberFormat::HongKongDollar),
|
||||
16 => ::std::option::Option::Some(NumberFormat::NewZealandDollar),
|
||||
17 => ::std::option::Option::Some(NumberFormat::Krona),
|
||||
18 => ::std::option::Option::Some(NumberFormat::NorwegianKrone),
|
||||
19 => ::std::option::Option::Some(NumberFormat::MexicanPeso),
|
||||
20 => ::std::option::Option::Some(NumberFormat::Rand),
|
||||
21 => ::std::option::Option::Some(NumberFormat::NewTaiwanDollar),
|
||||
22 => ::std::option::Option::Some(NumberFormat::DanishKrone),
|
||||
23 => ::std::option::Option::Some(NumberFormat::Baht),
|
||||
24 => ::std::option::Option::Some(NumberFormat::Forint),
|
||||
25 => ::std::option::Option::Some(NumberFormat::Koruna),
|
||||
26 => ::std::option::Option::Some(NumberFormat::Shekel),
|
||||
27 => ::std::option::Option::Some(NumberFormat::ChileanPeso),
|
||||
28 => ::std::option::Option::Some(NumberFormat::PhilippinePeso),
|
||||
29 => ::std::option::Option::Some(NumberFormat::Dirham),
|
||||
30 => ::std::option::Option::Some(NumberFormat::ColombianPeso),
|
||||
31 => ::std::option::Option::Some(NumberFormat::Riyal),
|
||||
32 => ::std::option::Option::Some(NumberFormat::Ringgit),
|
||||
33 => ::std::option::Option::Some(NumberFormat::Leu),
|
||||
34 => ::std::option::Option::Some(NumberFormat::ArgentinePeso),
|
||||
35 => ::std::option::Option::Some(NumberFormat::UruguayanPeso),
|
||||
36 => ::std::option::Option::Some(NumberFormat::Percent),
|
||||
_ => ::std::option::Option::None
|
||||
}
|
||||
}
|
||||
|
||||
fn values() -> &'static [Self] {
|
||||
static values: &'static [NumberFormat] = &[
|
||||
NumberFormat::Number,
|
||||
NumberFormat::USD,
|
||||
NumberFormat::CanadianDollar,
|
||||
NumberFormat::EUR,
|
||||
NumberFormat::Pound,
|
||||
NumberFormat::Yen,
|
||||
NumberFormat::Ruble,
|
||||
NumberFormat::Rupee,
|
||||
NumberFormat::Won,
|
||||
NumberFormat::Yuan,
|
||||
NumberFormat::Real,
|
||||
NumberFormat::Lira,
|
||||
NumberFormat::Rupiah,
|
||||
NumberFormat::Franc,
|
||||
NumberFormat::HongKongDollar,
|
||||
NumberFormat::NewZealandDollar,
|
||||
NumberFormat::Krona,
|
||||
NumberFormat::NorwegianKrone,
|
||||
NumberFormat::MexicanPeso,
|
||||
NumberFormat::Rand,
|
||||
NumberFormat::NewTaiwanDollar,
|
||||
NumberFormat::DanishKrone,
|
||||
NumberFormat::Baht,
|
||||
NumberFormat::Forint,
|
||||
NumberFormat::Koruna,
|
||||
NumberFormat::Shekel,
|
||||
NumberFormat::ChileanPeso,
|
||||
NumberFormat::PhilippinePeso,
|
||||
NumberFormat::Dirham,
|
||||
NumberFormat::ColombianPeso,
|
||||
NumberFormat::Riyal,
|
||||
NumberFormat::Ringgit,
|
||||
NumberFormat::Leu,
|
||||
NumberFormat::ArgentinePeso,
|
||||
NumberFormat::UruguayanPeso,
|
||||
NumberFormat::Percent,
|
||||
];
|
||||
values
|
||||
}
|
||||
|
||||
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
|
||||
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
|
||||
descriptor.get(|| {
|
||||
::protobuf::reflect::EnumDescriptor::new_pb_name::<NumberFormat>("NumberFormat", file_descriptor_proto())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::marker::Copy for NumberFormat {
|
||||
}
|
||||
|
||||
impl ::std::default::Default for NumberFormat {
|
||||
fn default() -> Self {
|
||||
NumberFormat::Number
|
||||
}
|
||||
}
|
||||
|
||||
impl ::protobuf::reflect::ProtobufValue for NumberFormat {
|
||||
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
|
||||
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
|
||||
}
|
||||
}
|
||||
|
||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\n\x18number_type_option.proto\"\xa0\x01\n\x10NumberTypeOption\x12%\n\
|
||||
\x06format\x18\x01\x20\x01(\x0e2\r.NumberFormatR\x06format\x12\x14\n\x05\
|
||||
scale\x18\x02\x20\x01(\rR\x05scale\x12\x16\n\x06symbol\x18\x03\x20\x01(\
|
||||
\tR\x06symbol\x12#\n\rsign_positive\x18\x04\x20\x01(\x08R\x0csignPositiv\
|
||||
e\x12\x12\n\x04name\x18\x05\x20\x01(\tR\x04name*\xf8\x03\n\x0cNumberForm\
|
||||
at\x12\n\n\x06Number\x10\0\x12\x07\n\x03USD\x10\x01\x12\x12\n\x0eCanadia\
|
||||
nDollar\x10\x02\x12\x07\n\x03EUR\x10\x04\x12\t\n\x05Pound\x10\x05\x12\
|
||||
\x07\n\x03Yen\x10\x06\x12\t\n\x05Ruble\x10\x07\x12\t\n\x05Rupee\x10\x08\
|
||||
\x12\x07\n\x03Won\x10\t\x12\x08\n\x04Yuan\x10\n\x12\x08\n\x04Real\x10\
|
||||
\x0b\x12\x08\n\x04Lira\x10\x0c\x12\n\n\x06Rupiah\x10\r\x12\t\n\x05Franc\
|
||||
\x10\x0e\x12\x12\n\x0eHongKongDollar\x10\x0f\x12\x14\n\x10NewZealandDoll\
|
||||
ar\x10\x10\x12\t\n\x05Krona\x10\x11\x12\x12\n\x0eNorwegianKrone\x10\x12\
|
||||
\x12\x0f\n\x0bMexicanPeso\x10\x13\x12\x08\n\x04Rand\x10\x14\x12\x13\n\
|
||||
\x0fNewTaiwanDollar\x10\x15\x12\x0f\n\x0bDanishKrone\x10\x16\x12\x08\n\
|
||||
\x04Baht\x10\x17\x12\n\n\x06Forint\x10\x18\x12\n\n\x06Koruna\x10\x19\x12\
|
||||
\n\n\x06Shekel\x10\x1a\x12\x0f\n\x0bChileanPeso\x10\x1b\x12\x12\n\x0ePhi\
|
||||
lippinePeso\x10\x1c\x12\n\n\x06Dirham\x10\x1d\x12\x11\n\rColombianPeso\
|
||||
\x10\x1e\x12\t\n\x05Riyal\x10\x1f\x12\x0b\n\x07Ringgit\x10\x20\x12\x07\n\
|
||||
\x03Leu\x10!\x12\x11\n\rArgentinePeso\x10\"\x12\x11\n\rUruguayanPeso\x10\
|
||||
#\x12\x0b\n\x07Percent\x10$b\x06proto3\
|
||||
\n\x18number_type_option.proto\x1a\x0cformat.proto\"\xa0\x01\n\x10Number\
|
||||
TypeOption\x12%\n\x06format\x18\x01\x20\x01(\x0e2\r.NumberFormatR\x06for\
|
||||
mat\x12\x14\n\x05scale\x18\x02\x20\x01(\rR\x05scale\x12\x16\n\x06symbol\
|
||||
\x18\x03\x20\x01(\tR\x06symbol\x12#\n\rsign_positive\x18\x04\x20\x01(\
|
||||
\x08R\x0csignPositive\x12\x12\n\x04name\x18\x05\x20\x01(\tR\x04nameb\x06\
|
||||
proto3\
|
||||
";
|
||||
|
||||
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
|
||||
|
40
frontend/rust-lib/flowy-grid/src/protobuf/proto/format.proto
Normal file
40
frontend/rust-lib/flowy-grid/src/protobuf/proto/format.proto
Normal file
@ -0,0 +1,40 @@
|
||||
syntax = "proto3";
|
||||
|
||||
enum NumberFormat {
|
||||
Number = 0;
|
||||
USD = 1;
|
||||
CanadianDollar = 2;
|
||||
EUR = 4;
|
||||
Pound = 5;
|
||||
Yen = 6;
|
||||
Ruble = 7;
|
||||
Rupee = 8;
|
||||
Won = 9;
|
||||
Yuan = 10;
|
||||
Real = 11;
|
||||
Lira = 12;
|
||||
Rupiah = 13;
|
||||
Franc = 14;
|
||||
HongKongDollar = 15;
|
||||
NewZealandDollar = 16;
|
||||
Krona = 17;
|
||||
NorwegianKrone = 18;
|
||||
MexicanPeso = 19;
|
||||
Rand = 20;
|
||||
NewTaiwanDollar = 21;
|
||||
DanishKrone = 22;
|
||||
Baht = 23;
|
||||
Forint = 24;
|
||||
Koruna = 25;
|
||||
Shekel = 26;
|
||||
ChileanPeso = 27;
|
||||
PhilippinePeso = 28;
|
||||
Dirham = 29;
|
||||
ColombianPeso = 30;
|
||||
Riyal = 31;
|
||||
Ringgit = 32;
|
||||
Leu = 33;
|
||||
ArgentinePeso = 34;
|
||||
UruguayanPeso = 35;
|
||||
Percent = 36;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
syntax = "proto3";
|
||||
import "format.proto";
|
||||
|
||||
message NumberTypeOption {
|
||||
NumberFormat format = 1;
|
||||
@ -7,41 +8,3 @@ message NumberTypeOption {
|
||||
bool sign_positive = 4;
|
||||
string name = 5;
|
||||
}
|
||||
enum NumberFormat {
|
||||
Number = 0;
|
||||
USD = 1;
|
||||
CanadianDollar = 2;
|
||||
EUR = 4;
|
||||
Pound = 5;
|
||||
Yen = 6;
|
||||
Ruble = 7;
|
||||
Rupee = 8;
|
||||
Won = 9;
|
||||
Yuan = 10;
|
||||
Real = 11;
|
||||
Lira = 12;
|
||||
Rupiah = 13;
|
||||
Franc = 14;
|
||||
HongKongDollar = 15;
|
||||
NewZealandDollar = 16;
|
||||
Krona = 17;
|
||||
NorwegianKrone = 18;
|
||||
MexicanPeso = 19;
|
||||
Rand = 20;
|
||||
NewTaiwanDollar = 21;
|
||||
DanishKrone = 22;
|
||||
Baht = 23;
|
||||
Forint = 24;
|
||||
Koruna = 25;
|
||||
Shekel = 26;
|
||||
ChileanPeso = 27;
|
||||
PhilippinePeso = 28;
|
||||
Dirham = 29;
|
||||
ColombianPeso = 30;
|
||||
Riyal = 31;
|
||||
Ringgit = 32;
|
||||
Leu = 33;
|
||||
ArgentinePeso = 34;
|
||||
UruguayanPeso = 35;
|
||||
Percent = 36;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use bytes::Bytes;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::entities::{CellMeta, RowMeta, RowMetaChangeset, RowOrder};
|
||||
use flowy_grid_data_model::entities::{CellMeta, GridBlockMetaData, RowMeta, RowMetaChangeset, RowOrder};
|
||||
use flowy_revision::{RevisionCloudService, RevisionCompactor, RevisionManager, RevisionObjectBuilder};
|
||||
use flowy_sync::client_grid::{GridBlockMetaChange, GridBlockMetaPad};
|
||||
use flowy_sync::entities::revision::Revision;
|
||||
@ -41,6 +41,10 @@ impl GridBlockMetaEditor {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn duplicate_block_meta_data(&self, duplicated_block_id: &str) -> GridBlockMetaData {
|
||||
self.pad.read().await.duplicate_data(duplicated_block_id).await
|
||||
}
|
||||
|
||||
/// return current number of rows and the inserted index. The inserted index will be None if the start_row_id is None
|
||||
pub(crate) async fn create_row(
|
||||
&self,
|
||||
|
@ -47,7 +47,7 @@ impl GridBlockManager {
|
||||
debug_assert!(!block_id.is_empty());
|
||||
match self.block_editor_map.get(block_id) {
|
||||
None => {
|
||||
tracing::error!("The is a fatal error, block is not exist");
|
||||
tracing::error!("This is a fatal error, block with id:{} is not exist", block_id);
|
||||
let editor = Arc::new(make_block_meta_editor(&self.user, block_id).await?);
|
||||
self.block_editor_map.insert(block_id.to_owned(), editor.clone());
|
||||
Ok(editor)
|
||||
@ -267,6 +267,7 @@ async fn make_block_meta_editor_map(
|
||||
}
|
||||
|
||||
async fn make_block_meta_editor(user: &Arc<dyn GridUser>, block_id: &str) -> FlowyResult<GridBlockMetaEditor> {
|
||||
tracing::trace!("Open block:{} meta editor", block_id);
|
||||
let token = user.token()?;
|
||||
let user_id = user.user_id()?;
|
||||
let pool = user.db_pool()?;
|
||||
|
@ -42,7 +42,7 @@ impl_type_option!(CheckboxTypeOption, FieldType::Checkbox);
|
||||
const YES: &str = "Yes";
|
||||
const NO: &str = "No";
|
||||
|
||||
impl CellDataOperation<String, String> for CheckboxTypeOption {
|
||||
impl CellDataOperation<String> for CheckboxTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
|
@ -1,17 +1,16 @@
|
||||
use crate::entities::{CellIdentifier, CellIdentifierPayload};
|
||||
use crate::impl_type_option;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData, EncodedCellData};
|
||||
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData};
|
||||
use bytes::Bytes;
|
||||
use chrono::format::strftime::StrftimeItems;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{NaiveDateTime, Timelike};
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::entities::{
|
||||
CellChangeset, CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
// Date
|
||||
@ -29,35 +28,36 @@ pub struct DateTypeOption {
|
||||
impl_type_option!(DateTypeOption, FieldType::DateTime);
|
||||
|
||||
impl DateTypeOption {
|
||||
fn today_desc_from_timestamp(&self, timestamp: i64, time: &Option<String>) -> String {
|
||||
let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
|
||||
self.today_desc_from_native(native, time)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn today_desc_from_str(&self, s: String, time: &Option<String>) -> String {
|
||||
match NaiveDateTime::parse_from_str(&s, &self.date_fmt(time)) {
|
||||
Ok(native) => self.today_desc_from_native(native, time),
|
||||
Err(_) => "".to_owned(),
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn today_desc_from_timestamp(&self, timestamp: i64) -> DateCellData {
|
||||
let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
|
||||
self.date_from_native(native)
|
||||
}
|
||||
|
||||
fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData {
|
||||
if native.timestamp() == 0 {
|
||||
return DateCellData::default();
|
||||
}
|
||||
}
|
||||
|
||||
fn today_desc_from_native(&self, native: chrono::NaiveDateTime, time: &Option<String>) -> String {
|
||||
let time = native.time();
|
||||
let has_time = time.hour() != 0 || time.second() != 0;
|
||||
|
||||
let utc = self.utc_date_time_from_native(native);
|
||||
// let china_timezone = FixedOffset::east(8 * 3600);
|
||||
// let a = utc.with_timezone(&china_timezone);
|
||||
let fmt = self.date_fmt(time);
|
||||
let output = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt)));
|
||||
output
|
||||
}
|
||||
let fmt = self.date_format.format_str();
|
||||
let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt)));
|
||||
|
||||
fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
|
||||
let native = NaiveDateTime::from_timestamp(timestamp, 0);
|
||||
self.utc_date_time_from_native(native)
|
||||
}
|
||||
let mut time = "".to_string();
|
||||
if has_time {
|
||||
let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str());
|
||||
time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, "");
|
||||
}
|
||||
|
||||
fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime<chrono::Utc> {
|
||||
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
|
||||
let timestamp = native.timestamp();
|
||||
DateCellData { date, time, timestamp }
|
||||
}
|
||||
|
||||
fn date_fmt(&self, time: &Option<String>) -> String {
|
||||
@ -77,14 +77,6 @@ impl DateTypeOption {
|
||||
}
|
||||
}
|
||||
|
||||
fn date_desc_from_timestamp(&self, serde_cell_data: &DateCellDataSerde) -> String {
|
||||
if serde_cell_data.timestamp == 0 {
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
self.today_desc_from_timestamp(serde_cell_data.timestamp, &serde_cell_data.time)
|
||||
}
|
||||
|
||||
fn timestamp_from_utc_with_time(
|
||||
&self,
|
||||
utc: &chrono::DateTime<chrono::Utc>,
|
||||
@ -113,9 +105,18 @@ impl DateTypeOption {
|
||||
|
||||
Ok(utc.timestamp())
|
||||
}
|
||||
|
||||
fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
|
||||
let native = NaiveDateTime::from_timestamp(timestamp, 0);
|
||||
self.utc_date_time_from_native(native)
|
||||
}
|
||||
|
||||
fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime<chrono::Utc> {
|
||||
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation<EncodedCellData<DateCellDataSerde>, DateCellDataSerde> for DateTypeOption {
|
||||
impl CellDataOperation<String> for DateTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
@ -123,7 +124,7 @@ impl CellDataOperation<EncodedCellData<DateCellDataSerde>, DateCellDataSerde> fo
|
||||
_field_meta: &FieldMeta,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<EncodedCellData<DateCellDataSerde>>,
|
||||
T: Into<String>,
|
||||
{
|
||||
// Return default data if the type_option_cell_data is not FieldType::DateTime.
|
||||
// It happens when switching from one field to another.
|
||||
@ -133,33 +134,29 @@ impl CellDataOperation<EncodedCellData<DateCellDataSerde>, DateCellDataSerde> fo
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let encoded_data = encoded_data.into().try_into_inner()?;
|
||||
let date = self.date_desc_from_timestamp(&encoded_data);
|
||||
let time = encoded_data.time.unwrap_or_else(|| "".to_owned());
|
||||
let timestamp = encoded_data.timestamp;
|
||||
|
||||
DecodedCellData::try_from_bytes(DateCellData { date, time, timestamp })
|
||||
let timestamp = encoded_data.into().parse::<i64>().unwrap_or(0);
|
||||
let date = self.today_desc_from_timestamp(timestamp);
|
||||
DecodedCellData::try_from_bytes(date)
|
||||
}
|
||||
|
||||
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<DateCellDataSerde, FlowyError>
|
||||
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<String, FlowyError>
|
||||
where
|
||||
C: Into<CellContentChangeset>,
|
||||
{
|
||||
let content_changeset: DateCellContentChangeset = serde_json::from_str(&changeset.into())?;
|
||||
let cell_data = match content_changeset.date_timestamp() {
|
||||
None => DateCellDataSerde::default(),
|
||||
None => 0,
|
||||
Some(date_timestamp) => match (self.include_time, content_changeset.time) {
|
||||
(true, Some(time)) => {
|
||||
let time = Some(time.trim().to_uppercase());
|
||||
let utc = self.utc_date_time_from_timestamp(date_timestamp);
|
||||
let timestamp = self.timestamp_from_utc_with_time(&utc, &time)?;
|
||||
DateCellDataSerde::new(timestamp, time, &self.time_format)
|
||||
self.timestamp_from_utc_with_time(&utc, &time)?
|
||||
}
|
||||
_ => DateCellDataSerde::from_timestamp(date_timestamp, Some(default_time_str(&self.time_format))),
|
||||
_ => date_timestamp,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(cell_data)
|
||||
Ok(cell_data.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,46 +280,6 @@ pub struct DateCellData {
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct DateCellDataSerde {
|
||||
pub timestamp: i64,
|
||||
pub time: Option<String>,
|
||||
}
|
||||
|
||||
impl DateCellDataSerde {
|
||||
fn new(timestamp: i64, time: Option<String>, time_format: &TimeFormat) -> Self {
|
||||
Self {
|
||||
timestamp,
|
||||
time: Some(time.unwrap_or_else(|| default_time_str(time_format))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_timestamp(timestamp: i64, time: Option<String>) -> Self {
|
||||
Self { timestamp, time }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DateCellDataSerde {
|
||||
type Err = FlowyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_str::<DateCellDataSerde>(s).map_err(internal_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for DateCellDataSerde {
|
||||
fn to_string(&self) -> String {
|
||||
serde_json::to_string(&self).unwrap_or_else(|_| "".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_time_str(time_format: &TimeFormat) -> String {
|
||||
match time_format {
|
||||
TimeFormat::TwelveHour => "12:00 AM".to_string(),
|
||||
TimeFormat::TwentyFourHour => "00:00".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
pub struct DateChangesetPayload {
|
||||
#[pb(index = 1)]
|
||||
@ -399,15 +356,13 @@ impl std::convert::From<DateCellContentChangeset> for CellContentChangeset {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{
|
||||
DateCellContentChangeset, DateCellData, DateCellDataSerde, DateFormat, DateTypeOption, TimeFormat,
|
||||
};
|
||||
use crate::services::row::{CellDataOperation, EncodedCellData};
|
||||
use flowy_grid_data_model::entities::{FieldMeta, FieldType};
|
||||
use crate::services::field::{DateCellContentChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat};
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::entities::{FieldMeta, FieldType, TypeOptionDataEntry};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn date_description_invalid_input_test() {
|
||||
fn date_type_option_invalid_input_test() {
|
||||
let type_option = DateTypeOption::default();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
@ -424,7 +379,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_description_date_format_test() {
|
||||
fn date_type_option_date_format_test() {
|
||||
let mut type_option = DateTypeOption::default();
|
||||
let field_meta = FieldBuilder::from_field_type(&FieldType::DateTime).build();
|
||||
for date_format in DateFormat::iter() {
|
||||
@ -447,7 +402,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_description_time_format_test() {
|
||||
fn date_type_option_time_format_test() {
|
||||
let mut type_option = DateTypeOption::default();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
@ -465,7 +420,7 @@ mod tests {
|
||||
},
|
||||
&field_type,
|
||||
&field_meta,
|
||||
"May 27,2022 00:00",
|
||||
"May 27,2022",
|
||||
);
|
||||
assert_changeset_result(
|
||||
&type_option,
|
||||
@ -487,9 +442,9 @@ mod tests {
|
||||
},
|
||||
&field_type,
|
||||
&field_meta,
|
||||
"May 27,2022 12:00 AM",
|
||||
"May 27,2022",
|
||||
);
|
||||
|
||||
//
|
||||
assert_changeset_result(
|
||||
&type_option,
|
||||
DateCellContentChangeset {
|
||||
@ -517,8 +472,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_description_apply_changeset_test() {
|
||||
let mut type_option = DateTypeOption::default();
|
||||
fn date_type_option_apply_changeset_test() {
|
||||
let mut type_option = DateTypeOption::new();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
let date_timestamp = "1653609600".to_owned();
|
||||
@ -543,7 +498,7 @@ mod tests {
|
||||
},
|
||||
&field_type,
|
||||
&field_meta,
|
||||
"May 27,2022 00:00",
|
||||
"May 27,2022",
|
||||
);
|
||||
|
||||
assert_changeset_result(
|
||||
@ -572,30 +527,53 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_description_apply_changeset_error_test() {
|
||||
let mut type_option = DateTypeOption::default();
|
||||
fn date_type_option_apply_changeset_error_test() {
|
||||
let mut type_option = DateTypeOption::new();
|
||||
type_option.include_time = true;
|
||||
let _field_meta = FieldBuilder::from_field_type(&FieldType::DateTime).build();
|
||||
let field_meta = FieldBuilder::from_field_type(&type_option.field_type()).build();
|
||||
let date_timestamp = "1653609600".to_owned();
|
||||
|
||||
let changeset = DateCellContentChangeset {
|
||||
date: Some(date_timestamp.clone()),
|
||||
time: Some("1:a0".to_owned()),
|
||||
};
|
||||
let _ = type_option.apply_changeset(changeset, None).unwrap();
|
||||
assert_changeset_result(
|
||||
&type_option,
|
||||
DateCellContentChangeset {
|
||||
date: Some(date_timestamp.clone()),
|
||||
time: Some("1:".to_owned()),
|
||||
},
|
||||
&type_option.field_type(),
|
||||
&field_meta,
|
||||
"May 27,2022 01:00",
|
||||
);
|
||||
|
||||
let changeset = DateCellContentChangeset {
|
||||
date: Some(date_timestamp),
|
||||
time: Some("1:".to_owned()),
|
||||
};
|
||||
let _ = type_option.apply_changeset(changeset, None).unwrap();
|
||||
assert_changeset_result(
|
||||
&type_option,
|
||||
DateCellContentChangeset {
|
||||
date: Some(date_timestamp),
|
||||
time: Some("1:00".to_owned()),
|
||||
},
|
||||
&type_option.field_type(),
|
||||
&field_meta,
|
||||
"May 27,2022 01:00",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_description_invalid_data_test() {
|
||||
let type_option = DateTypeOption::default();
|
||||
type_option.apply_changeset("he", None).unwrap();
|
||||
fn date_type_option_twelve_hours_to_twenty_four_hours() {
|
||||
let mut type_option = DateTypeOption::new();
|
||||
type_option.include_time = true;
|
||||
let field_meta = FieldBuilder::from_field_type(&type_option.field_type()).build();
|
||||
let date_timestamp = "1653609600".to_owned();
|
||||
|
||||
assert_changeset_result(
|
||||
&type_option,
|
||||
DateCellContentChangeset {
|
||||
date: Some(date_timestamp),
|
||||
time: Some("1:00 am".to_owned()),
|
||||
},
|
||||
&type_option.field_type(),
|
||||
&field_meta,
|
||||
"May 27,2022 01:00",
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_changeset_result(
|
||||
@ -605,7 +583,7 @@ mod tests {
|
||||
field_meta: &FieldMeta,
|
||||
expected: &str,
|
||||
) {
|
||||
let encoded_data = EncodedCellData(Some(type_option.apply_changeset(changeset, None).unwrap()));
|
||||
let encoded_data = type_option.apply_changeset(changeset, None).unwrap();
|
||||
assert_eq!(
|
||||
expected.to_owned(),
|
||||
decode_cell_data(encoded_data, type_option, field_meta)
|
||||
@ -613,24 +591,37 @@ mod tests {
|
||||
}
|
||||
|
||||
fn assert_decode_timestamp(timestamp: i64, type_option: &DateTypeOption, field_meta: &FieldMeta, expected: &str) {
|
||||
let serde_json = DateCellDataSerde { timestamp, time: None }.to_string();
|
||||
let encoded_data = type_option
|
||||
.apply_changeset(
|
||||
DateCellContentChangeset {
|
||||
date: Some(timestamp.to_string()),
|
||||
time: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected.to_owned(),
|
||||
decode_cell_data(serde_json, type_option, field_meta)
|
||||
decode_cell_data(encoded_data, type_option, field_meta)
|
||||
);
|
||||
}
|
||||
|
||||
fn decode_cell_data<T: Into<EncodedCellData<DateCellDataSerde>>>(
|
||||
fn decode_cell_data<T: Into<String>>(
|
||||
encoded_data: T,
|
||||
type_option: &DateTypeOption,
|
||||
field_meta: &FieldMeta,
|
||||
) -> String {
|
||||
type_option
|
||||
let decoded_data = type_option
|
||||
.decode_cell_data(encoded_data, &FieldType::DateTime, field_meta)
|
||||
.unwrap()
|
||||
.parse::<DateCellData>()
|
||||
.unwrap()
|
||||
.date
|
||||
.unwrap();
|
||||
|
||||
if type_option.include_time {
|
||||
format!("{}{}", decoded_data.date, decoded_data.time)
|
||||
} else {
|
||||
decoded_data.date
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,182 +1,16 @@
|
||||
use crate::impl_type_option;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData};
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::entities::{
|
||||
CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
|
||||
};
|
||||
use flowy_derive::ProtoBuf_Enum;
|
||||
use lazy_static::lazy_static;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use rusty_money::define_currency_set;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
lazy_static! {
|
||||
static ref STRIP_SYMBOL: Vec<String> = make_strip_symbol();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NumberTypeOptionBuilder(NumberTypeOption);
|
||||
impl_into_box_type_option_builder!(NumberTypeOptionBuilder);
|
||||
impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption);
|
||||
|
||||
impl NumberTypeOptionBuilder {
|
||||
pub fn name(mut self, name: &str) -> Self {
|
||||
self.0.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_format(mut self, format: NumberFormat) -> Self {
|
||||
self.0.set_format(format);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scale(mut self, scale: u32) -> Self {
|
||||
self.0.scale = scale;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn positive(mut self, positive: bool) -> Self {
|
||||
self.0.sign_positive = positive;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionBuilder for NumberTypeOptionBuilder {
|
||||
fn field_type(&self) -> FieldType {
|
||||
self.0.field_type()
|
||||
}
|
||||
|
||||
fn entry(&self) -> &dyn TypeOptionDataEntry {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Number
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct NumberTypeOption {
|
||||
#[pb(index = 1)]
|
||||
pub format: NumberFormat,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub scale: u32,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub symbol: String,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub sign_positive: bool,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub name: String,
|
||||
}
|
||||
impl_type_option!(NumberTypeOption, FieldType::Number);
|
||||
|
||||
impl CellDataOperation<String, String> for NumberTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
decoded_field_type: &FieldType,
|
||||
_field_meta: &FieldMeta,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
if decoded_field_type.is_date() {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let cell_data = encoded_data.into();
|
||||
match self.format {
|
||||
NumberFormat::Number => {
|
||||
if let Ok(v) = cell_data.parse::<f64>() {
|
||||
return Ok(DecodedCellData::new(v.to_string()));
|
||||
}
|
||||
|
||||
if let Ok(v) = cell_data.parse::<i64>() {
|
||||
return Ok(DecodedCellData::new(v.to_string()));
|
||||
}
|
||||
|
||||
Ok(DecodedCellData::default())
|
||||
}
|
||||
NumberFormat::Percent => {
|
||||
let content = cell_data.parse::<f64>().map_or(String::new(), |v| v.to_string());
|
||||
Ok(DecodedCellData::new(content))
|
||||
}
|
||||
_ => {
|
||||
let content = self.money_from_str(&cell_data);
|
||||
Ok(DecodedCellData::new(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<String, FlowyError>
|
||||
where
|
||||
C: Into<CellContentChangeset>,
|
||||
{
|
||||
let changeset = changeset.into();
|
||||
let mut data = changeset.trim().to_string();
|
||||
|
||||
if self.format != NumberFormat::Number {
|
||||
data = self.strip_symbol(data);
|
||||
if !data.chars().all(char::is_numeric) {
|
||||
return Err(FlowyError::invalid_data().context("Should only contain numbers"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for NumberTypeOption {
|
||||
fn default() -> Self {
|
||||
let format = NumberFormat::default();
|
||||
let symbol = format.symbol();
|
||||
NumberTypeOption {
|
||||
format,
|
||||
scale: 0,
|
||||
symbol,
|
||||
sign_positive: true,
|
||||
name: "Number".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NumberTypeOption {
|
||||
pub fn set_format(&mut self, format: NumberFormat) {
|
||||
self.format = format;
|
||||
self.symbol = format.symbol();
|
||||
}
|
||||
|
||||
fn money_from_str(&self, s: &str) -> String {
|
||||
match Decimal::from_str(s) {
|
||||
Ok(mut decimal) => {
|
||||
match decimal.set_scale(self.scale) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("Set decimal scale failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
decimal.set_sign_positive(self.sign_positive);
|
||||
let money = rusty_money::Money::from_decimal(decimal, self.format.currency());
|
||||
money.to_string()
|
||||
}
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_symbol<T: ToString>(&self, s: T) -> String {
|
||||
let mut s = s.to_string();
|
||||
if !s.chars().all(char::is_numeric) {
|
||||
s.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
|
||||
}
|
||||
s
|
||||
}
|
||||
pub static ref CURRENCY_SYMBOL: Vec<String> = NumberFormat::iter()
|
||||
.map(|format| format.symbol())
|
||||
.collect::<Vec<String>>();
|
||||
pub static ref STRIP_SYMBOL: Vec<String> = vec![",".to_owned(), ".".to_owned()];
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
|
||||
@ -609,137 +443,3 @@ impl NumberFormat {
|
||||
self.currency().symbol.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_strip_symbol() -> Vec<String> {
|
||||
let mut symbols = vec![",".to_owned(), ".".to_owned()];
|
||||
for format in NumberFormat::iter() {
|
||||
symbols.push(format.symbol());
|
||||
}
|
||||
symbols
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{NumberFormat, NumberTypeOption};
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::entities::{FieldMeta, FieldType};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn number_description_invalid_input_test() {
|
||||
let type_option = NumberTypeOption::default();
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
assert_equal(&type_option, "", "", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_description_test() {
|
||||
let mut type_option = NumberTypeOption::default();
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
assert_eq!(type_option.strip_symbol("¥18,443"), "18443".to_owned());
|
||||
assert_eq!(type_option.strip_symbol("$18,443"), "18443".to_owned());
|
||||
assert_eq!(type_option.strip_symbol("€18.443"), "18443".to_owned());
|
||||
|
||||
for format in NumberFormat::iter() {
|
||||
type_option.format = format;
|
||||
match format {
|
||||
NumberFormat::Number => {
|
||||
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::USD => {
|
||||
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "", "", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yen => {
|
||||
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yuan => {
|
||||
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::EUR => {
|
||||
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_meta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_description_scale_test() {
|
||||
let mut type_option = NumberTypeOption {
|
||||
scale: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
|
||||
for format in NumberFormat::iter() {
|
||||
type_option.format = format;
|
||||
match format {
|
||||
NumberFormat::Number => {
|
||||
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::USD => {
|
||||
assert_equal(&type_option, "18443", "$1,844.3", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yen => {
|
||||
assert_equal(&type_option, "18443", "¥1,844.3", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::EUR => {
|
||||
assert_equal(&type_option, "18443", "€1.844,3", &field_type, &field_meta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_description_sign_test() {
|
||||
let mut type_option = NumberTypeOption {
|
||||
sign_positive: false,
|
||||
..Default::default()
|
||||
};
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
|
||||
for format in NumberFormat::iter() {
|
||||
type_option.format = format;
|
||||
match format {
|
||||
NumberFormat::Number => {
|
||||
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::USD => {
|
||||
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yen => {
|
||||
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::EUR => {
|
||||
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_meta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_equal(
|
||||
type_option: &NumberTypeOption,
|
||||
cell_data: &str,
|
||||
expected_str: &str,
|
||||
field_type: &FieldType,
|
||||
field_meta: &FieldMeta,
|
||||
) {
|
||||
assert_eq!(
|
||||
type_option
|
||||
.decode_cell_data(cell_data, field_type, field_meta)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
expected_str.to_owned()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
mod format;
|
||||
mod number_type_option;
|
||||
|
||||
pub use format::*;
|
||||
pub use number_type_option::*;
|
@ -0,0 +1,346 @@
|
||||
use crate::impl_type_option;
|
||||
|
||||
use crate::services::field::type_options::number_type_option::format::*;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData};
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::entities::{
|
||||
CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry,
|
||||
};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NumberTypeOptionBuilder(NumberTypeOption);
|
||||
impl_into_box_type_option_builder!(NumberTypeOptionBuilder);
|
||||
impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption);
|
||||
|
||||
impl NumberTypeOptionBuilder {
|
||||
pub fn name(mut self, name: &str) -> Self {
|
||||
self.0.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_format(mut self, format: NumberFormat) -> Self {
|
||||
self.0.set_format(format);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scale(mut self, scale: u32) -> Self {
|
||||
self.0.scale = scale;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn positive(mut self, positive: bool) -> Self {
|
||||
self.0.sign_positive = positive;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionBuilder for NumberTypeOptionBuilder {
|
||||
fn field_type(&self) -> FieldType {
|
||||
self.0.field_type()
|
||||
}
|
||||
|
||||
fn entry(&self) -> &dyn TypeOptionDataEntry {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
// Number
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct NumberTypeOption {
|
||||
#[pb(index = 1)]
|
||||
pub format: NumberFormat,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub scale: u32,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub symbol: String,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub sign_positive: bool,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub name: String,
|
||||
}
|
||||
impl_type_option!(NumberTypeOption, FieldType::Number);
|
||||
|
||||
impl NumberTypeOption {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn cell_content_from_number_str(&self, s: &str) -> FlowyResult<String> {
|
||||
match self.format {
|
||||
NumberFormat::Number => {
|
||||
if let Ok(v) = s.parse::<f64>() {
|
||||
return Ok(v.to_string());
|
||||
}
|
||||
|
||||
if let Ok(v) = s.parse::<i64>() {
|
||||
return Ok(v.to_string());
|
||||
}
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
NumberFormat::Percent => {
|
||||
let content = s.parse::<f64>().map_or(String::new(), |v| v.to_string());
|
||||
Ok(content)
|
||||
}
|
||||
_ => self.money_from_number_str(s),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_format(&mut self, format: NumberFormat) {
|
||||
self.format = format;
|
||||
self.symbol = format.symbol();
|
||||
}
|
||||
|
||||
fn money_from_number_str(&self, s: &str) -> FlowyResult<String> {
|
||||
let mut number = self.strip_currency_symbol(s);
|
||||
|
||||
if s.is_empty() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
match Decimal::from_str(&number) {
|
||||
Ok(mut decimal) => {
|
||||
decimal.set_sign_positive(self.sign_positive);
|
||||
let money = rusty_money::Money::from_decimal(decimal, self.format.currency()).to_string();
|
||||
Ok(money)
|
||||
}
|
||||
Err(_) => match rusty_money::Money::from_str(&number, self.format.currency()) {
|
||||
Ok(money) => Ok(money.to_string()),
|
||||
Err(_) => {
|
||||
number.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
|
||||
if number.chars().all(char::is_numeric) {
|
||||
self.money_from_number_str(&number)
|
||||
} else {
|
||||
Err(FlowyError::invalid_data().context("Should only contain numbers"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_currency_symbol<T: ToString>(&self, s: T) -> String {
|
||||
let mut s = s.to_string();
|
||||
for symbol in CURRENCY_SYMBOL.iter() {
|
||||
if s.starts_with(symbol) {
|
||||
s = s.strip_prefix(symbol).unwrap_or("").to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation<String> for NumberTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
decoded_field_type: &FieldType,
|
||||
_field_meta: &FieldMeta,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
if decoded_field_type.is_date() {
|
||||
return Ok(DecodedCellData::default());
|
||||
}
|
||||
|
||||
let cell_data = encoded_data.into();
|
||||
match self.format {
|
||||
NumberFormat::Number => {
|
||||
if let Ok(v) = cell_data.parse::<f64>() {
|
||||
return Ok(DecodedCellData::new(v.to_string()));
|
||||
}
|
||||
|
||||
if let Ok(v) = cell_data.parse::<i64>() {
|
||||
return Ok(DecodedCellData::new(v.to_string()));
|
||||
}
|
||||
|
||||
Ok(DecodedCellData::default())
|
||||
}
|
||||
NumberFormat::Percent => {
|
||||
let content = cell_data.parse::<f64>().map_or(String::new(), |v| v.to_string());
|
||||
Ok(DecodedCellData::new(content))
|
||||
}
|
||||
_ => {
|
||||
let content = self
|
||||
.money_from_number_str(&cell_data)
|
||||
.unwrap_or_else(|_| "".to_string());
|
||||
Ok(DecodedCellData::new(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_changeset<C>(&self, changeset: C, _cell_meta: Option<CellMeta>) -> Result<String, FlowyError>
|
||||
where
|
||||
C: Into<CellContentChangeset>,
|
||||
{
|
||||
let changeset = changeset.into();
|
||||
let data = changeset.trim().to_string();
|
||||
let _ = self.cell_content_from_number_str(&data)?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for NumberTypeOption {
|
||||
fn default() -> Self {
|
||||
let format = NumberFormat::default();
|
||||
let symbol = format.symbol();
|
||||
NumberTypeOption {
|
||||
format,
|
||||
scale: 0,
|
||||
symbol,
|
||||
sign_positive: true,
|
||||
name: "Number".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{NumberFormat, NumberTypeOption};
|
||||
use crate::services::row::CellDataOperation;
|
||||
use flowy_grid_data_model::entities::{FieldMeta, FieldType};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn number_type_option_invalid_input_test() {
|
||||
let type_option = NumberTypeOption::default();
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
assert_equal(&type_option, "", "", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_type_option_strip_symbol_test() {
|
||||
let mut type_option = NumberTypeOption::new();
|
||||
type_option.format = NumberFormat::USD;
|
||||
assert_eq!(type_option.strip_currency_symbol("$18,443"), "18,443".to_owned());
|
||||
|
||||
type_option.format = NumberFormat::Yuan;
|
||||
assert_eq!(type_option.strip_currency_symbol("$0.2"), "0.2".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_type_option_format_number_test() {
|
||||
let mut type_option = NumberTypeOption::default();
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
|
||||
for format in NumberFormat::iter() {
|
||||
type_option.format = format;
|
||||
match format {
|
||||
NumberFormat::Number => {
|
||||
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::USD => {
|
||||
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yen => {
|
||||
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yuan => {
|
||||
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::EUR => {
|
||||
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_meta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_type_option_format_str_test() {
|
||||
let mut type_option = NumberTypeOption::default();
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
|
||||
for format in NumberFormat::iter() {
|
||||
type_option.format = format;
|
||||
match format {
|
||||
NumberFormat::Number => {
|
||||
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "0.2", "0.2", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::USD => {
|
||||
assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "", "", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "abc", "", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yen => {
|
||||
assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yuan => {
|
||||
assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::EUR => {
|
||||
assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_meta);
|
||||
assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_meta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_description_sign_test() {
|
||||
let mut type_option = NumberTypeOption {
|
||||
sign_positive: false,
|
||||
..Default::default()
|
||||
};
|
||||
let field_type = FieldType::Number;
|
||||
let field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
|
||||
for format in NumberFormat::iter() {
|
||||
type_option.format = format;
|
||||
match format {
|
||||
NumberFormat::Number => {
|
||||
assert_equal(&type_option, "18443", "18443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::USD => {
|
||||
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::Yen => {
|
||||
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_meta);
|
||||
}
|
||||
NumberFormat::EUR => {
|
||||
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_meta);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_equal(
|
||||
type_option: &NumberTypeOption,
|
||||
cell_data: &str,
|
||||
expected_str: &str,
|
||||
field_type: &FieldType,
|
||||
field_meta: &FieldMeta,
|
||||
) {
|
||||
assert_eq!(
|
||||
type_option
|
||||
.decode_cell_data(cell_data, field_type, field_meta)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
expected_str.to_owned()
|
||||
);
|
||||
}
|
||||
}
|
@ -95,7 +95,7 @@ impl SelectOptionOperation for SingleSelectTypeOption {
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation<String, String> for SingleSelectTypeOption {
|
||||
impl CellDataOperation<String> for SingleSelectTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
@ -193,7 +193,7 @@ impl SelectOptionOperation for MultiSelectTypeOption {
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation<String, String> for MultiSelectTypeOption {
|
||||
impl CellDataOperation<String> for MultiSelectTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
|
@ -27,11 +27,11 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct RichTextTypeOption {
|
||||
#[pb(index = 1)]
|
||||
data: String, //It's not used.
|
||||
data: String, //It's not used yet
|
||||
}
|
||||
impl_type_option!(RichTextTypeOption, FieldType::RichText);
|
||||
|
||||
impl CellDataOperation<String, String> for RichTextTypeOption {
|
||||
impl CellDataOperation<String> for RichTextTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
@ -80,10 +80,10 @@ mod tests {
|
||||
// date
|
||||
let field_type = FieldType::DateTime;
|
||||
let date_time_field_meta = FieldBuilder::from_field_type(&field_type).build();
|
||||
let json = serde_json::to_string(&DateCellDataSerde::from_timestamp(1647251762, None)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
type_option
|
||||
.decode_cell_data(json, &field_type, &date_time_field_meta)
|
||||
.decode_cell_data(1647251762.to_string(), &field_type, &date_time_field_meta)
|
||||
.unwrap()
|
||||
.parse::<DateCellData>()
|
||||
.unwrap()
|
||||
|
@ -30,11 +30,11 @@ impl TypeOptionBuilder for URLTypeOptionBuilder {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
|
||||
pub struct URLTypeOption {
|
||||
#[pb(index = 1)]
|
||||
data: String, //It's not used.
|
||||
data: String, //It's not used yet.
|
||||
}
|
||||
impl_type_option!(URLTypeOption, FieldType::URL);
|
||||
|
||||
impl CellDataOperation<EncodedCellData<URLCellData>, String> for URLTypeOption {
|
||||
impl CellDataOperation<EncodedCellData<URLCellData>> for URLTypeOption {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
@ -56,28 +56,31 @@ impl CellDataOperation<EncodedCellData<URLCellData>, String> for URLTypeOption {
|
||||
C: Into<CellContentChangeset>,
|
||||
{
|
||||
let changeset = changeset.into();
|
||||
let mut cell_data = URLCellData {
|
||||
url: "".to_string(),
|
||||
content: changeset.to_string(),
|
||||
};
|
||||
|
||||
let mut url = "".to_string();
|
||||
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
|
||||
// Only support https scheme by now
|
||||
match url::Url::parse(m.as_str()) {
|
||||
Ok(url) => {
|
||||
if url.scheme() == "https" {
|
||||
cell_data.url = url.into();
|
||||
} else {
|
||||
cell_data.url = format!("https://{}", m.as_str());
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
cell_data.url = format!("https://{}", m.as_str());
|
||||
}
|
||||
url = auto_append_scheme(m.as_str());
|
||||
}
|
||||
URLCellData {
|
||||
url,
|
||||
content: changeset.to_string(),
|
||||
}
|
||||
.to_json()
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_append_scheme(s: &str) -> String {
|
||||
// Only support https scheme by now
|
||||
match url::Url::parse(s) {
|
||||
Ok(url) => {
|
||||
if url.scheme() == "https" {
|
||||
url.into()
|
||||
} else {
|
||||
format!("https://{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
cell_data.to_json()
|
||||
Err(_) => {
|
||||
format!("https://{}", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -487,6 +487,35 @@ impl GridMetaEditor {
|
||||
self.grid_pad.read().await.delta_bytes()
|
||||
}
|
||||
|
||||
pub async fn duplicate_grid(&self) -> FlowyResult<BuildGridContext> {
|
||||
let grid_pad = self.grid_pad.read().await;
|
||||
let original_blocks = grid_pad.get_block_metas();
|
||||
let (duplicated_fields, duplicated_blocks) = grid_pad.duplicate_grid_meta().await;
|
||||
|
||||
let mut blocks_meta_data = vec![];
|
||||
if original_blocks.len() == duplicated_blocks.len() {
|
||||
for (index, original_block_meta) in original_blocks.iter().enumerate() {
|
||||
let grid_block_meta_editor = self.block_manager.get_editor(&original_block_meta.block_id).await?;
|
||||
let duplicated_block_id = &duplicated_blocks[index].block_id;
|
||||
|
||||
tracing::trace!("Duplicate block:{} meta data", duplicated_block_id);
|
||||
let duplicated_block_meta_data = grid_block_meta_editor
|
||||
.duplicate_block_meta_data(duplicated_block_id)
|
||||
.await;
|
||||
blocks_meta_data.push(duplicated_block_meta_data);
|
||||
}
|
||||
} else {
|
||||
debug_assert_eq!(original_blocks.len(), duplicated_blocks.len());
|
||||
}
|
||||
drop(grid_pad);
|
||||
|
||||
Ok(BuildGridContext {
|
||||
field_metas: duplicated_fields,
|
||||
blocks: duplicated_blocks,
|
||||
blocks_meta_data,
|
||||
})
|
||||
}
|
||||
|
||||
async fn modify<F>(&self, f: F) -> FlowyResult<()>
|
||||
where
|
||||
F: for<'a> FnOnce(&'a mut GridMetaPad) -> FlowyResult<Option<GridChangeset>>,
|
||||
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub trait CellDataOperation<D, CO: ToString> {
|
||||
pub trait CellDataOperation<ED> {
|
||||
fn decode_cell_data<T>(
|
||||
&self,
|
||||
encoded_data: T,
|
||||
@ -14,14 +14,14 @@ pub trait CellDataOperation<D, CO: ToString> {
|
||||
field_meta: &FieldMeta,
|
||||
) -> FlowyResult<DecodedCellData>
|
||||
where
|
||||
T: Into<D>;
|
||||
T: Into<ED>;
|
||||
|
||||
//
|
||||
fn apply_changeset<C: Into<CellContentChangeset>>(
|
||||
&self,
|
||||
changeset: C,
|
||||
cell_meta: Option<CellMeta>,
|
||||
) -> FlowyResult<CO>;
|
||||
) -> FlowyResult<String>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -128,9 +128,7 @@ pub fn apply_cell_data_changeset<T: Into<CellContentChangeset>>(
|
||||
let s = match field_meta.field_type {
|
||||
FieldType::RichText => RichTextTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
|
||||
FieldType::Number => NumberTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
|
||||
FieldType::DateTime => DateTypeOption::from(field_meta)
|
||||
.apply_changeset(changeset, cell_meta)
|
||||
.map(|data| data.to_string()),
|
||||
FieldType::DateTime => DateTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
|
||||
FieldType::SingleSelect => SingleSelectTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
|
||||
FieldType::MultiSelect => MultiSelectTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
|
||||
FieldType::Checkbox => CheckboxTypeOption::from(field_meta).apply_changeset(changeset, cell_meta),
|
||||
|
@ -173,7 +173,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn delta_bytes(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
fn view_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
@ -197,7 +197,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn process_create_view_data(
|
||||
fn process_view_delta_data(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_view_id: &str,
|
||||
@ -245,13 +245,13 @@ impl ViewDataProcessor for GridViewDataProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn delta_bytes(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
fn view_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
let view_id = view_id.to_string();
|
||||
let grid_manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
let editor = grid_manager.open_grid(view_id).await?;
|
||||
let delta_bytes = editor.delta_bytes().await;
|
||||
Ok(delta_bytes)
|
||||
let delta_bytes = editor.duplicate_grid().await?;
|
||||
Ok(delta_bytes.into())
|
||||
})
|
||||
}
|
||||
|
||||
@ -264,7 +264,7 @@ impl ViewDataProcessor for GridViewDataProcessor {
|
||||
FutureResult::new(async move { make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await })
|
||||
}
|
||||
|
||||
fn process_create_view_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError> {
|
||||
fn process_view_delta_data(&self, user_id: &str, view_id: &str, data: Vec<u8>) -> FutureResult<Bytes, FlowyError> {
|
||||
let user_id = user_id.to_string();
|
||||
let view_id = view_id.to_string();
|
||||
let grid_manager = self.0.clone();
|
||||
|
@ -9,16 +9,16 @@ pub fn create(block_manager: Arc<TextBlockManager>) -> Module {
|
||||
let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager);
|
||||
|
||||
module = module
|
||||
.event(BlockEvent::GetBlockData, get_block_data_handler)
|
||||
.event(BlockEvent::ApplyDelta, apply_delta_handler)
|
||||
.event(BlockEvent::ExportDocument, export_handler);
|
||||
.event(TextBlockEvent::GetBlockData, get_block_data_handler)
|
||||
.event(TextBlockEvent::ApplyDelta, apply_delta_handler)
|
||||
.event(TextBlockEvent::ExportDocument, export_handler);
|
||||
|
||||
module
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
#[event_err = "FlowyError"]
|
||||
pub enum BlockEvent {
|
||||
pub enum TextBlockEvent {
|
||||
#[event(input = "TextBlockId", output = "TextBlockDelta")]
|
||||
GetBlockData = 0,
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user