Repository: pichillilorenzo/flutter_browser_app
Branch: master
Commit: 2377098d015c
Files: 129
Total size: 600.0 KB
Directory structure:
gitextract_n9elqeox/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── BUG_REPORT.md
│ │ └── FEATURE_REQUEST.md
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .metadata
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android/
│ ├── .gitignore
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── debug/
│ │ │ └── AndroidManifest.xml
│ │ ├── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin/
│ │ │ │ └── com/
│ │ │ │ └── pichillilorenzo/
│ │ │ │ └── flutter_browser/
│ │ │ │ └── MainActivity.kt
│ │ │ └── res/
│ │ │ ├── drawable/
│ │ │ │ ├── launch_background.xml
│ │ │ │ ├── scrollbar_vertical_thumb.xml
│ │ │ │ └── scrollbar_vertical_track.xml
│ │ │ ├── drawable-v21/
│ │ │ │ └── launch_background.xml
│ │ │ ├── values/
│ │ │ │ └── styles.xml
│ │ │ └── values-night/
│ │ │ └── styles.xml
│ │ └── profile/
│ │ └── AndroidManifest.xml
│ ├── build.gradle
│ ├── gradle/
│ │ └── wrapper/
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ └── settings.gradle
├── ios/
│ ├── .gitignore
│ ├── Flutter/
│ │ ├── AppFrameworkInfo.plist
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Podfile
│ ├── Runner/
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── LaunchImage.imageset/
│ │ │ ├── Contents.json
│ │ │ └── README.md
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ └── Runner-Bridging-Header.h
│ ├── Runner.xcodeproj/
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace/
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata/
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ └── Runner.xcscheme
│ └── Runner.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ ├── IDEWorkspaceChecks.plist
│ └── WorkspaceSettings.xcsettings
├── lib/
│ ├── animated_flutter_browser_logo.dart
│ ├── app_bar/
│ │ ├── browser_app_bar.dart
│ │ ├── certificates_info_popup.dart
│ │ ├── custom_app_bar_wrapper.dart
│ │ ├── desktop_app_bar.dart
│ │ ├── find_on_page_app_bar.dart
│ │ ├── tab_viewer_app_bar.dart
│ │ ├── url_info_popup.dart
│ │ └── webview_tab_app_bar.dart
│ ├── browser.dart
│ ├── custom_image.dart
│ ├── custom_popup_dialog.dart
│ ├── custom_popup_menu_item.dart
│ ├── empty_tab.dart
│ ├── javascript_console_result.dart
│ ├── long_press_alert_dialog.dart
│ ├── main.dart
│ ├── material_transparent_page_route.dart
│ ├── models/
│ │ ├── browser_model.dart
│ │ ├── favorite_model.dart
│ │ ├── search_engine_model.dart
│ │ ├── web_archive_model.dart
│ │ ├── webview_model.dart
│ │ └── window_model.dart
│ ├── multiselect_dialog.dart
│ ├── pages/
│ │ ├── developers/
│ │ │ ├── javascript_console.dart
│ │ │ ├── main.dart
│ │ │ ├── network_info.dart
│ │ │ └── storage_manager.dart
│ │ └── settings/
│ │ ├── android_settings.dart
│ │ ├── cross_platform_settings.dart
│ │ ├── ios_settings.dart
│ │ └── main.dart
│ ├── popup_menu_actions.dart
│ ├── project_info_popup.dart
│ ├── tab_popup_menu_actions.dart
│ ├── tab_viewer.dart
│ ├── tab_viewer_popup_menu_actions.dart
│ ├── util.dart
│ └── webview_tab.dart
├── macos/
│ ├── .gitignore
│ ├── Flutter/
│ │ ├── Flutter-Debug.xcconfig
│ │ ├── Flutter-Release.xcconfig
│ │ └── GeneratedPluginRegistrant.swift
│ ├── Podfile
│ ├── Runner/
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets/
│ │ │ └── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Base.lproj/
│ │ │ └── MainMenu.xib
│ │ ├── Configs/
│ │ │ ├── AppInfo.xcconfig
│ │ │ ├── Debug.xcconfig
│ │ │ ├── Release.xcconfig
│ │ │ └── Warnings.xcconfig
│ │ ├── DebugProfile.entitlements
│ │ ├── Info.plist
│ │ ├── MainFlutterWindow.swift
│ │ └── Release.entitlements
│ ├── Runner.xcodeproj/
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace/
│ │ │ └── xcshareddata/
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ ├── RunnerTests/
│ │ └── RunnerTests.swift
│ └── packaging/
│ └── dmg/
│ └── make_config.yaml
├── pubspec.yaml
├── test/
│ └── widget_test.dart
└── windows/
├── .gitignore
├── CMakeLists.txt
├── flutter/
│ ├── CMakeLists.txt
│ ├── generated_plugin_registrant.cc
│ ├── generated_plugin_registrant.h
│ └── generated_plugins.cmake
├── packaging/
│ └── msix/
│ └── make_config.yaml
└── runner/
├── CMakeLists.txt
├── Runner.rc
├── flutter_window.cpp
├── flutter_window.h
├── main.cpp
├── resource.h
├── runner.exe.manifest
├── utils.cpp
├── utils.h
├── win32_window.cpp
└── win32_window.h
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://www.paypal.me/LorenzoPichilli'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.md
================================================
---
name: Bug report
about: Something is crashing or not working as intended
labels: bug
---
## Environment
| Technology | Version |
| -------------------- | ------------- |
| Flutter version | |
| App version | |
| Android version | |
| iOS version | |
| Xcode version | |
Device information:
## Description
**Expected behavior:**
**Current behavior:**
## Steps to reproduce
1. This
2. Than that
3. Then
## Images
## Stacktrace/Logcat
================================================
FILE: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
================================================
---
name: Feature request
about: Suggest an idea for this project
---
## Environment
**Flutter version:**
**App version:**
**Android version:**
**iOS version:**
**Xcode version:**
**Device information:**
## Description
**What you'd like to happen:**
**Alternatives you've considered:**
**Images:**
================================================
FILE: .github/workflows/main.yml
================================================
name: "Build & Release"
on:
push:
tags:
- "v*"
jobs:
build-mac-ios-android:
runs-on: macos-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: master
- name: Download Android keystore
id: android_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: keystore.jks
encodedString: ${{ secrets.KEYSTORE_BASE64 }}
- name: Create key.properties
run: |
echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties
echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: "17"
cache: 'gradle'
- name: Flutter action
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.x'
cache: true
- name: Restore packages
run: |
flutter pub get
- name: Install appdmg
run: npm install -g appdmg
- name: Install flutter_distributor
run: dart pub global activate flutter_distributor
- name: Build APK
run: |
flutter build apk --release --split-per-abi
- name: Upload APK to Artifacts
uses: actions/upload-artifact@v3
with:
name: android
path: |
build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
build/app/outputs/flutter-apk/app-x86_64-release.apk
- name: Build IPA
run: |
flutter build ios --release --no-codesign
- name: Create IPA
run: |
mkdir build/ios/iphoneos/Payload
cp -R build/ios/iphoneos/Runner.app build/ios/iphoneos/Payload/Runner.app
cd build/ios/iphoneos/
zip -q -r ios_no_sign.ipa Payload
cd ../../..
- name: Upload IPA to Artifacts
uses: actions/upload-artifact@v3
with:
name: ios
path: |
build/ios/iphoneos/ios_no_sign.ipa
- name: Build MacOS
run: |
flutter_distributor package --platform macos --targets dmg,zip --skip-clean
- name: Upload MacOS to Artifacts
uses: actions/upload-artifact@v4
with:
name: mac
path: |
dist/*/*.dmg
dist/*/*.zip
- name: Extract version from pubspec.yaml
id: yq
run: |
yq -r '.version' 'pubspec.yaml'
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
name: "${{ steps.yq.outputs.result }}"
token: ${{ secrets.TOKEN }}
files: |
build/app/outputs/flutter-apk/app-x86_64-release.apk
build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
build/ios/iphoneos/ios_no_sign.ipa
dist/*/*.dmg
dist/*/*.zip
- run: echo "🍏 This job's status is ${{ job.status }}."
build-windows:
runs-on: windows-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: master
- name: Install yq command line utility
run: choco install yq
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.24.x"
cache: true
- name: Restore Packages
run: |
flutter pub get
- name: Install flutter_distributor
run: dart pub global activate flutter_distributor
- name: Build Windows
run: |
flutter_distributor package --platform windows --targets msix,zip --skip-clean
- name: Upload Windows APP to Artifacts
uses: actions/upload-artifact@v4
with:
name: windows
path: |
dist/*/*.msix
dist/*/*.zip
- name: Extract version from pubspec.yaml
id: yq
run: |
yq -r '.version' 'pubspec.yaml'
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
name: "${{ steps.yq.outputs.result }}"
token: ${{ secrets.TOKEN }}
files: |
dist/*/*.msix
dist/*/*.zip
- run: echo "🍏 Windows job's status is ${{ job.status }}."
================================================
FILE: .gitignore
================================================
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
.fvm/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
================================================
FILE: .metadata
================================================
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: macos
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: windows
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2020 Lorenzo Pichilli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Flutter Browser App

