46 Commits

Author SHA1 Message Date
8ec264cebe Merge pull request 'settings-feature' (#7) from settings-feature into main
Some checks failed
Flutter APK Build / Build Flutter APK (push) Has been cancelled
Reviewed-on: #7
2026-01-23 18:04:38 +01:00
83bfdf322b added persisted app settings
Some checks failed
Flutter APK Build / Build Flutter APK (pull_request) Has been cancelled
2026-01-23 18:03:46 +01:00
cad43c7664 added app settings api 2026-01-23 17:02:57 +01:00
5c44574949 created settings model 2026-01-23 16:49:26 +01:00
336be6cb72 visually changed settings page
Some checks failed
Flutter APK Build / Build Flutter APK (pull_request) Has been cancelled
2026-01-23 16:41:53 +01:00
214ae08bb9 fixed wrong return value 2026-01-23 16:41:41 +01:00
100b86d3f9 added list tile content padding 2026-01-23 16:41:29 +01:00
ff1b102047 fixed visual bug 2026-01-23 16:41:16 +01:00
d51f3d4ba7 Merge pull request 'Data import and export' (#6) from development into main
All checks were successful
Flutter APK Build / Build Flutter APK (push) Successful in 7m54s
Reviewed-on: #6
2026-01-22 18:47:02 +01:00
b4016e6e5b localized dialog
All checks were successful
Flutter APK Build / Build Flutter APK (pull_request) Successful in 7m28s
2026-01-22 18:23:05 +01:00
eae4a853e9 removed unnecessary code 2026-01-22 18:20:08 +01:00
8eb4cadc85 Merge pull request 'Json export and import feature' (#5) from json-export-feature into development
Reviewed-on: #5
2026-01-22 18:12:01 +01:00
06c5ca9910 added minimal error handling and user feedback 2026-01-22 18:09:34 +01:00
27c3804b1e removed path provider 2026-01-22 17:52:18 +01:00
debf960d70 simple working json import and export 2026-01-22 17:51:50 +01:00
1029bad20f added localization for settings 2026-01-22 17:16:37 +01:00
cef23a1c83 added constant global values 2026-01-22 17:00:23 +01:00
c4fe32e4b1 replaced file_picker with file_selector 2026-01-22 16:58:33 +01:00
893a1b558f added file picker 2026-01-22 16:44:43 +01:00
b0eebb5ee8 added permission service 2026-01-22 16:28:05 +01:00
56daf1b940 added localization and permission error snackbar 2026-01-22 16:27:41 +01:00
8687b7788b updated gitignore to ignore generated localization files 2026-01-22 16:27:02 +01:00
eeae1d919e updated gitignore 2026-01-22 16:21:39 +01:00
d02684bb84 requested storage permission 2026-01-22 16:19:46 +01:00
ea961da678 added permisson_handler package 2026-01-22 16:19:33 +01:00
632da54311 added error texts 2026-01-22 16:19:20 +01:00
bc20593661 added button to navigate to settings 2026-01-22 14:55:46 +01:00
045f8b5b6b added localization for settings 2026-01-22 14:55:31 +01:00
81f7b45619 added settings page 2026-01-22 14:42:07 +01:00
06a76afc42 Merge pull request 'Theme changes' (#4) from development into main
All checks were successful
Flutter APK Build / Build Flutter APK (push) Successful in 6m46s
Reviewed-on: #4
2026-01-21 16:38:03 +01:00
3032e13dc9 Added dismiss button
Some checks failed
Flutter APK Build / Build Flutter APK (pull_request) Has been cancelled
2026-01-21 16:37:03 +01:00
d0492b2f79 changed snackbar styling 2026-01-21 16:31:43 +01:00
c2506fab7a refactored code so change in bookmark count is visible immediately 2026-01-21 16:30:30 +01:00
dca8c64555 Merge pull request 'small theme changes' (#3) from development into main
All checks were successful
Flutter APK Build / Build Flutter APK (push) Successful in 6m48s
Reviewed-on: #3
2026-01-21 15:05:11 +01:00
c80606b7d0 changed appearance of lists
All checks were successful
Flutter APK Build / Build Flutter APK (pull_request) Successful in 6m44s
2026-01-21 15:04:24 +01:00
be6020d6c5 added label text 2026-01-21 14:44:36 +01:00
5eb58d7cf2 Changed theme to globally enable rounded borders for textfields 2026-01-21 14:43:02 +01:00
306a38a36a revert workflow to a working state 2026-01-21 14:30:25 +01:00
1aaea5f6d9 Merge pull request '[fix] Added basic locatlization' (#2) from development into main
All checks were successful
Flutter APK Build / Calculate Version (push) Successful in 13s
Flutter APK Build / Build Flutter APK (push) Successful in 6m34s
Flutter APK Build / Create Release (push) Has been skipped
Reviewed-on: #2
2026-01-21 14:12:41 +01:00
99a8aaa409 added localizations
Some checks failed
Flutter APK Build / Calculate Version (pull_request) Successful in 15s
Flutter APK Build / Create Release (pull_request) Has been cancelled
Flutter APK Build / Build Flutter APK (pull_request) Has been cancelled
2026-01-21 14:11:50 +01:00
85d57c6e1c changed workflow to support pull requests 2026-01-21 14:06:06 +01:00
77a647d17d [fix] Added basic locatlization
All checks were successful
Flutter APK Build / Calculate Version (pull_request) Successful in 14s
Flutter APK Build / Build Flutter APK (pull_request) Successful in 6m35s
Flutter APK Build / Create Release (pull_request) Has been skipped
2026-01-21 13:50:24 +01:00
3a54a077f3 Merge pull request '[fix] added bookmark count number to collections page' (#1) from development into main
All checks were successful
Flutter APK Build / Calculate Version (push) Successful in 15s
Flutter APK Build / Build Flutter APK (push) Successful in 6m37s
Flutter APK Build / Create Release (push) Has been skipped
Reviewed-on: #1
2026-01-21 13:20:49 +01:00
cf88a9a371 added bookmark count number in collections view
All checks were successful
Flutter APK Build / Calculate Version (pull_request) Successful in 12s
Flutter APK Build / Build Flutter APK (pull_request) Successful in 6m38s
Flutter APK Build / Create Release (pull_request) Has been skipped
2026-01-21 13:19:45 +01:00
5feb535cf3 changed app version 2026-01-21 13:19:13 +01:00
2d23207497 added automatic versioning in workflow 2026-01-21 12:40:52 +01:00
28 changed files with 949 additions and 125 deletions

View File

@@ -23,13 +23,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
# - name: Cache pub deps
# uses: actions/cache@v3
# with:
# path: ~/.pub-cache
# key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }}
# restore-keys: ${{ runner.os }}-pub-
- name: Setup Java (Temurin 17)
uses: actions/setup-java@v3
with:
@@ -66,7 +59,6 @@ jobs:
env:
GITEA_TOKEN: ${{ secrets.RUNNER_CREATE_RELEASE }}
with:
# tag & name: adjust to your versioning
tag_name: "v0.1.${{ github.run_number }}"
name: "Flutter Android v0.1.${{ github.run_number }}"
body: "Automated build from CI"

155
.gitignore vendored
View File

@@ -1,16 +1,23 @@
# Do not remove or rename entries in this file, only add new ones
# See https://github.com/flutter/flutter/issues/128635 for more context.
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
lib/l10n/app_localizations*
# As packages are no longer pinned, we use a lockfile for testing locally.
# When unpinning packages, Using lockfiles ensures that failures in PRs are
# actually due to those PRs, not due to a package being updated.
!/pubspec.lock
# IntelliJ related
*.iml
@@ -18,28 +25,142 @@ migrate_working_dir/
*.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/
# Visual Studio Code related
.classpath
.project
.settings/
.vscode/*
.ccls-cache
# This file, on the master branch, should never exist or be checked-in.
#
# On a *final* release branch, that is, what will ship to stable or beta, the
# file can be force added (git add --force) and checked-in in order to effectively
# "pin" the engine artifact version so the flutter tool does not need to use git
# to determine the engine artifacts.
#
# See https://github.com/flutter/flutter/blob/main/docs/tool/Engine-artifacts.md.
/bin/internal/engine.version
# Flutter repo-specific
/bin/cache/
/bin/internal/bootstrap.bat
/bin/internal/bootstrap.sh
/bin/internal/engine.realm
/bin/mingit/
/dev/benchmarks/mega_gallery/
/dev/bots/.recipe_deps
/dev/bots/android_tools/
/dev/devicelab/ABresults*.json
/dev/docs/doc/
/dev/docs/api_docs.zip
/dev/docs/flutter.docs.zip
/dev/docs/lib/
/dev/docs/pubspec.yaml
/dev/integration_tests/**/xcuserdata
/dev/integration_tests/**/Pods
/packages/flutter/coverage/
version
analysis_benchmark.json
# packages file containing multi-root paths
.packages.generated
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
**/generated_plugin_registrant.dart
.packages
.pub-preload-cache/
.pub-cache/
.pub/
/build/
/coverage/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# Symbolication related
# Android related
**/android/**/gradle-wrapper.jar
.gradle/
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
local.properties
**/.cxx/
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# macOS
**/Flutter/ephemeral/
**/Pods/
**/macos/Flutter/GeneratedPluginRegistrant.swift
**/macos/Flutter/ephemeral
**/xcuserdata/
# Windows
**/windows/flutter/ephemeral/
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Linux
**/linux/flutter/ephemeral/
**/linux/flutter/generated_plugin_registrant.cc
**/linux/flutter/generated_plugin_registrant.h
**/linux/flutter/generated_plugins.cmake
# Coverage
coverage/
# Symbols
app.*.symbols
# Obfuscation related
app.*.map.json
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Monorepo
.cipd
.gclient
.gclient_entries
.python-version
.gclient_previous_custom_vars
.gclient_previous_sync_commits

View File

@@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:label="maps_bookmarks"
android:name="${applicationName}"

3
l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View File

@@ -0,0 +1,3 @@
const String appName = 'Maps Bookmarks';
const String jsonFileName = 'MapsBookmarksData.json';
const String defaultAndroidExportDirectory = '/storage/emulated/0/Documents';

43
lib/l10n/app_de.arb Normal file
View File

@@ -0,0 +1,43 @@
{
"@@locale": "de",
"addToCollection": "Speichern in {collection_name}",
"@addToCollection": {
"placeholders": {
"collection_name" : {
"type": "String"
}
}
},
"cancel": "Abbrechen",
"chooseCollection": "Sammlung auswählen",
"collections": "Sammlungen",
"tipCreateCollections": "Erstelle deine erste Sammlung!",
"search": "Suche",
"createBookmark": "Lesezeichen erstellen",
"createCollection": "Sammlung erstellen",
"create": "Erstellen",
"delete": "Löschen",
"add": "Hinzufügen",
"startSearching": "Suche etwas",
"tipNoResults": "Keine Suchergebnisse gefunden",
"collectionName": "Name der Sammlung",
"bookmarkTitle": "Titel des Lesezeichens",
"url": "Url",
"description": "Beschreibung",
"settings": "Einstellungen",
"appData": "App-Daten",
"export": "Exportieren",
"import": "Importieren",
"activateJsonExport": "Immer als JSON speichern",
"@@comment": "Info",
"exportSuccess": "Daten exportiert",
"importSuccess": "Daten importiert",
"@@comment": "Errors",
"errorStoragePermisson": "Zugriff auf Speicher verwehrt",
"errorCouldNotLaunchUrl": "Konnte Url nicht öffnen",
"errorInvalidUrl": "Fehlerhafte Url",
"exportFailed": "Export fehlgeschlagen",
"importFailed": "Import fehlgeschlagen"
}

44
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,44 @@
{
"@@locale": "en",
"addToCollection": "Add to {collection_name}",
"@addToCollection": {
"placeholders": {
"collection_name" : {
"type": "String"
}
}
},
"cancel": "Cancel",
"chooseCollection": "Choose Collection",
"collections": "Collections",
"tipCreateCollections": "Create your first Collection to get started!",
"search": "Search",
"createBookmark": "Create Bookmark",
"createCollection": "Create Collection",
"create": "Create",
"delete": "Delete",
"add": "Add",
"startSearching": "Start searching",
"tipNoResults": "There are no results that match your search",
"collectionName": "Collection Name",
"bookmarkTitle": "Bookmark Title",
"url": "Url",
"description": "Description",
"settings": "Settings",
"appData": "App data",
"export": "Export",
"import": "Import",
"activateJsonExport": "Always save to JSON",
"@@comment": "Info",
"exportSuccess": "Exported data",
"importSuccess": "Imported data",
"@@comment": "Errors",
"errorStoragePermisson": "Storage permissions denied",
"errorCouldNotLaunchUrl": "Could not launch Url",
"errorInvalidUrl": "Invalid Url",
"exportFailed": "Export failed",
"importFailed": "Import failed"
}

View File

@@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'l10n/app_localizations.dart';
import 'pages/collection_page.dart';
import 'pages/collections_list_page.dart';
import 'pages/search_page.dart';
import 'pages/settings_page.dart';
import 'service/search_provider.dart';
import 'service/settings_provider.dart';
import 'service/shared_link_provider.dart';
import 'service/storage.dart';
import 'service/share_intent_service.dart';
@@ -17,6 +21,7 @@ void main() async {
providers: [
ChangeNotifierProvider(create: (_) => SharedLinkProvider()),
ChangeNotifierProvider(create: (_) => SearchProvider()),
ChangeNotifierProvider(create: (_) => SettingsProvider()),
],
child: const MapsBookmarks(),
),
@@ -66,6 +71,13 @@ class _MapsBookmarksState extends State<MapsBookmarks>
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [Locale('en'), Locale('de')],
navigatorKey: _navigatorKey,
theme: lightTheme,
darkTheme: darkTheme,
@@ -74,6 +86,7 @@ class _MapsBookmarksState extends State<MapsBookmarks>
CollectionsListPage.routeName: (context) => const CollectionsListPage(),
CollectionPage.routeName: (context) => const CollectionPage(),
SearchPage.routeName: (context) => const SearchPage(),
SettingsPage.routeName: (context) => const SettingsPage(),
},
);
}

39
lib/model/settings.dart Normal file
View File

@@ -0,0 +1,39 @@
import '../assets/constants.dart' as constants;
class Settings {
final String exportDirectoryPath;
final bool alwaysExportEnabled;
Settings._({
required this.exportDirectoryPath,
required this.alwaysExportEnabled,
});
Map<String, dynamic> toJson() {
return {
'exportDirectoryPath': exportDirectoryPath,
'alwaysExportEnabled': alwaysExportEnabled,
};
}
factory Settings.fromJson(Map<String, dynamic> json) {
return Settings._(
exportDirectoryPath: json['exportDirectoryPath'] as String,
alwaysExportEnabled: json['alwaysExportEnabled'] as bool,
);
}
factory Settings.defaults() {
return Settings._(
exportDirectoryPath: constants.defaultAndroidExportDirectory,
alwaysExportEnabled: false,
);
}
Settings copyWith({String? exportDirectoryPath, bool? alwaysExportEnabled}) {
return Settings._(
exportDirectoryPath: exportDirectoryPath ?? this.exportDirectoryPath,
alwaysExportEnabled: alwaysExportEnabled ?? this.alwaysExportEnabled,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart';
import '../model/bookmark.dart';
import '../model/maps_link_metadata.dart';
import '../service/bookmarks_provider.dart';
@@ -89,20 +90,28 @@ class _CollectionPageState extends State<CollectionPage> {
return Scaffold(
appBar: AppBar(
title: selectedMapsLink != null
? Text('Add to ${collection.name}')
? Text(
AppLocalizations.of(context)!.addToCollection(collection.name),
)
: Text(collection.name),
actions: [
if (selectedMapsLink != null)
TextButton(
onPressed: () => provider.removeCurrentMapsLink(),
child: Text('Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
],
),
body: ListView.builder(
itemBuilder: (context, index) =>
bookmarksListItemBuilder(context, bookmarks.elementAt(index)),
itemCount: bookmarks.length,
body: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: ListView.separated(
itemBuilder: (context, index) =>
bookmarksListItemBuilder(context, bookmarks.elementAt(index)),
itemCount: bookmarks.length,
separatorBuilder: (context, index) => SizedBox(height: 10),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: onAddButtonPressed,

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart';
import '../model/collection.dart';
import '../service/bookmarks_provider.dart';
import '../service/shared_link_provider.dart';
@@ -8,6 +9,7 @@ import '../service/storage.dart';
import '../widgets/create_bookmark_collection_dialog.dart';
import 'collection_page.dart';
import 'search_page.dart' show SearchPage;
import 'settings_page.dart';
class CollectionsListPage extends StatefulWidget {
const CollectionsListPage({super.key});
@@ -20,6 +22,7 @@ class CollectionsListPage extends StatefulWidget {
class _CollectionsListPageState extends State<CollectionsListPage> {
bool addingNewBookmark = false;
var bookmarkCountMap = <int, int>{};
Widget bottomSheetBuilder(BuildContext context) {
final titleTextFieldController = TextEditingController(
@@ -53,6 +56,11 @@ class _CollectionsListPageState extends State<CollectionsListPage> {
title: Text(collection.name),
onTap: () => navigateToCollection(collection.id),
onLongPress: () => onEditCollection(collection),
leading: const Icon(Icons.list_rounded),
trailing: Text(
bookmarkCountMap[collection.id]?.toString() ?? '0',
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
@@ -77,25 +85,32 @@ class _CollectionsListPageState extends State<CollectionsListPage> {
@override
Widget build(BuildContext context) {
final collections = Storage.loadCollections();
final provider = context.watch<SharedLinkProvider>();
addingNewBookmark = provider.currentMapsLinkMetadata != null;
bookmarkCountMap = Storage.loadPerCollectionBookmarkCount();
addingNewBookmark =
context.watch<SharedLinkProvider>().currentMapsLinkMetadata != null;
return Scaffold(
appBar: AppBar(
title: addingNewBookmark
? Text('Choose Collection')
: Text('Collections'),
? Text(AppLocalizations.of(context)!.chooseCollection)
: Text(AppLocalizations.of(context)!.collections),
actions: [
if (addingNewBookmark)
TextButton(
onPressed: () => provider.removeCurrentMapsLink(),
child: Text('Cancel'),
onPressed: () =>
context.read<SharedLinkProvider>().removeCurrentMapsLink(),
child: Text(AppLocalizations.of(context)!.cancel),
)
else
IconButton(
onPressed: () =>
Navigator.of(context).pushNamed(SearchPage.routeName),
icon: Icon(Icons.search),
icon: Icon(Icons.search_rounded),
),
IconButton(
onPressed: () =>
Navigator.of(context).pushNamed(SettingsPage.routeName),
icon: Icon(Icons.settings_rounded),
),
],
),
floatingActionButton: FloatingActionButton(
@@ -103,14 +118,22 @@ class _CollectionsListPageState extends State<CollectionsListPage> {
child: Icon(Icons.add),
),
body: collections.isNotEmpty
? ListView.builder(
itemBuilder: (context, index) => collectionsListItemBuilder(
context,
collections.elementAt(index),
? Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: ListView.separated(
itemBuilder: (context, index) => collectionsListItemBuilder(
context,
collections.elementAt(index),
),
itemCount: collections.length,
separatorBuilder: (context, index) => SizedBox(height: 10),
),
),
itemCount: collections.length,
)
: Center(child: Text('Create your first Collection to get started!')),
: Center(
child: Text(AppLocalizations.of(context)!.tipCreateCollections),
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart';
import '../service/search_provider.dart';
import '../widgets/search_widgets/search_bar_widget.dart';
import '../widgets/search_widgets/search_results_widget.dart';
@@ -12,15 +13,22 @@ class SearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search')),
body: Column(
children: [
SearchBarWidget(
onEditingComplete: context.read<SearchProvider>().setSearchText,
onResetSearch: context.read<SearchProvider>().removeSearchText,
appBar: AppBar(title: Text(AppLocalizations.of(context)!.search)),
body: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: Column(
children: [
Padding(padding: EdgeInsetsGeometry.only(top: 10)),
SearchBarWidget(
onEditingComplete: context.read<SearchProvider>().setSearchText,
onResetSearch: context.read<SearchProvider>().removeSearchText,
),
Padding(padding: EdgeInsetsGeometry.only(top: 10)),
Expanded(child: SearchResultsWidget()),
],
),
Expanded(child: SearchResultsWidget()),
],
),
),
);
}

View File

@@ -0,0 +1,190 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import '../l10n/app_localizations.dart';
import '../service/notifying.dart';
import '../service/permission_service.dart';
import '../service/settings_provider.dart';
import '../service/storage.dart';
class SettingsPage extends StatefulWidget {
static const routeName = '/settings';
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool storagePermissionIsGranted = false;
final tileSpacing = 16.0;
@override
void initState() {
PermissionService.storagePermissionStatus.then((value) {
storagePermissionIsGranted = value.isGranted;
if (context.mounted) setState(() {});
});
super.initState();
}
// TODO: Localize
@override
Widget build(BuildContext context) {
final titlePadding = Theme.of(context).listTileTheme.contentPadding!;
checkStoragePermission();
final alwaysExportEnabled = context
.watch<SettingsProvider>()
.settings
.alwaysExportEnabled;
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)),
body: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: titlePadding,
child: Text(
AppLocalizations.of(context)!.appData,
style: Theme.of(context).textTheme.titleLarge,
),
),
SizedBox(height: tileSpacing),
ListTile(
title: Text('Grant storage permisson'),
subtitle: storagePermissionIsGranted
? Text('Storage permission granted')
: Text(
'For app-data settings to work, you need to grant the app permissions to manage internal storage.',
),
onTap: () => PermissionService.requestStoragePermission,
trailing: Icon(Icons.arrow_forward_ios_rounded),
enabled: !storagePermissionIsGranted,
),
SizedBox(height: tileSpacing),
ListTile(
title: Text(AppLocalizations.of(context)!.import),
subtitle: Text('Import app-data from a json file.'),
onTap: () => onJsonImportPressed(),
trailing: Icon(Icons.arrow_forward_ios_rounded),
enabled: storagePermissionIsGranted,
),
SizedBox(height: tileSpacing),
ListTile(
title: Text(AppLocalizations.of(context)!.export),
subtitle: Text(
'Export app-data to a json file in the selected directory.',
),
onTap: () => onJsonExportPressed(),
trailing: Icon(Icons.arrow_forward_ios_rounded),
enabled: storagePermissionIsGranted,
),
SizedBox(height: tileSpacing),
ListTile(
title: Text('Always save to file'),
subtitle: Text(
'Export app data to a directory, every time you make a change',
),
onTap: () => onAlwaysSaveToJsonPressed(),
trailing: Checkbox(
value: alwaysExportEnabled,
onChanged: (value) {
onAlwaysSaveToJsonPressed();
},
),
enabled: storagePermissionIsGranted,
),
if (alwaysExportEnabled) SizedBox(height: tileSpacing),
if (alwaysExportEnabled)
ListTile(
title: Text('Change export directory'),
subtitle: Text(
context
.watch<SettingsProvider>()
.settings
.exportDirectoryPath,
),
onTap: () => onChangeExportDirectoryPressed(),
trailing: Icon(Icons.arrow_forward_ios_rounded),
enabled: storagePermissionIsGranted,
),
],
),
),
),
);
}
void onJsonExportPressed() async {
if (!await checkStoragePermission()) return;
Storage.exportToJsonFile().then(showExportInfo);
}
void onJsonImportPressed() async {
if (!await checkStoragePermission()) return;
Storage.importFromJsonFile().then(showImportInfo);
}
void onAlwaysSaveToJsonPressed() async {
if (context.read<SettingsProvider>().settings.alwaysExportEnabled) {
context.read<SettingsProvider>().setExportDirectoryPath('', silent: true);
context.read<SettingsProvider>().setAlwaysExportEnabled(false);
return;
}
if (!await PermissionService.storagePermissionStatus.isGranted) return;
final dir = await Storage.selectDirectoryPath();
if (dir.isEmpty || !context.mounted) return;
// ignore: use_build_context_synchronously
context.read<SettingsProvider>().setExportDirectoryPath(dir, silent: true);
// ignore: use_build_context_synchronously
Storage.saveDataToFile().whenComplete(
// ignore: use_build_context_synchronously
() => context.read<SettingsProvider>().setAlwaysExportEnabled(true),
);
}
void onChangeExportDirectoryPressed() async {
if (!await PermissionService.storagePermissionStatus.isGranted) return;
final dir = await Storage.selectDirectoryPath();
if (dir.isEmpty || !context.mounted) return;
// ignore: use_build_context_synchronously
context.read<SettingsProvider>().setExportDirectoryPath(dir);
}
Future<bool> checkStoragePermission() async {
PermissionService.storagePermissionStatus.then((value) {
if (context.mounted && value.isGranted != storagePermissionIsGranted) {
storagePermissionIsGranted = value.isGranted;
setState(() {});
}
});
return storagePermissionIsGranted;
}
void showExportInfo(bool success) => Notifying.showSnackbar(
context,
text: success
? AppLocalizations.of(context)!.exportSuccess
: AppLocalizations.of(context)!.exportFailed,
isError: !success,
);
void showImportInfo(bool success) => Notifying.showSnackbar(
context,
text: success
? AppLocalizations.of(context)!.importSuccess
: AppLocalizations.of(context)!.importFailed,
isError: !success,
);
}

View File

@@ -0,0 +1,90 @@
import 'dart:convert';
import 'dart:io';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/services.dart';
import '../model/bookmark.dart';
import '../model/collection.dart';
import '../assets/constants.dart' as constants;
class JsonFileService {
static Future<bool> exportToJson({
required List<Collection> collections,
required List<Bookmark> bookmarks,
}) async {
try {
final dir = await selectDirectoryPath();
if (dir.isEmpty) return false;
saveDataToFile(collections, bookmarks, dir);
} catch (e) {
return false;
}
return true;
}
static Future<({List<Collection> collections, List<Bookmark> bookmarks})>
importFromJson() async {
try {
const typeGroup = XTypeGroup(label: 'json', extensions: <String>['json']);
final XFile? file = await openFile(
acceptedTypeGroups: <XTypeGroup>[typeGroup],
);
if (file == null) {
return (collections: <Collection>[], bookmarks: <Bookmark>[]);
}
final jsonString = await file.readAsString();
final data = jsonDecode(jsonString) as Map<String, dynamic>;
final collections = (data['collections'] as List<dynamic>? ?? [])
.map((json) => Collection.fromJson(json as Map<String, dynamic>))
.toList();
final bookmarks = (data['bookmarks'] as List<dynamic>? ?? [])
.map((json) => Bookmark.fromJson(json as Map<String, dynamic>))
.toList();
return (collections: collections, bookmarks: bookmarks);
} catch (e) {
return (collections: <Collection>[], bookmarks: <Bookmark>[]);
}
}
static Future<bool> saveDataToFile(
List<Collection> collections,
List<Bookmark> bookmarks,
String directory,
) async {
try {
final data = jsonEncode({
'collections': collections.map((c) => c.toJson()).toList(),
'bookmarks': bookmarks.map((b) => b.toJson()).toList(),
}).codeUnits;
final file = XFile.fromData(
Uint8List.fromList(data),
mimeType: 'application/json',
name: constants.jsonFileName,
);
file.saveTo('$directory/${constants.jsonFileName}');
} catch (e) {
return false;
}
return true;
}
static Future<String> selectDirectoryPath() async {
if (Platform.isAndroid) {
return await getDirectoryPath(
initialDirectory: constants.defaultAndroidExportDirectory,
) ??
'';
}
return await getDirectoryPath() ?? '';
}
}

View File

@@ -1,15 +1,9 @@
// service/maps_launcher_service.dart
import 'dart:io' show Platform;
import 'package:android_intent_plus/android_intent.dart';
class MapsLauncherService {
/// Opens a URL in Google Maps app
/// Falls back to browser if Maps app is not installed
static Future<bool> openInGoogleMaps(String url) async {
if (!Platform.isAndroid) {
// Handle iOS or other platforms if needed
return false;
}
if (!Platform.isAndroid) return false;
try {
// Try to open in Google Maps app
@@ -28,36 +22,16 @@ class MapsLauncherService {
await browserIntent.launch();
return true;
} catch (e) {
print('Failed to open maps link: $e');
return false;
}
}
}
/// Opens navigation to specific coordinates
static Future<bool> navigateToCoordinates(
String latitude,
String longitude,
) async {
final url = 'google.navigation:q=$latitude,$longitude';
return openInGoogleMaps(url);
}
/// Opens a search query in Google Maps
static Future<bool> searchInMaps(String query) async {
final encodedQuery = Uri.encodeComponent(query);
final url = 'geo:0,0?q=$encodedQuery';
return openInGoogleMaps(url);
}
/// Shares a Google Maps link or location via Android share sheet
static Future<bool> shareLocation({
required String text,
String? subject,
}) async {
if (!Platform.isAndroid) {
return false;
}
if (!Platform.isAndroid) return false;
try {
final intent = AndroidIntent(
@@ -72,7 +46,6 @@ class MapsLauncherService {
await intent.launch();
return true;
} catch (e) {
print('Failed to share location: $e');
return false;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
import 'url_launcher.dart' show UrlLaunchErrorCode;
class Notifying {
@@ -8,13 +9,31 @@ class Notifying {
required String text,
bool isError = false,
}) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
text,
style: isError
? TextStyle(color: Theme.of(context).colorScheme.error)
: null,
backgroundColor: isError ? Theme.of(context).colorScheme.error : null,
content: SizedBox(
height: 30,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
text,
style: isError
? TextStyle(color: Theme.of(context).colorScheme.onError)
: null,
),
IconButton(
onPressed: () =>
ScaffoldMessenger.of(context).hideCurrentSnackBar(),
icon: Icon(
Icons.close_rounded,
color: isError ? Theme.of(context).colorScheme.onError : null,
),
),
],
),
),
),
);
@@ -28,10 +47,18 @@ class Notifying {
if (errorCode == UrlLaunchErrorCode.none) {
return;
} else if (errorCode == UrlLaunchErrorCode.couldNotLaunch) {
errorText = 'Could not launch Url';
errorText = AppLocalizations.of(context)!.errorCouldNotLaunchUrl;
} else {
errorText = 'Invalid Url';
errorText = AppLocalizations.of(context)!.errorInvalidUrl;
}
showSnackbar(context, text: errorText, isError: true);
}
static void showErrorSnackbar(BuildContext context, String message) {
showSnackbar(context, text: message, isError: true);
}
static void showMessageSnackbar(BuildContext context, String message) {
showSnackbar(context, text: message, isError: false);
}
}

View File

@@ -0,0 +1,9 @@
import 'package:permission_handler/permission_handler.dart';
class PermissionService {
static Future<PermissionStatus> get storagePermissionStatus =>
Permission.manageExternalStorage.status;
static Future<PermissionStatus> get requestStoragePermission =>
Permission.manageExternalStorage.request();
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import '../model/settings.dart';
import 'storage.dart';
class SettingsProvider extends ChangeNotifier {
SettingsProvider() : _settings = Storage.loadSettings();
Settings _settings;
Settings get settings => _settings;
void setExportDirectoryPath(String path, {bool silent = false}) {
_settings = _settings.copyWith(exportDirectoryPath: path);
Storage.saveSettings(_settings);
if (!silent) notifyListeners();
}
void setAlwaysExportEnabled(bool enabled, {bool silent = false}) {
_settings = _settings.copyWith(alwaysExportEnabled: enabled);
Storage.saveSettings(_settings);
if (!silent) notifyListeners();
}
}

View File

@@ -4,21 +4,49 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../model/bookmark.dart';
import '../model/collection.dart';
import '../model/settings.dart';
import 'json_file_service.dart';
class Storage {
static const String _bookmarksKey = 'bookmarks';
static const String _collectionsKey = 'collections';
static SharedPreferencesWithCache? _prefsWithCache;
static const String _settingsKey = 'settings';
static const String _statsKey = 'stats';
static Settings _currentSettings = Settings.defaults();
static Future<void> initialize() async {
_prefsWithCache = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: <String>{_collectionsKey, _bookmarksKey, _statsKey},
allowList: <String>{
_collectionsKey,
_bookmarksKey,
_statsKey,
_settingsKey,
},
),
);
}
static Settings loadSettings() {
final jsonString = _prefs.getString(_settingsKey);
if (jsonString != null) {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
_currentSettings = Settings.fromJson(json);
return _currentSettings;
} else {
final settings = Settings.defaults();
saveSettings(settings);
return settings;
}
}
static Future<void> saveSettings(Settings settings) {
final json = jsonEncode(settings.toJson());
_currentSettings = settings;
return _prefs.setString(_settingsKey, json);
}
static List<Collection> loadCollections() {
final jsonString = _prefs.getString(_collectionsKey) ?? '[]';
final jsonList = jsonDecode(jsonString) as List;
@@ -38,11 +66,13 @@ class Storage {
static Future<void> saveCollections(List<Collection> collections) async {
final jsonList = collections.map((c) => c.toJson()).toList();
await _prefs.setString(_collectionsKey, jsonEncode(jsonList));
if (_currentSettings.alwaysExportEnabled) saveDataToFile();
}
static Future<void> saveBookmarks(List<Bookmark> bookmarks) async {
final jsonList = bookmarks.map((b) => b.toJson()).toList();
await _prefs.setString(_bookmarksKey, jsonEncode(jsonList));
if (_currentSettings.alwaysExportEnabled) saveDataToFile();
}
static List<Bookmark> loadBookmarksForCollection(int collectionId) {
@@ -50,6 +80,14 @@ class Storage {
return allBookmarks.where((b) => b.collectionId == collectionId).toList();
}
static Map<int, int> loadPerCollectionBookmarkCount() {
return loadBookmarks().fold(<int, int>{}, (map, bookmark) {
map[bookmark.collectionId] ??= 0;
map[bookmark.collectionId] = map[bookmark.collectionId]! + 1;
return map;
});
}
static Future<void> addBookmark(Bookmark bookmark) async {
final bookmarks = loadBookmarks();
bookmarks.add(bookmark);
@@ -147,6 +185,31 @@ class Storage {
await _prefs.setString(_statsKey, jsonEncode(stats));
}
static Future<bool> exportToJsonFile() => JsonFileService.exportToJson(
collections: loadCollections(),
bookmarks: loadBookmarks(),
);
static Future<bool> importFromJsonFile() async {
final import = await JsonFileService.importFromJson();
if (import.bookmarks.isNotEmpty || import.collections.isNotEmpty) {
saveBookmarks(import.bookmarks);
saveCollections(import.collections);
return true;
}
return false;
}
static Future<String> selectDirectoryPath() =>
JsonFileService.selectDirectoryPath();
static Future<bool> saveDataToFile() => JsonFileService.saveDataToFile(
loadCollections(),
loadBookmarks(),
_currentSettings.exportDirectoryPath,
);
static SharedPreferencesWithCache get _prefs {
if (_prefsWithCache == null) {
throw StateError(

View File

@@ -1,15 +1,27 @@
import 'package:flutter/material.dart';
ThemeData get lightTheme => ThemeData.from(colorScheme: _lightColorScheme);
const _seed = Colors.deepPurple;
ThemeData get darkTheme => ThemeData.from(colorScheme: _darkColorScheme);
ColorScheme get _lightColorScheme =>
ColorScheme.fromSeed(seedColor: _seed, brightness: Brightness.light);
ColorScheme get _darkColorScheme => ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
);
ColorScheme get _darkColorScheme =>
ColorScheme.fromSeed(seedColor: _seed, brightness: Brightness.dark);
ColorScheme get _lightColorScheme => ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
);
ThemeData get lightTheme => _baseTheme(_lightColorScheme);
ThemeData get darkTheme => _baseTheme(_darkColorScheme);
ThemeData _baseTheme(ColorScheme scheme) =>
ThemeData.from(colorScheme: scheme, useMaterial3: true).copyWith(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
listTileTheme: ListTileThemeData(
tileColor: scheme.surfaceContainer,
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusGeometry.circular(12),
),
contentPadding: EdgeInsetsDirectional.only(start: 16.0, end: 24.0),
),
);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../l10n/app_localizations.dart';
import '../model/collection.dart';
import 'edit_dialog_widgets/edit_dialog_actions.dart' show EditDialogActions;
import 'edit_dialog_widgets/edit_dialog_title.dart';
@@ -40,9 +41,7 @@ class CreateBookmarkCollectionDialog extends StatelessWidget {
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
],
decoration: InputDecoration(
// TODO: Localize
labelText: 'Collection Name',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
labelText: AppLocalizations.of(context)!.collectionName,
),
),
actions: [

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../l10n/app_localizations.dart';
import '../model/bookmark.dart';
import '../model/maps_link_metadata.dart';
import 'edit_dialog_widgets/edit_dialog_actions.dart';
@@ -59,10 +60,7 @@ class CreateBookmarkDialog extends StatelessWidget {
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
],
decoration: InputDecoration(
labelText: 'Bookmark Title',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
labelText: AppLocalizations.of(context)!.bookmarkTitle,
),
),
TextField(
@@ -76,10 +74,7 @@ class CreateBookmarkDialog extends StatelessWidget {
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
],
decoration: InputDecoration(
labelText: 'Url',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
labelText: AppLocalizations.of(context)!.url,
),
),
TextField(
@@ -93,10 +88,7 @@ class CreateBookmarkDialog extends StatelessWidget {
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
],
decoration: InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
labelText: AppLocalizations.of(context)!.description,
),
),
],

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart'
show TextButton, FloatingActionButton, Icons;
import 'package:flutter/widgets.dart';
import '../../l10n/app_localizations.dart';
class EditDialogActions extends StatelessWidget {
const EditDialogActions({super.key, required this.onSavePressed});
final VoidCallback onSavePressed;
@@ -13,7 +15,7 @@ class EditDialogActions extends StatelessWidget {
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
FloatingActionButton(onPressed: onSavePressed, child: Icon(Icons.save)),
],

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart' show TextButton, Theme;
import 'package:flutter/widgets.dart';
import '../../l10n/app_localizations.dart';
class EditDialogTitle extends StatelessWidget {
const EditDialogTitle({
super.key,
@@ -15,11 +17,10 @@ class EditDialogTitle extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// TODO: Localize
if (dialogType == DialogType.bookmark)
Text('Create Bookmark')
Text(AppLocalizations.of(context)!.createBookmark)
else
Text('Create Collection'),
Text(AppLocalizations.of(context)!.createCollection),
if (onDeletePressed != null)
TextButton(
@@ -28,7 +29,7 @@ class EditDialogTitle extends StatelessWidget {
Navigator.of(context).pop();
},
child: Text(
'Delete',
AppLocalizations.of(context)!.delete,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
class SearchBarWidget extends StatelessWidget {
const SearchBarWidget({
super.key,
@@ -28,6 +30,7 @@ class SearchBarWidget extends StatelessWidget {
},
icon: Icon(Icons.delete_outline_outlined),
),
labelText: AppLocalizations.of(context)!.search,
),
);
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../l10n/app_localizations.dart';
import '../../model/bookmark.dart';
import '../../service/notifying.dart';
import '../../service/search_provider.dart';
@@ -46,11 +47,12 @@ class _SearchResultsWidgetState extends State<SearchResultsWidget> {
@override
Widget build(BuildContext context) {
if (filteredBookmarks.isNotEmpty) {
return ListView.builder(
return ListView.separated(
itemBuilder: bookmarkListItemBuilder,
itemCount: filteredBookmarks.length,
separatorBuilder: (context, index) => SizedBox(height: 10),
);
}
return Center(child: Text('Start searching'));
return Center(child: Text(AppLocalizations.of(context)!.tipNoResults));
}
}

View File

@@ -49,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
url: "https://pub.dev"
source: hosted
version: "0.3.5+1"
csslib:
dependency: transitive
description:
@@ -89,6 +97,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector:
dependency: "direct main"
description:
name: file_selector
sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a
url: "https://pub.dev"
source: hosted
version: "1.1.0"
file_selector_android:
dependency: transitive
description:
name: file_selector_android
sha256: "51e8fd0446de75e4b62c065b76db2210c704562d072339d333bd89c57a7f8a7c"
url: "https://pub.dev"
source: hosted
version: "0.5.2+4"
file_selector_ios:
dependency: transitive
description:
name: file_selector_ios
sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca
url: "https://pub.dev"
source: hosted
version: "0.5.3+5"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_web:
dependency: transitive
description:
name: file_selector_web
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
flutter:
dependency: "direct main"
description: flutter
@@ -102,6 +174,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -136,6 +213,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
@@ -240,6 +325,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:
@@ -486,5 +619,5 @@ packages:
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"

View File

@@ -3,7 +3,7 @@ description: "A new way to save google maps bookmarks"
publish_to: 'none'
version: 1.0.0+1
version: 0.0.18
environment:
sdk: ^3.9.2
@@ -18,6 +18,11 @@ dependencies:
provider: ^6.1.5+1
metadata_fetch: ^0.4.2
url_launcher: ^6.3.2
flutter_localizations:
sdk: flutter
intl: any
permission_handler: ^12.0.1
file_selector: ^1.1.0
dev_dependencies:
flutter_test:
@@ -26,5 +31,5 @@ dev_dependencies:
flutter_lints: ^6.0.0
flutter:
generate: true
uses-material-design: true