mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-11-27 14:49:54 +03:00
feat: support editing name when creating a new page on mobile (#6501)
* feat: support editing name when creating a new page on mobile * chore: add defaultName in layout extension * test: add cover title test on mobile * fix: cover title test on mobile * feat: add integration runner 4 * chore: update translations * chore: disable subpage feature
This commit is contained in:
parent
ba59514464
commit
5f1f536181
@ -52,7 +52,7 @@ runs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libmpv-dev mpv
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
@ -75,4 +75,4 @@ runs:
|
||||
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
|
||||
sudo apt-get install network-manager
|
||||
flutter test ${{ inputs.test_path }} -d Linux --coverage
|
||||
shell: bash
|
||||
shell: bash
|
||||
|
72
.github/workflows/flutter_ci.yaml
vendored
72
.github/workflows/flutter_ci.yaml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ windows-2019 ]
|
||||
os: [windows-2019]
|
||||
include:
|
||||
- os: windows-2019
|
||||
flutter_profile: development-windows-x86
|
||||
@ -100,7 +100,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ macos-latest ]
|
||||
os: [macos-latest]
|
||||
include:
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac-x86_64
|
||||
@ -122,12 +122,12 @@ jobs:
|
||||
flutter_profile: ${{ matrix.flutter_profile }}
|
||||
|
||||
unit_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@ -173,7 +173,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@ -216,11 +216,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
cloud_integration_test:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@ -292,7 +292,7 @@ jobs:
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv
|
||||
sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev
|
||||
shell: bash
|
||||
|
||||
- name: Enable Flutter Desktop
|
||||
@ -325,12 +325,12 @@ jobs:
|
||||
|
||||
# split the integration tests into different machines to minimize the time
|
||||
integration_test_1:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
@ -339,12 +339,6 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install video dependency
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libmpv-dev mpv
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Integration Test 1
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
@ -355,12 +349,12 @@ jobs:
|
||||
rust_target: ${{ matrix.target }}
|
||||
|
||||
integration_test_2:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
@ -369,12 +363,6 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install video dependency
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libmpv-dev mpv
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Integration Test 2
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
@ -385,12 +373,12 @@ jobs:
|
||||
rust_target: ${{ matrix.target }}
|
||||
|
||||
integration_test_3:
|
||||
needs: [ prepare-linux ]
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
@ -399,12 +387,6 @@ jobs:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install video dependency
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libmpv-dev mpv
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Integration Test 3
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
@ -413,3 +395,27 @@ jobs:
|
||||
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
|
||||
rust_target: ${{ matrix.target }}
|
||||
|
||||
integration_test_4:
|
||||
needs: [prepare-linux]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Flutter Integration Test 4
|
||||
uses: ./.github/actions/flutter_integration_test
|
||||
with:
|
||||
test_path: integration_test/desktop_runner_4.dart
|
||||
flutter_version: ${{ env.FLUTTER_VERSION }}
|
||||
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
|
||||
rust_target: ${{ matrix.target }}
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -368,7 +368,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install -y alien libnotify-dev libmpv-dev mpv
|
||||
sudo apt-get install -y alien libnotify-dev
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
|
@ -24,7 +24,7 @@ import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
|
||||
void startTesting() {
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
@ -47,4 +47,6 @@ void startTesting() {
|
||||
document_more_actions_test.main();
|
||||
document_with_file_test.main();
|
||||
document_shortcuts_test.main();
|
||||
|
||||
// Don't add new tests here. Add them to document_test_runner_2.dart
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'document_app_lifecycle_test.dart' as document_app_lifecycle_test;
|
||||
import 'document_title_test.dart' as document_title_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
document_title_test.main();
|
||||
// Disable subPage test temporarily, enable it in version 0.7.2
|
||||
// document_sub_page_test.main();
|
||||
document_app_lifecycle_test.main();
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'desktop/document/document_test_runner.dart' as document_test_runner;
|
||||
import 'desktop/document/document_test_runner_1.dart' as document_test_runner;
|
||||
import 'desktop/uncategorized/empty_test.dart' as first_test;
|
||||
import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;
|
||||
|
||||
@ -15,7 +15,7 @@ Future<void> runIntegration1OnDesktop() async {
|
||||
first_test.main();
|
||||
|
||||
switch_folder_test.main();
|
||||
document_test_runner.startTesting();
|
||||
document_test_runner.main();
|
||||
|
||||
// DON'T add more tests here. This is the first test runner for desktop.
|
||||
// DON'T add more tests here.
|
||||
}
|
||||
|
@ -3,16 +3,12 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'desktop/board/board_test_runner.dart' as board_test_runner;
|
||||
import 'desktop/database/database_row_cover_test.dart'
|
||||
as database_row_cover_test;
|
||||
import 'desktop/document/document_title_test.dart' as document_title_test;
|
||||
import 'desktop/document/document_app_lifecycle_test.dart'
|
||||
as document_app_lifecycle_test;
|
||||
import 'desktop/document/document_sub_page_test.dart' as document_sub_page_test;
|
||||
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
|
||||
import 'desktop/grid/grid_filter_and_sort_test.dart'
|
||||
as grid_filter_and_sort_test_runner;
|
||||
import 'desktop/grid/grid_reopen_test.dart' as grid_reopen_test_runner;
|
||||
import 'desktop/grid/grid_reorder_row_test.dart'
|
||||
as grid_reorder_row_test_runner;
|
||||
import 'desktop/grid/grid_edit_row_test.dart' as grid_edit_row_test_runner;
|
||||
import 'desktop/grid/grid_row_test.dart' as grid_create_row_test_runner;
|
||||
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
|
||||
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
|
||||
@ -51,7 +47,6 @@ Future<void> runIntegration3OnDesktop() async {
|
||||
grid_filter_and_sort_test_runner.main();
|
||||
grid_edit_row_test_runner.main();
|
||||
zoom_in_out_test.main();
|
||||
document_title_test.main();
|
||||
document_sub_page_test.main();
|
||||
document_app_lifecycle_test.main();
|
||||
|
||||
// DON'T add more tests here. Add them to desktop_runner_4.dart
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'desktop/document/document_test_runner_2.dart' as document_test_runner_2;
|
||||
import 'desktop/uncategorized/empty_test.dart' as first_test;
|
||||
|
||||
Future<void> main() async {
|
||||
await runIntegration4OnDesktop();
|
||||
}
|
||||
|
||||
Future<void> runIntegration4OnDesktop() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// This test must be run first, otherwise the CI will fail.
|
||||
first_test.main();
|
||||
|
||||
document_test_runner_2.main();
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'page_style_test.dart' as page_style_test;
|
||||
import 'title_test.dart' as title_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Document integration tests
|
||||
title_test.main();
|
||||
page_style_test.main();
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/home.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../shared/dir.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document title:', () {
|
||||
testWidgets('create a new page, the title should be empty', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
final createPageButton = find.byKey(
|
||||
BottomNavigationBarItemType.add.valueKey,
|
||||
);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
|
||||
final title = tester.editor.findDocumentTitle('');
|
||||
expect(title, findsOneWidget);
|
||||
final textField = tester.widget<TextField>(title);
|
||||
expect(textField.focusNode!.hasFocus, isTrue);
|
||||
|
||||
// input new name and press done button
|
||||
const name = 'test document';
|
||||
await tester.enterText(title, name);
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
final newTitle = tester.editor.findDocumentTitle(name);
|
||||
expect(newTitle, findsOneWidget);
|
||||
expect(textField.controller!.text, name);
|
||||
|
||||
// the document should get focus
|
||||
final editor = tester.widget<AppFlowyEditorPage>(
|
||||
find.byType(AppFlowyEditorPage),
|
||||
);
|
||||
expect(
|
||||
editor.editorState.selection,
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'desktop_runner_1.dart';
|
||||
import 'desktop_runner_2.dart';
|
||||
import 'desktop_runner_3.dart';
|
||||
import 'desktop_runner_4.dart';
|
||||
import 'mobile_runner.dart';
|
||||
|
||||
/// The main task runner for all integration tests in AppFlowy.
|
||||
@ -17,6 +18,7 @@ Future<void> main() async {
|
||||
await runIntegration1OnDesktop();
|
||||
await runIntegration2OnDesktop();
|
||||
await runIntegration3OnDesktop();
|
||||
await runIntegration4OnDesktop();
|
||||
} else if (Platform.isIOS || Platform.isAndroid) {
|
||||
await runIntegrationOnMobile();
|
||||
} else {
|
||||
|
@ -1,11 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -17,10 +11,12 @@ import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
@ -40,6 +36,10 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
@ -324,14 +324,10 @@ extension CommonOperations on WidgetTester {
|
||||
}
|
||||
await pumpAndSettle();
|
||||
|
||||
final defaultPageName = layout == ViewLayoutPB.Document
|
||||
? '' // the document name is empty by default
|
||||
: LocaleKeys.menuAppHeader_defaultNewPageName.tr();
|
||||
|
||||
// hover on it and change it's name
|
||||
if (name != null) {
|
||||
await hoverOnPageName(
|
||||
defaultPageName,
|
||||
layout.defaultName,
|
||||
layout: layout,
|
||||
onHover: () async {
|
||||
await renamePage(name);
|
||||
@ -345,7 +341,7 @@ extension CommonOperations on WidgetTester {
|
||||
if (openAfterCreated) {
|
||||
await openPage(
|
||||
// if the name is null, use the default name
|
||||
name ?? defaultPageName,
|
||||
name ?? layout.defaultName,
|
||||
layout: layout,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
|
@ -1,14 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_title.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
||||
@ -19,8 +17,11 @@ import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'util.dart';
|
||||
|
||||
@ -330,8 +331,12 @@ class EditorOperations {
|
||||
}
|
||||
|
||||
Finder findDocumentTitle(String? title) {
|
||||
final parent = UniversalPlatform.isDesktop
|
||||
? find.byType(CoverTitle)
|
||||
: find.byType(DocumentImmersiveCover);
|
||||
|
||||
return find.descendant(
|
||||
of: find.byType(CoverTitle),
|
||||
of: parent,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is! TextField) {
|
||||
|
@ -7,6 +7,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -93,7 +94,7 @@ enum MobilePaneActionType {
|
||||
Navigator.of(sheetContext).pop();
|
||||
viewBloc.add(
|
||||
ViewEvent.createView(
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout.defaultName,
|
||||
layout,
|
||||
section: spaceType!.toViewSectionPB,
|
||||
),
|
||||
|
@ -93,7 +93,7 @@ class MobileSpace extends StatelessWidget {
|
||||
Navigator.of(sheetContext).pop();
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
name: layout.defaultName,
|
||||
layout: layout,
|
||||
index: 0,
|
||||
),
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';
|
||||
@ -11,10 +10,10 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -187,7 +186,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
name: layout.defaultName,
|
||||
layout: layout,
|
||||
),
|
||||
);
|
||||
@ -195,7 +194,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
// only support create document in section
|
||||
context.read<SidebarSectionsBloc>().add(
|
||||
SidebarSectionsEvent.createRootViewInSection(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
name: layout.defaultName,
|
||||
index: 0,
|
||||
viewSection: FolderSpaceType.public.toViewSectionPB,
|
||||
),
|
||||
|
@ -29,23 +29,41 @@ final PropertyValueNotifier<ViewLayoutPB?> mobileCreateNewPageNotifier =
|
||||
final ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =
|
||||
ValueNotifier(BottomNavigationBarActionType.home);
|
||||
|
||||
const _homeLabel = 'home';
|
||||
const _addLabel = 'add';
|
||||
const _notificationLabel = 'notification';
|
||||
enum BottomNavigationBarItemType {
|
||||
home,
|
||||
add,
|
||||
notification;
|
||||
|
||||
String get label {
|
||||
return switch (this) {
|
||||
BottomNavigationBarItemType.home => 'home',
|
||||
BottomNavigationBarItemType.add => 'add',
|
||||
BottomNavigationBarItemType.notification => 'notification',
|
||||
};
|
||||
}
|
||||
|
||||
ValueKey get valueKey {
|
||||
return ValueKey(label);
|
||||
}
|
||||
}
|
||||
|
||||
final _items = <BottomNavigationBarItem>[
|
||||
const BottomNavigationBarItem(
|
||||
label: _homeLabel,
|
||||
icon: FlowySvg(FlowySvgs.m_home_unselected_m),
|
||||
activeIcon: FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
|
||||
BottomNavigationBarItem(
|
||||
key: BottomNavigationBarItemType.home.valueKey,
|
||||
label: BottomNavigationBarItemType.home.label,
|
||||
icon: const FlowySvg(FlowySvgs.m_home_unselected_m),
|
||||
activeIcon: const FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
label: _addLabel,
|
||||
icon: FlowySvg(FlowySvgs.m_home_add_m),
|
||||
BottomNavigationBarItem(
|
||||
key: BottomNavigationBarItemType.add.valueKey,
|
||||
label: BottomNavigationBarItemType.add.label,
|
||||
icon: const FlowySvg(FlowySvgs.m_home_add_m),
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
label: _notificationLabel,
|
||||
icon: _NotificationNavigationBarItemIcon(),
|
||||
activeIcon: _NotificationNavigationBarItemIcon(
|
||||
BottomNavigationBarItem(
|
||||
key: BottomNavigationBarItemType.notification.valueKey,
|
||||
label: BottomNavigationBarItemType.notification.label,
|
||||
icon: const _NotificationNavigationBarItemIcon(),
|
||||
activeIcon: const _NotificationNavigationBarItemIcon(
|
||||
isActive: true,
|
||||
),
|
||||
),
|
||||
@ -234,11 +252,11 @@ class _HomePageNavigationBar extends StatelessWidget {
|
||||
closePopupMenu();
|
||||
|
||||
final label = _items[bottomBarIndex].label;
|
||||
if (label == _addLabel) {
|
||||
if (label == BottomNavigationBarItemType.add.label) {
|
||||
// show an add dialog
|
||||
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
|
||||
return;
|
||||
} else if (label == _notificationLabel) {
|
||||
} else if (label == BottomNavigationBarItemType.notification.label) {
|
||||
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
|
||||
}
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@ -8,6 +9,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -260,7 +262,9 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
// title
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
widget.view.name,
|
||||
widget.view.name.isEmpty
|
||||
? LocaleKeys.menuAppHeader_defaultNewPageName.tr()
|
||||
: widget.view.name,
|
||||
fontSize: 16.0,
|
||||
figmaLineHeight: 20.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
@ -28,6 +26,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
@ -162,6 +161,8 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(
|
||||
builder: (context, styleState) => AppFlowyEditorPage(
|
||||
editorState: state.editorState!,
|
||||
// if the view's name is empty, focus on the title
|
||||
autoFocus: widget.view.name.isEmpty ? false : null,
|
||||
styleCustomizer: EditorStyleCustomizer(
|
||||
context: context,
|
||||
width: width,
|
||||
|
@ -1,8 +1,5 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';
|
||||
@ -21,6 +18,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
@ -382,7 +381,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
|
||||
dateOrReminderSlashMenuItem,
|
||||
photoGallerySlashMenuItem,
|
||||
fileSlashMenuItem,
|
||||
subPageSlashMenuItem,
|
||||
// disable subPageSlashMenuItem temporarily, enable it in version 0.7.2
|
||||
// subPageSlashMenuItem,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import 'package:appflowy/shared/google_fonts_extension.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -57,6 +58,9 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectionNotifier?.addListener(_unfocus);
|
||||
if (widget.view.name.isEmpty) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -161,12 +165,12 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
minFontSize: 18.0,
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintText: '',
|
||||
hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
scrollController: scrollController,
|
||||
@ -183,11 +187,15 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
const Duration(milliseconds: 300),
|
||||
() => _rename(name),
|
||||
),
|
||||
onSubmitted: (name) => Debounce.debounce(
|
||||
'rename',
|
||||
const Duration(milliseconds: 300),
|
||||
() => _rename(name),
|
||||
),
|
||||
onSubmitted: (name) {
|
||||
// focus on the document
|
||||
_createNewLine();
|
||||
Debounce.debounce(
|
||||
'rename',
|
||||
const Duration(milliseconds: 300),
|
||||
() => _rename(name),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -314,4 +322,35 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
scrollController.position.jumpTo(0);
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(name));
|
||||
}
|
||||
|
||||
Future<void> _createNewLine() async {
|
||||
focusNode.unfocus();
|
||||
|
||||
final selection = textEditingController.selection;
|
||||
final text = textEditingController.text;
|
||||
// split the text into two lines based on the cursor position
|
||||
final parts = [
|
||||
text.substring(0, selection.baseOffset),
|
||||
text.substring(selection.baseOffset),
|
||||
];
|
||||
textEditingController.text = parts[0];
|
||||
|
||||
final editorState = context.read<DocumentBloc>().state.editorState;
|
||||
if (editorState == null) {
|
||||
Log.info('editorState is null when creating new line');
|
||||
return;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNode([0], paragraphNode(text: parts[1]));
|
||||
await editorState.apply(transaction);
|
||||
|
||||
// update selection instead of using afterSelection in transaction,
|
||||
// because it will cause the cursor to jump
|
||||
await editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
// trigger the keyboard service.
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/chat.dart';
|
||||
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
|
||||
@ -15,6 +16,7 @@ import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PluginArgumentKeys {
|
||||
@ -297,6 +299,11 @@ extension ViewLayoutExtension on ViewLayoutPB {
|
||||
ViewLayoutPB.Document || ViewLayoutPB.Chat => false,
|
||||
_ => throw Exception('Unknown layout type'),
|
||||
};
|
||||
|
||||
String get defaultName => switch (this) {
|
||||
ViewLayoutPB.Document => '',
|
||||
_ => LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
};
|
||||
}
|
||||
|
||||
extension ViewFinder on List<ViewPB> {
|
||||
|
@ -862,13 +862,6 @@ void moveViewCrossSpace(
|
||||
return;
|
||||
}
|
||||
|
||||
final fromSection = spaceType == FolderSpaceType.public
|
||||
? ViewSectionPB.Private
|
||||
: ViewSectionPB.Public;
|
||||
final toSection = spaceType == FolderSpaceType.public
|
||||
? ViewSectionPB.Public
|
||||
: ViewSectionPB.Private;
|
||||
|
||||
final currentSpace = context.read<SpaceBloc>().state.currentSpace;
|
||||
if (currentSpace != null &&
|
||||
toSpace != null &&
|
||||
@ -884,14 +877,8 @@ void moveViewCrossSpace(
|
||||
from,
|
||||
toId,
|
||||
null,
|
||||
fromSection,
|
||||
toSection,
|
||||
),
|
||||
);
|
||||
context.read<ViewBloc>().add(
|
||||
ViewEvent.updateViewVisibility(
|
||||
from,
|
||||
spaceType == FolderSpaceType.public,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -876,7 +876,7 @@
|
||||
"itemThree": "5 GB",
|
||||
"itemFour": "yes",
|
||||
"itemFive": "yes",
|
||||
"itemSix": "100 lifetime",
|
||||
"itemSix": "20 lifetime",
|
||||
"itemFileUpload": "Up to 7 MB",
|
||||
"intelligentSearch": "Intelligent search"
|
||||
},
|
||||
@ -2696,4 +2696,4 @@
|
||||
"refreshNote": "After successful upgrade, click <refresh/> to activate your new features.",
|
||||
"refresh": "here"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -854,7 +854,7 @@
|
||||
"itemThree": "5 GB",
|
||||
"itemFour": "はい",
|
||||
"itemFive": "はい",
|
||||
"itemSix": "100回のライフタイムレスポンス",
|
||||
"itemSix": "20回のライフタイムレスポンス",
|
||||
"itemFileUpload": "最大7 MB",
|
||||
"intelligentSearch": "インテリジェント検索"
|
||||
},
|
||||
@ -2546,4 +2546,4 @@
|
||||
"zoomIn": "ズームイン",
|
||||
"zoomOut": "ズームアウト"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -817,7 +817,7 @@
|
||||
"itemThree": "5 GB",
|
||||
"itemFour": "evet",
|
||||
"itemFive": "evet",
|
||||
"itemSix": "100 ömür boyu",
|
||||
"itemSix": "20 ömür boyu",
|
||||
"itemSeven": " "
|
||||
},
|
||||
"proLabels": {
|
||||
@ -2221,4 +2221,4 @@
|
||||
"signInError": "Oturum açma hatası",
|
||||
"login": "Kaydolun veya giriş yapın"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -856,7 +856,7 @@
|
||||
"itemThree": "5 ГБ",
|
||||
"itemFour": "так",
|
||||
"itemFive": "так",
|
||||
"itemSix": "100 років життя",
|
||||
"itemSix": "20 років життя",
|
||||
"itemFileUpload": "До 7 Мб",
|
||||
"intelligentSearch": "Інтелектуальний пошук"
|
||||
},
|
||||
@ -2585,4 +2585,4 @@
|
||||
"uploadFailedDescription": "Не вдалося завантажити файл",
|
||||
"uploadingDescription": "Файл завантажується"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -854,7 +854,7 @@
|
||||
"itemThree": "5 GB",
|
||||
"itemFour": "Đúng",
|
||||
"itemFive": "Đúng",
|
||||
"itemSix": "100 trọn đời",
|
||||
"itemSix": "20 trọn đời",
|
||||
"intelligentSearch": "Tìm kiếm thông minh"
|
||||
},
|
||||
"proLabels": {
|
||||
@ -2505,4 +2505,4 @@
|
||||
"uploadFailedDescription": "Tải tệp lên không thành công",
|
||||
"uploadingDescription": "Tập tin đang được tải lên"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user