41 Commits

Author SHA1 Message Date
83ec068a27 added license 2026-01-29 19:03:42 +01:00
a984269c15 Merge branch 'main' into development 2026-01-27 14:29:01 +01:00
600ff26016 added action bar for selected bookmark item 2026-01-27 14:16:43 +01:00
9c85d565a9 changed listtile theme 2026-01-27 13:52:14 +01:00
446ef9a57a fixed settings page not updating on storage permission granted
All checks were successful
Flutter APK Build / Build Flutter APK (push) Successful in 6m42s
2026-01-23 18:26:08 +01:00
6103d0b679 Merge branch 'development'
Some checks failed
Flutter APK Build / Build Flutter APK (push) Has been cancelled
2026-01-23 18:24:29 +01:00
5fd690197a fixed workflow running on pull request 2026-01-23 18:21:41 +01:00
31c0ade243 fixed settings page not refreshing on granting storage permission
Some checks failed
Flutter APK Build / Build Flutter APK (pull_request) Has been cancelled
2026-01-23 18:18:25 +01:00
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
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
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
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
26 changed files with 1227 additions and 461 deletions

View File

@@ -4,9 +4,6 @@ on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

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

339
License Normal file
View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

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}"

View File

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

View File

@@ -23,5 +23,21 @@
"collectionName": "Name der Sammlung",
"bookmarkTitle": "Titel des Lesezeichens",
"url": "Url",
"description": "Beschreibung"
"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"
}

View File

@@ -23,5 +23,22 @@
"collectionName": "Collection Name",
"bookmarkTitle": "Bookmark Title",
"url": "Url",
"description": "Description"
"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,236 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en'),
];
/// No description provided for @addToCollection.
///
/// In en, this message translates to:
/// **'Add to {collection_name}'**
String addToCollection(String collection_name);
/// No description provided for @cancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancel;
/// No description provided for @chooseCollection.
///
/// In en, this message translates to:
/// **'Choose Collection'**
String get chooseCollection;
/// No description provided for @collections.
///
/// In en, this message translates to:
/// **'Collections'**
String get collections;
/// No description provided for @tipCreateCollections.
///
/// In en, this message translates to:
/// **'Create your first Collection to get started!'**
String get tipCreateCollections;
/// No description provided for @search.
///
/// In en, this message translates to:
/// **'Search'**
String get search;
/// No description provided for @createBookmark.
///
/// In en, this message translates to:
/// **'Create Bookmark'**
String get createBookmark;
/// No description provided for @createCollection.
///
/// In en, this message translates to:
/// **'Create Collection'**
String get createCollection;
/// No description provided for @create.
///
/// In en, this message translates to:
/// **'Create'**
String get create;
/// No description provided for @delete.
///
/// In en, this message translates to:
/// **'Delete'**
String get delete;
/// No description provided for @add.
///
/// In en, this message translates to:
/// **'Add'**
String get add;
/// No description provided for @startSearching.
///
/// In en, this message translates to:
/// **'Start searching'**
String get startSearching;
/// No description provided for @tipNoResults.
///
/// In en, this message translates to:
/// **'There are no results that match your search'**
String get tipNoResults;
/// No description provided for @collectionName.
///
/// In en, this message translates to:
/// **'Collection Name'**
String get collectionName;
/// No description provided for @bookmarkTitle.
///
/// In en, this message translates to:
/// **'Bookmark Title'**
String get bookmarkTitle;
/// No description provided for @url.
///
/// In en, this message translates to:
/// **'Url'**
String get url;
/// No description provided for @description.
///
/// In en, this message translates to:
/// **'Description'**
String get description;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['de', 'en'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}

View File

@@ -1,63 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for German (`de`).
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String addToCollection(String collection_name) {
return 'Speichern in $collection_name';
}
@override
String get cancel => 'Abbrechen';
@override
String get chooseCollection => 'Sammlung auswählen';
@override
String get collections => 'Sammlungen';
@override
String get tipCreateCollections => 'Erstelle deine erste Sammlung!';
@override
String get search => 'Suche';
@override
String get createBookmark => 'Lesezeichen erstellen';
@override
String get createCollection => 'Sammlung erstellen';
@override
String get create => 'Erstellen';
@override
String get delete => 'Löschen';
@override
String get add => 'Hinzufügen';
@override
String get startSearching => 'Suche etwas';
@override
String get tipNoResults => 'Keine Suchergebnisse gefunden';
@override
String get collectionName => 'Name der Sammlung';
@override
String get bookmarkTitle => 'Titel des Lesezeichens';
@override
String get url => 'Url';
@override
String get description => 'Beschreibung';
}

