├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── .zenodo.json
├── LICENSE
├── PRIVACY.md
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── Gemfile
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── app
│ │ │ │ └── wispar
│ │ │ │ └── wispar
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ └── ic_bg_service_small.png
│ │ │ ├── drawable-mdpi
│ │ │ └── ic_bg_service_small.png
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable-xhdpi
│ │ │ └── ic_bg_service_small.png
│ │ │ ├── drawable-xxhdpi
│ │ │ └── ic_bg_service_small.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ └── ic_bg_service_small.png
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── fastlane
│ ├── Appfile
│ ├── Fastfile
│ ├── README.md
│ ├── metadata
│ │ └── android
│ │ │ ├── en-US
│ │ │ ├── full_description.txt
│ │ │ ├── images
│ │ │ │ ├── featureGraphic.png
│ │ │ │ ├── icon.png
│ │ │ │ ├── phoneScreenshots
│ │ │ │ │ ├── 1_en-US.png
│ │ │ │ │ ├── 2_en-US.png
│ │ │ │ │ ├── 3_en-US.png
│ │ │ │ │ ├── 4_en-US.png
│ │ │ │ │ ├── 5_en-US.png
│ │ │ │ │ ├── 6_en-US.png
│ │ │ │ │ └── 7_en-US.png
│ │ │ │ └── tenInchScreenshots
│ │ │ │ │ ├── 1_en-US.png
│ │ │ │ │ ├── 2_en-US.png
│ │ │ │ │ ├── 3_en-US.png
│ │ │ │ │ ├── 4_en-US.png
│ │ │ │ │ ├── 5_en-US.png
│ │ │ │ │ ├── 6_en-US.png
│ │ │ │ │ └── 7_en-US.png
│ │ │ ├── short_description.txt
│ │ │ ├── title.txt
│ │ │ └── video.txt
│ │ │ └── fr-CA
│ │ │ ├── full_description.txt
│ │ │ ├── short_description.txt
│ │ │ ├── title.txt
│ │ │ └── video.txt
│ └── report.xml
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
└── icon
│ └── icon.png
├── devtools_options.yaml
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── PrivacyInfo.xcprivacy
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-50x50@1x.png
│ │ │ ├── Icon-App-50x50@2x.png
│ │ │ ├── Icon-App-57x57@1x.png
│ │ │ ├── Icon-App-57x57@2x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-72x72@1x.png
│ │ │ ├── Icon-App-72x72@2x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ ├── Runner-Bridging-Header.h
│ ├── Runner.entitlements
│ └── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
└── RunnerTests
│ └── RunnerTests.swift
├── l10n.yaml
├── lib
├── l10n
│ ├── app_de.arb
│ ├── app_en.arb
│ ├── app_es.arb
│ ├── app_fa.arb
│ ├── app_fr.arb
│ ├── app_id.arb
│ ├── app_ja.arb
│ ├── app_nb.arb
│ ├── app_nl.arb
│ ├── app_pt.arb
│ ├── app_ru.arb
│ ├── app_ta.arb
│ ├── app_tr.arb
│ ├── app_zh.arb
│ └── app_zh_Hans.arb
├── locale_provider.dart
├── main.dart
├── models
│ ├── crossref_journals_models.dart
│ ├── crossref_journals_works_models.dart
│ ├── journal_entity.dart
│ ├── openAlex_works_models.dart
│ └── unpaywall_models.dart
├── screens
│ ├── article_screen.dart
│ ├── article_search_results_screen.dart
│ ├── article_website.dart
│ ├── database_settings_screen.dart
│ ├── display_settings_screen.dart
│ ├── downloads_screen.dart
│ ├── favorites_screen.dart
│ ├── hidden_articles_screen.dart
│ ├── home_screen.dart
│ ├── institutions_screen.dart
│ ├── introduction_screen.dart
│ ├── journals_details_screen.dart
│ ├── journals_search_results_screen.dart
│ ├── library_screen.dart
│ ├── logs_screen.dart
│ ├── pdf_reader.dart
│ ├── search_screen.dart
│ ├── settings_screen.dart
│ └── zotero_settings_screen.dart
├── services
│ ├── abstract_helper.dart
│ ├── abstract_scraper.dart
│ ├── background_service.dart
│ ├── crossref_api.dart
│ ├── database_helper.dart
│ ├── feed_api.dart
│ ├── feed_service.dart
│ ├── libproxydb_api.dart
│ ├── logs_helper.dart
│ ├── mathml_converter.dart
│ ├── openAlex_api.dart
│ ├── string_format_helper.dart
│ ├── unpaywall_api.dart
│ └── zotero_api.dart
├── theme_provider.dart
└── widgets
│ ├── article_crossref_search_form.dart
│ ├── article_doi_search_form.dart
│ ├── article_openAlex_search_form.dart
│ ├── article_query_search_form.dart
│ ├── article_search_form.dart
│ ├── author_search_form.dart
│ ├── downloaded_card.dart
│ ├── journal_card.dart
│ ├── journal_follow_button.dart
│ ├── journal_header.dart
│ ├── journal_search_form.dart
│ ├── journal_search_results_card.dart
│ ├── journals_tab_content.dart
│ ├── latest_works_header.dart
│ ├── publication_card.dart
│ ├── queries_tab_content.dart
│ ├── search_query_card.dart
│ ├── sortbydialog.dart
│ └── sortorderdialog.dart
├── macos
└── Podfile
├── metadata
└── en-US
│ ├── full_description.txt
│ ├── images
│ ├── featureGraphic.png
│ ├── icon.png
│ ├── phoneScreenshots
│ │ ├── 1_en-US.png
│ │ ├── 2_en-US.png
│ │ ├── 3_en-US.png
│ │ ├── 4_en-US.png
│ │ ├── 5_en-US.png
│ │ ├── 6_en-US.png
│ │ └── 7_en-US.png
│ └── tenInchScreenshots
│ │ ├── 1_en-US.png
│ │ ├── 2_en-US.png
│ │ ├── 3_en-US.png
│ │ ├── 4_en-US.png
│ │ ├── 5_en-US.png
│ │ ├── 6_en-US.png
│ │ └── 7_en-US.png
│ └── short_description.txt
├── pubspec.lock
├── pubspec.yaml
├── renovate.json
└── wispar.iml
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: scriptbash # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Device (please complete the following information):**
27 | - Device: [e.g. iPhone6, Pixel 7]
28 | - OS: [e.g. iOS8.1, Android 14]
29 | - App version [e.g. 0.1.0]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | pull_request:
5 | workflow_dispatch:
6 |
7 | jobs:
8 | build-android:
9 | name: Build for Android
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Install Ninja
14 | run: sudo apt-get install -y ninja-build
15 | - uses: actions/setup-java@v4
16 | with:
17 | distribution: 'temurin'
18 | java-version: '21'
19 | - uses: subosito/flutter-action@v2
20 | with:
21 | channel: 'stable'
22 | - run: flutter pub get
23 | - name: Patch pdfrx
24 | run: |
25 | PDFRX_PATH=$(find $HOME/.pub-cache -type d -name "pdfrx*" | head -n 1)
26 | CMAKE_FILE="$PDFRX_PATH/android/CMakeLists.txt"
27 | if [ -f "$CMAKE_FILE" ]; then
28 | sed -i '2i add_link_options("LINKER:--build-id=none")' "$CMAKE_FILE"
29 | echo "Patched CMakeLists.txt in $CMAKE_FILE"
30 | else
31 | echo "CMakeLists.txt not found in expected location"
32 | exit 1
33 | fi
34 | - run: flutter build apk --debug
35 | - uses: actions/upload-artifact@v4
36 | with:
37 | name: release-apk
38 | path: build/app/outputs/apk/debug/app-debug.apk
39 | build-ios:
40 | name: Build for iOS
41 | runs-on: macos-13
42 | steps:
43 | - uses: actions/checkout@v4
44 | - uses: maxim-lobanov/setup-xcode@v1
45 | with:
46 | xcode-version: '15.1'
47 | - uses: subosito/flutter-action@v2
48 | with:
49 | channel: 'stable'
50 | architecture: x64
51 | - run: flutter pub get
52 | - run: flutter build ios --release --no-codesign
53 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Signed builds and upload to Play Store
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | build-android:
8 | name: Build for Android
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Install Ninja
13 | run: sudo apt-get install -y ninja-build
14 | - uses: actions/setup-java@v4
15 | with:
16 | distribution: 'temurin'
17 | java-version: '21'
18 | - uses: subosito/flutter-action@v2
19 | with:
20 | channel: 'stable'
21 | - run: flutter pub get
22 | - name: Patch pdfrx
23 | run: |
24 | PDFRX_PATH=$(find $HOME/.pub-cache -type d -name "pdfrx*" | head -n 1)
25 | CMAKE_FILE="$PDFRX_PATH/android/CMakeLists.txt"
26 | if [ -f "$CMAKE_FILE" ]; then
27 | sed -i '2i add_link_options("LINKER:--build-id=none")' "$CMAKE_FILE"
28 | echo "Patched CMakeLists.txt in $CMAKE_FILE"
29 | else
30 | echo "CMakeLists.txt not found in expected location"
31 | exit 1
32 | fi
33 | - name: Decode Keystore
34 | run: |
35 | echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
36 | - name: Create key.properties
37 | run: |
38 | echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties
39 | echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
40 | echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
41 | echo "storeFile=keystore.jks" >> android/key.properties
42 | - name: Build APK
43 | run: flutter build apk --release
44 | - name: Build appBundle
45 | run: flutter build appbundle
46 | - name: Upload Artifacts
47 | uses: actions/upload-artifact@v4
48 | with:
49 | name: Releases
50 | path: |
51 | build/app/outputs/flutter-apk/app-release.apk
52 | build/app/outputs/bundle/release/app-release.aab
53 | - name: Extract version from pubspec.yaml
54 | id: extract_version
55 | run: |
56 | version=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r')
57 | echo "VERSION=$version" >> $GITHUB_ENV
58 | - name: Create GitHub Release
59 | uses: ncipollo/release-action@v1
60 | with:
61 | artifacts: "build/app/outputs/flutter-apk/app-release.apk,build/app/outputs/bundle/release/app-release.aab"
62 | tag: v${{ env.VERSION }}
63 | token: ${{ secrets.TOKEN }}
64 | draft: true
65 | generateReleaseNotes: true
66 | - name: Create google_service_account.json
67 | run: |
68 | echo "${{ secrets.GOOGLE_SERVICE_ACCOUNT }}" | base64 --decode > android/google_service_account.json
69 | - name: Setup ruby
70 | uses: ruby/setup-ruby@v1
71 | with:
72 | ruby-version: '3.3.0'
73 | bundler-cache: true
74 | working-directory: 'android'
75 | - name: Deploy to Play Store
76 | uses: maierj/fastlane-action@v3.1.0
77 | with:
78 | lane: deploy
79 | subdirectory: android
80 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Do not remove or rename entries in this file, only add new ones
2 | # See https://github.com/flutter/flutter/issues/128635 for more context.
3 |
4 | # Miscellaneous
5 | *.class
6 | *.lock
7 | *.log
8 | *.pyc
9 | *.swp
10 | .DS_Store
11 | .atom/
12 | .buildlog/
13 | .history
14 | .svn/
15 |
16 | # IntelliJ related
17 | *.iml
18 | *.ipr
19 | *.iws
20 | .idea/
21 |
22 | # Visual Studio Code related
23 | .classpath
24 | .project
25 | .settings/
26 | .vscode/*
27 |
28 | # Flutter repo-specific
29 | /bin/cache/
30 | /bin/internal/bootstrap.bat
31 | /bin/internal/bootstrap.sh
32 | /bin/mingit/
33 | /dev/benchmarks/mega_gallery/
34 | /dev/bots/.recipe_deps
35 | /dev/bots/android_tools/
36 | /dev/devicelab/ABresults*.json
37 | /dev/docs/doc/
38 | /dev/docs/api_docs.zip
39 | /dev/docs/flutter.docs.zip
40 | /dev/docs/lib/
41 | /dev/docs/pubspec.yaml
42 | /dev/integration_tests/**/xcuserdata
43 | /dev/integration_tests/**/Pods
44 | /packages/flutter/coverage/
45 | version
46 | analysis_benchmark.json
47 |
48 | # packages file containing multi-root paths
49 | .packages.generated
50 | /lib/generated_l10n/
51 |
52 | # Flutter/Dart/Pub related
53 | **/doc/api/
54 | .dart_tool/
55 | .flutter-plugins
56 | .flutter-plugins-dependencies
57 | **/generated_plugin_registrant.dart
58 | .packages
59 | .pub-preload-cache/
60 | .pub-cache/
61 | .pub/
62 | build/
63 | flutter_*.png
64 | linked_*.ds
65 | unlinked.ds
66 | unlinked_spec.ds
67 |
68 | # Android related
69 | **/android/**/gradle-wrapper.jar
70 | .gradle/
71 | **/android/captures/
72 | **/android/gradlew
73 | **/android/gradlew.bat
74 | **/android/local.properties
75 | **/android/**/GeneratedPluginRegistrant.java
76 | **/android/key.properties
77 | *.jks
78 |
79 | # iOS/XCode related
80 | **/ios/**/*.mode1v3
81 | **/ios/**/*.mode2v3
82 | **/ios/**/*.moved-aside
83 | **/ios/**/*.pbxuser
84 | **/ios/**/*.perspectivev3
85 | **/ios/**/*sync/
86 | **/ios/**/.sconsign.dblite
87 | **/ios/**/.tags*
88 | **/ios/**/.vagrant/
89 | **/ios/**/DerivedData/
90 | **/ios/**/Icon?
91 | **/ios/**/Pods/
92 | **/ios/**/.symlinks/
93 | **/ios/**/profile
94 | **/ios/**/xcuserdata
95 | **/ios/.generated/
96 | **/ios/Flutter/.last_build_id
97 | **/ios/Flutter/App.framework
98 | **/ios/Flutter/Flutter.framework
99 | **/ios/Flutter/Flutter.podspec
100 | **/ios/Flutter/Generated.xcconfig
101 | **/ios/Flutter/ephemeral
102 | **/ios/Flutter/app.flx
103 | **/ios/Flutter/app.zip
104 | **/ios/Flutter/flutter_assets/
105 | **/ios/Flutter/flutter_export_environment.sh
106 | **/ios/ServiceDefinitions.json
107 | **/ios/Runner/GeneratedPluginRegistrant.*
108 |
109 | # macOS
110 | **/Flutter/ephemeral/
111 | **/Pods/
112 | **/macos/Flutter/GeneratedPluginRegistrant.swift
113 | **/macos/Flutter/ephemeral
114 | **/xcuserdata/
115 |
116 | # Windows
117 | **/windows/flutter/generated_plugin_registrant.cc
118 | **/windows/flutter/generated_plugin_registrant.h
119 | **/windows/flutter/generated_plugins.cmake
120 |
121 | # Linux
122 | **/linux/flutter/generated_plugin_registrant.cc
123 | **/linux/flutter/generated_plugin_registrant.h
124 | **/linux/flutter/generated_plugins.cmake
125 |
126 | # Coverage
127 | coverage/
128 |
129 | # Symbols
130 | app.*.symbols
131 |
132 | # Exceptions to above rules.
133 | !**/ios/**/default.mode1v3
134 | !**/ios/**/default.mode2v3
135 | !**/ios/**/default.pbxuser
136 | !**/ios/**/default.perspectivev3
137 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
138 | !/dev/ci/**/Gemfile.lock
139 | !.vscode/settings.json
140 | !pubspec.lock
141 |
142 |
--------------------------------------------------------------------------------
/.zenodo.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Wispar",
3 | "description": "Wispar is a user-friendly and privacy-friendly Android/iOS app that seamlessly searches scientific journals and articles using the Crossref and OpenAlex APIs. Stay updated on your preferred journals by following them and receive new article abstracts in your main feed. No account required. The integration of Unpaywall ensures convenient access to open-access articles, while EZproxy helps overcome subscription barriers.",
4 | "license": {"id": "GPL-3.0"},
5 | "creators": [
6 | {
7 | "affiliation": "Université de Sherbrooke",
8 | "name": "Lapointe, Francis",
9 | "orcid": "0000-0002-7638-4018"
10 | },
11 | {
12 | "affiliation": "Université de Sherbrooke",
13 | "name": "Redondo, Sergio Andrés",
14 | "orcid": "0000-0002-2364-5667"
15 | },
16 | {
17 | "affiliation": "Université de Sherbrooke",
18 | "name": "Amani, Alireza",
19 | "orcid": "0000-0002-0011-1945"
20 | },
21 | {
22 | "name": "Nordhøy, Allan"
23 | },
24 | {
25 | "name": "தமிழ் நேரம்"
26 | },
27 | {
28 | "name": "Tachi44d"
29 | },
30 | {
31 | "name": "MMignolet"
32 | },
33 | {
34 | "name": "Ale"
35 | },
36 | {
37 | "name": "Holi"
38 | },
39 | {
40 | "name": "Xapitonov"
41 | },
42 | {
43 | "name": "Arslanbakan, Arda"
44 | },
45 | {
46 | "name": "bio278"
47 | },
48 | {
49 | "name": "Wibowo, Fitri"
50 | }
51 | ],
52 | "access_right": "open",
53 | "upload_type": "software",
54 | "keywords": ["Android", "iOS", "Crossref", "OpenAlex", "Unpaywall", "EZproxy", "Zotero", "science", "journals", "research"]
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | Wispar is an open-source mobile app that prioritizes user privacy. Our app does not collect any data or access personal information.
2 |
3 | However, Wispar integrates with external services to enhance functionality. Users should be aware that these services could be collecting information such as IP addresses and device-related data. We encourage users to review the privacy policies of each service for a comprehensive understanding of their data collection practices.
4 |
5 | Third-party services used by Wispar:
6 |
7 | - Crossref: https://www.crossref.org/operations-and-sustainability/privacy
8 | - Unpaywall: https://unpaywall.org/legal/privacy
9 | - Zotero: https://www.zotero.org/support/privacy
10 | - OpenAlex: https://openalex.org/OpenAlex_privacy_policy.pdf
11 |
12 | Please review the privacy policies of these services to understand how they handle data. Wispar does not have control over the data collection practices of these external services.
13 |
14 | Inquiries can be submitted to wispar-app@protonmail.com
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Stay up-to-date with academic journals and the latest research articles!
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 | ---
31 |
32 | ## Description
33 |
34 | Wispar is a user-friendly and privacy-friendly Android/iOS app that seamlessly searches scientific journals and articles using the Crossref and OpenAlex APIs. Stay updated on your preferred journals by following them and receive new article abstracts in your main feed. No account required. The integration of Unpaywall ensures convenient access to open-access articles, while EZproxy helps overcome subscription barriers.
35 |
36 |
37 | ## Features overview
38 |
39 | - [x] Search and follow journals
40 | - [x] Search for articles and save the queries for easy access later. You can even include them in your feed!
41 | - [x] Download articles for offline access *
42 | - [x] EZproxy and Unpaywall integration
43 | - [x] Send articles to Zotero
44 | - [x] Share articles
45 | - [x] Scrape missing abstracts
46 | - [x] Export/Import the database
47 | - [x] Notifications and background journals updates
48 | - [x] Filters
49 |
50 | * The download feature is currently limited to some publishers. Flutter tools and publishers like Elsevier and Wiley make it hard to get the PDF file.
51 |
52 | ### Planned features
53 |
54 | - [ ] Deep links
55 | - [ ] Get downloads working for more publishers
56 |
57 |
58 |
59 | ## Translations
60 |
61 |
62 | Wispar uses Weblate to manage translations. You can find the hosted instance at https://hosted.weblate.org/engage/wispar/
63 |
64 | A huge thank you to Weblate for hosting the translations for free :heart:.
65 |
66 | Translation status:
67 |
68 |
69 |
70 |
71 |
72 | ## Contribute
73 |
74 |
75 | - There are many ways you can contribute to improving Wispar—and it's not just about writing code!
76 | - You can help translate Wispar into your language by using our hosted Weblate instance.
77 | - Additionally, providing feedback and reporting bugs are invaluable ways to contribute!
78 |
79 | If you contribute to the project, feel free to add yourself to the .zenodo.json file to be credited!
80 |
81 |
82 |
83 | ## Help
84 |
85 | If you run into any issue while using Wispar, have a question or want to share your feedback, please open an issue here : https://github.com/Scriptbash/Wispar/issues
86 |
87 |
88 | ## Credits
89 |
96 |
97 | ## Screenshots
98 |
99 |
100 |
101 | |  |  |  |
102 | |---|---|---|
103 | |  |  |  |
104 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | id "dev.flutter.flutter-gradle-plugin"
5 | }
6 |
7 | def localProperties = new Properties()
8 | def localPropertiesFile = rootProject.file('local.properties')
9 | if (localPropertiesFile.exists()) {
10 | localPropertiesFile.withReader('UTF-8') { reader ->
11 | localProperties.load(reader)
12 | }
13 | }
14 |
15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
16 | if (flutterVersionCode == null) {
17 | flutterVersionCode = '1'
18 | }
19 |
20 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
21 | if (flutterVersionName == null) {
22 | flutterVersionName = '1.0'
23 | }
24 |
25 | def keystoreProperties = new Properties()
26 | def keystorePropertiesFile = rootProject.file('key.properties')
27 | if (keystorePropertiesFile.exists()) {
28 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
29 | }
30 |
31 | android {
32 | namespace "app.wispar.wispar"
33 | compileSdkVersion 35
34 | ndkVersion "27.0.12077973"
35 |
36 | compileOptions {
37 | coreLibraryDesugaringEnabled true
38 | sourceCompatibility JavaVersion.VERSION_1_8
39 | targetCompatibility JavaVersion.VERSION_1_8
40 | }
41 |
42 | kotlinOptions {
43 | jvmTarget = '1.8'
44 | }
45 |
46 | sourceSets {
47 | main.java.srcDirs += 'src/main/kotlin'
48 | }
49 |
50 | defaultConfig {
51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
52 | applicationId "app.wispar.wispar"
53 | // You can update the following values to match your application needs.
54 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
55 | minSdkVersion flutter.minSdkVersion
56 | targetSdkVersion flutter.targetSdkVersion
57 | versionCode flutterVersionCode.toInteger()
58 | versionName flutterVersionName
59 | }
60 |
61 | signingConfigs {
62 | release {
63 | keyAlias = keystoreProperties['keyAlias']
64 | keyPassword = keystoreProperties['keyPassword']
65 | storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
66 | storePassword = keystoreProperties['storePassword']
67 | }
68 | }
69 |
70 | buildTypes {
71 | release {
72 | signingConfig = signingConfigs.release
73 | }
74 | }
75 |
76 | dependenciesInfo {
77 | // Disables dependency metadata when building APKs.
78 | includeInApk = false
79 | // Disables dependency metadata when building Android App Bundles.
80 | includeInBundle = false
81 | }
82 | }
83 |
84 | flutter {
85 | source '../..'
86 | }
87 |
88 | allprojects {
89 | repositories {
90 | google()
91 | mavenCentral()
92 | // [required] background_fetch
93 | maven { url "${project(':background_fetch').projectDir}/libs" }
94 | }
95 | }
96 |
97 | dependencies {coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'}
98 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
20 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
35 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/app/wispar/wispar/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package app.wispar.wispar
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.os.Build
6 | import android.os.Bundle
7 | import io.flutter.embedding.android.FlutterActivity
8 |
9 | class MainActivity: FlutterActivity() {
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 |
13 | // Create notification channel for foreground service on Android 8.0 and above
14 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
15 | val channel = NotificationChannel(
16 | "wispar_channel",
17 | "Wispar Updates",
18 | NotificationManager.IMPORTANCE_HIGH
19 | ).apply {
20 | description = "Notification when new articles from followed journals and saved queries are available"
21 | }
22 |
23 | val manager = getSystemService(NotificationManager::class.java)
24 | manager.createNotificationChannel(channel)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_bg_service_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/drawable-xxxhdpi/ic_bg_service_small.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '2.1.21'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
10 | }
11 | }
12 |
13 | allprojects {
14 | repositories {
15 | google()
16 | mavenCentral()
17 | }
18 | }
19 |
20 | rootProject.buildDir = '../build'
21 | subprojects {
22 | project.buildDir = "${rootProject.buildDir}/${project.name}"
23 | }
24 | subprojects {
25 | project.evaluationDependsOn(':app')
26 | }
27 |
28 | tasks.register("clean", Delete) {
29 | delete rootProject.buildDir
30 | }
31 |
--------------------------------------------------------------------------------
/android/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | package_name("app.wispar.wispar") # e.g. com.krausefx.app
2 |
--------------------------------------------------------------------------------
/android/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:android)
17 |
18 | platform :android do
19 | desc "Deploy a new version to Google Play"
20 | lane :deploy do
21 | #version = flutter_version()
22 | upload_to_play_store(
23 | track: 'production', # Can be 'internal', 'alpha', 'beta', 'production'
24 | json_key: 'google_service_account.json',
25 | skip_upload_apk: true,
26 | validate_only: false,
27 | skip_upload_metadata: true, # Skip uploading metadata
28 | skip_upload_images: true, # Skip uploading screenshots
29 | skip_upload_screenshots: true, # Skip uploading screenshots
30 | release_status: "draft", # Can be 'draft', 'completed', 'halted'
31 | aab: '../build/app/outputs/bundle/release/app-release.aab', # Path to your AAB file
32 | #version_code: version["version_code"], # From pubspec.yaml
33 | #version_name: version["version_name"] + version["version_code"],
34 | )
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/android/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ----
3 |
4 | # Installation
5 |
6 | Make sure you have the latest version of the Xcode command line tools installed:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
13 |
14 | # Available Actions
15 |
16 | ## Android
17 |
18 | ### android deploy
19 |
20 | ```sh
21 | [bundle exec] fastlane android deploy
22 | ```
23 |
24 | Deploy a new version to Google Play
25 |
26 | ----
27 |
28 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
29 |
30 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
31 |
32 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
33 |
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Wispar is your personal research companion — a privacy-friendly app for exploring academic journals and scientific articles.
2 |
3 | Using the Crossref and OpenAlex APIs, Wispar helps you stay updated on your favourite journals. Follow journals to receive the latest research abstracts in your main feed — no account required.
4 |
5 | - Search scholarly articles and journals
6 | - Follow journals to stay updated
7 | - Add custom keywords and queries to your feed
8 | - Access open-access articles via Unpaywall
9 | - Use EZproxy to unlock paywalled research
10 | - Send articles to your Zotero library
11 | - Built for researchers, students, and the academically curious
12 |
13 | Wispar is user-friendly, open-source, and designed to respect your privacy.
14 |
15 | As a community-built project, contributions are always welcome! If you'd like to contribute, please visit our GitHub repository: https://github.com/Scriptbash/Wispar
16 |
17 | If the app isn't available in your language, please consider helping with translations by visiting our hosted Weblate instance: https://hosted.weblate.org/engage/wispar
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/1_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/2_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/3_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/4_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/5_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/6_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/6_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/7_en-US.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/images/tenInchScreenshots/7_en-US.png
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Stay up-to-date with academic journals and the latest research articles!
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Wispar: Research Butler
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/en-US/video.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/en-US/video.txt
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/fr-CA/full_description.txt:
--------------------------------------------------------------------------------
1 | Wispar is a user-friendly and privacy-friendly app that seamlessly searches scientific journals and articles using the Crossref API. Stay updated on your preferred journals by following them and receive new article abstracts in your main feed. No account required. The integration of Unpaywall ensures convenient access to open-access articles, while EZproxy helps overcome subscription barriers.
2 |
3 | This is an open-source project, and contributions are welcome! If you'd like to contribute, please visit our GitHub repository: https://github.com/Scriptbash/Wispar
4 |
5 | If the app isn't available in your language, please consider helping with translations by visiting our hosted Weblate instance: https://hosted.weblate.org/engage/wispar
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/fr-CA/short_description.txt:
--------------------------------------------------------------------------------
1 | Stay up-to-date with articles in your field of study!
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/fr-CA/title.txt:
--------------------------------------------------------------------------------
1 | Wispar
--------------------------------------------------------------------------------
/android/fastlane/metadata/android/fr-CA/video.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/android/fastlane/metadata/android/fr-CA/video.txt
--------------------------------------------------------------------------------
/android/fastlane/report.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }
9 | settings.ext.flutterSdkPath = flutterSdkPath()
10 |
11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
12 |
13 | repositories {
14 | google()
15 | mavenCentral()
16 | gradlePluginPortal()
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
21 | }
22 | }
23 |
24 | plugins {
25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
26 | id "com.android.application" version "8.10.1" apply false
27 | }
28 |
29 | include ":app"
30 |
--------------------------------------------------------------------------------
/assets/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/assets/icon/icon.png
--------------------------------------------------------------------------------
/devtools_options.yaml:
--------------------------------------------------------------------------------
1 | extensions:
2 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 16.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | platform :ios, '12.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
35 | target 'RunnerTests' do
36 | inherit! :search_paths
37 | end
38 | end
39 |
40 | post_install do |installer|
41 | installer.pods_project.targets.each do |target|
42 | flutter_additional_ios_build_settings(target)
43 | target.build_configurations.each do |config|
44 | # You can remove unused permissions here
45 | # for more information: https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
46 | # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
47 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
48 | '$(inherited)',
49 |
50 | ## dart: PermissionGroup.notification
51 | 'PERMISSION_NOTIFICATIONS=1',
52 | ]
53 |
54 | end
55 |
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/ios/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 | NSPrivacyAccessedAPITypes
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 |
19 | NSPrivacyAccessedAPITypeReasons
20 |
21 | CA92.1
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
38 |
39 |
40 |
41 |
44 |
50 |
51 |
52 |
53 |
54 |
66 |
68 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @main
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | if #available(iOS 10.0, *) {
11 | UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
12 | }
13 | GeneratedPluginRegistrant.register(with: self)
14 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scriptbash/Wispar/8fd34601c20731cdcb484b752c17090cc201ea35/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleDisplayName
10 | Wispar
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | wispar
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | $(FLUTTER_BUILD_NAME)
23 | CFBundleSignature
24 | ????
25 | CFBundleVersion
26 | $(FLUTTER_BUILD_NUMBER)
27 | LSRequiresIPhoneOS
28 |
29 | NSPhotoLibraryUsageDescription
30 | A dependency of the app requires access to the photo library. The app uses this permission to allow users to backup and restore their database.
31 | UIApplicationSupportsIndirectInputEvents
32 |
33 | UIBackgroundModes
34 |
35 | fetch
36 |
37 | BGTaskSchedulerPermittedIdentifiers
38 |
39 | com.transistorsoft.fetch
40 |
41 | UILaunchStoryboardName
42 | LaunchScreen
43 | UIMainStoryboardFile
44 | Main
45 | UISupportedInterfaceOrientations
46 |
47 | UIInterfaceOrientationPortrait
48 | UIInterfaceOrientationLandscapeLeft
49 | UIInterfaceOrientationLandscapeRight
50 |
51 | UISupportedInterfaceOrientations~ipad
52 |
53 | UIInterfaceOrientationPortrait
54 | UIInterfaceOrientationPortraitUpsideDown
55 | UIInterfaceOrientationLandscapeLeft
56 | UIInterfaceOrientationLandscapeRight
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/ios/Runner/Runner.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ios/Runner/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/l10n
2 | template-arb-file: app_en.arb
3 | output-localization-file: app_localizations.dart
4 | output-dir: lib/generated_l10n
5 | synthetic-package: false
--------------------------------------------------------------------------------
/lib/l10n/app_de.arb:
--------------------------------------------------------------------------------
1 | {
2 | "welcomeWispar": "Willkommen bei Wispar!",
3 | "@welcomeWispar": {}
4 | }
5 |
--------------------------------------------------------------------------------
/lib/l10n/app_fa.arb:
--------------------------------------------------------------------------------
1 | {
2 | "welcomeWispar": "به ویسپار خوش آمدید!",
3 | "@welcomeWispar": {},
4 | "appDescription": "با این برنامهٔ متنباز، از آخرین مقالات علمی باخبر باشید. سریع راهاندازیش میکنیم!",
5 | "@appDescription": {},
6 | "setupInstitutionalAccess": "راه اندازی دسترسی از طریق مؤسسه یا دانشگاه شما (اختیاری)",
7 | "@setupInstitutionalAccess": {}
8 | }
9 |
--------------------------------------------------------------------------------
/lib/l10n/app_ja.arb:
--------------------------------------------------------------------------------
1 | {
2 | "save": "保存",
3 | "@save": {},
4 | "home": "ホーム",
5 | "@home": {
6 | "description": "The home menu button and the app bar title when in the home screen."
7 | },
8 | "getStarted": "始める",
9 | "@getStarted": {},
10 | "light": "明るい",
11 | "@light": {},
12 | "setupOtherSettings": "その他の設定",
13 | "@setupOtherSettings": {},
14 | "setupLinkZotero": "Zoteroをリンクする",
15 | "@setupLinkZotero": {},
16 | "favorites": "お気に入り",
17 | "@favorites": {
18 | "description": "The favorites menu button and the app bar title when in the favorites screen."
19 | },
20 | "downloads": "ダウンロード",
21 | "@downloads": {
22 | "description": "The downloads menu button and the app bar title when in the downloads screen."
23 | },
24 | "downloadSuccessful": "論文がダウンロードされました!",
25 | "@downloadSuccessful": {},
26 | "library": "ライブラリ",
27 | "@library": {
28 | "description": "The library menu button and the app bar title when in the library screen."
29 | },
30 | "settings": "設定",
31 | "@settings": {
32 | "description": "The settings option menu button and the app bar title when in the settings screen."
33 | },
34 | "noDownloads": "ダウンロードがありません。",
35 | "@noDownloads": {},
36 | "skip": "スキップ",
37 | "@skip": {},
38 | "delete": "削除",
39 | "@delete": {},
40 | "everything": "すべて",
41 | "@everything": {},
42 | "viewarticle": "論文を読む",
43 | "@viewarticle": {},
44 | "dark": "暗い",
45 | "@dark": {},
46 | "zoteroSettings": "Zoteroの設定",
47 | "@zoteroSettings": {},
48 | "savedOn": "{date}に保存済み",
49 | "@savedOn": {
50 | "placeholders": {
51 | "date": {
52 | "type": "DateTime",
53 | "format": "yMMMMd"
54 | }
55 | }
56 | },
57 | "hours": "時間",
58 | "@hours": {},
59 | "saveSettings": "設定を保存",
60 | "@saveSettings": {},
61 | "settingsSaved": "設定を保存しました!",
62 | "@settingsSaved": {},
63 | "download": "ダウンロードする",
64 | "@download": {
65 | "description": "The verb to download, without the 'to'."
66 | },
67 | "welcomeWispar": "Wisparにようこそ!",
68 | "@welcomeWispar": {},
69 | "articles": "論文",
70 | "@articles": {
71 | "description": "As in scientific articles."
72 | },
73 | "sendToZotero": "Zoteroに送る",
74 | "@sendToZotero": {},
75 | "setupLinkMyZotero": "私のZoteroアカウントをリンクする",
76 | "@setupLinkMyZotero": {},
77 | "abstract": "抄録",
78 | "@abstract": {},
79 | "language": "言語",
80 | "@language": {},
81 | "search": "検索",
82 | "@search": {
83 | "description": "Text shown inside the search screen app bar and for the seach button."
84 | },
85 | "searchPlaceholder": "検索…",
86 | "@searchPlaceholder": {
87 | "description": "Place holder text in the search input widget."
88 | },
89 | "theme": "テーマ",
90 | "@theme": {},
91 | "systemtheme": "システムのテーマ",
92 | "@systemtheme": {},
93 | "database": "データベース",
94 | "@database": {},
95 | "sourceCode": "ソースコード",
96 | "@sourceCode": {},
97 | "databaseSettings": "データベースの設定",
98 | "@databaseSettings": {},
99 | "system": "システム",
100 | "@system": {},
101 | "queries": "クエリ",
102 | "@queries": {
103 | "description": "Title of the saved search queries tab."
104 | },
105 | "saveQuery": "クエリを保存",
106 | "@saveQuery": {},
107 | "queryName": "クエリの名前",
108 | "@queryName": {},
109 | "noSavedQueries": "保存されたクエリがありません。",
110 | "@noSavedQueries": {},
111 | "zoteroValidKey": "APIキーが保存された!",
112 | "@zoteroValidKey": {
113 | "description": "Snackbar shown when a valid Zotero API key has been saved."
114 | },
115 | "journals": "学術誌",
116 | "@journals": {
117 | "description": "The journals menu button and the app bar title when in the journals screen."
118 | },
119 | "follow": "フォロー",
120 | "@follow": {
121 | "description": "The button text shown on journal cards when it is not followed."
122 | },
123 | "unfollow": "フォロー解除",
124 | "@unfollow": {
125 | "description": "The button text shown on journal cards when it is followed."
126 | },
127 | "titleAndAbstract": "論文名と抄録",
128 | "@titleAndAbstract": {},
129 | "title": "論文名",
130 | "@title": {},
131 | "journaltitle": "学術誌名",
132 | "@journaltitle": {},
133 | "articletitle": "論文名",
134 | "@articletitle": {},
135 | "notificationContent": "新しい論文があります!",
136 | "@notificationContent": {}
137 | }
138 |
--------------------------------------------------------------------------------
/lib/l10n/app_zh.arb:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/lib/locale_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 |
4 | class LocaleProvider extends ChangeNotifier {
5 | Locale? _locale;
6 |
7 | Locale? get locale => _locale;
8 |
9 | LocaleProvider() {
10 | _loadLocale();
11 | }
12 |
13 | Future _loadLocale() async {
14 | final prefs = await SharedPreferences.getInstance();
15 | final code = prefs.getString('locale');
16 | if (code != null) {
17 | _locale = Locale(code);
18 | notifyListeners();
19 | }
20 | }
21 |
22 | Future setLocale(String code) async {
23 | _locale = Locale(code);
24 | final prefs = await SharedPreferences.getInstance();
25 | await prefs.setString('locale', code);
26 | notifyListeners();
27 | }
28 |
29 | Future clearLocale() async {
30 | _locale = null;
31 | final prefs = await SharedPreferences.getInstance();
32 | await prefs.remove('locale');
33 | notifyListeners();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/models/journal_entity.dart:
--------------------------------------------------------------------------------
1 | class Journal {
2 | final int? id;
3 | final List issn;
4 | final String title;
5 | final String publisher;
6 | final String? dateFollowed;
7 | final String? lastUpdated;
8 |
9 | Journal({
10 | this.id,
11 | required this.issn,
12 | required this.title,
13 | required this.publisher,
14 | this.dateFollowed,
15 | this.lastUpdated,
16 | });
17 |
18 | Map toMap() {
19 | return {
20 | 'title': title,
21 | 'publisher': publisher,
22 | 'dateFollowed': DateTime.now().toIso8601String().substring(0, 10),
23 | 'lastUpdated': lastUpdated,
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/models/openAlex_works_models.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import '../services/string_format_helper.dart';
3 |
4 | class OpenAlexWorks {
5 | final String title;
6 | final String? doi;
7 | final String? url;
8 | final List authors;
9 | final String? abstract;
10 | final String? journalTitle;
11 | final String? publishedDate;
12 | final String? landingPageUrl;
13 | final String? displayName;
14 | final List? issn;
15 | final String? publisher;
16 | final String? license;
17 |
18 | OpenAlexWorks({
19 | required this.title,
20 | this.doi,
21 | this.url,
22 | required this.authors,
23 | this.abstract,
24 | this.journalTitle,
25 | this.publishedDate,
26 | this.landingPageUrl,
27 | this.displayName,
28 | this.issn,
29 | this.publisher,
30 | this.license,
31 | });
32 |
33 | factory OpenAlexWorks.fromJson(Map json) {
34 | final primaryLocation = json['primary_location'];
35 |
36 | String? extractedDoi;
37 | if (json['doi'] != null && json['doi'].startsWith('https://doi.org/')) {
38 | extractedDoi = json['doi'].replaceFirst('https://doi.org/', '');
39 | }
40 |
41 | String? license = primaryLocation?["license"];
42 | if (license == null) {
43 | license = "All rights reserved";
44 | } else if (license.toLowerCase().contains("cc-by")) {
45 | license = "Creative-Commons";
46 | }
47 |
48 | return OpenAlexWorks(
49 | title: cleanTitle(json['title']),
50 | doi: extractedDoi ?? json['doi'],
51 | url: primaryLocation?['landing_page_url'],
52 | authors: (json['authorships'] as List?)
53 | ?.map((a) => a['author']?['display_name'] as String?)
54 | .whereType()
55 | .toList() ??
56 | [],
57 | abstract: reconstructAbstract(json['abstract_inverted_index']),
58 | journalTitle: cleanText(
59 | primaryLocation?['source']?['display_name'],
60 | ),
61 | publishedDate: json['publication_date'],
62 | issn:
63 | (primaryLocation?['source']?['issn'] as List?)?.cast() ?? [],
64 | publisher: primaryLocation?['source']?['host_organization_name'],
65 | license: license,
66 | );
67 | }
68 | }
69 |
70 | String? reconstructAbstract(Map? invertedIndex) {
71 | if (invertedIndex == null) return null;
72 |
73 | int maxIndex = invertedIndex.values
74 | .expand((positions) => positions)
75 | .reduce((a, b) => a > b ? a : b);
76 |
77 | List words = List.filled(maxIndex + 1, '', growable: false);
78 |
79 | invertedIndex.forEach((word, positions) {
80 | for (int pos in positions) {
81 | words[pos] = word;
82 | }
83 | });
84 |
85 | String rawAbstract = words.join(' ');
86 | return cleanAbstract(rawAbstract);
87 | }
88 |
89 | String cleanText(String? text) {
90 | if (text == null) return '';
91 | try {
92 | String decoded = utf8.decode(text.codeUnits, allowMalformed: true);
93 | return decoded.replaceAll(RegExp(r'[^\x20-\x7E]'), ''); // Remove non-ASCII
94 | } catch (e) {
95 | return text;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/screens/article_search_results_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import '../widgets/publication_card.dart';
4 | import '../models/crossref_journals_works_models.dart' as journalsWorks;
5 | import '../services/crossref_api.dart';
6 | import '../services/openAlex_api.dart';
7 | import '../services/logs_helper.dart';
8 |
9 | class ArticleSearchResultsScreen extends StatefulWidget {
10 | final List initialSearchResults;
11 | final bool initialHasMore;
12 | final Map queryParams;
13 | final String source;
14 |
15 | const ArticleSearchResultsScreen({
16 | Key? key,
17 | required this.initialSearchResults,
18 | required this.initialHasMore,
19 | required this.queryParams,
20 | required this.source,
21 | }) : super(key: key);
22 |
23 | @override
24 | _ArticleSearchResultsScreenState createState() =>
25 | _ArticleSearchResultsScreenState();
26 | }
27 |
28 | class _ArticleSearchResultsScreenState
29 | extends State {
30 | final logger = LogsService().logger;
31 | late List _searchResults;
32 | final ScrollController _scrollController = ScrollController();
33 | bool _isLoadingMore = false;
34 | bool _hasMoreResults = true;
35 | int _currentOpenAlexPage = 1;
36 |
37 | @override
38 | void initState() {
39 | super.initState();
40 | _searchResults = widget.initialSearchResults;
41 | _hasMoreResults = widget.initialHasMore;
42 |
43 | _scrollController.addListener(() {
44 | if (_scrollController.position.pixels >=
45 | _scrollController.position.maxScrollExtent - 70 &&
46 | !_isLoadingMore &&
47 | _hasMoreResults) {
48 | _loadMoreResults();
49 | }
50 | });
51 | }
52 |
53 | Future _loadMoreResults() async {
54 | if (_isLoadingMore || !_hasMoreResults) return;
55 |
56 | setState(() {
57 | _isLoadingMore = true;
58 | });
59 |
60 | try {
61 | List newResults;
62 | bool hasMore = false;
63 |
64 | if (widget.source == 'Crossref') {
65 | final ListAndMore response =
66 | await CrossRefApi.getWorksByQuery(widget.queryParams);
67 | newResults = response.list;
68 | hasMore =
69 | response.hasMore && _searchResults.length < response.totalResults;
70 | } else {
71 | newResults = await OpenAlexApi.getOpenAlexWorksByQuery(
72 | widget.queryParams['query'] ?? '',
73 | widget.queryParams['scope'] ?? 1,
74 | widget.queryParams['sortField'],
75 | widget.queryParams['sortOrder'],
76 | page: _currentOpenAlexPage,
77 | );
78 |
79 | if (newResults.isNotEmpty) {
80 | _currentOpenAlexPage++;
81 | } else {
82 | hasMore = false;
83 | _isLoadingMore = false;
84 | }
85 | }
86 |
87 | setState(() {
88 | _searchResults.addAll(newResults);
89 | _hasMoreResults = hasMore;
90 | });
91 | } catch (e, stackTrace) {
92 | ScaffoldMessenger.of(context).showSnackBar(
93 | SnackBar(
94 | content: Text(AppLocalizations.of(context)!.failedLoadMoreResults)),
95 | );
96 | logger.severe(
97 | 'Failed to load more article search results.', e, stackTrace);
98 | } finally {
99 | setState(() {
100 | _isLoadingMore = false;
101 | });
102 | }
103 | }
104 |
105 | @override
106 | void dispose() {
107 | _scrollController.dispose();
108 | super.dispose();
109 | }
110 |
111 | @override
112 | Widget build(BuildContext context) {
113 | return Scaffold(
114 | appBar: AppBar(
115 | title: Text(AppLocalizations.of(context)!.searchresults),
116 | ),
117 | body: _searchResults.isNotEmpty
118 | ? ListView.builder(
119 | controller: _scrollController,
120 | itemCount: _searchResults.length + (_hasMoreResults ? 1 : 0),
121 | cacheExtent: 1000.0,
122 | itemBuilder: (context, index) {
123 | if (index == _searchResults.length) {
124 | return Center(
125 | child: Padding(
126 | padding: const EdgeInsets.all(8.0),
127 | child: CircularProgressIndicator(),
128 | ),
129 | );
130 | }
131 |
132 | final item = _searchResults[index];
133 | return PublicationCard(
134 | title: item.title,
135 | abstract: item.abstract,
136 | journalTitle: item.journalTitle,
137 | issn: item.issn,
138 | publishedDate: item.publishedDate,
139 | doi: item.doi,
140 | authors: item.authors,
141 | url: item.url,
142 | license: item.license,
143 | licenseName: item.licenseName,
144 | publisher: item.publisher,
145 | );
146 | },
147 | )
148 | : Center(
149 | child: Text(AppLocalizations.of(context)!.noresultsfound),
150 | ),
151 | );
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/lib/screens/hidden_articles_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../services/database_helper.dart';
3 | import '../widgets/publication_card.dart';
4 | import '../generated_l10n/app_localizations.dart';
5 |
6 | class HiddenArticlesScreen extends StatefulWidget {
7 | const HiddenArticlesScreen({super.key});
8 |
9 | @override
10 | _HiddenArticlesScreenState createState() => _HiddenArticlesScreenState();
11 | }
12 |
13 | class _HiddenArticlesScreenState extends State {
14 | final DatabaseHelper dbHelper = DatabaseHelper();
15 | List _hiddenPublications = [];
16 |
17 | @override
18 | void initState() {
19 | super.initState();
20 | _loadHiddenPublications();
21 | }
22 |
23 | Future _loadHiddenPublications() async {
24 | final hidden = await dbHelper.getHiddenPublications();
25 |
26 | setState(() {
27 | _hiddenPublications = hidden.map((card) {
28 | return PublicationCard(
29 | doi: card.doi,
30 | title: card.title,
31 | issn: card.issn,
32 | abstract: card.abstract,
33 | journalTitle: card.journalTitle,
34 | publishedDate: card.publishedDate,
35 | authors: card.authors,
36 | url: card.url,
37 | license: card.license,
38 | licenseName: card.licenseName,
39 | showHideBtn: true,
40 | isHidden: true,
41 | onHide: () async {
42 | await dbHelper.unhideArticle(card.doi);
43 | _loadHiddenPublications();
44 | },
45 | );
46 | }).toList();
47 | });
48 | }
49 |
50 | @override
51 | Widget build(BuildContext context) {
52 | return Scaffold(
53 | appBar: AppBar(title: Text(AppLocalizations.of(context)!.hiddenArticles)),
54 | body: _hiddenPublications.isEmpty
55 | ? Center(child: Text(AppLocalizations.of(context)!.noHiddenArticles))
56 | : ListView.builder(
57 | itemCount: _hiddenPublications.length,
58 | itemBuilder: (context, index) {
59 | return _hiddenPublications[index];
60 | },
61 | ),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/screens/institutions_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import '../services/libproxydb_api.dart';
4 | import '../services/logs_helper.dart';
5 |
6 | class InstitutionScreen extends StatefulWidget {
7 | @override
8 | _InstitutionScreenState createState() => _InstitutionScreenState();
9 | }
10 |
11 | class _InstitutionScreenState extends State {
12 | final logger = LogsService().logger;
13 | List allProxies = [];
14 | List filteredProxies = [];
15 | TextEditingController searchController = TextEditingController();
16 |
17 | @override
18 | void initState() {
19 | super.initState();
20 | fetchProxies();
21 | }
22 |
23 | void fetchProxies() async {
24 | try {
25 | List proxies = await ProxyService.fetchProxies();
26 | setState(() {
27 | // Appends "No institution" at the top of the proxy list
28 | proxies = [
29 | ProxyData(name: AppLocalizations.of(context)!.noinstitution, url: ''),
30 | ...proxies,
31 | ];
32 | allProxies = proxies;
33 | filteredProxies = proxies;
34 | });
35 | } catch (e, stackTrace) {
36 | logger.severe('Unable to load the proxy list', e, stackTrace);
37 | }
38 | }
39 |
40 | void filterProxies(String query) {
41 | List filteredList = allProxies
42 | .where((proxy) =>
43 | proxy.name.toLowerCase().contains(query.toLowerCase()) ||
44 | proxy.url.toLowerCase().contains(query.toLowerCase()))
45 | .toList();
46 | setState(() {
47 | filteredProxies = filteredList;
48 | });
49 | }
50 |
51 | void onInstitutionSelected(String name, String url) {
52 | Navigator.pop(context, {'name': name, 'url': url});
53 | }
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | return Scaffold(
58 | appBar: AppBar(
59 | title: Text(AppLocalizations.of(context)!.selectinstitution),
60 | ),
61 | body: Column(
62 | children: [
63 | Padding(
64 | padding: const EdgeInsets.all(8.0),
65 | child: TextField(
66 | controller: searchController,
67 | onChanged: (query) {
68 | filterProxies(query);
69 | },
70 | decoration: InputDecoration(
71 | hintText: AppLocalizations.of(context)!.searchPlaceholder,
72 | prefixIcon: Icon(Icons.search),
73 | border: OutlineInputBorder(
74 | borderRadius: BorderRadius.circular(30.0),
75 | ),
76 | ),
77 | ),
78 | ),
79 | Expanded(
80 | child: ListView.builder(
81 | itemCount: filteredProxies.length,
82 | itemBuilder: (context, index) {
83 | return ListTile(
84 | title: Text(filteredProxies[index].name),
85 | subtitle: Text(filteredProxies[index].url.isNotEmpty
86 | ? filteredProxies[index].url
87 | : ''),
88 | onTap: () {
89 | if (filteredProxies[index].url.isEmpty) {
90 | // Handle "No institution"
91 | onInstitutionSelected('None', 'None');
92 | } else {
93 | // Handle selected proxy
94 | onInstitutionSelected(
95 | filteredProxies[index].name,
96 | filteredProxies[index].url,
97 | );
98 | }
99 | },
100 | );
101 | },
102 | ),
103 | ),
104 | ],
105 | ),
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/screens/journals_search_results_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import '../services/crossref_api.dart';
4 | import '../models/crossref_journals_models.dart' as Journals;
5 | import '../widgets/journal_search_results_card.dart';
6 | import '../services/logs_helper.dart';
7 |
8 | class SearchResultsScreen extends StatefulWidget {
9 | final ListAndMore searchResults;
10 | final String searchQuery;
11 |
12 | const SearchResultsScreen({
13 | Key? key,
14 | required this.searchResults,
15 | required this.searchQuery,
16 | }) : super(key: key);
17 |
18 | @override
19 | _SearchResultsScreenState createState() => _SearchResultsScreenState();
20 | }
21 |
22 | class _SearchResultsScreenState extends State {
23 | final logger = LogsService().logger;
24 | List items = [];
25 | bool isLoading = false;
26 | late ScrollController _scrollController;
27 | bool hasMoreResults = true;
28 |
29 | @override
30 | void initState() {
31 | super.initState();
32 | _scrollController = ScrollController();
33 | _scrollController.addListener(_onScroll);
34 |
35 | if (widget.searchResults.list.isNotEmpty) {
36 | items = widget.searchResults.list;
37 | hasMoreResults = widget.searchResults.hasMore;
38 | } else {
39 | hasMoreResults = false;
40 | }
41 | }
42 |
43 | @override
44 | Widget build(BuildContext context) {
45 | return Scaffold(
46 | appBar: AppBar(
47 | title: Text(AppLocalizations.of(context)!.searchresults),
48 | ),
49 | body: items.isEmpty
50 | ? Center(
51 | child: Text(AppLocalizations.of(context)!.noPublicationFound),
52 | )
53 | : ListView.builder(
54 | itemCount: items.length + (isLoading ? 1 : 0),
55 | itemBuilder: (context, index) {
56 | if (index == items.length && isLoading) {
57 | return Padding(
58 | padding: const EdgeInsets.all(16.0),
59 | child: Center(child: CircularProgressIndicator()),
60 | );
61 | } else {
62 | Journals.Item currentItem = items[index];
63 |
64 | // Skip invalid items
65 | if (currentItem.issn.isEmpty) return SizedBox.shrink();
66 |
67 | return JournalsSearchResultCard(
68 | key: UniqueKey(),
69 | item: currentItem,
70 | isFollowed: false,
71 | );
72 | }
73 | },
74 | controller: _scrollController,
75 | ),
76 | );
77 | }
78 |
79 | void _onScroll() {
80 | if (_scrollController.position.pixels >=
81 | _scrollController.position.maxScrollExtent * 0.8 &&
82 | !isLoading &&
83 | hasMoreResults) {
84 | loadMoreItems(widget.searchQuery);
85 | }
86 | }
87 |
88 | Future loadMoreItems(String query) async {
89 | setState(() => isLoading = true);
90 |
91 | try {
92 | ListAndMore newResults =
93 | await CrossRefApi.queryJournalsByName(query);
94 |
95 | setState(() {
96 | if (newResults.list.isNotEmpty) {
97 | items.addAll(newResults.list);
98 | hasMoreResults = newResults.hasMore;
99 | } else {
100 | hasMoreResults = false;
101 | }
102 | });
103 | } catch (e, stackTrace) {
104 | logger.severe('Failed to load more journals.', e, stackTrace);
105 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(
106 | content:
107 | Text(AppLocalizations.of(context)!.failLoadMorePublication)));
108 | } finally {
109 | setState(() => isLoading = false);
110 | }
111 | }
112 |
113 | @override
114 | void dispose() {
115 | _scrollController.dispose();
116 | super.dispose();
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/lib/screens/library_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import '../services/database_helper.dart';
4 | import '../widgets/journals_tab_content.dart';
5 | import '../widgets/queries_tab_content.dart';
6 |
7 | class LibraryScreen extends StatefulWidget {
8 | const LibraryScreen({super.key});
9 |
10 | @override
11 | _LibraryScreenState createState() => _LibraryScreenState();
12 | }
13 |
14 | class _LibraryScreenState extends State
15 | with TickerProviderStateMixin {
16 | bool isSearching = false;
17 | late DatabaseHelper dbHelper;
18 | late FocusNode searchFocusNode;
19 | late final TabController _tabController;
20 |
21 | int journalsSortBy = 0; // Set the sort by option to Journal title by default
22 | int journalsSortOrder = 0; // Set the sort order to Ascending by default
23 | int authorsSortBy = 0; // Set the sort by option to Journal title by default
24 | int authorsSortOrder = 0; // Set the sort order to Ascending by default
25 | int queriesSortBy = 0; // Set the sort by option to Queriy name by default
26 | int queriesSortOrder = 0; // Set the sort order to Ascending by default
27 |
28 | @override
29 | void initState() {
30 | super.initState();
31 | dbHelper = DatabaseHelper();
32 | searchFocusNode = FocusNode();
33 | _tabController = TabController(length: 2, vsync: this);
34 | }
35 |
36 | @override
37 | void dispose() {
38 | _tabController.dispose();
39 | super.dispose();
40 | }
41 |
42 | @override
43 | Widget build(BuildContext context) {
44 | return Scaffold(
45 | appBar: AppBar(
46 | centerTitle: false,
47 | title: Text(AppLocalizations.of(context)!.library),
48 | bottom: TabBar(
49 | controller: _tabController,
50 | tabs: [
51 | Tab(
52 | icon: Icon(Icons.menu_book_rounded),
53 | text: AppLocalizations.of(context)!.journals,
54 | ),
55 | /*ab(
56 | icon: Icon(Icons.person_2_outlined),
57 | text: AppLocalizations.of(context)!.authors,
58 | ),*/
59 | Tab(
60 | icon: Icon(Icons.format_quote_rounded),
61 | text: AppLocalizations.of(context)!.queries,
62 | ),
63 | ],
64 | ),
65 | ),
66 | body: TabBarView(
67 | controller: _tabController,
68 | children: [
69 | JournalsTabContent(
70 | initialSortBy: journalsSortBy,
71 | initialSortOrder: journalsSortOrder,
72 | onSortByChanged: (int value) {
73 | setState(() {
74 | journalsSortBy = value;
75 | });
76 | },
77 | onSortOrderChanged: (int value) {
78 | setState(() {
79 | journalsSortOrder = value;
80 | });
81 | },
82 | ),
83 | /*AuthorsTabContent(
84 | initialSortBy: authorsSortBy,
85 | initialSortOrder: authorsSortOrder,
86 | onSortByChanged: (int value) {
87 | setState(() {
88 | authorsSortBy = value;
89 | });
90 | },
91 | onSortOrderChanged: (int value) {
92 | setState(() {
93 | authorsSortOrder = value;
94 | });
95 | },
96 | ),*/
97 | QueriesTabContent(
98 | initialSortBy: queriesSortBy,
99 | initialSortOrder: queriesSortOrder,
100 | onSortByChanged: (int value) {
101 | setState(() {
102 | queriesSortBy = value;
103 | });
104 | },
105 | onSortOrderChanged: (int value) {
106 | setState(() {
107 | queriesSortOrder = value;
108 | });
109 | },
110 | ),
111 | ],
112 | ),
113 | );
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/lib/screens/logs_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:logging/logging.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:wispar/generated_l10n/app_localizations.dart';
5 | import '../services/logs_helper.dart';
6 |
7 | class LogsScreen extends StatelessWidget {
8 | const LogsScreen({Key? key}) : super(key: key);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Scaffold(
13 | appBar: AppBar(
14 | title: Text(AppLocalizations.of(context)!.logs),
15 | centerTitle: false,
16 | actions: [
17 | IconButton(
18 | icon: Icon(Icons.download,
19 | color: Theme.of(context).colorScheme.primary),
20 | tooltip: AppLocalizations.of(context)!.saveLogs,
21 | onPressed: () async {
22 | await LogsService().saveLogsToFile(context);
23 | },
24 | ),
25 | IconButton(
26 | icon:
27 | Icon(Icons.share, color: Theme.of(context).colorScheme.primary),
28 | tooltip: AppLocalizations.of(context)!.shareLogs,
29 | onPressed: () async {
30 | await LogsService().shareLogs(context);
31 | },
32 | ),
33 | IconButton(
34 | icon: Icon(Icons.delete,
35 | color: Theme.of(context).colorScheme.primary),
36 | tooltip: AppLocalizations.of(context)!.deleteLogs,
37 | onPressed: () {
38 | LogsService().clearLogs();
39 | ScaffoldMessenger.of(context).showSnackBar(
40 | SnackBar(
41 | content: Text(AppLocalizations.of(context)!.logsDeleted),
42 | ),
43 | );
44 | },
45 | ),
46 | ],
47 | ),
48 | body: ValueListenableBuilder>(
49 | valueListenable: LogsService().logsNotifier,
50 | builder: (context, logs, _) {
51 | if (logs.isEmpty) {
52 | return Center(
53 | child: Text(AppLocalizations.of(context)!.logsUnavailable));
54 | }
55 |
56 | return ListView.builder(
57 | padding: const EdgeInsets.all(8),
58 | itemCount: logs.length,
59 | itemBuilder: (context, index) {
60 | final log = logs[index];
61 | return GestureDetector(
62 | onLongPress: () {
63 | final content =
64 | '[${log.level.name}] ${log.time.toLocal()} — ${log.message}'
65 | '${log.error != null ? '\nError: ${log.error}' : ''}'
66 | '${log.stackTrace != null ? '\nStack trace:\n${log.stackTrace}' : ''}';
67 | Clipboard.setData(ClipboardData(text: content));
68 | ScaffoldMessenger.of(context).showSnackBar(
69 | SnackBar(
70 | content: Text(AppLocalizations.of(context)!.logCopied)),
71 | );
72 | },
73 | child: Card(
74 | color: _logColor(log.level),
75 | child: Padding(
76 | padding: const EdgeInsets.all(8.0),
77 | child: Column(
78 | crossAxisAlignment: CrossAxisAlignment.start,
79 | children: [
80 | Text(
81 | '[${log.level.name}] ${log.time.toLocal()} — ${log.message}',
82 | style: const TextStyle(
83 | fontWeight: FontWeight.bold, color: Colors.black),
84 | ),
85 | if (log.error != null)
86 | Padding(
87 | padding: const EdgeInsets.only(top: 4),
88 | child: Text(
89 | 'Error: ${log.error}',
90 | style: const TextStyle(
91 | color: Colors.red,
92 | fontWeight: FontWeight.bold),
93 | ),
94 | ),
95 | if (log.stackTrace != null)
96 | Padding(
97 | padding: const EdgeInsets.only(top: 4),
98 | child: Text(
99 | 'Stack trace:\n${log.stackTrace}',
100 | style: const TextStyle(
101 | fontSize: 12, color: Colors.black),
102 | ),
103 | ),
104 | ],
105 | ),
106 | ),
107 | ),
108 | );
109 | },
110 | );
111 | },
112 | ),
113 | );
114 | }
115 |
116 | Color _logColor(Level level) {
117 | if (level >= Level.SEVERE) return Colors.red.shade100;
118 | if (level >= Level.WARNING) return Colors.orange.shade100;
119 | if (level >= Level.INFO) return Colors.blue.shade50;
120 | return Colors.grey.shade100;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/screens/pdf_reader.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import 'package:pdfrx/pdfrx.dart';
4 | import '../widgets/publication_card.dart';
5 | import '../services/database_helper.dart';
6 | import 'package:url_launcher/url_launcher.dart';
7 | import 'package:open_filex/open_filex.dart';
8 | import 'package:path_provider/path_provider.dart';
9 | import 'package:path/path.dart' as p;
10 | import '../services/logs_helper.dart';
11 |
12 | class PdfReader extends StatefulWidget {
13 | final String pdfUrl;
14 | final PublicationCard publicationCard;
15 |
16 | PdfReader({Key? key, required this.pdfUrl, required this.publicationCard})
17 | : super(key: key);
18 |
19 | @override
20 | _PdfReaderState createState() => _PdfReaderState();
21 | }
22 |
23 | class _PdfReaderState extends State {
24 | final logger = LogsService().logger;
25 | final controller = PdfViewerController();
26 | late DatabaseHelper databaseHelper;
27 | late String resolvedPdfPath = "";
28 | bool isPathResolved = false;
29 | bool isDownloaded = false;
30 |
31 | @override
32 | void dispose() {
33 | super.dispose();
34 | }
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 | databaseHelper = DatabaseHelper();
40 | resolvePdfPath();
41 | }
42 |
43 | void resolvePdfPath() async {
44 | final directory = await getApplicationDocumentsDirectory();
45 | final pdfFileName = p.basename(widget.pdfUrl);
46 | final newPdfPath = p.join(directory.path, pdfFileName);
47 |
48 | setState(() {
49 | resolvedPdfPath = newPdfPath;
50 | isPathResolved = true;
51 | });
52 |
53 | checkIfDownloaded();
54 | }
55 |
56 | @override
57 | Widget build(BuildContext context) {
58 | return Scaffold(
59 | appBar: AppBar(
60 | centerTitle: false,
61 | title: Text(AppLocalizations.of(context)!.articleViewer),
62 | actions: [
63 | IconButton(
64 | icon: const Icon(Icons.open_in_browser),
65 | tooltip: AppLocalizations.of(context)!.openExternalPdfApp,
66 | onPressed: () async {
67 | try {
68 | OpenFilex.open(resolvedPdfPath);
69 | } catch (e, stackTrace) {
70 | logger.severe(
71 | 'Unable to open the PDF file in an external app.',
72 | e,
73 | stackTrace);
74 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(
75 | content: Text(AppLocalizations.of(context)!
76 | .errorOpenExternalPdfApp)));
77 | }
78 | },
79 | ),
80 | isDownloaded == false
81 | ? IconButton(
82 | icon: const Icon(Icons.download_outlined),
83 | tooltip: AppLocalizations.of(context)!.download,
84 | onPressed: () async {
85 | // Otherwise, insert a new article
86 | await databaseHelper.insertArticle(
87 | widget.publicationCard,
88 | isDownloaded: true,
89 | pdfPath: p.basename(widget.pdfUrl),
90 | );
91 | setState(() {
92 | isDownloaded = true;
93 | });
94 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(
95 | content: Text(AppLocalizations.of(context)!
96 | .downloadSuccessful)));
97 | })
98 | : IconButton(
99 | icon: const Icon(Icons.delete_outlined),
100 | tooltip: AppLocalizations.of(context)!.delete,
101 | onPressed: () async {
102 | await databaseHelper.removeDownloaded(
103 | widget.publicationCard.doi,
104 | );
105 | setState(() {
106 | isDownloaded = false;
107 | });
108 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(
109 | content: Text(
110 | AppLocalizations.of(context)!.downloadDeleted)));
111 | },
112 | ),
113 | ]),
114 | body: isPathResolved
115 | ? Stack(children: [
116 | PdfViewer.file(resolvedPdfPath,
117 | controller: controller,
118 | params: PdfViewerParams(
119 | enableTextSelection: false,
120 | maxScale: 8,
121 | loadingBannerBuilder:
122 | (context, bytesDownloaded, totalBytes) {
123 | return Center(
124 | child: CircularProgressIndicator(
125 | value: totalBytes != null
126 | ? bytesDownloaded / totalBytes
127 | : null,
128 | backgroundColor: Colors.grey,
129 | ),
130 | );
131 | },
132 | linkHandlerParams: PdfLinkHandlerParams(
133 | linkColor: const Color.fromARGB(20, 255, 235, 59),
134 | onLinkTap: (link) {
135 | if (link.url != null) {
136 | launchUrl(link.url!);
137 | } else if (link.dest != null) {
138 | controller.goToDest(link.dest);
139 | }
140 | },
141 | ),
142 | ))
143 | ])
144 | : const Center(child: CircularProgressIndicator()),
145 | );
146 | }
147 |
148 | void checkIfDownloaded() async {
149 | bool downloaded =
150 | await databaseHelper.isArticleDownloaded(widget.publicationCard.doi);
151 | setState(() {
152 | isDownloaded = downloaded;
153 | });
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/lib/screens/search_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import '../widgets/article_search_form.dart';
4 | import '../widgets/journal_search_form.dart';
5 |
6 | class SearchScreen extends StatefulWidget {
7 | const SearchScreen({super.key});
8 |
9 | @override
10 | _SearchScreenState createState() => _SearchScreenState();
11 | }
12 |
13 | class _SearchScreenState extends State {
14 | int selectedSearchType = 1; // Default to article type
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Scaffold(
19 | appBar: AppBar(
20 | centerTitle: false,
21 | title: Text(AppLocalizations.of(context)!.search),
22 | ),
23 | body: Padding(
24 | padding: const EdgeInsets.all(16.0),
25 | child: Column(
26 | crossAxisAlignment: CrossAxisAlignment.start,
27 | children: [
28 | /*Text(
29 | AppLocalizations.of(context)!.category,
30 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
31 | ),
32 | SizedBox(height: 8),*/
33 | LayoutBuilder(
34 | builder: (context, constraints) {
35 | return ToggleButtons(
36 | borderRadius: BorderRadius.circular(8.0),
37 | isSelected: [
38 | selectedSearchType == 1,
39 | selectedSearchType == 2,
40 | ],
41 | onPressed: (int index) {
42 | setState(() {
43 | selectedSearchType = index == 0 ? 1 : 2;
44 | });
45 | },
46 | children: [
47 | Container(
48 | width: constraints.maxWidth / 2 - 1.5,
49 | alignment: Alignment.center,
50 | padding: EdgeInsets.symmetric(vertical: 10),
51 | child: Text(
52 | AppLocalizations.of(context)!.articles,
53 | textAlign: TextAlign.center,
54 | ),
55 | ),
56 | Container(
57 | width: constraints.maxWidth / 2 - 1.5,
58 | alignment: Alignment.center,
59 | padding: EdgeInsets.symmetric(vertical: 10),
60 | child: Text(
61 | AppLocalizations.of(context)!.journals,
62 | textAlign: TextAlign.center,
63 | ),
64 | ),
65 | ],
66 | );
67 | },
68 | ),
69 | SizedBox(height: 8),
70 | Divider(thickness: 1, color: Colors.grey[300]),
71 | SizedBox(height: 8),
72 | Expanded(
73 | child:
74 | _getSearchForm()), // Show form based on selected category
75 | ],
76 | ),
77 | ),
78 | );
79 | }
80 |
81 | // Returns the form based on the selected search type
82 | Widget _getSearchForm() {
83 | switch (selectedSearchType) {
84 | case 1:
85 | return ArticleSearchScreen();
86 | case 2:
87 | return JournalSearchForm();
88 | default:
89 | return ArticleSearchScreen();
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/screens/zotero_settings_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import 'package:url_launcher/url_launcher.dart';
4 | import 'package:shared_preferences/shared_preferences.dart';
5 | import '../services/zotero_api.dart';
6 |
7 | class ZoteroSettings extends StatefulWidget {
8 | const ZoteroSettings({Key? key});
9 |
10 | @override
11 | _ZoteroSettingsState createState() => _ZoteroSettingsState();
12 | }
13 |
14 | class _ZoteroSettingsState extends State {
15 | TextEditingController _apiKeyController = TextEditingController();
16 | bool passwordVisible = false;
17 |
18 | @override
19 | void initState() {
20 | super.initState();
21 | _loadApiKey();
22 | }
23 |
24 | _loadApiKey() async {
25 | SharedPreferences prefs = await SharedPreferences.getInstance();
26 | String? apiKey = prefs.getString('zoteroApiKey');
27 | if (apiKey != null && apiKey.isNotEmpty) {
28 | setState(() {
29 | _apiKeyController.text = apiKey;
30 | });
31 | }
32 | }
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | return Scaffold(
37 | appBar: AppBar(
38 | centerTitle: false,
39 | title: Text(AppLocalizations.of(context)!.zoteroSettings),
40 | ),
41 | body: Padding(
42 | padding: const EdgeInsets.symmetric(horizontal: 16.0),
43 | child: Column(
44 | children: [
45 | Expanded(
46 | child: SingleChildScrollView(
47 | child: Column(
48 | crossAxisAlignment: CrossAxisAlignment.start,
49 | children: [
50 | Text(AppLocalizations.of(context)!.zoteroPermissions1),
51 | Text(
52 | '\n${AppLocalizations.of(context)!.zoteroPermissions2}\n',
53 | ),
54 | SizedBox(
55 | width: double.infinity,
56 | child: FilledButton.tonal(
57 | onPressed: () {
58 | launchUrl(
59 | Uri.parse(
60 | 'https://www.zotero.org/settings/keys/new'),
61 | );
62 | },
63 | child:
64 | Text(AppLocalizations.of(context)!.zoteroCreateKey),
65 | ),
66 | ),
67 | Text(
68 | '\n${AppLocalizations.of(context)!.zoteroPermissions3}\n',
69 | ),
70 | TextField(
71 | controller: _apiKeyController,
72 | obscureText: !passwordVisible,
73 | decoration: InputDecoration(
74 | hintText: AppLocalizations.of(context)!.zoteroEnterKey,
75 | suffixIcon: IconButton(
76 | icon: Icon(
77 | passwordVisible
78 | ? Icons.visibility_outlined
79 | : Icons.visibility_off_outlined,
80 | ),
81 | onPressed: () {
82 | setState(() {
83 | passwordVisible = !passwordVisible;
84 | });
85 | },
86 | ),
87 | ),
88 | onChanged: (value) {},
89 | ),
90 | SizedBox(height: 16),
91 | SizedBox(
92 | width: double.infinity,
93 | child: FilledButton(
94 | onPressed: () async {
95 | String apiKey = _apiKeyController.text;
96 | if (apiKey.isNotEmpty) {
97 | int userId = await ZoteroService.getUserId(apiKey);
98 | if (userId != 0) {
99 | SharedPreferences prefs =
100 | await SharedPreferences.getInstance();
101 | await prefs.setString('zoteroApiKey', apiKey);
102 | await prefs.setString(
103 | 'zoteroUserId', userId.toString());
104 | ScaffoldMessenger.of(context).showSnackBar(
105 | SnackBar(
106 | content: Text(
107 | AppLocalizations.of(context)!
108 | .zoteroValidKey),
109 | duration: const Duration(seconds: 2)));
110 | } else {
111 | ScaffoldMessenger.of(context)
112 | .showSnackBar(SnackBar(
113 | content: Text(AppLocalizations.of(context)!
114 | .zoteroInvalidKey),
115 | duration: const Duration(seconds: 3),
116 | ));
117 | }
118 | }
119 | },
120 | child: Text(AppLocalizations.of(context)!.save),
121 | ),
122 | ),
123 | ],
124 | ),
125 | ),
126 | ),
127 | ],
128 | ),
129 | ),
130 | );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/lib/services/abstract_helper.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import '../generated_l10n/app_localizations.dart';
3 | import 'package:shared_preferences/shared_preferences.dart';
4 |
5 | enum AbstractSetting { showAll, hideAll, hideMissing }
6 |
7 | class AbstractHelper {
8 | static Future getAbstractSetting() async {
9 | final prefs = await SharedPreferences.getInstance();
10 | final int setting = prefs.getInt('publicationCardAbstractSetting') ?? 1;
11 |
12 | switch (setting) {
13 | case 1:
14 | return AbstractSetting.hideMissing;
15 | case 2:
16 | return AbstractSetting.hideAll;
17 | case 3:
18 | default:
19 | return AbstractSetting.showAll;
20 | }
21 | }
22 |
23 | static Future buildAbstract(
24 | BuildContext context, String abstract) async {
25 | AbstractSetting setting = await getAbstractSetting();
26 |
27 | switch (setting) {
28 | case AbstractSetting.hideAll:
29 | return ''; // Return an empty string if abstracts should be hidden
30 | case AbstractSetting.hideMissing:
31 | return abstract.isNotEmpty ? abstract : ''; // Only show if not empty
32 | case AbstractSetting.showAll:
33 | default:
34 | return abstract.isNotEmpty
35 | ? abstract
36 | : AppLocalizations.of(context)!
37 | .abstractunavailable; // Show default message if empty
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/services/abstract_scraper.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'package:flutter_inappwebview/flutter_inappwebview.dart';
3 | import './string_format_helper.dart';
4 | import './logs_helper.dart';
5 |
6 | class AbstractScraper {
7 | Completer _completer = Completer();
8 |
9 | Future scrapeAbstract(String url) async {
10 | final logger = LogsService().logger;
11 | _completer = Completer();
12 |
13 | HeadlessInAppWebView? headlessWebView;
14 |
15 | headlessWebView = HeadlessInAppWebView(
16 | initialSettings: InAppWebViewSettings(
17 | userAgent:
18 | "Mozilla/5.0 (Android 15; Mobile; rv:133.0) Gecko/133.0 Firefox/133.0",
19 | ),
20 | initialUrlRequest: URLRequest(url: WebUri(url)),
21 | onLoadStop: (controller, loadedUrl) async {
22 | await Future.delayed(Duration(seconds: 3)); // Allow JS-loaded content
23 |
24 | if (_completer.isCompleted) return;
25 |
26 | try {
27 | logger
28 | .info("Attempting to scrape missing abstract from ${loadedUrl}");
29 | String? abstractText = await controller.evaluateJavascript(
30 | source: """
31 | (() => {
32 | function extractFullText(element) {
33 | if (!element) return null;
34 | return element.innerText.trim();
35 | }
36 |
37 | // Special case: Springer (Abstract is inside c-article-section__content)
38 | let springerHeader = document.querySelector('h2.c-article-section__title');
39 | if (springerHeader && /abstract/i.test(springerHeader.innerText)) {
40 | let springerAbstract = springerHeader.nextElementSibling;
41 | if (springerAbstract && springerAbstract.classList.contains('c-article-section__content')) {
42 | return extractFullText(springerAbstract);
43 | }
44 | }
45 |
46 | // Special case: Elsevier (abstract is inside '.abstract.author')
47 | let elsevierAbstract = document.querySelector('.abstract.author');
48 | if (elsevierAbstract) {
49 | return extractFullText(elsevierAbstract);
50 | }
51 |
52 | // General case: Look for any div/section with 'abstract' in class or id
53 | let abstractDiv = [...document.querySelectorAll('div, section')]
54 | .find(el => /abstract/i.test(el.className) || /abstract/i.test(el.id));
55 |
56 | if (abstractDiv) {
57 | return extractFullText(abstractDiv);
58 | }
59 |
60 | // Fallback to meta description
61 | let metaDesc = document.querySelector('meta[name="description"]');
62 | if (metaDesc) return metaDesc.content.trim();
63 |
64 | return null;
65 | })();
66 | """,
67 | );
68 | abstractText = cleanAbstract(abstractText!);
69 | logger.info('The missing abstract was successfully scraped.');
70 | _completer.complete(abstractText);
71 | } catch (e, stackTrace) {
72 | logger.severe(
73 | 'The missing abstract could not be scraped.', e, stackTrace);
74 | _completer.completeError(e);
75 | } finally {
76 | await InAppWebViewController.clearAllCache();
77 | headlessWebView?.dispose();
78 | }
79 | },
80 | );
81 |
82 | await headlessWebView.run();
83 | return _completer.future;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/lib/services/feed_api.dart:
--------------------------------------------------------------------------------
1 | import 'package:http/http.dart' as http;
2 | import 'dart:convert';
3 | import '../models/crossref_journals_works_models.dart' as journalWorks;
4 | import '../models/openAlex_works_models.dart';
5 |
6 | class FeedApi {
7 | static const String baseUrl = 'https://api.crossref.org';
8 | static const String journalsEndpoint = '/journals';
9 | static const String worksEndpoint = '/works';
10 | static const String email = 'mailto=wispar-app@protonmail.com';
11 |
12 | static const String baseUrlOpenAlex = 'https://api.openalex.org';
13 | static const String worksEndpointOpenAlex = '/works?';
14 |
15 | static Future> getRecentFeed(
16 | List issn) async {
17 | final String issnFilter = issn.map((e) => 'issn:$e').join(',');
18 | final response = await http.get(Uri.parse(
19 | '$baseUrl$worksEndpoint?rows=100&sort=created&order=desc&$email&filter=$issnFilter'));
20 |
21 | if (response.statusCode == 200) {
22 | final responseData =
23 | journalWorks.JournalWork.fromJson(json.decode(response.body));
24 | List feedItems = responseData.message.items;
25 | return feedItems;
26 | } else {
27 | throw Exception('Failed to fetch recent feed');
28 | }
29 | }
30 |
31 | static Future> getSavedQueryWorks(
32 | Map queryParams) async {
33 | String url = '$baseUrl$worksEndpoint';
34 | String queryString = queryParams.entries
35 | .map((entry) =>
36 | '${Uri.encodeQueryComponent(entry.key)}=${Uri.encodeQueryComponent(entry.value.toString())}')
37 | .join('&');
38 | String apiUrl = '$url?$queryString&rows=50&$email';
39 |
40 | final response = await http.get(Uri.parse(apiUrl));
41 |
42 | if (response.statusCode == 200) {
43 | final responseData =
44 | journalWorks.JournalWork.fromJson(json.decode(response.body));
45 | List feedItems = responseData.message.items;
46 | return feedItems;
47 | } else {
48 | throw Exception('Failed to fetch results');
49 | }
50 | }
51 |
52 | static Future> getSavedQueryOpenAlex(
53 | String query) async {
54 | final url = Uri.parse(
55 | '$baseUrlOpenAlex$worksEndpointOpenAlex$query&per-page=50&$email');
56 |
57 | final response = await http.get(url);
58 |
59 | if (response.statusCode == 200) {
60 | final jsonResponse = jsonDecode(response.body);
61 | final results = (jsonResponse['results'] as List?)
62 | ?.map((item) => OpenAlexWorks.fromJson(item))
63 | .toList() ??
64 | [];
65 |
66 | return results
67 | .map((result) => journalWorks.Item(
68 | title: result.title,
69 | abstract: result.abstract ?? '',
70 | journalTitle: result.journalTitle ?? '',
71 | publishedDate: result.publishedDate != null
72 | ? DateTime.tryParse(result.publishedDate!) ??
73 | DateTime(1970, 1, 1)
74 | : DateTime(1970, 1, 1),
75 | doi: result.doi ?? '',
76 | authors: result.authors.map((fullName) {
77 | List parts = fullName.split(' ');
78 | String given = parts.isNotEmpty ? parts.first : '';
79 | String family =
80 | parts.length > 1 ? parts.sublist(1).join(' ') : '';
81 | return journalWorks.PublicationAuthor(
82 | given: given, family: family);
83 | }).toList(),
84 | url: result.url ?? '',
85 | primaryUrl: result.url ?? '',
86 | license: '',
87 | licenseName: result.license ?? '',
88 | publisher: result.publisher ?? '',
89 | issn: result.issn ?? [],
90 | ))
91 | .toList();
92 | } else {
93 | throw Exception('Failed to fetch results: ${response.reasonPhrase}');
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/services/libproxydb_api.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:http/http.dart' as http;
3 |
4 | class ProxyData {
5 | final String name;
6 | final String url;
7 |
8 | ProxyData({required this.name, required this.url});
9 |
10 | factory ProxyData.fromJson(Map json) {
11 | return ProxyData(
12 | name: json['name'] ?? '',
13 | url: json['url'] ?? '',
14 | );
15 | }
16 | }
17 |
18 | class ProxyService {
19 | static Future> fetchProxies() async {
20 | final response =
21 | await http.get(Uri.parse('https://libproxy-db.org/proxies.json'));
22 |
23 | if (response.statusCode == 200) {
24 | final decoded = utf8.decode(response.bodyBytes);
25 | final List jsonList = json.decode(decoded);
26 |
27 | return jsonList.map((json) => ProxyData.fromJson(json)).toList();
28 | } else {
29 | throw Exception('Failed to load proxy data');
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/services/logs_helper.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/foundation.dart';
4 | import 'package:logging/logging.dart';
5 | import 'package:file_picker/file_picker.dart';
6 | import 'package:path_provider/path_provider.dart';
7 | import 'package:share_plus/share_plus.dart';
8 | import 'package:wispar/generated_l10n/app_localizations.dart';
9 |
10 | class LogsService {
11 | static final LogsService _instance = LogsService._internal();
12 | factory LogsService() => _instance;
13 |
14 | final Logger _logger = Logger('Wispar');
15 | final ValueNotifier> logsNotifier = ValueNotifier([]);
16 |
17 | LogsService._internal() {
18 | _init();
19 | }
20 |
21 | void _init() {
22 | Logger.root.level = Level.ALL;
23 | Logger.root.onRecord.listen((record) {
24 | logsNotifier.value = [...logsNotifier.value, record];
25 |
26 | if (kDebugMode) {
27 | print(
28 | '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
29 | if (record.error != null) print('Error: ${record.error}');
30 | if (record.stackTrace != null)
31 | print('Stacktrace: ${record.stackTrace}');
32 | }
33 | });
34 | }
35 |
36 | Logger get logger => _logger;
37 |
38 | void clearLogs() {
39 | logsNotifier.value = [];
40 | }
41 |
42 | Future saveLogsToFile(BuildContext context) async {
43 | final logs = LogsService().logsNotifier.value;
44 | final buffer = StringBuffer();
45 |
46 | try {
47 | for (var record in logs) {
48 | buffer.writeln(
49 | '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
50 | if (record.error != null) buffer.writeln('Error: ${record.error}');
51 | if (record.stackTrace != null)
52 | buffer.writeln('Stacktrace: ${record.stackTrace}');
53 | buffer.writeln();
54 | }
55 |
56 | final Uint8List logBytes =
57 | Uint8List.fromList(buffer.toString().codeUnits);
58 |
59 | final String? outputPath = await FilePicker.platform.saveFile(
60 | dialogTitle: AppLocalizations.of(context)!.selectLogsLocation,
61 | fileName: 'wispar_logs.txt',
62 | bytes: logBytes,
63 | );
64 |
65 | if (outputPath == null) {
66 | return;
67 | }
68 | ScaffoldMessenger.of(context).showSnackBar(
69 | SnackBar(
70 | content:
71 | Text(AppLocalizations.of(context)!.logsExportedSuccessfully)),
72 | );
73 | } catch (e, stackTrace) {
74 | logger.severe('Error saving logs.', e, stackTrace);
75 | ScaffoldMessenger.of(context).showSnackBar(
76 | SnackBar(
77 | content: Text(AppLocalizations.of(context)!.logsExportedError)),
78 | );
79 | }
80 | }
81 |
82 | Future shareLogs(BuildContext context) async {
83 | final logs = LogsService().logsNotifier.value;
84 | final buffer = StringBuffer();
85 |
86 | try {
87 | for (var record in logs) {
88 | buffer.writeln(
89 | '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
90 | if (record.error != null) buffer.writeln('Error: ${record.error}');
91 | if (record.stackTrace != null)
92 | buffer.writeln('Stacktrace: ${record.stackTrace}');
93 | buffer.writeln();
94 | }
95 |
96 | final tempDir = await getTemporaryDirectory();
97 | final tempFile = File('${tempDir.path}/wispar_logs.txt');
98 | await tempFile.writeAsString(buffer.toString());
99 |
100 | await SharePlus.instance.share(ShareParams(
101 | files: [XFile(tempFile.path)],
102 | text: AppLocalizations.of(context)!.logs));
103 |
104 | await tempFile.delete();
105 | } catch (e, stackTrace) {
106 | logger.severe('Error sharing logs.', e, stackTrace);
107 | ScaffoldMessenger.of(context).showSnackBar(
108 | SnackBar(
109 | content: Text(AppLocalizations.of(context)!.logsExportedError)),
110 | );
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib/services/mathml_converter.dart:
--------------------------------------------------------------------------------
1 | import 'package:xml/xml.dart';
2 | import './logs_helper.dart';
3 |
4 | // Converts MathML to latex equations so that they can be rendered with Latext
5 | class MathmlToLatexConverter {
6 | final logger = LogsService().logger;
7 | String convert(String raw) {
8 | try {
9 | final wrapped = '$raw';
10 | final document = XmlDocument.parse(wrapped);
11 | return _convertChildren(document.rootElement.children);
12 | } catch (e, stackTrace) {
13 | logger.warning('Unable to convert MathML to Latex.', e, stackTrace);
14 | return raw;
15 | }
16 | }
17 |
18 | String _convertChildren(List nodes) {
19 | final buffer = StringBuffer();
20 | final formulaBuffer = StringBuffer();
21 | bool insideFormula = false;
22 |
23 | for (int i = 0; i < nodes.length; i++) {
24 | final node = nodes[i];
25 |
26 | if (node is XmlText) {
27 | final text = node.value;
28 | final trimmed = text.trimRight();
29 |
30 | final isChemicalSymbol =
31 | RegExp(r'^[A-Z][a-z]?$').hasMatch(trimmed.trim());
32 |
33 | if (isChemicalSymbol) {
34 | // Start formula buffering if not already
35 | if (!insideFormula) {
36 | insideFormula = true;
37 | formulaBuffer.clear();
38 | }
39 | formulaBuffer.write(trimmed.trim());
40 | } else {
41 | // Flush formula if any
42 | if (insideFormula) {
43 | buffer.write('\$${formulaBuffer.toString()}\$');
44 | formulaBuffer.clear();
45 | insideFormula = false;
46 | }
47 | buffer.write(trimmed); // regular text
48 | }
49 | } else if (node is XmlElement && node.name.local == 'math') {
50 | final mathContent = _convertChildren(node.children);
51 | if (!insideFormula) {
52 | insideFormula = true;
53 | formulaBuffer.clear();
54 | }
55 | formulaBuffer.write(mathContent);
56 |
57 | // Look ahead, if next is not another