A Full-Featured Mobile and Desktop Browser App (such as the Google Chrome mobile browser) created using Flutter and the features offered by the [flutter_inappwebview](https://github.com/pichillilorenzo/flutter_inappwebview) plugin.
It is available on the **Google Play Store** at [https://play.google.com/store/apps/details?id=com.pichillilorenzo.flutter_browser](https://play.google.com/store/apps/details?id=com.pichillilorenzo.flutter_browser)
For Desktop builds, check the [Releases](https://github.com/pichillilorenzo/flutter_browser_app/releases) page.
## Introduction
**Old article**: [Creating a Full-Featured Browser using WebViews in Flutter](https://medium.com/flutter-community/creating-a-full-featured-browser-using-webviews-in-flutter-9c8f2923c574?source=friends_link&sk=55fc8267f351082aa9e73ced546f6bcb).
**OLD**: Check out also the article that introduces the [flutter_inappwebview](https://github.com/pichillilorenzo/flutter_inappwebview) plugin here: [InAppWebView: The Real Power of WebViews in Flutter](https://medium.com/flutter-community/inappwebview-the-real-power-of-webviews-in-flutter-c6d52374209d?source=friends_link&sk=cb74487219bcd85e610a670ee0b447d0).
## Features
- **Multi-Window Support on Desktop**;
- **WebView Tab**, with custom on long-press link/image preview, and how to move from one tab to another without losing the WebView state;
- **Browser App Bar** with the current URL and all popup menu actions such as opening a new tab, a new incognito tab, saving the current URL to the favorite list, saving a page to offline usage, viewing the SSL Certificate used by the website, enable Desktop Mode, etc. (features similar to the Google Chrome App);
- **Developer console**, where you can execute JavaScript code, see some network info, manage the browser storage such as cookies, window.localStorage, etc;
- **Settings page**, where you can update the browser general settings and enable/disable all the features offered by the flutter_inappwebview for each WebView Tab, such as enabling/disabling JavaScript, caching, scrollbars, setting custom user-agent, etc., and all the Android and iOS-specific features;
- **Save** and **restore** the current Browser state.
### Final Result
**Old video**: [Flutter Browser App Final Result](https://drive.google.com/file/d/1wE2yUGwjNBiUy72GOjPIYyDXYQn3ewYn/view?usp=sharing).
If you found this useful and you like the [flutter_inappwebview](https://github.com/pichillilorenzo/flutter_inappwebview) plugin and this App project, give a star to these projects, thanks!
================================================
FILE: analysis_options.yaml
================================================
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
================================================
FILE: android/.gitignore
================================================
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
================================================
FILE: android/app/build.gradle
================================================
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
compileSdk 34
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "com.pichillilorenzo.flutter_browser"
minSdkVersion flutter.minSdkVersion
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
ndk {
debugSymbolLevel 'FULL'
}
}
}
namespace 'com.pichillilorenzo.flutter_browser'
lint {
disable 'InvalidPackage'
}
}
flutter {
source '../..'
}
dependencies {
implementation 'com.google.android.material:material:1.12.0'
def multidex_version = "2.0.1"
implementation "androidx.multidex:multidex:$multidex_version"
}
================================================
FILE: android/app/src/debug/AndroidManifest.xml
================================================
================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: android/app/src/main/kotlin/com/pichillilorenzo/flutter_browser/MainActivity.kt
================================================
package com.pichillilorenzo.flutter_browser
import android.app.SearchManager
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.speech.RecognizerResultsIntent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: FlutterActivity() {
private var url: String? = null;
//private var headers: Map? = null;
private val CHANNEL = "com.pichillilorenzo.flutter_browser.intent_data"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Log.d("intent URI", intent.toUri(0));
var url: String? = null
//var headers: Map? = null
val action = intent.action
if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS == action) {
return
}
if (Intent.ACTION_VIEW == action) {
val data: Uri? = intent.data
if (data != null) url = data.toString()
} else if (Intent.ACTION_SEARCH == action || MediaStore.INTENT_ACTION_MEDIA_SEARCH == action
|| Intent.ACTION_WEB_SEARCH == action) {
url = intent.getStringExtra(SearchManager.QUERY)
}
// if (url != null && url.startsWith("http")) {
// val pairs = intent
// .getBundleExtra(Browser.EXTRA_HEADERS)
// if (pairs != null && !pairs.isEmpty) {
// val iter: Iterator = pairs.keySet().iterator()
// headers = HashMap()
// while (iter.hasNext()) {
// val key = iter.next()
// headers.put(key, pairs.getString(key)!!)
// }
// }
// }
this.url = url
//this.headers = headers
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
val methodName = call.method
if (methodName == "getIntentData") {
// val data = ArrayList();
// data.add(url)
// data.add(headers)
result.success(url)
this.url = null;
//this.headers = null;
}
}
}
}
================================================
FILE: android/app/src/main/res/drawable/launch_background.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/scrollbar_vertical_thumb.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/scrollbar_vertical_track.xml
================================================
================================================
FILE: android/app/src/main/res/drawable-v21/launch_background.xml
================================================
================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
================================================
FILE: android/app/src/main/res/values-night/styles.xml
================================================
================================================
FILE: android/app/src/profile/AndroidManifest.xml
================================================
================================================
FILE: android/build.gradle
================================================
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
================================================
FILE: android/gradle.properties
================================================
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
================================================
FILE: android/settings.gradle
================================================
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '7.4.2' apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"
================================================
FILE: ios/.gitignore
================================================
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
================================================
FILE: ios/Flutter/AppFrameworkInfo.plist
================================================
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
App
CFBundleIdentifier
io.flutter.flutter.app
CFBundleInfoDictionaryVersion
6.0
CFBundleName
App
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
1.0
MinimumOSVersion
12.0
================================================
FILE: ios/Flutter/Debug.xcconfig
================================================
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
================================================
FILE: ios/Flutter/Release.xcconfig
================================================
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
================================================
FILE: ios/Podfile
================================================
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
================================================
FILE: ios/Runner/AppDelegate.swift
================================================
import UIKit
import Flutter
import flutter_downloader
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
FlutterDownloaderPlugin.setPluginRegistrantCallback(registerPlugins)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
private func registerPlugins(registry: FlutterPluginRegistry) {
if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
}
}
================================================
FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
================================================
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
================================================
FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard
================================================
================================================
FILE: ios/Runner/Base.lproj/Main.storyboard
================================================
================================================
FILE: ios/Runner/Info.plist
================================================
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
FlutterBrowser
CFBundlePackageType
APPL
CFBundleShortVersionString
$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
NSAppTransportSecurity
NSAllowsArbitraryLoads
NSAllowsArbitraryLoadsInWebContent
NSAllowsLocalNetworking
NSBonjourServices
_dartobservatory._tcp
NSCameraUsageDescription
$(PRODUCT_NAME) camera use
NSMicrophoneUsageDescription
$(PRODUCT_NAME) microphone use
UIApplicationSupportsIndirectInputEvents
UIBackgroundModes
fetch
remote-notification
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
================================================
FILE: ios/Runner/Runner-Bridging-Header.h
================================================
#import "GeneratedPluginRegistrant.h"
================================================
FILE: ios/Runner.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
420340CC0D9D0D1B41D8C7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65580FF38E583F75628F4B2B /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
3D42713954B5CD677C63D861 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
58603F292996258C3A0A1BB4 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
65580FF38E583F75628F4B2B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
9BF35EFC29634C4D7E14C52D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
420340CC0D9D0D1B41D8C7D1 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
04C19C2BB5F0C41EB406429B /* Frameworks */ = {
isa = PBXGroup;
children = (
65580FF38E583F75628F4B2B /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
B49E7B33350EC7421B2098A9 /* Pods */,
04C19C2BB5F0C41EB406429B /* Frameworks */,
);
sourceTree = "";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
);
name = "Supporting Files";
sourceTree = "";
};
B49E7B33350EC7421B2098A9 /* Pods */ = {
isa = PBXGroup;
children = (
58603F292996258C3A0A1BB4 /* Pods-Runner.debug.xcconfig */,
3D42713954B5CD677C63D861 /* Pods-Runner.release.xcconfig */,
9BF35EFC29634C4D7E14C52D /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
13F2619FF9F3F4C52B0076CA /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
37FF70F36850709A3BFADFDB /* [CP] Embed Pods Frameworks */,
ADCA64C961EA4FED14CC71A2 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
13F2619FF9F3F4C52B0076CA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
37FF70F36850709A3BFADFDB /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
ADCA64C961EA4FED14CC71A2 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PFP8UV45Y6;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.pichillilorenzo.flutter-browser-app2";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PFP8UV45Y6;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.pichillilorenzo.flutter-browser-app2";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PFP8UV45Y6;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.pichillilorenzo.flutter-browser-app2";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
PreviewsEnabled
================================================
FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
================================================
================================================
FILE: ios/Runner.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
PreviewsEnabled
================================================
FILE: lib/animated_flutter_browser_logo.dart
================================================
import 'package:flutter/material.dart';
class AnimatedFlutterBrowserLogo extends StatefulWidget {
final Duration animationDuration;
final double size;
const AnimatedFlutterBrowserLogo({
super.key,
this.animationDuration = const Duration(milliseconds: 1000),
this.size = 100.0,
});
@override
State createState() => _AnimatedFlutterBrowserLogoState();
}
class _AnimatedFlutterBrowserLogoState extends State
with TickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(duration: widget.animationDuration, vsync: this);
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: Tween(begin: 0.75, end: 2.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut)),
child: SizedBox(
height: widget.size,
width: widget.size,
child: const CircleAvatar(
backgroundImage: AssetImage("assets/icon/icon.png")),
),
);
}
}
================================================
FILE: lib/app_bar/browser_app_bar.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_browser/app_bar/desktop_app_bar.dart';
import 'package:flutter_browser/app_bar/find_on_page_app_bar.dart';
import 'package:flutter_browser/app_bar/webview_tab_app_bar.dart';
import 'package:flutter_browser/util.dart';
class BrowserAppBar extends StatefulWidget implements PreferredSizeWidget {
BrowserAppBar({super.key})
: preferredSize =
Size.fromHeight(Util.isMobile() ? kToolbarHeight : 90.0);
@override
State createState() => _BrowserAppBarState();
@override
final Size preferredSize;
}
class _BrowserAppBarState extends State {
bool _isFindingOnPage = false;
@override
Widget build(BuildContext context) {
final List children = [];
if (Util.isDesktop()) {
children.add(const DesktopAppBar());
}
children.add(_isFindingOnPage
? FindOnPageAppBar(
hideFindOnPage: () {
setState(() {
_isFindingOnPage = false;
});
},
)
: WebViewTabAppBar(
showFindOnPage: () {
setState(() {
_isFindingOnPage = true;
});
},
));
return Column(
children: children,
);
}
}
================================================
FILE: lib/app_bar/certificates_info_popup.dart
================================================
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_browser/models/webview_model.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:collection/collection.dart';
import 'package:url_launcher/url_launcher.dart';
class CertificateInfoPopup extends StatefulWidget {
const CertificateInfoPopup({super.key});
@override
State createState() => _CertificateInfoPopupState();
}
class _CertificateInfoPopupState extends State {
final List _otherCertificates = [];
X509Certificate? _topMainCertificate;
X509Certificate? _selectedCertificate;
@override
Widget build(BuildContext context) {
return _build();
}
Widget _build() {
if (_topMainCertificate == null) {
var webViewModel = Provider.of(context, listen: true);
return FutureBuilder(
future: webViewModel.webViewController?.getCertificate(),
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return Container();
}
SslCertificate sslCertificate = snapshot.data as SslCertificate;
_topMainCertificate = sslCertificate.x509Certificate;
_selectedCertificate = _topMainCertificate!;
return FutureBuilder(
future: _getOtherCertificatesFromTopMain(
_otherCertificates, _topMainCertificate!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return _buildCertificatesInfoAlertDialog();
}
return Center(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(2.5))),
padding: const EdgeInsets.all(25.0),
width: 100.0,
height: 100.0,
child: const CircularProgressIndicator(
strokeWidth: 4.0,
),
),
);
},
);
},
);
} else {
return _buildCertificatesInfoAlertDialog();
}
}
Future _getOtherCertificatesFromTopMain(
List otherCertificates,
X509Certificate x509certificate) async {
var authorityInfoAccess = x509certificate.authorityInfoAccess;
if (authorityInfoAccess != null && authorityInfoAccess.infoAccess != null) {
for (var i = 0; i < authorityInfoAccess.infoAccess!.length; i++) {
try {
var caIssuerUrl = authorityInfoAccess
.infoAccess![i].location; // [OID.caIssuers.toValue()];
HttpClientRequest request =
await HttpClient().getUrl(Uri.parse(caIssuerUrl));
HttpClientResponse response = await request.close();
var certData = Uint8List.fromList(await response.first);
var cert = X509Certificate.fromData(data: certData);
otherCertificates.add(cert);
await _getOtherCertificatesFromTopMain(otherCertificates, cert);
} catch (e) {
log(e.toString());
}
}
}
var cRLDistributionPoints = x509certificate.cRLDistributionPoints;
if (cRLDistributionPoints != null && cRLDistributionPoints.crls != null) {
for (var i = 0; i < cRLDistributionPoints.crls!.length; i++) {
var crlUrl = cRLDistributionPoints.crls![i];
try {
HttpClientRequest request =
await HttpClient().getUrl(Uri.parse(crlUrl));
HttpClientResponse response = await request.close();
var certData = Uint8List.fromList(await response.first);
var cert = X509Certificate.fromData(data: certData);
otherCertificates.add(cert);
await _getOtherCertificatesFromTopMain(otherCertificates, cert);
} catch (e) {
log(e.toString());
}
}
}
}
AlertDialog _buildCertificatesInfoAlertDialog() {
var webViewModel = Provider.of(context, listen: true);
var url = webViewModel.url;
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.all(Radius.circular(5.0))),
padding: const EdgeInsets.all(5.0),
child: const Icon(
Icons.lock,
color: Colors.white,
size: 20.0,
),
),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
url?.host ?? "",
style: const TextStyle(
fontSize: 16.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 15.0,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
"Flutter Browser has verified that ${_topMainCertificate?.issuer(dn: ASN1DistinguishedNames.COMMON_NAME)} has emitted the web site certificate.",
softWrap: true,
style: const TextStyle(fontSize: 12.0),
),
),
],
),
const SizedBox(
height: 15.0,
),
RichText(
text: TextSpan(
text: "Certificate info",
style: const TextStyle(
color: Colors.blue, fontSize: 12.0),
recognizer: TapGestureRecognizer()
..onTap = () {
showDialog(
context: context,
builder: (context) {
List certificates = [
_topMainCertificate!
];
certificates.addAll(_otherCertificates);
return AlertDialog(
content: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context)
.size
.width /
2.5),
child: StatefulBuilder(
builder: (context, setState) {
List<
DropdownMenuItem<
X509Certificate>>
dropdownMenuItems = [];
for (var certificate
in certificates) {
var name = _findCommonName(
x509certificate:
certificate,
isSubject: true) ??
_findOrganizationName(
x509certificate:
certificate,
isSubject: true) ??
"";
if (name != "") {
dropdownMenuItems.add(
DropdownMenuItem<
X509Certificate>(
value: certificate,
child: Text(name),
));
}
}
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Certificate Viewer",
style: TextStyle(
fontSize: 24.0,
color: Colors.black,
fontWeight:
FontWeight.bold),
),
const SizedBox(
height: 15.0,
),
DropdownButton<
X509Certificate>(
isExpanded: true,
onChanged: (value) {
setState(() {
_selectedCertificate =
value;
});
},
value: _selectedCertificate,
items: dropdownMenuItems,
),
const SizedBox(
height: 15.0,
),
Flexible(
child:
SingleChildScrollView(
child: _buildCertificateInfo(
_selectedCertificate!),
),
),
],
);
},
)),
);
},
);
}),
)
],
),
),
),
],
)
],
),
);
}
Widget _buildCertificateInfo(X509Certificate x509certificate) {
var issuedToSection = _buildIssuedToSection(x509certificate);
var issuedBySection = _buildIssuedBySection(x509certificate);
var validityPeriodSection = _buildValidityPeriodSection(x509certificate);
var publicKeySection = _buildPublicKeySection(x509certificate);
var fingerprintSection = _buildFingerprintSection(x509certificate);
var extensionSection = _buildExtensionSection(x509certificate);
var children = [];
children.addAll(issuedToSection);
children.addAll(issuedBySection);
children.addAll(validityPeriodSection);
children.addAll(publicKeySection);
children.addAll(fingerprintSection);
children.addAll(extensionSection);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
String? _findCountryName(
{required X509Certificate x509certificate, required bool isSubject}) {
try {
return (isSubject
? x509certificate.subject(dn: ASN1DistinguishedNames.COUNTRY_NAME)
: x509certificate.issuer(
dn: ASN1DistinguishedNames.COUNTRY_NAME)) ??
x509certificate.block1
?.findOid(oid: OID.countryName)
?.parent
?.sub
?.last
.value;
} catch (e) {
log(e.toString());
}
return null;
}
String? _findStateOrProvinceName(
{required X509Certificate x509certificate, required bool isSubject}) {
try {
return (isSubject
? x509certificate.subject(
dn: ASN1DistinguishedNames.STATE_OR_PROVINCE_NAME)
: x509certificate.issuer(
dn: ASN1DistinguishedNames.STATE_OR_PROVINCE_NAME)) ??
x509certificate.block1
?.findOid(oid: OID.stateOrProvinceName)
?.parent
?.sub
?.last
.value;
} catch (e) {
log(e.toString());
}
return null;
}
String? _findCommonName(
{required X509Certificate x509certificate, required bool isSubject}) {
try {
return (isSubject
? x509certificate.subject(dn: ASN1DistinguishedNames.COMMON_NAME)
: x509certificate.issuer(
dn: ASN1DistinguishedNames.COMMON_NAME)) ??
x509certificate.block1
?.findOid(oid: OID.commonName)
?.parent
?.sub
?.last
.value;
} catch (e) {
log(e.toString());
}
return null;
}
String? _findOrganizationName(
{required X509Certificate x509certificate, required bool isSubject}) {
try {
return (isSubject
? x509certificate.subject(
dn: ASN1DistinguishedNames.ORGANIZATION_NAME)
: x509certificate.issuer(
dn: ASN1DistinguishedNames.ORGANIZATION_NAME)) ??
x509certificate.block1
?.findOid(oid: OID.organizationName)
?.parent
?.sub
?.last
.value;
} catch (e) {
log(e.toString());
}
return null;
}
String? _findOrganizationUnitName(
{required X509Certificate x509certificate, required bool isSubject}) {
try {
return (isSubject
? x509certificate.subject(
dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME)
: x509certificate.issuer(
dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME)) ??
x509certificate.block1
?.findOid(oid: OID.organizationalUnitName)
?.parent
?.sub
?.last
.value;
} catch (e) {
log(e.toString());
}
return null;
}
List _buildIssuedToSection(X509Certificate x509certificate) {
var subjectCountryName =
_findCountryName(x509certificate: x509certificate, isSubject: true) ??
"";
var subjectStateOrProvinceName = _findStateOrProvinceName(
x509certificate: x509certificate, isSubject: true) ??
"";
var subjectCN =
_findCommonName(x509certificate: x509certificate, isSubject: true) ??
"";
var subjectO = _findOrganizationName(
x509certificate: x509certificate, isSubject: true) ??
"";
var subjectU = _findOrganizationUnitName(
x509certificate: x509certificate, isSubject: true) ??
"";
return [
const Text(
"ISSUED TO",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
const Text(
"Common Name (CN)",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
subjectCN,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Organization (O)",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
subjectO,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Organizational Unit (U)",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
subjectU,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Country",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
subjectCountryName,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"State/Province",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
subjectStateOrProvinceName,
style: const TextStyle(fontSize: 14.0),
),
];
}
List _buildIssuedBySection(X509Certificate x509certificate) {
var issuerCountryName =
_findCountryName(x509certificate: x509certificate, isSubject: false) ??
"";
var issuerStateOrProvinceName = _findStateOrProvinceName(
x509certificate: x509certificate, isSubject: false) ??
"";
var issuerCN =
_findCommonName(x509certificate: x509certificate, isSubject: false) ??
"";
var issuerO = _findOrganizationName(
x509certificate: x509certificate, isSubject: false) ??
"";
var issuerU = _findOrganizationUnitName(
x509certificate: x509certificate, isSubject: false) ??
"";
var serialNumber = x509certificate.serialNumber
?.map((byte) {
var hexValue = byte.toRadixString(16);
if (byte == 0 || hexValue.length == 1) {
hexValue = "0$hexValue";
}
return hexValue.toUpperCase();
})
.toList()
.join(":") ??
"";
var version =
x509certificate.version?.toString() ?? "";
var sigAlgName = x509certificate.sigAlgName ?? "";
return [
const SizedBox(
height: 15.0,
),
const Text(
"ISSUED BY",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
const Text(
"Common Name (CN)",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
issuerCN,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Organization (O)",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
issuerO,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Organizational Unit (U)",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
issuerU,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Country",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
issuerCountryName,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"State/Province",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
issuerStateOrProvinceName,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Serial Number",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
serialNumber,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Version",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
version,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Signature Algorithm",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
sigAlgName,
style: const TextStyle(fontSize: 14.0),
),
];
}
List _buildValidityPeriodSection(X509Certificate x509certificate) {
var issuedOnDate = x509certificate.notBefore != null
? DateFormat("dd MMM yyyy HH:mm:ss").format(x509certificate.notBefore!)
: "";
var expiresOnDate = x509certificate.notAfter != null
? DateFormat("dd MMM yyyy HH:mm:ss").format(x509certificate.notAfter!)
: "";
return [
const SizedBox(
height: 15.0,
),
const Text(
"VALIDITY PERIOD",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
const Text(
"Issued on date",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
issuedOnDate,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Expires on date",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
expiresOnDate,
style: const TextStyle(fontSize: 14.0),
),
];
}
List _buildPublicKeySection(X509Certificate x509certificate) {
var publicKey = x509certificate.publicKey;
var publicKeyAlg = "";
var publicKeyAlgParams = "";
if (publicKey != null) {
if (publicKey.algOid != null) {
publicKeyAlg =
"${OID.fromValue(publicKey.algOid)!.name()} ( ${publicKey.algOid} )";
}
if (publicKey.algParams != null) {
publicKeyAlgParams =
"${OID.fromValue(publicKey.algParams)!.name()} ( ${publicKey.algParams} )";
}
}
return [
const SizedBox(
height: 15.0,
),
const Text(
"PUBLIC KEY",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
const Text(
"Algorithm",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
publicKeyAlg,
style: const TextStyle(fontSize: 14.0),
),
const SizedBox(
height: 5.0,
),
const Text(
"Parameters",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
Text(
publicKeyAlgParams,
style: const TextStyle(fontSize: 14.0),
),
];
}
List _buildFingerprintSection(X509Certificate x509certificate) {
return [
const SizedBox(
height: 15.0,
),
const Text(
"FINGERPRINT",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
const Text(
"Fingerprint SHA-256",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
FutureBuilder(
future: x509certificate.encoded != null
? sha256.bind(Stream.value(x509certificate.encoded!)).first
: null,
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return const Text("");
}
Digest digest = snapshot.data as Digest;
return Text(
digest.bytes
.map((byte) {
var hexValue = byte.toRadixString(16);
if (byte == 0 || hexValue.length == 1) {
hexValue = "0$hexValue";
}
return hexValue.toUpperCase();
})
.toList()
.join(" "),
style: const TextStyle(fontSize: 14.0),
);
},
),
const SizedBox(
height: 5.0,
),
const Text(
"Fingerprint SHA-1",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
FutureBuilder(
future: sha1.bind(Stream.value(x509certificate.encoded!)).first,
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return const Text("");
}
Digest digest = snapshot.data as Digest;
return Text(
digest.bytes
.map((byte) {
var hexValue = byte.toRadixString(16);
if (byte == 0 || hexValue.length == 1) {
hexValue = "0$hexValue";
}
return hexValue.toUpperCase();
})
.toList()
.join(" "),
style: const TextStyle(fontSize: 14.0),
);
},
),
];
}
List _buildExtensionSection(X509Certificate x509certificate) {
var extensionSection = [
const SizedBox(
height: 15.0,
),
const Text(
"EXTENSIONS",
style: TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
extensionSection.addAll(_buildKeyUsageSection(x509certificate));
extensionSection.addAll(_buildBasicConstraints(x509certificate));
extensionSection.addAll(_buildExtendedKeyUsage(x509certificate));
extensionSection.addAll(_buildSubjectKeyIdentifier(x509certificate));
extensionSection.addAll(_buildAuthorityKeyIdentifier(x509certificate));
extensionSection.addAll(_buildCertificatePolicies(x509certificate));
extensionSection.addAll(_buildCRLDistributionPoints(x509certificate));
extensionSection.addAll(_buildAuthorityInfoAccess(x509certificate));
extensionSection.addAll(_buildSubjectAlternativeNames(x509certificate));
return extensionSection;
}
List _buildKeyUsageSection(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var keyUsage = x509certificate.keyUsage;
var keyUsageSection = [
const SizedBox(
height: 15.0,
),
Text(
"Key Usage ( ${OID.keyUsage.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
var keyUsageIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull((oid) => oid == OID.keyUsage.toValue()) !=
null
? "YES"
: "NO";
if (keyUsage.isNotEmpty) {
for (var i = 0; i < keyUsage.length; i++) {
if (keyUsage[i]) {
keyUsageSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: keyUsageIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Usage ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: KeyUsage.fromIndex(i)!.name(),
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
}
}
} else {
keyUsageSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return keyUsageSection;
}
List _buildBasicConstraints(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var basicConstraints = x509certificate.basicConstraints;
var basicConstraintsSection = [
const SizedBox(
height: 15.0,
),
Text(
"Basic Constraints ( ${OID.basicConstraints.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
var basicConstraintsIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull(
(oid) => oid == OID.basicConstraints.toValue()) !=
null
? "YES"
: "NO";
if (basicConstraints != null && basicConstraints.pathLenConstraint == -1) {
basicConstraintsSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: basicConstraintsIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: const TextSpan(children: [
TextSpan(
text: "Certificate Authority ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: "NO",
style: TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
} else {
basicConstraintsSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: basicConstraintsIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: const TextSpan(children: [
TextSpan(
text: "Certificate Authority ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: "YES",
style: TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Path Length Constraints ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: basicConstraints.toString(),
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
}
return basicConstraintsSection;
}
List _buildExtendedKeyUsage(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var extendedKeyUsage = x509certificate.extendedKeyUsage;
var extendedKeyUsageSection = [
const SizedBox(
height: 15.0,
),
Text(
"Extended Key Usage ( ${OID.extKeyUsage.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
var extendedKeyUsageIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull((oid) => oid == OID.extKeyUsage.toValue()) !=
null
? "YES"
: "NO";
if (extendedKeyUsage.isNotEmpty) {
for (var i = 0; i < extendedKeyUsage.length; i++) {
OID oid = OID.fromValue(extendedKeyUsage[i])!;
extendedKeyUsageSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: extendedKeyUsageIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: TextSpan(children: [
TextSpan(
text: "Purpose #${i + 1} ",
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: "${oid.name()} ( ${oid.toValue()} )",
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
}
} else {
extendedKeyUsageSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return extendedKeyUsageSection;
}
List _buildSubjectKeyIdentifier(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var subjectKeyIdentifier = x509certificate.subjectKeyIdentifier;
var subjectKeyIdentifierSection = [
const SizedBox(
height: 15.0,
),
Text(
"Subject Key Identifier ( ${OID.subjectKeyIdentifier.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
var subjectKeyIdentifierIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull(
(oid) => oid == OID.subjectKeyIdentifier.toValue()) !=
null
? "YES"
: "NO";
if (subjectKeyIdentifier?.value != null &&
subjectKeyIdentifier!.value!.isNotEmpty) {
var subjectKeyIdentifierToHexValue = subjectKeyIdentifier.value!
.map((byte) {
var hexValue = byte.toRadixString(16);
if (byte == 0 || hexValue.length == 1) {
hexValue = "0$hexValue";
}
return hexValue.toUpperCase();
})
.toList()
.join(" ");
subjectKeyIdentifierSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: subjectKeyIdentifierIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Key ID ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: subjectKeyIdentifierToHexValue,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
)
]);
} else {
subjectKeyIdentifierSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return subjectKeyIdentifierSection;
}
List _buildAuthorityKeyIdentifier(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var authorityKeyIdentifier = x509certificate.authorityKeyIdentifier;
var authorityKeyIdentifierSection = [
const SizedBox(
height: 15.0,
),
Text(
"Authority Key Identifier ( ${OID.authorityKeyIdentifier.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
var authorityKeyIdentifierIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull(
(oid) => oid == OID.authorityKeyIdentifier.toValue()) !=
null
? "YES"
: "NO";
if (authorityKeyIdentifier?.keyIdentifier != null &&
authorityKeyIdentifier!.keyIdentifier!.isNotEmpty) {
var authorityKeyIdentifierToHexValue =
authorityKeyIdentifier.keyIdentifier!
.map((byte) {
var hexValue = byte.toRadixString(16);
if (byte == 0 || hexValue.length == 1) {
hexValue = "0$hexValue";
}
return hexValue.toUpperCase();
})
.toList()
.join(" ");
authorityKeyIdentifierSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: authorityKeyIdentifierIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Key ID ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: authorityKeyIdentifierToHexValue,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
)
]);
} else {
authorityKeyIdentifierSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return authorityKeyIdentifierSection;
}
List _buildCertificatePolicies(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var certificatePolicies = x509certificate.certificatePolicies;
var certificatePoliciesIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull((oid) => oid == OID.extKeyUsage.toValue()) !=
null
? "YES"
: "NO";
var certificatePoliciesSection = [
const SizedBox(
height: 15.0,
),
Text(
"Certificate Policies ( ${OID.certificatePolicies.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: certificatePoliciesIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
];
if (certificatePolicies?.policies != null &&
certificatePolicies!.policies!.isNotEmpty) {
for (var i = 0; i < certificatePolicies.policies!.length; i++) {
OID? oid = OID.fromValue(certificatePolicies.policies![i].oid);
certificatePoliciesSection.addAll([
RichText(
text: TextSpan(children: [
TextSpan(
text: "ID policy num. ${i + 1} ",
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: (oid != null)
? "${oid.name()} ( ${oid.toValue()} )"
: "( ${certificatePolicies.policies![i].oid} )",
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
}
} else {
certificatePoliciesSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return certificatePoliciesSection;
}
List _buildCRLDistributionPoints(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var cRLDistributionPoints = x509certificate.cRLDistributionPoints;
var cRLDistributionPointsIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull(
(oid) => oid == OID.cRLDistributionPoints.toValue()) !=
null
? "YES"
: "NO";
var cRLDistributionPointsSection = [
const SizedBox(
height: 15.0,
),
Text(
"CRL Distribution Points ( ${OID.cRLDistributionPoints.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: cRLDistributionPointsIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
];
if (cRLDistributionPoints?.crls != null &&
cRLDistributionPoints!.crls!.isNotEmpty) {
for (var i = 0; i < cRLDistributionPoints.crls!.length; i++) {
cRLDistributionPointsSection.addAll([
RichText(
text: TextSpan(children: [
const TextSpan(
text: "URI ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: cRLDistributionPoints.crls![i],
style: const TextStyle(fontSize: 12.0, color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () async {
final crlUrl = cRLDistributionPoints.crls![i];
try {
Directory? directory =
await getExternalStorageDirectory();
await FlutterDownloader.enqueue(
url: crlUrl,
savedDir: directory!.path,
showNotification: true,
// show download progress in status bar (for Android)
openFileFromNotification:
true, // click on notification to open downloaded file (for Android)
);
} catch (e) {
final crlUri = Uri.tryParse(crlUrl);
if (crlUri != null && await canLaunchUrl(crlUri)) {
launchUrl(crlUri);
}
}
})
]),
),
]);
}
} else {
cRLDistributionPointsSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return cRLDistributionPointsSection;
}
List _buildAuthorityInfoAccess(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var authorityInfoAccess = x509certificate.authorityInfoAccess;
var authorityInfoAccessIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull(
(oid) => oid == OID.authorityInfoAccess.toValue()) !=
null
? "YES"
: "NO";
var authorityInfoAccessSection = [
const SizedBox(
height: 15.0,
),
Text(
"Authority Info Access ( ${OID.authorityInfoAccess.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: authorityInfoAccessIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
];
if (authorityInfoAccess?.infoAccess != null &&
authorityInfoAccess!.infoAccess!.isNotEmpty) {
for (var i = 0; i < authorityInfoAccess.infoAccess!.length; i++) {
var infoAccess = authorityInfoAccess.infoAccess![i];
var value = infoAccess.location;
var oid = OID.fromValue(infoAccess.method);
authorityInfoAccessSection.addAll([
RichText(
text: TextSpan(children: [
TextSpan(
text: "Method #${i + 1} ",
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: oid != null
? "${oid.name()} ( ${oid.toValue()} )"
: infoAccess.method,
style: const TextStyle(fontSize: 12.0, color: Colors.black),
)
]),
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "URI ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: value,
style: const TextStyle(fontSize: 12.0, color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () async {
Directory? directory =
await getExternalStorageDirectory();
await FlutterDownloader.enqueue(
url: value,
savedDir: directory!.path,
showNotification: true,
// show download progress in status bar (for Android)
openFileFromNotification:
true, // click on notification to open downloaded file (for Android)
);
})
]),
),
]);
}
} else {
authorityInfoAccessSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return authorityInfoAccessSection;
}
List _buildSubjectAlternativeNames(X509Certificate x509certificate) {
var criticalExtensionOIDs = x509certificate.criticalExtensionOIDs;
var subjectAlternativeNames = x509certificate.subjectAlternativeNames;
var subjectAlternativeNamesSection = [
const SizedBox(
height: 15.0,
),
Text(
"Subject Alternative Names ( ${OID.subjectAltName.toValue()} )",
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold),
),
];
var subjectAlternativeNamesIsCritical = criticalExtensionOIDs
.map((e) => e)
.firstWhereOrNull(
(oid) => oid == OID.subjectAltName.toValue()) !=
null
? "YES"
: "NO";
if (subjectAlternativeNames.isNotEmpty) {
subjectAlternativeNamesSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "Critical ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: subjectAlternativeNamesIsCritical,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
for (var subjectAlternativeName in subjectAlternativeNames) {
subjectAlternativeNamesSection.addAll([
const SizedBox(
height: 5.0,
),
RichText(
text: TextSpan(children: [
const TextSpan(
text: "DNS Name ",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Colors.black)),
TextSpan(
text: subjectAlternativeName,
style: const TextStyle(fontSize: 12.0, color: Colors.black))
]),
),
]);
}
} else {
subjectAlternativeNamesSection.addAll([
const SizedBox(
height: 5.0,
),
const Text(
"",
style: TextStyle(fontSize: 14.0),
),
]);
}
return subjectAlternativeNamesSection;
}
}
================================================
FILE: lib/app_bar/custom_app_bar_wrapper.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_browser/app_bar/desktop_app_bar.dart';
import 'package:flutter_browser/util.dart';
class CustomAppBarWrapper extends StatefulWidget implements PreferredSizeWidget {
final AppBar appBar;
CustomAppBarWrapper({super.key, required this.appBar})
: preferredSize = Util.isMobile()
? _PreferredAppBarSize(
kToolbarHeight, appBar.bottom?.preferredSize.height)
: Size.fromHeight(_PreferredAppBarSize(
kToolbarHeight, appBar.bottom?.preferredSize.height)
.height +
40);
@override
State createState() => _CustomAppBarWrapperState();
@override
final Size preferredSize;
}
class _CustomAppBarWrapperState extends State {
@override
Widget build(BuildContext context) {
final List children = [];
if (Util.isDesktop()) {
children.add(const DesktopAppBar(showTabs: false,));
} else {
return widget.appBar;
}
children.add(Flexible(child: widget.appBar));
return Column(
children: children,
);
}
}
class _PreferredAppBarSize extends Size {
_PreferredAppBarSize(this.toolbarHeight, this.bottomHeight)
: super.fromHeight(
(toolbarHeight ?? kToolbarHeight) + (bottomHeight ?? 0));
final double? toolbarHeight;
final double? bottomHeight;
}
================================================
FILE: lib/app_bar/desktop_app_bar.dart
================================================
import 'package:collection/collection.dart';
import 'package:context_menus/context_menus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:provider/provider.dart';
import 'package:window_manager_plus/window_manager_plus.dart';
import '../custom_image.dart';
import '../models/browser_model.dart';
import '../models/webview_model.dart';
import '../models/window_model.dart';
import '../util.dart';
import '../webview_tab.dart';
class DesktopAppBar extends StatefulWidget {
final bool showTabs;
const DesktopAppBar({super.key, this.showTabs = true});
@override
State createState() => _DesktopAppBarState();
}
class _DesktopAppBarState extends State {
@override
Widget build(BuildContext context) {
final windowModel = Provider.of(context, listen: true);
final tabSelectors = !widget.showTabs
? []
: windowModel.webViewTabs.map((webViewTab) {
final index = windowModel.webViewTabs.indexOf(webViewTab);
final currentIndex = windowModel.getCurrentTabIndex();
return Flexible(
flex: 1,
fit: FlexFit.loose,
child: IntrinsicHeight(
child: Row(
children: [
Expanded(
child: WebViewTabSelector(
tab: webViewTab, index: index)),
SizedBox(
height: 15,
child: VerticalDivider(
thickness: 1,
width: 1,
color: index == currentIndex - 1 ||
index == currentIndex
? Colors.transparent
: Colors.black45),
)
],
),
));
}).toList();
final windowActions = [];
if (!Util.isWindows()) {
windowActions.addAll([
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
WindowManagerPlus.current.close();
},
constraints: const BoxConstraints(
maxWidth: 13,
minWidth: 13,
maxHeight: 13,
minHeight: 13,
),
padding: EdgeInsets.zero,
style: ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: const WidgetStatePropertyAll(Colors.red),
iconColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.hovered)
? Colors.black45
: Colors.red,
)),
color: Colors.red,
icon: const Icon(
Icons.close,
size: 10,
)),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () async {
if (!(await WindowManagerPlus.current.isFullScreen())) {
WindowManagerPlus.current.minimize();
}
},
constraints: const BoxConstraints(
maxWidth: 13,
minWidth: 13,
maxHeight: 13,
minHeight: 13,
),
padding: EdgeInsets.zero,
style: ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: const WidgetStatePropertyAll(Colors.amber),
iconColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.hovered)
? Colors.black45
: Colors.amber,
)),
color: Colors.amber,
icon: const Icon(
Icons.remove,
size: 10,
)),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () async {
WindowManagerPlus.current
.setFullScreen(!(await WindowManagerPlus.current.isFullScreen()));
},
constraints: const BoxConstraints(
maxWidth: 13,
minWidth: 13,
maxHeight: 13,
minHeight: 13,
),
padding: EdgeInsets.zero,
style: ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: const WidgetStatePropertyAll(Colors.green),
iconColor: WidgetStateProperty.resolveWith(
(states) => states.contains(WidgetState.hovered)
? Colors.black45
: Colors.green,
)),
color: Colors.green,
icon: const Icon(
Icons.open_in_full,
size: 10,
)),
const SizedBox(
width: 8,
),
]);
}
final children = [
Container(
constraints:
BoxConstraints(maxWidth: MediaQuery.of(context).size.width - 100),
child: IntrinsicWidth(
child: Row(
children: [
...windowActions,
Flexible(
child: Container(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: tabSelectors.isNotEmpty
? tabSelectors
: [
const SizedBox(
height: 30,
)
],
),
)),
const SizedBox(
width: 5,
),
!widget.showTabs
? null
: IconButton(
onPressed: () {
_addNewTab();
},
constraints: const BoxConstraints(
maxWidth: 25,
minWidth: 25,
maxHeight: 25,
minHeight: 25,
),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.add,
size: 15,
color: Colors.white,
)),
].whereNotNull().toList().cast(),
),
),
),
Flexible(
child: MouseRegion(
hitTestBehavior: HitTestBehavior.opaque,
onEnter: (details) {
if (!Util.isWindows()) {
WindowManagerPlus.current.setMovable(true);
}
setState(() {});
},
onExit: (details) {
if (!Util.isWindows()) {
WindowManagerPlus.current.setMovable(false);
}
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onDoubleTap: () async {
await WindowManagerPlus.current.maximize();
},
child: !widget.showTabs
? const SizedBox(
height: 30,
width: double.infinity,
)
: ContextMenuRegion(
behavior: const [ContextMenuShowBehavior.secondaryTap],
contextMenu: GenericContextMenu(
buttonConfigs: [
ContextMenuButtonConfig(
"New Tab",
onPressed: () {
_addNewTab();
},
),
ContextMenuButtonConfig(
"Close All",
onPressed: () {
windowModel.closeAllTabs();
},
),
],
),
child: const SizedBox(
height: 30,
width: double.infinity,
)),
))),
!widget.showTabs
? null
: OpenTabsViewer(
webViewTabs: windowModel.webViewTabs,
),
].whereNotNull().toList();
return Container(
color: Theme.of(context).colorScheme.primary,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: children,
),
);
}
void _addNewTab() {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
final settings = browserModel.getSettings();
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: WebUri(settings.searchEngine.url)),
));
}
}
class WebViewTabSelector extends StatefulWidget {
final WebViewTab tab;
final int index;
const WebViewTabSelector({super.key, required this.tab, required this.index});
@override
State createState() => _WebViewTabSelectorState();
}
class _WebViewTabSelectorState extends State {
bool isHover = false;
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
final windowModel = Provider.of(context, listen: true);
final isCurrentTab = windowModel.getCurrentTabIndex() == widget.index;
final tab = widget.tab;
final url = tab.webViewModel.url;
var tabName = tab.webViewModel.title ?? url?.toString() ?? '';
if (tabName.isEmpty) {
tabName = 'New Tab';
}
final tooltipText =
'$tabName\n${(url?.host ?? '').isEmpty ? url?.toString() : url?.host}'
.trim();
final faviconUrl = tab.webViewModel.favicon != null
? tab.webViewModel.favicon!.url
: (url != null && ["http", "https"].contains(url.scheme)
? Uri.parse("${url.origin}/favicon.ico")
: null);
return MouseRegion(
onEnter: (event) {
setState(() {
isHover = true;
});
},
onExit: (event) {
setState(() {
isHover = false;
});
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
windowModel.showTab(widget.index);
},
child: ContextMenuRegion(
contextMenu: GenericContextMenu(
buttonConfigs: [
ContextMenuButtonConfig(
"Reload",
onPressed: () {
tab.webViewModel.webViewController?.reload();
},
),
ContextMenuButtonConfig(
"Duplicate",
onPressed: () {
if (tab.webViewModel.url != null) {
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: tab.webViewModel.url),
));
}
},
),
ContextMenuButtonConfig(
"Close",
onPressed: () {
windowModel.closeTab(widget.index);
},
),
],
),
child: Container(
height: 30,
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 250),
padding: const EdgeInsets.only(right: 5.0),
decoration: !isCurrentTab
? null
: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(5))),
child: Tooltip(
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.all(Radius.circular(5))),
richMessage: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Text(
tooltipText,
overflow: TextOverflow.ellipsis,
maxLines: 3,
style: const TextStyle(color: Colors.white),
),
)),
waitDuration: const Duration(milliseconds: 500),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
child: CustomImage(
url: faviconUrl, maxWidth: 20.0, height: 20.0),
),
Flexible(
child: Text(tabName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: TextStyle(
fontSize: 12,
color:
!isCurrentTab ? Colors.white : null))),
],
)),
IconButton(
onPressed: () {
windowModel.closeTab(widget.index);
},
constraints: const BoxConstraints(
maxWidth: 20,
minWidth: 20,
maxHeight: 20,
minHeight: 20,
),
padding: EdgeInsets.zero,
icon: Icon(
Icons.cancel,
color: !isCurrentTab ? Colors.white : null,
size: 15,
)),
],
),
),
)),
),
);
}
}
class OpenTabsViewer extends StatefulWidget {
final List webViewTabs;
const OpenTabsViewer({super.key, required this.webViewTabs});
@override
State createState() => _OpenTabsViewerState();
}
class _OpenTabsViewerState extends State {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4.0),
child: MenuAnchor(
builder: (context, controller, child) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
constraints: const BoxConstraints(
maxWidth: 25,
minWidth: 25,
maxHeight: 25,
minHeight: 25,
),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.keyboard_arrow_down,
size: 15,
color: Colors.white,
));
},
menuChildren: [
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 200,
),
child: TextFormField(
controller: _controller,
maxLines: 1,
style: Theme.of(context).textTheme.labelLarge,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'Search open tabs',
contentPadding: EdgeInsets.only(top: 15),
isDense: true,
),
onChanged: (value) {
setState(() {});
},
),
),
MenuItemButton(
onPressed: null,
child: Text(
widget.webViewTabs.isEmpty ? 'No tabs open' : 'Tabs open',
style: Theme.of(context).textTheme.labelLarge,
),
),
...(widget.webViewTabs.where(
(element) {
final search = _controller.text.toLowerCase().trim();
final containsInTitle = element.webViewModel.title
?.toLowerCase()
.contains(search) ??
false;
final containsInUrl = element.webViewModel.url
?.toString()
.toLowerCase()
.contains(search) ??
false;
return search.isEmpty || containsInTitle || containsInUrl;
},
).map((w) {
final url = w.webViewModel.url;
final title = (w.webViewModel.title ?? '').isNotEmpty
? w.webViewModel.title!
: 'New Tab';
var subtitle =
(url?.host ?? '').isEmpty ? url?.toString() : url?.host;
final diffTime =
DateTime.now().difference(w.webViewModel.lastOpenedTime);
var diffTimeSubtitle = 'now';
if (diffTime.inDays > 0) {
diffTimeSubtitle =
'${diffTime.inDays} ${diffTime.inDays == 1 ? 'day' : 'days'} ago';
} else if (diffTime.inMinutes > 0) {
diffTimeSubtitle = '${diffTime.inMinutes} min ago';
} else if (diffTime.inSeconds > 0) {
diffTimeSubtitle = '${diffTime.inSeconds} sec ago';
}
final faviconUrl = w.webViewModel.favicon != null
? w.webViewModel.favicon!.url
: (url != null && ["http", "https"].contains(url.scheme)
? Uri.parse("${url.origin}/favicon.ico")
: null);
return MenuItemButton(
onPressed: () {
final windowModel =
Provider.of(context, listen: false);
windowModel.showTab(windowModel.webViewTabs.indexOf(w));
},
leadingIcon: Container(
padding: const EdgeInsets.all(8),
child: CustomImage(url: faviconUrl, maxWidth: 15, height: 15),
),
trailingIcon: IconButton(
onPressed: () {
final windowModel =
Provider.of(context, listen: false);
windowModel.closeTab(widget.webViewTabs.indexOf(w));
},
constraints: const BoxConstraints(
maxWidth: 25,
minWidth: 25,
maxHeight: 25,
minHeight: 25,
),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.cancel,
size: 15,
)),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 250,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelMedium,
),
Row(
children: [
Flexible(
child: Text(
subtitle ?? '',
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.labelSmall,
)),
Text(
" - $diffTimeSubtitle",
style: Theme.of(context).textTheme.labelSmall,
)
],
)
].whereNotNull().toList(),
)),
);
}).toList())
].whereNotNull().toList(),
));
}
}
================================================
FILE: lib/app_bar/find_on_page_app_bar.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_browser/models/browser_model.dart';
import 'package:provider/provider.dart';
import '../models/window_model.dart';
class FindOnPageAppBar extends StatefulWidget {
final void Function()? hideFindOnPage;
const FindOnPageAppBar({super.key, this.hideFindOnPage});
@override
State createState() => _FindOnPageAppBarState();
}
class _FindOnPageAppBarState extends State {
final TextEditingController _finOnPageController = TextEditingController();
OutlineInputBorder outlineBorder = const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent, width: 0.0),
borderRadius: BorderRadius.all(
Radius.circular(50.0),
),
);
@override
void dispose() {
_finOnPageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final windowModel = Provider.of(context, listen: false);
final webViewModel = windowModel.getCurrentTab()?.webViewModel;
final findInteractionController = webViewModel?.findInteractionController;
return AppBar(
titleSpacing: 10.0,
title: SizedBox(
height: 40.0,
child: TextField(
onSubmitted: (value) {
findInteractionController?.findAll(find: value);
},
controller: _finOnPageController,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(10.0),
filled: true,
fillColor: Colors.white,
border: outlineBorder,
focusedBorder: outlineBorder,
enabledBorder: outlineBorder,
hintText: "Find on page ...",
hintStyle: const TextStyle(color: Colors.black54, fontSize: 16.0),
),
style: const TextStyle(color: Colors.black, fontSize: 16.0),
)),
actions: [
IconButton(
icon: const Icon(Icons.keyboard_arrow_up),
onPressed: () {
findInteractionController?.findNext(forward: false);
},
),
IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
onPressed: () {
findInteractionController?.findNext(forward: true);
},
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
findInteractionController?.clearMatches();
_finOnPageController.text = "";
if (widget.hideFindOnPage != null) {
widget.hideFindOnPage!();
}
},
),
],
);
}
}
================================================
FILE: lib/app_bar/tab_viewer_app_bar.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_browser/models/browser_model.dart';
import 'package:flutter_browser/models/webview_model.dart';
import 'package:flutter_browser/pages/settings/main.dart';
import 'package:flutter_browser/webview_tab.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:provider/provider.dart';
import '../custom_popup_menu_item.dart';
import '../models/window_model.dart';
import '../tab_viewer_popup_menu_actions.dart';
class TabViewerAppBar extends StatefulWidget implements PreferredSizeWidget {
const TabViewerAppBar({super.key})
: preferredSize = const Size.fromHeight(kToolbarHeight);
@override
State createState() => _TabViewerAppBarState();
@override
final Size preferredSize;
}
class _TabViewerAppBarState extends State {
GlobalKey tabInkWellKey = GlobalKey();
@override
Widget build(BuildContext context) {
return AppBar(
titleSpacing: 10.0,
leading: _buildAddTabButton(),
actions: _buildActionsMenu(),
);
}
Widget _buildAddTabButton() {
return IconButton(
icon: const Icon(Icons.add),
onPressed: () {
addNewTab();
},
);
}
List _buildActionsMenu() {
final browserModel = Provider.of(context, listen: true);
final windowModel = Provider.of(context, listen: true);
final settings = browserModel.getSettings();
return [
InkWell(
key: tabInkWellKey,
onTap: () {
if (windowModel.webViewTabs.isNotEmpty) {
browserModel.showTabScroller = !browserModel.showTabScroller;
} else {
browserModel.showTabScroller = false;
}
},
child: Padding(
padding: settings.homePageEnabled
? const EdgeInsets.only(
left: 20.0, top: 15.0, right: 10.0, bottom: 15.0)
: const EdgeInsets.only(
left: 10.0, top: 15.0, right: 10.0, bottom: 15.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 2.0),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(5.0)),
constraints: const BoxConstraints(minWidth: 25.0),
child: Center(
child: Text(
windowModel.webViewTabs.length.toString(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0),
)),
),
),
),
PopupMenuButton(
onSelected: _popupMenuChoiceAction,
itemBuilder: (popupMenuContext) {
var items = >[];
items.addAll(TabViewerPopupMenuActions.choices.map((choice) {
switch (choice) {
case TabViewerPopupMenuActions.NEW_TAB:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.add,
color: Colors.black,
)
]),
);
case TabViewerPopupMenuActions.NEW_INCOGNITO_TAB:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
MaterialCommunityIcons.incognito,
color: Colors.black,
)
]),
);
case TabViewerPopupMenuActions.CLOSE_ALL_TABS:
return CustomPopupMenuItem(
enabled: windowModel.webViewTabs.isNotEmpty,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.close,
color: Colors.black,
)
]),
);
case TabViewerPopupMenuActions.SETTINGS:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.settings,
color: Colors.grey,
)
]),
);
default:
return CustomPopupMenuItem(
value: choice,
child: Text(choice),
);
}
}).toList());
return items;
},
)
];
}
void _popupMenuChoiceAction(String choice) async {
switch (choice) {
case TabViewerPopupMenuActions.NEW_TAB:
Future.delayed(const Duration(milliseconds: 300), () {
addNewTab();
});
break;
case TabViewerPopupMenuActions.NEW_INCOGNITO_TAB:
Future.delayed(const Duration(milliseconds: 300), () {
addNewIncognitoTab();
});
break;
case TabViewerPopupMenuActions.CLOSE_ALL_TABS:
Future.delayed(const Duration(milliseconds: 300), () {
closeAllTabs();
});
break;
case TabViewerPopupMenuActions.SETTINGS:
Future.delayed(const Duration(milliseconds: 300), () {
goToSettingsPage();
});
break;
}
}
void addNewTab({WebUri? url}) {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
final settings = browserModel.getSettings();
url ??= settings.homePageEnabled && settings.customUrlHomePage.isNotEmpty
? WebUri(settings.customUrlHomePage)
: WebUri(settings.searchEngine.url);
browserModel.showTabScroller = false;
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: url),
));
}
void addNewIncognitoTab({WebUri? url}) {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
final settings = browserModel.getSettings();
url ??= settings.homePageEnabled && settings.customUrlHomePage.isNotEmpty
? WebUri(settings.customUrlHomePage)
: WebUri(settings.searchEngine.url);
browserModel.showTabScroller = false;
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: url, isIncognitoMode: true),
));
}
void closeAllTabs() {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
browserModel.showTabScroller = false;
windowModel.closeAllTabs();
}
void goToSettingsPage() {
Navigator.push(
context, MaterialPageRoute(builder: (context) => const SettingsPage()));
}
}
================================================
FILE: lib/app_bar/url_info_popup.dart
================================================
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_browser/app_bar/certificates_info_popup.dart';
import 'package:flutter_browser/models/webview_model.dart';
import 'package:provider/provider.dart';
import '../custom_popup_dialog.dart';
class UrlInfoPopup extends StatefulWidget {
final CustomPopupDialogPageRoute route;
final Duration transitionDuration;
final Function()? onWebViewTabSettingsClicked;
const UrlInfoPopup(
{super.key,
required this.route,
required this.transitionDuration,
this.onWebViewTabSettingsClicked});
@override
State createState() => _UrlInfoPopupState();
}
class _UrlInfoPopupState extends State {
var text1 = "Your connection to this website is not protected";
var text2 =
"You should not enter sensitive data on this site (e.g. passwords or credit cards) because they could be intercepted by malicious users.";
var showFullInfoUrl = false;
var defaultTextSpanStyle = const TextStyle(
color: Colors.black54,
fontSize: 12.5,
);
@override
Widget build(BuildContext context) {
var webViewModel = Provider.of(context, listen: true);
if (webViewModel.isSecure) {
text1 = "Your connection is protected";
text2 =
"Your sensitive data (e.g. passwords or credit card numbers) remains private when it is sent to this site.";
}
var url = webViewModel.url;
return SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatefulBuilder(
builder: (context, setState) {
return GestureDetector(
onTap: () {
setState(() {
showFullInfoUrl = !showFullInfoUrl;
});
},
child: Container(
padding: const EdgeInsets.only(bottom: 15.0),
constraints: const BoxConstraints(maxHeight: 100.0),
child: RichText(
maxLines: showFullInfoUrl ? null : 2,
overflow: showFullInfoUrl
? TextOverflow.clip
: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: url?.scheme,
style: defaultTextSpanStyle.copyWith(
color: webViewModel.isSecure
? Colors.green
: Colors.black54,
fontWeight: FontWeight.bold)),
TextSpan(
text: webViewModel.url?.toString() == "about:blank"
? ':'
: '://',
style: defaultTextSpanStyle),
TextSpan(
text: url?.host,
style: defaultTextSpanStyle.copyWith(
color: Colors.black)),
TextSpan(text: url?.path, style: defaultTextSpanStyle),
TextSpan(text: url?.query, style: defaultTextSpanStyle),
],
),
)),
);
},
),
Container(
padding: const EdgeInsets.only(bottom: 10.0),
child: Text(text1,
style: const TextStyle(
fontSize: 16.0,
)),
),
RichText(
text: TextSpan(
style: const TextStyle(fontSize: 12.0, color: Colors.black87),
children: [
TextSpan(
text: "$text2 ",
),
TextSpan(
text: "Details",
style: const TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () async {
Navigator.maybePop(context);
await widget.route.popped;
await Future.delayed(Duration(
milliseconds:
widget.transitionDuration.inMilliseconds - 200));
showDialog(
context: context,
builder: (context) {
return const CertificateInfoPopup();
},
);
},
),
])),
const SizedBox(
height: 30.0,
),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
child: const Text(
"WebView Tab Settings",
),
onPressed: () async {
Navigator.maybePop(context);
await widget.route.popped;
Future.delayed(widget.transitionDuration, () {
if (widget.onWebViewTabSettingsClicked != null) {
widget.onWebViewTabSettingsClicked!();
}
});
},
),
),
],
));
}
}
================================================
FILE: lib/app_bar/webview_tab_app_bar.dart
================================================
// import 'package:cached_network_image/cached_network_image.dart';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_browser/app_bar/url_info_popup.dart';
import 'package:flutter_browser/custom_image.dart';
import 'package:flutter_browser/main.dart';
import 'package:flutter_browser/models/browser_model.dart';
import 'package:flutter_browser/models/favorite_model.dart';
import 'package:flutter_browser/models/web_archive_model.dart';
import 'package:flutter_browser/models/webview_model.dart';
import 'package:flutter_browser/pages/developers/main.dart';
import 'package:flutter_browser/pages/settings/main.dart';
import 'package:flutter_browser/tab_popup_menu_actions.dart';
import 'package:flutter_browser/util.dart';
import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import '../animated_flutter_browser_logo.dart';
import '../custom_popup_dialog.dart';
import '../custom_popup_menu_item.dart';
import '../models/window_model.dart';
import '../popup_menu_actions.dart';
import '../project_info_popup.dart';
import '../webview_tab.dart';
class WebViewTabAppBar extends StatefulWidget {
final void Function()? showFindOnPage;
const WebViewTabAppBar({super.key, this.showFindOnPage});
@override
State createState() => _WebViewTabAppBarState();
}
class _WebViewTabAppBarState extends State
with SingleTickerProviderStateMixin {
TextEditingController? _searchController = TextEditingController();
FocusNode? _focusNode;
GlobalKey tabInkWellKey = GlobalKey();
Duration customPopupDialogTransitionDuration =
const Duration(milliseconds: 300);
CustomPopupDialogPageRoute? route;
OutlineInputBorder outlineBorder = const OutlineInputBorder(
borderSide: BorderSide(color: Colors.transparent, width: 0.0),
borderRadius: BorderRadius.all(
Radius.circular(50.0),
),
);
bool shouldSelectText = true;
@override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode?.addListener(() async {
if (_focusNode != null &&
!_focusNode!.hasFocus &&
_searchController != null &&
_searchController!.text.isEmpty) {
final windowModel = Provider.of(context, listen: false);
final webViewModel = windowModel.getCurrentTab()?.webViewModel;
var webViewController = webViewModel?.webViewController;
_searchController!.text =
(await webViewController?.getUrl())?.toString() ?? "";
}
});
}
@override
void dispose() {
_focusNode?.dispose();
_focusNode = null;
_searchController?.dispose();
_searchController = null;
super.dispose();
}
int _prevTabIndex = -1;
@override
Widget build(BuildContext context) {
return Selector(
selector: (context, webViewModel) => (item1: webViewModel.url, item2: webViewModel.tabIndex),
builder: (context, record, child) {
if (_prevTabIndex != record.item2) {
_searchController?.text = record.item1?.toString() ?? '';
_prevTabIndex = record.item2 ?? _prevTabIndex;
_focusNode?.unfocus();
} else {
if (record.item1 == null) {
_searchController?.text = "";
}
if (record.item1 != null && _focusNode != null &&
!_focusNode!.hasFocus) {
_searchController?.text = record.item1.toString();
}
}
Widget? leading = _buildAppBarHomePageWidget();
return Selector(
selector: (context, webViewModel) => webViewModel.isIncognitoMode,
builder: (context, isIncognitoMode, child) {
return leading != null
? AppBar(
backgroundColor: isIncognitoMode
? Colors.black38
: Theme.of(context).colorScheme.primaryContainer,
leading: leading,
leadingWidth: 130,
titleSpacing: 0.0,
title: _buildSearchTextField(),
actions: _buildActionsMenu(),
)
: AppBar(
backgroundColor: isIncognitoMode
? Colors.black38
: Theme.of(context).colorScheme.primaryContainer,
titleSpacing: 10.0,
title: _buildSearchTextField(),
actions: _buildActionsMenu(),
);
});
});
}
Widget? _buildAppBarHomePageWidget() {
var browserModel = Provider.of(context, listen: true);
var settings = browserModel.getSettings();
var webViewModel = Provider.of(context, listen: true);
if (Util.isMobile() && !settings.homePageEnabled) {
return null;
}
final children = [];
if (Util.isDesktop()) {
children.addAll([
IconButton(
icon: const Icon(
Icons.arrow_back,
size: 20,
),
constraints: const BoxConstraints(
maxWidth: 30,
minWidth: 30,
maxHeight: 30,
minHeight: 30,
),
padding: EdgeInsets.zero,
onPressed: () async {
webViewModel.webViewController?.goBack();
},
),
IconButton(
icon: const Icon(
Icons.arrow_forward,
size: 20,
),
constraints: const BoxConstraints(
maxWidth: 30,
minWidth: 30,
maxHeight: 30,
minHeight: 30,
),
padding: EdgeInsets.zero,
onPressed: () async {
webViewModel.webViewController?.goForward();
},
),
IconButton(
icon: const Icon(
Icons.refresh,
size: 20,
),
constraints: const BoxConstraints(
maxWidth: 30,
minWidth: 30,
maxHeight: 30,
minHeight: 30,
),
padding: EdgeInsets.zero,
onPressed: () async {
webViewModel.webViewController?.reload();
},
)
]);
}
if (settings.homePageEnabled || Util.isDesktop()) {
children.add(IconButton(
icon: const Icon(
Icons.home,
size: 20,
),
constraints: const BoxConstraints(
maxWidth: 30,
minWidth: 30,
maxHeight: 30,
minHeight: 30,
),
padding: EdgeInsets.zero,
onPressed: () {
if (webViewModel.webViewController != null) {
var url = settings.homePageEnabled &&
settings.customUrlHomePage.isNotEmpty
? WebUri(settings.customUrlHomePage)
: WebUri(settings.searchEngine.url);
webViewModel.webViewController
?.loadUrl(urlRequest: URLRequest(url: url));
} else {
addNewTab();
}
},
));
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
child: Row(
children: children,
),
);
}
Widget _buildSearchTextField() {
final browserModel = Provider.of(context, listen: true);
final settings = browserModel.getSettings();
final webViewModel = Provider.of(context, listen: true);
return SizedBox(
height: 40.0,
child: Stack(
children: [
TextField(
onSubmitted: (value) {
var url = WebUri(value.trim());
if (Util.isLocalizedContent(url) ||
(url.isValidUri && url.toString().split(".").length > 1)) {
url = url.scheme.isEmpty ? WebUri("https://$url") : url;
} else {
url = WebUri(settings.searchEngine.searchUrl + value);
}
if (webViewModel.webViewController != null) {
webViewModel.webViewController
?.loadUrl(urlRequest: URLRequest(url: url));
} else {
addNewTab(url: url);
webViewModel.url = url;
}
},
onTap: () {
if (!shouldSelectText ||
_searchController == null ||
_searchController!.text.isEmpty) return;
shouldSelectText = false;
_searchController!.selection = TextSelection(
baseOffset: 0, extentOffset: _searchController!.text.length);
},
onTapOutside: (event) {
shouldSelectText = true;
},
keyboardType: TextInputType.url,
focusNode: _focusNode,
autofocus: false,
controller: _searchController,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(
left: 45.0, top: 10.0, right: 10.0, bottom: 10.0),
filled: true,
fillColor: Colors.white,
border: outlineBorder,
focusedBorder: outlineBorder,
enabledBorder: outlineBorder,
hintText: "Search for or type a web address",
hintStyle: const TextStyle(color: Colors.black54, fontSize: 16.0),
),
style: const TextStyle(color: Colors.black, fontSize: 16.0),
),
IconButton(
icon: Selector(
selector: (context, webViewModel) => webViewModel.isSecure,
builder: (context, isSecure, child) {
var icon = Icons.info_outline;
if (webViewModel.isIncognitoMode) {
icon = MaterialCommunityIcons.incognito;
} else if (isSecure) {
if (webViewModel.url != null &&
webViewModel.url!.scheme == "file") {
icon = Icons.offline_pin;
} else {
icon = Icons.lock;
}
}
return Icon(
icon,
color: isSecure ? Colors.green : Colors.grey,
);
},
),
onPressed: () {
showUrlInfo();
},
),
],
),
);
}
List _buildActionsMenu() {
final browserModel = Provider.of(context, listen: true);
final windowModel = Provider.of(context, listen: true);
final settings = browserModel.getSettings();
return [
settings.homePageEnabled
? const SizedBox(
width: 10.0,
)
: Container(),
Util.isDesktop()
? null
: InkWell(
key: tabInkWellKey,
onLongPress: () {
final RenderBox? box = tabInkWellKey.currentContext!
.findRenderObject() as RenderBox?;
if (box == null) {
return;
}
Offset position = box.localToGlobal(Offset.zero);
showMenu(
context: context,
position: RelativeRect.fromLTRB(position.dx,
position.dy + box.size.height, box.size.width, 0),
items: TabPopupMenuActions.choices
.map((tabPopupMenuAction) {
IconData? iconData;
switch (tabPopupMenuAction) {
case TabPopupMenuActions.CLOSE_TABS:
iconData = Icons.cancel;
break;
case TabPopupMenuActions.NEW_TAB:
iconData = Icons.add;
break;
case TabPopupMenuActions.NEW_INCOGNITO_TAB:
iconData = MaterialCommunityIcons.incognito;
break;
}
return PopupMenuItem(
value: tabPopupMenuAction,
child: Row(children: [
Icon(
iconData,
color: Colors.black,
),
Container(
padding: const EdgeInsets.only(left: 10.0),
child: Text(tabPopupMenuAction),
)
]),
);
}).toList())
.then((value) {
switch (value) {
case TabPopupMenuActions.CLOSE_TABS:
windowModel.closeAllTabs();
break;
case TabPopupMenuActions.NEW_TAB:
addNewTab();
break;
case TabPopupMenuActions.NEW_INCOGNITO_TAB:
addNewIncognitoTab();
break;
}
});
},
onTap: () async {
if (windowModel.webViewTabs.isNotEmpty) {
var webViewModel = windowModel.getCurrentTab()?.webViewModel;
var webViewController = webViewModel?.webViewController;
if (View.of(context).viewInsets.bottom > 0.0) {
SystemChannels.textInput.invokeMethod('TextInput.hide');
if (FocusManager.instance.primaryFocus != null) {
FocusManager.instance.primaryFocus!.unfocus();
}
if (webViewController != null) {
await webViewController.evaluateJavascript(
source: "document.activeElement.blur();");
}
await Future.delayed(const Duration(milliseconds: 300));
}
if (webViewModel != null && webViewController != null) {
webViewModel.screenshot = await webViewController
.takeScreenshot(
screenshotConfiguration: ScreenshotConfiguration(
compressFormat: CompressFormat.JPEG,
quality: 20))
.timeout(
const Duration(milliseconds: 1500),
onTimeout: () => null,
);
}
browserModel.showTabScroller = true;
}
},
child: Container(
margin: const EdgeInsets.only(
left: 10.0, top: 15.0, right: 10.0, bottom: 15.0),
decoration: BoxDecoration(
border: Border.all(width: 2.0),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(5.0)),
constraints: const BoxConstraints(minWidth: 25.0),
child: Center(
child: Text(
windowModel.webViewTabs.length.toString(),
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14.0),
)),
),
),
const SizedBox.square(
dimension: 5,
),
PopupMenuButton(
icon: const Icon(
Icons.more_vert,
),
position: PopupMenuPosition.under,
onSelected: _popupMenuChoiceAction,
itemBuilder: (popupMenuContext) {
var items = [
CustomPopupMenuItem(
enabled: true,
isIconButtonRow: true,
child: StatefulBuilder(
builder: (statefulContext, setState) {
var browserModel =
Provider.of(statefulContext, listen: true);
var webViewModel =
Provider.of(statefulContext, listen: true);
var isFavorite = false;
FavoriteModel? favorite;
if (webViewModel.url != null &&
webViewModel.url!.toString().isNotEmpty) {
favorite = FavoriteModel(
url: webViewModel.url,
title: webViewModel.title ?? "",
favicon: webViewModel.favicon);
isFavorite = browserModel.containsFavorite(favorite);
}
var children = [];
if (Util.isIOS() || Util.isMacOS() || Util.isWindows()) {
children.add(
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
Icons.arrow_back,
color: Colors.black,
),
onPressed: () {
webViewModel.webViewController?.goBack();
Navigator.pop(popupMenuContext);
})),
);
}
children.addAll([
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
Icons.arrow_forward,
color: Colors.black,
),
onPressed: () {
webViewModel.webViewController?.goForward();
Navigator.pop(popupMenuContext);
})),
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: Icon(
isFavorite ? Icons.star : Icons.star_border,
color: Colors.black,
),
onPressed: () {
setState(() {
if (favorite != null) {
if (!browserModel
.containsFavorite(favorite)) {
browserModel.addFavorite(favorite);
} else if (browserModel
.containsFavorite(favorite)) {
browserModel.removeFavorite(favorite);
}
}
});
})),
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
Icons.file_download,
color: Colors.black,
),
onPressed: () async {
Navigator.pop(popupMenuContext);
if (webViewModel.url != null &&
webViewModel.url!.scheme.startsWith("http")) {
var url = webViewModel.url;
if (url == null) {
return;
}
String webArchivePath =
"$WEB_ARCHIVE_DIR${Platform.pathSeparator}${url.scheme}-${url.host}${url.path.replaceAll("/", "-")}${DateTime.now().microsecondsSinceEpoch}.${Util.isAndroid() ? WebArchiveFormat.MHT.toValue() : WebArchiveFormat.WEBARCHIVE.toValue()}";
String? savedPath = (await webViewModel
.webViewController
?.saveWebArchive(
filePath: webArchivePath,
autoname: false));
var webArchiveModel = WebArchiveModel(
url: url,
path: savedPath,
title: webViewModel.title,
favicon: webViewModel.favicon,
timestamp: DateTime.now());
if (savedPath != null) {
browserModel.addWebArchive(
url.toString(), webArchiveModel);
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(
"${webViewModel.url} saved offline!"),
));
}
browserModel.save();
} else {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(
content: Text("Unable to save!"),
));
}
}
}
})),
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
Icons.info_outline,
color: Colors.black,
),
onPressed: () async {
Navigator.pop(popupMenuContext);
await route?.completed;
showUrlInfo();
})),
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
MaterialCommunityIcons.cellphone_screenshot,
color: Colors.black,
),
onPressed: () async {
Navigator.pop(popupMenuContext);
await route?.completed;
takeScreenshotAndShow();
})),
SizedBox(
width: 35.0,
child: IconButton(
padding: const EdgeInsets.all(0.0),
icon: const Icon(
Icons.refresh,
color: Colors.black,
),
onPressed: () {
webViewModel.webViewController?.reload();
Navigator.pop(popupMenuContext);
})),
]);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: children,
);
},
),
)
];
items.addAll(PopupMenuActions.choices.map((choice) {
switch (choice) {
case PopupMenuActions.OPEN_NEW_WINDOW:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.open_in_new,
)
]),
);
case PopupMenuActions.SAVE_WINDOW:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
Selector(
selector: (context, windowModel) =>
windowModel.shouldSave,
builder: (context, value, child) {
return Icon(
value
? Icons.check_box
: Icons.check_box_outline_blank,
color: Colors.black,
);
},
)
]),
);
case PopupMenuActions.SAVED_WINDOWS:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.window,
)
]),
);
case PopupMenuActions.NEW_TAB:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.add,
color: Colors.black,
)
]),
);
case PopupMenuActions.NEW_INCOGNITO_TAB:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
MaterialCommunityIcons.incognito,
color: Colors.black,
)
]),
);
case PopupMenuActions.FAVORITES:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.star,
color: Colors.yellow,
)
]),
);
case PopupMenuActions.WEB_ARCHIVES:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.offline_pin,
color: Colors.blue,
)
]),
);
case PopupMenuActions.DESKTOP_MODE:
return CustomPopupMenuItem(
enabled: windowModel.getCurrentTab() != null,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
Selector(
selector: (context, webViewModel) =>
webViewModel.isDesktopMode,
builder: (context, value, child) {
return Icon(
value
? Icons.check_box
: Icons.check_box_outline_blank,
color: Colors.black,
);
},
)
]),
);
case PopupMenuActions.HISTORY:
return CustomPopupMenuItem(
enabled: windowModel.getCurrentTab() != null,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.history,
color: Colors.black,
)
]),
);
case PopupMenuActions.SHARE:
return CustomPopupMenuItem(
enabled: windowModel.getCurrentTab() != null,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Ionicons.logo_whatsapp,
color: Colors.green,
)
]),
);
case PopupMenuActions.SETTINGS:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.settings,
color: Colors.grey,
)
]),
);
case PopupMenuActions.DEVELOPERS:
return CustomPopupMenuItem(
enabled: windowModel.getCurrentTab() != null,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.developer_mode,
color: Colors.black,
)
]),
);
case PopupMenuActions.FIND_ON_PAGE:
return CustomPopupMenuItem(
enabled: windowModel.getCurrentTab() != null,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
const Icon(
Icons.search,
color: Colors.black,
)
]),
);
case PopupMenuActions.INAPPWEBVIEW_PROJECT:
return CustomPopupMenuItem(
enabled: true,
value: choice,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(choice),
Container(
padding: const EdgeInsets.only(right: 6),
child: const AnimatedFlutterBrowserLogo(
size: 12.5,
),
)
]),
);
default:
return CustomPopupMenuItem(
value: choice,
child: Text(choice),
);
}
}).toList());
return items;
},
)
].whereNotNull().toList();
}
void _popupMenuChoiceAction(String choice) async {
var currentWebViewModel = Provider.of(context, listen: false);
switch (choice) {
case PopupMenuActions.OPEN_NEW_WINDOW:
openNewWindow();
break;
case PopupMenuActions.SAVE_WINDOW:
setShouldSave();
break;
case PopupMenuActions.SAVED_WINDOWS:
showSavedWindows();
break;
case PopupMenuActions.NEW_TAB:
addNewTab();
break;
case PopupMenuActions.NEW_INCOGNITO_TAB:
addNewIncognitoTab();
break;
case PopupMenuActions.FAVORITES:
showFavorites();
break;
case PopupMenuActions.HISTORY:
showHistory();
break;
case PopupMenuActions.WEB_ARCHIVES:
showWebArchives();
break;
case PopupMenuActions.FIND_ON_PAGE:
var isFindInteractionEnabled =
currentWebViewModel.settings?.isFindInteractionEnabled ?? false;
var findInteractionController =
currentWebViewModel.findInteractionController;
if ((Util.isIOS() || Util.isMacOS()) &&
isFindInteractionEnabled &&
findInteractionController != null) {
await findInteractionController.presentFindNavigator();
} else if (widget.showFindOnPage != null) {
widget.showFindOnPage!();
}
break;
case PopupMenuActions.SHARE:
share();
break;
case PopupMenuActions.DESKTOP_MODE:
toggleDesktopMode();
break;
case PopupMenuActions.DEVELOPERS:
Future.delayed(const Duration(milliseconds: 300), () {
goToDevelopersPage();
});
break;
case PopupMenuActions.SETTINGS:
Future.delayed(const Duration(milliseconds: 300), () {
goToSettingsPage();
});
break;
case PopupMenuActions.INAPPWEBVIEW_PROJECT:
Future.delayed(const Duration(milliseconds: 300), () {
openProjectPopup();
});
break;
}
}
void addNewTab({WebUri? url}) {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
final settings = browserModel.getSettings();
url ??= settings.homePageEnabled && settings.customUrlHomePage.isNotEmpty
? WebUri(settings.customUrlHomePage)
: WebUri(settings.searchEngine.url);
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: url),
));
}
void addNewIncognitoTab({WebUri? url}) {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
final settings = browserModel.getSettings();
url ??= settings.homePageEnabled && settings.customUrlHomePage.isNotEmpty
? WebUri(settings.customUrlHomePage)
: WebUri(settings.searchEngine.url);
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: url, isIncognitoMode: true),
));
}
void showSavedWindows() {
showDialog(
context: context,
builder: (context) {
final browserModel = Provider.of(context, listen: true);
return AlertDialog(
contentPadding: const EdgeInsets.all(0.0),
content: SizedBox(
width: double.maxFinite,
child: StatefulBuilder(
builder: (context, setState) {
return FutureBuilder(
future: browserModel.getWindows(),
builder: (context, snapshot) {
final savedWindows = (snapshot.data ?? []);
savedWindows.sortBy(
(e) => e.updatedTime,
);
return ListView(
children: savedWindows.map((window) {
return ListTile(
title: Text(
window.name.isNotEmpty
? window.name
: window.id,
maxLines: 2,
overflow: TextOverflow.ellipsis),
onTap: () async {
await browserModel.openWindow(window);
setState(() {
Navigator.pop(context);
});
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close, size: 20.0),
onPressed: () async {
await browserModel.removeWindow(window);
setState(() {
if (savedWindows.isEmpty ||
savedWindows.length == 1) {
Navigator.pop(context);
}
});
},
)
],
),
);
}).toList(),
);
},
);
},
)));
});
}
void showFavorites() {
showDialog(
context: context,
builder: (context) {
var browserModel = Provider.of(context, listen: true);
return AlertDialog(
contentPadding: const EdgeInsets.all(0.0),
content: SizedBox(
width: double.maxFinite,
child: ListView(
children: browserModel.favorites.map((favorite) {
var url = favorite.url;
var faviconUrl = favorite.favicon != null
? favorite.favicon!.url
: WebUri("${url?.origin ?? ""}/favicon.ico");
return ListTile(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// CachedNetworkImage(
// placeholder: (context, url) =>
// CircularProgressIndicator(),
// imageUrl: faviconUrl,
// height: 30,
// )
CustomImage(
url: faviconUrl,
maxWidth: 30.0,
height: 30.0,
)
],
),
title: Text(
favorite.title ?? favorite.url?.toString() ?? "",
maxLines: 2,
overflow: TextOverflow.ellipsis),
subtitle: Text(favorite.url?.toString() ?? "",
maxLines: 2, overflow: TextOverflow.ellipsis),
isThreeLine: true,
onTap: () {
setState(() {
addNewTab(url: favorite.url);
Navigator.pop(context);
});
},
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close, size: 20.0),
onPressed: () {
setState(() {
browserModel.removeFavorite(favorite);
if (browserModel.favorites.isEmpty) {
Navigator.pop(context);
}
});
},
)
],
),
);
}).toList(),
)));
});
}
void showHistory() {
showDialog(
context: context,
builder: (context) {
var webViewModel = Provider.of(context, listen: true);
return AlertDialog(
contentPadding: const EdgeInsets.all(0.0),
content: FutureBuilder(
future:
webViewModel.webViewController?.getCopyBackForwardList(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
}
WebHistory history = snapshot.data as WebHistory;
return SizedBox(
width: double.maxFinite,
child: ListView(
children: history.list?.reversed.map((historyItem) {
var url = historyItem.url;
return ListTile(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// CachedNetworkImage(
// placeholder: (context, url) =>
// CircularProgressIndicator(),
// imageUrl: (url?.origin ?? "") + "/favicon.ico",
// height: 30,
// )
CustomImage(
url: WebUri(
"${url?.origin ?? ""}/favicon.ico"),
maxWidth: 30.0,
height: 30.0)
],
),
title: Text(historyItem.title ?? url.toString(),
maxLines: 2,
overflow: TextOverflow.ellipsis),
subtitle: Text(url?.toString() ?? "",
maxLines: 2,
overflow: TextOverflow.ellipsis),
isThreeLine: true,
onTap: () {
webViewModel.webViewController
?.goTo(historyItem: historyItem);
Navigator.pop(context);
},
);
}).toList() ??
[],
));
},
));
});
}
void showWebArchives() async {
showDialog(
context: context,
builder: (context) {
var browserModel = Provider.of(context, listen: true);
var webArchives = browserModel.webArchives;
var listViewChildren = [];
webArchives.forEach((key, webArchive) {
var path = webArchive.path;
// String fileName = path.substring(path.lastIndexOf('/') + 1);
var url = webArchive.url;
listViewChildren.add(ListTile(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// CachedNetworkImage(
// placeholder: (context, url) => CircularProgressIndicator(),
// imageUrl: (url?.origin ?? "") + "/favicon.ico",
// height: 30,
// )
CustomImage(
url: WebUri("${url?.origin ?? ""}/favicon.ico"),
maxWidth: 30.0,
height: 30.0)
],
),
title: Text(webArchive.title ?? url?.toString() ?? "",
maxLines: 2, overflow: TextOverflow.ellipsis),
subtitle: Text(url?.toString() ?? "",
maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
setState(() {
browserModel.removeWebArchive(webArchive);
browserModel.save();
});
},
),
isThreeLine: true,
onTap: () {
if (path != null) {
final windowModel =
Provider.of(context, listen: false);
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: WebUri("file://$path")),
));
}
Navigator.pop(context);
},
));
});
return AlertDialog(
contentPadding: const EdgeInsets.all(0.0),
content: Builder(
builder: (context) {
return SizedBox(
width: double.maxFinite,
child: ListView(
children: listViewChildren,
));
},
));
});
}
void share() {
final windowModel = Provider.of(context, listen: false);
final webViewModel = windowModel.getCurrentTab()?.webViewModel;
final url = webViewModel?.url;
if (url != null) {
Share.share(url.toString(), subject: webViewModel?.title);
}
}
void openNewWindow() {
final browserModel = Provider.of(context, listen: false);
browserModel.openWindow(null);
}
void setShouldSave() {
final windowModel = Provider.of(context, listen: false);
windowModel.shouldSave = !windowModel.shouldSave;
}
void toggleDesktopMode() async {
final windowModel = Provider.of(context, listen: false);
final webViewModel = windowModel.getCurrentTab()?.webViewModel;
final webViewController = webViewModel?.webViewController;
final currentWebViewModel =
Provider.of(context, listen: false);
if (webViewController != null) {
webViewModel?.isDesktopMode = !webViewModel.isDesktopMode;
currentWebViewModel.isDesktopMode = webViewModel?.isDesktopMode ?? false;
final currentSettings = await webViewController.getSettings();
if (currentSettings != null) {
currentSettings.preferredContentMode =
webViewModel?.isDesktopMode ?? false
? UserPreferredContentMode.DESKTOP
: UserPreferredContentMode.RECOMMENDED;
await webViewController.setSettings(settings: currentSettings);
}
await webViewController.reload();
}
}
void showUrlInfo() {
var webViewModel = Provider.of(context, listen: false);
var url = webViewModel.url;
if (url == null || url.toString().isEmpty) {
return;
}
route = CustomPopupDialog.show(
context: context,
transitionDuration: customPopupDialogTransitionDuration,
builder: (context) {
return UrlInfoPopup(
route: route!,
transitionDuration: customPopupDialogTransitionDuration,
onWebViewTabSettingsClicked: () {
goToSettingsPage();
},
);
},
);
}
void goToDevelopersPage() {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const DevelopersPage()));
}
void goToSettingsPage() {
Navigator.push(
context, MaterialPageRoute(builder: (context) => const SettingsPage()));
}
void openProjectPopup() {
showGeneralDialog(
context: context,
barrierDismissible: false,
pageBuilder: (context, animation, secondaryAnimation) {
return const ProjectInfoPopup();
},
transitionDuration: const Duration(milliseconds: 300),
);
}
void takeScreenshotAndShow() async {
var webViewModel = Provider.of(context, listen: false);
var screenshot = await webViewModel.webViewController?.takeScreenshot();
if (screenshot != null) {
var dir = await getApplicationDocumentsDirectory();
File file = File(
"${dir.path}/screenshot_${DateTime.now().microsecondsSinceEpoch}.png");
await file.writeAsBytes(screenshot);
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Image.memory(screenshot),
actions: [
ElevatedButton(
child: const Text("Share"),
onPressed: () async {
await Share.shareXFiles([XFile(file.path)]);
},
)
],
);
},
);
file.delete();
}
}
}
================================================
FILE: lib/browser.dart
================================================
import 'dart:async';
// import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_browser/custom_image.dart';
import 'package:flutter_browser/tab_viewer.dart';
import 'package:flutter_browser/app_bar/browser_app_bar.dart';
import 'package:flutter_browser/models/webview_model.dart';
import 'package:flutter_browser/util.dart';
import 'package:flutter_browser/webview_tab.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:provider/provider.dart';
import 'app_bar/tab_viewer_app_bar.dart';
import 'empty_tab.dart';
import 'models/browser_model.dart';
import 'models/window_model.dart';
class Browser extends StatefulWidget {
const Browser({super.key});
@override
State createState() => _BrowserState();
}
class _BrowserState extends State with SingleTickerProviderStateMixin {
static const platform =
MethodChannel('com.pichillilorenzo.flutter_browser.intent_data');
var _isRestored = false;
@override
void initState() {
super.initState();
getIntentData();
}
getIntentData() async {
if (Util.isAndroid()) {
String? url = await platform.invokeMethod("getIntentData");
if (url != null) {
if (mounted) {
final windowModel = Provider.of(context, listen: false);
windowModel.addTab(WebViewTab(
key: GlobalKey(),
webViewModel: WebViewModel(url: WebUri(url)),
));
}
}
}
}
restore() async {
final browserModel = Provider.of(context, listen: false);
final windowModel = Provider.of(context, listen: false);
browserModel.restore();
windowModel.restoreInfo();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_isRestored) {
_isRestored = true;
restore();
}
precacheImage(const AssetImage("assets/icon/icon.png"), context);
}
@override
Widget build(BuildContext context) {
return _buildBrowser();
}
Widget _buildBrowser() {
final currentWebViewModel = Provider.of(context, listen: true);
final browserModel = Provider.of(context, listen: true);
final windowModel = Provider.of(context, listen: true);
browserModel.addListener(() {
browserModel.save();
});
windowModel.addListener(() {
windowModel.saveInfo();
});
currentWebViewModel.addListener(() {
windowModel.saveInfo();
});
var canShowTabScroller =
browserModel.showTabScroller && windowModel.webViewTabs.isNotEmpty;
return IndexedStack(
index: canShowTabScroller ? 1 : 0,
children: [
_buildWebViewTabs(),
canShowTabScroller ? _buildWebViewTabsViewer() : Container()
],
);
}
Widget _buildWebViewTabs() {
return WillPopScope(
onWillPop: () async {
final windowModel = Provider.of(context, listen: false);
final webViewModel = windowModel.getCurrentTab()?.webViewModel;
final webViewController = webViewModel?.webViewController;
if (webViewController != null) {
if (await webViewController.canGoBack()) {
webViewController.goBack();
return false;
}
}
if (webViewModel != null && webViewModel.tabIndex != null) {
setState(() {
windowModel.closeTab(webViewModel.tabIndex!);
});
if (mounted) {
FocusScope.of(context).unfocus();
}
return false;
}
return windowModel.webViewTabs.isEmpty;
},
child: Listener(
onPointerUp: (_) {
if (Util.isIOS() || Util.isAndroid()) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus &&
currentFocus.focusedChild != null) {
currentFocus.focusedChild!.unfocus();
}
}
},
child: Scaffold(
appBar: BrowserAppBar(), body: _buildWebViewTabsContent()),
));
}
Widget _buildWebViewTabsContent() {
final windowModel = Provider.of(context, listen: true);
if (windowModel.webViewTabs.isEmpty) {
return const EmptyTab();
}
for (final webViewTab in windowModel.webViewTabs) {
var isCurrentTab =
webViewTab.webViewModel.tabIndex == windowModel.getCurrentTabIndex();
if (isCurrentTab) {
Future.delayed(const Duration(milliseconds: 100), () {
webViewTabStateKey.currentState?.onShowTab();
});
} else {
webViewTabStateKey.currentState?.onHideTab();
}
}
var stackChildren = [
windowModel.getCurrentTab() ?? Container(),
_createProgressIndicator()
];
return Column(
children: [
Expanded(
child: Stack(
children: stackChildren,
))
],
);
}
Widget _createProgressIndicator() {
return Selector(
selector: (context, webViewModel) => webViewModel.progress,
builder: (context, progress, child) {
if (progress >= 1.0) {
return Container();
}
return PreferredSize(
preferredSize: const Size(double.infinity, 4.0),
child: SizedBox(
height: 4.0,
child: LinearProgressIndicator(
value: progress,
)));
});
}
Widget _buildWebViewTabsViewer() {
final browserModel = Provider.of(context, listen: true);
final windowModel = Provider.of(context, listen: true);
return WillPopScope(
onWillPop: () async {
browserModel.showTabScroller = false;
return false;
},
child: Scaffold(
appBar: const TabViewerAppBar(),
body: TabViewer(
currentIndex: windowModel.getCurrentTabIndex(),
children: windowModel.webViewTabs.map((webViewTab) {
webViewTabStateKey.currentState?.pause();
var screenshotData = webViewTab.webViewModel.screenshot;
Widget screenshotImage = Container(
decoration: const BoxDecoration(color: Colors.white),
width: double.infinity,
child: screenshotData != null
? Image.memory(screenshotData)
: null,
);
var url = webViewTab.webViewModel.url;
var faviconUrl = webViewTab.webViewModel.favicon != null
? webViewTab.webViewModel.favicon!.url
: (url != null && ["http", "https"].contains(url.scheme)
? Uri.parse("${url.origin}/favicon.ico")
: null);
var isCurrentTab = windowModel.getCurrentTabIndex() ==
webViewTab.webViewModel.tabIndex;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
color: isCurrentTab
? Colors.blue
: (webViewTab.webViewModel.isIncognitoMode
? Colors.black
: Colors.white),
child: ListTile(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// CachedNetworkImage(
// placeholder: (context, url) =>
// url == "about:blank"
// ? Container()
// : CircularProgressIndicator(),
// imageUrl: faviconUrl,
// height: 30,
// )
CustomImage(
url: faviconUrl, maxWidth: 30.0, height: 30.0)
],
),
title: Text(
webViewTab.webViewModel.title ??
webViewTab.webViewModel.url?.toString() ??
"",
maxLines: 2,
style: TextStyle(
color: webViewTab.webViewModel.isIncognitoMode ||
isCurrentTab
? Colors.white
: Colors.black,
),
overflow: TextOverflow.ellipsis),
subtitle:
Text(webViewTab.webViewModel.url?.toString() ?? "",
style: TextStyle(
color:
webViewTab.webViewModel.isIncognitoMode ||
isCurrentTab
? Colors.white60
: Colors.black54,
),
maxLines: 2,
overflow: TextOverflow.ellipsis),
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
Icons.close,
size: 20.0,
color:
webViewTab.webViewModel.isIncognitoMode ||
isCurrentTab
? Colors.white60
: Colors.black54,
),
onPressed: () {
setState(() {
if (webViewTab.webViewModel.tabIndex !=
null) {
windowModel.closeTab(
webViewTab.webViewModel.tabIndex!);
if (windowModel.webViewTabs.isEmpty) {
browserModel.showTabScroller = false;
}
}
});
},
)
],
),
),
),
Expanded(
child: screenshotImage,
)
],
);
}).toList(),
onTap: (index) async {
browserModel.showTabScroller = false;
windowModel.showTab(index);
},
)));
}
}
================================================
FILE: lib/custom_image.dart
================================================
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
class CustomImage extends StatelessWidget {
final double? width;
final double? height;
final double maxWidth;
final double maxHeight;
final double minWidth;
final double minHeight;
final Uri? url;
const CustomImage(
{super.key,
this.url,
this.width,
this.height,
this.maxWidth = double.infinity,
this.maxHeight = double.infinity,
this.minWidth = 0.0,
this.minHeight = 0.0});
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
minHeight: minHeight,
minWidth: minWidth),
width: width,
height: height,
child: getImage(),
);
}
Widget? getImage() {
if (url != null) {
if (url!.scheme == "data") {
Uint8List bytes = const Base64Decoder()
.convert(url.toString().replaceFirst("data:image/png;base64,", ""));
return Image.memory(
bytes,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => getBrokenImageIcon(),
);
}
return Image.network(
url.toString(),
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => getBrokenImageIcon(),
);
}
return getBrokenImageIcon();
}
Widget getBrokenImageIcon() {
return Icon(
Icons.broken_image,
size: width ?? height ?? maxWidth,
);
}
}
================================================
FILE: lib/custom_popup_dialog.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter_browser/material_transparent_page_route.dart';
class CustomPopupDialogPageRoute extends MaterialTransparentPageRoute {
final Color overlayColor;
final Duration? customTransitionDuration;
bool isPopped = false;
CustomPopupDialogPageRoute({
required super.builder,
Duration? transitionDuration,
Color? overlayColor,
super.settings,
}) : overlayColor = overlayColor ?? Colors.black.withOpacity(0.5),
customTransitionDuration = transitionDuration;
@override
Duration get transitionDuration => customTransitionDuration != null
? customTransitionDuration!
: const Duration(milliseconds: 300);
@override
bool didPop(T? result) {
isPopped = true;
return super.didPop(result);
}
@override
Widget buildTransitions(BuildContext context, Animation animation,
Animation secondaryAnimation, Widget child) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: () async {
if (!isPopped) {
Navigator.maybePop(context);
} else {
isPopped = true;
}
},
child: Opacity(
opacity: animation.value,
child: Container(color: overlayColor),
),
),
),
child,
],
));
// return child;
}
}
class CustomPopupDialog extends StatefulWidget {
final Widget child;
final Duration transitionDuration;
const CustomPopupDialog(
{super.key,
required this.child,
this.transitionDuration = const Duration(milliseconds: 300)});
@override
State createState() => _CustomPopupDialogState();
static CustomPopupDialogPageRoute show(
{required BuildContext context,
Widget? child,
WidgetBuilder? builder,
Color? overlayColor,
required Duration transitionDuration}) {
var route = CustomPopupDialogPageRoute(
transitionDuration: transitionDuration,
overlayColor: overlayColor,
builder: (context) {
return CustomPopupDialog(
transitionDuration: transitionDuration,
child: builder != null ? builder(context) : child!,
);
},
);
Navigator.push(context, route);
return route;
}
}
class _CustomPopupDialogState extends State
with SingleTickerProviderStateMixin {
late AnimationController _slideController;
late Animation