View File

@@ -1,64 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String addToCollection(String collection_name) {
return 'Add to $collection_name';
}
@override
String get cancel => 'Cancel';
@override
String get chooseCollection => 'Choose Collection';
@override
String get collections => 'Collections';
@override
String get tipCreateCollections =>
'Create your first Collection to get started!';
@override
String get search => 'Search';
@override
String get createBookmark => 'Create Bookmark';
@override
String get createCollection => 'Create Collection';
@override
String get create => 'Create';
@override
String get delete => 'Delete';
@override
String get add => 'Add';
@override
String get startSearching => 'Start searching';
@override
String get tipNoResults => 'There are no results that match your search';
@override
String get collectionName => 'Collection Name';
@override
String get bookmarkTitle => 'Bookmark Title';
@override
String get url => 'Url';
@override
String get description => 'Description';
}

View File

@@ -5,7 +5,9 @@ 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';
@@ -19,6 +21,7 @@ void main() async {
providers: [
ChangeNotifierProvider(create: (_) => SharedLinkProvider()),
ChangeNotifierProvider(create: (_) => SearchProvider()),
ChangeNotifierProvider(create: (_) => SettingsProvider()),
],
child: const MapsBookmarks(),
),
@@ -83,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

@@ -9,6 +9,7 @@ import '../service/notifying.dart';
import '../service/shared_link_provider.dart';
import '../service/storage.dart';
import '../service/url_launcher.dart';
import '../widgets/collection_page_widgets/list_item_actions_widget.dart';
import '../widgets/create_bookmark_dialog.dart';
class CollectionPage extends StatefulWidget {
@@ -22,6 +23,7 @@ class CollectionPage extends StatefulWidget {
class _CollectionPageState extends State<CollectionPage> {
MapsLinkMetadata? selectedMapsLink;
int selectedBookmarkId = -1;
@override
void initState() {
@@ -52,29 +54,46 @@ class _CollectionPageState extends State<CollectionPage> {
).whenComplete(() => setState(() {}));
},
),
);
).whenComplete(deselectBookmark);
void onBookmarkSaved(Bookmark bookmark) {
Storage.addOrUpdateBookmark(bookmark);
setState(() {});
context.read<SharedLinkProvider>().removeCurrentMapsLink();
Provider.of<SharedLinkProvider>(
context,
listen: false,
).removeCurrentMapsLink();
}
Widget bookmarksListItemBuilder(BuildContext context, Bookmark bookmark) {
final selected = selectedBookmarkId == bookmark.id;
return ListTile(
title: Text(bookmark.name),
onTap: () => launchUrlFromString(bookmark.link).then((errorCode) {
selected: selected,
onTap: () {
if (selected) {
onCancelSelectionPressed();
setState(() {});
} else if (selectedBookmarkId != -1 && !selected) {
selectedBookmarkId = bookmark.id;
setState(() {});
} else {
launchUrlFromString(bookmark.link).then((errorCode) {
if (context.mounted) {
return Notifying.showUrlErrorSnackbar(context, errorCode);
}
});
}
},
onLongPress: () => setState(() {
selectedBookmarkId = bookmark.id;
}),
onLongPress: () => editBookmark(bookmark),
);
}
@override
Widget build(BuildContext context) {
SharedLinkProvider provider = context.watch<SharedLinkProvider>();
SharedLinkProvider provider = Provider.of<SharedLinkProvider>(context);
selectedMapsLink = provider.currentMapsLinkMetadata;
if (BookmarksProvider.selectedCollectionId == null) {
@@ -87,11 +106,18 @@ class _CollectionPageState extends State<CollectionPage> {
(c) => c.id == BookmarksProvider.selectedCollectionId,
);
return Scaffold(
return PopScope(
canPop: selectedBookmarkId == -1,
onPopInvokedWithResult: (didPop, result) {
if (didPop == false) deselectBookmark();
},
child: Scaffold(
appBar: AppBar(
title: selectedMapsLink != null
? Text(
AppLocalizations.of(context)!.addToCollection(collection.name),
AppLocalizations.of(
context,
)!.addToCollection(collection.name),
)
: Text(collection.name),
actions: [
@@ -102,6 +128,17 @@ class _CollectionPageState extends State<CollectionPage> {
),
],
),
bottomNavigationBar: selectedBookmarkId > 0
? ListItemActionsWidget(
onDeletePressed: onDeleteBookmarkPressed,
onCancelPressed: onCancelSelectionPressed,
onEditPressed: () => editBookmark(
bookmarks.firstWhere(
(element) => element.id == selectedBookmarkId,
),
),
)
: null,
body: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
@@ -117,6 +154,21 @@ class _CollectionPageState extends State<CollectionPage> {
onPressed: onAddButtonPressed,
child: Icon(selectedMapsLink != null ? Icons.save : Icons.add),
),
),
);
}
void onCancelSelectionPressed() => deselectBookmark();
void onDeleteBookmarkPressed() {
Storage.deleteBookmarkById(
selectedBookmarkId,
).whenComplete(() => setState(() {}));
deselectBookmark();
}
void deselectBookmark() {
selectedBookmarkId = -1;
setState(() {});
}
}

View File

@@ -9,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});
@@ -86,7 +87,8 @@ class _CollectionsListPageState extends State<CollectionsListPage> {
final collections = Storage.loadCollections();
bookmarkCountMap = Storage.loadPerCollectionBookmarkCount();
addingNewBookmark =
context.watch<SharedLinkProvider>().currentMapsLinkMetadata != null;
Provider.of<SharedLinkProvider>(context).currentMapsLinkMetadata !=
null;
return Scaffold(
appBar: AppBar(
title: addingNewBookmark
@@ -95,15 +97,22 @@ class _CollectionsListPageState extends State<CollectionsListPage> {
actions: [
if (addingNewBookmark)
TextButton(
onPressed: () =>
context.read<SharedLinkProvider>().removeCurrentMapsLink(),
onPressed: () => Provider.of<SharedLinkProvider>(
context,
listen: false,
).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),
),
],
),

View File

@@ -0,0 +1,191 @@
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
.whenComplete(() => checkStoragePermission()),
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 {
@@ -11,7 +12,7 @@ class Notifying {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Theme.of(context).colorScheme.error,
backgroundColor: isError ? Theme.of(context).colorScheme.error : null,
content: SizedBox(
height: 30,
child: Row(
@@ -28,7 +29,7 @@ class Notifying {
ScaffoldMessenger.of(context).hideCurrentSnackBar(),
icon: Icon(
Icons.close_rounded,
color: Theme.of(context).colorScheme.onError,
color: isError ? Theme.of(context).colorScheme.onError : null,
),
),
],
@@ -46,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) {
@@ -155,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

@@ -22,5 +22,9 @@ ThemeData _baseTheme(ColorScheme scheme) =>
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusGeometry.circular(12),
),
textColor: scheme.onPrimaryContainer,
selectedTileColor: scheme.primaryContainer,
selectedColor: scheme.onPrimaryContainer,
contentPadding: EdgeInsetsDirectional.only(start: 16.0, end: 24.0),
),
);

View File

@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
class ListItemActionsWidget extends StatelessWidget {
final VoidCallback _onDeletePressed;
final VoidCallback _onCancelPressed;
final VoidCallback _onEditPressed;
const ListItemActionsWidget({
super.key,
required void Function() onDeletePressed,
required void Function() onCancelPressed,
required void Function() onEditPressed,
}) : _onEditPressed = onEditPressed,
_onCancelPressed = onCancelPressed,
_onDeletePressed = onDeletePressed;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsetsGeometry.fromLTRB(
10,
10,
10,
MediaQuery.of(context).viewPadding.bottom,
),
color: Theme.of(context).colorScheme.surfaceContainer,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
onPressed: _onCancelPressed,
icon: const Icon(Icons.close_rounded),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: _onDeletePressed,
icon: const Icon(Icons.delete_forever_rounded),
),
IconButton(
onPressed: _onEditPressed,
icon: const Icon(Icons.edit_rounded),
),
],
),
],
),
);
}
}

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

@@ -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
@@ -253,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:
@@ -499,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

@@ -21,6 +21,8 @@ dependencies:
flutter_localizations:
sdk: flutter
intl: any
permission_handler: ^12.0.1
file_selector: ^1.1.0
dev_dependencies:
flutter_test: