mirror of
https://github.com/PhilKes/NotallyX.git
synced 2025-06-30 13:19:54 +00:00
Compare commits
No commits in common. "main" and "v6.0-RC1" have entirely different histories.
610 changed files with 15058 additions and 333049 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1 +1 @@
|
|||
ko_fi: philkes
|
||||
patreon: omgodse
|
36
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
36
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
@ -1,36 +0,0 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ["bug"]
|
||||
projects: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: App Version
|
||||
description: What version of the app are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android Version (API Level)
|
||||
description: What Android version are you using?
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: (Optional) Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,14 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
10
.github/ISSUE_TEMPLATE/translation.md
vendored
10
.github/ISSUE_TEMPLATE/translation.md
vendored
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: Translation Update
|
||||
about: Update translations by uploading updated translations.xlsx
|
||||
title: '<INSERT LANGUAGE HERE> translations update'
|
||||
labels: translations
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Drag'n'drop your updated translations.xlsx file here 🙂
|
56
.github/workflows/deploy.yaml
vendored
56
.github/workflows/deploy.yaml
vendored
|
@ -1,56 +0,0 @@
|
|||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- documentation/**
|
||||
# Review gh actions docs if you want to further define triggers, paths, etc
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache-dependency-path: documentation/yarn.lock
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: documentation
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build website
|
||||
working-directory: documentation
|
||||
run: yarn build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: documentation/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -7,10 +7,3 @@
|
|||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*/.attach_pid*
|
||||
fastlane/*
|
||||
!fastlane/join-testers.png
|
||||
!fastlane/metadata
|
||||
Gemfile*
|
||||
*.sh
|
||||
!generate-changelogs.sh
|
|
@ -1,25 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Capture the list of initially staged Kotlin files
|
||||
initial_staged_files=$(git diff --name-only --cached -- '*.kt')
|
||||
|
||||
if [ -z "$initial_staged_files" ]; then
|
||||
echo "No Kotlin files staged for commit."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
formatted_files=$(echo "$initial_staged_files" | sed 's|^app/||' | paste -sd "," -)
|
||||
echo "Formatting Kotlin files: $formatted_files"
|
||||
./gradlew ktfmtPrecommit --include-only="$formatted_files"
|
||||
|
||||
./gradlew ktfmtPrecommit
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Kotlin formatting failed. Please fix the issues."
|
||||
exit 1
|
||||
fi
|
||||
git add .
|
||||
|
||||
# Re-stage only the initially staged Kotlin files
|
||||
for file in $initial_staged_files; do
|
||||
git add "$file"
|
||||
done
|
||||
echo "Kotlin files formatted and changes staged."
|
||||
exit 0
|
||||
|
||||
echo "Kotlin files formatted"
|
||||
|
|
|
@ -1,35 +1,13 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Capture the list of initially staged Kotlin files
|
||||
set "initial_staged_files="
|
||||
for /f "delims=" %%f in ('git diff --name-only --cached -- "*.kt"') do (
|
||||
set "initial_staged_files=!initial_staged_files! %%f,"
|
||||
)
|
||||
CALL ./gradlew.bat ktfmtPrecommit
|
||||
|
||||
rem Check if there are any staged Kotlin files
|
||||
if "%initial_staged_files%"=="" (
|
||||
echo No Kotlin files staged for commit.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
rem Remove the trailing comma from the list of formatted files
|
||||
set "formatted_files=%initial_staged_files:~0,-1%"
|
||||
|
||||
echo Formatting Kotlin files: %formatted_files%
|
||||
call gradlew ktfmtPrecommit --include-only="%formatted_files%"
|
||||
|
||||
rem Check if the formatting command was successful
|
||||
if errorlevel 1 (
|
||||
IF ERRORLEVEL 1 (
|
||||
echo Kotlin formatting failed. Please fix the issues.
|
||||
exit /b 1
|
||||
exit /B 1
|
||||
) ELSE (
|
||||
git add .
|
||||
)
|
||||
|
||||
rem Re-stage only the initially staged Kotlin files
|
||||
for %%f in (%initial_staged_files%) do (
|
||||
git add "%%f"
|
||||
)
|
||||
|
||||
echo Kotlin files formatted
|
||||
|
||||
exit /b 0
|
||||
echo Kotlin files formatted and changes staged.
|
||||
exit /B 0
|
371
CHANGELOG.md
371
CHANGELOG.md
|
@ -1,371 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
## [v7.4.0](https://github.com/PhilKes/NotallyX/tree/v7.4.0) (2025-04-18)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.3.1...v7.4.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Don't force capitalization when adding a label [\#532](https://github.com/PhilKes/NotallyX/issues/532)
|
||||
- Add a screen protection against screenshot attempts [\#386](https://github.com/PhilKes/NotallyX/issues/386)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Share pure text note error [\#544](https://github.com/PhilKes/NotallyX/issues/544)
|
||||
- Crash when deleting checked items in a list [\#539](https://github.com/PhilKes/NotallyX/issues/539)
|
||||
- Keyboard don't open after closing it on Android 7 [\#537](https://github.com/PhilKes/NotallyX/issues/537)
|
||||
- Unable to open links before changing view mode [\#527](https://github.com/PhilKes/NotallyX/issues/527)
|
||||
- Reminder popup cut on small screens [\#522](https://github.com/PhilKes/NotallyX/issues/522)
|
||||
- Auto Backup failed [\#514](https://github.com/PhilKes/NotallyX/issues/514)
|
||||
|
||||
## [v7.3.1](https://github.com/PhilKes/NotallyX/tree/v7.3.1) (2025-04-08)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.3.0...v7.3.1)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Button to close note search doesn't work [\#519](https://github.com/PhilKes/NotallyX/issues/519)
|
||||
- app crashes when pressing label [\#517](https://github.com/PhilKes/NotallyX/issues/517)
|
||||
|
||||
## [v7.3.0](https://github.com/PhilKes/NotallyX/tree/v7.3.0) (2025-04-07)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.2.1...v7.3.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Persist viewMode of each note individually [\#497](https://github.com/PhilKes/NotallyX/issues/497)
|
||||
- Read-only mode by default and new notes [\#495](https://github.com/PhilKes/NotallyX/issues/495)
|
||||
- Hide notes based on labels [\#401](https://github.com/PhilKes/NotallyX/issues/401)
|
||||
- An archived note should be visible in its label's folder. [\#398](https://github.com/PhilKes/NotallyX/issues/398)
|
||||
- Sharing notes from the app [\#380](https://github.com/PhilKes/NotallyX/issues/380)
|
||||
- Add support for json notes import [\#377](https://github.com/PhilKes/NotallyX/issues/377)
|
||||
- Sharing images to the app [\#281](https://github.com/PhilKes/NotallyX/issues/281)
|
||||
- Strikethrough checked items lists [\#250](https://github.com/PhilKes/NotallyX/issues/250)
|
||||
- Click on list element to check it [\#248](https://github.com/PhilKes/NotallyX/issues/248)
|
||||
- Add long press actions to undo/redo buttons [\#244](https://github.com/PhilKes/NotallyX/issues/244)
|
||||
- Convert Note \<=\> List [\#190](https://github.com/PhilKes/NotallyX/issues/190)
|
||||
- Edit labels inside notes [\#180](https://github.com/PhilKes/NotallyX/issues/180)
|
||||
- Support Wallpaper color themes [\#175](https://github.com/PhilKes/NotallyX/issues/175)
|
||||
- View Mode [\#76](https://github.com/PhilKes/NotallyX/issues/76)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Android 7.0 Navigation bar color issue [\#515](https://github.com/PhilKes/NotallyX/issues/515)
|
||||
- Search Mode loop for Android \< 9.0 [\#508](https://github.com/PhilKes/NotallyX/issues/508)
|
||||
- BaseNote.viewMode database migration error [\#505](https://github.com/PhilKes/NotallyX/issues/505)
|
||||
- New list items can't be added with linebreak/enter [\#496](https://github.com/PhilKes/NotallyX/issues/496)
|
||||
- Undo changes more than the last changed character [\#472](https://github.com/PhilKes/NotallyX/issues/472)
|
||||
- Auto Backup failed [\#468](https://github.com/PhilKes/NotallyX/issues/468)
|
||||
- Amount of backups to keep in periodic backups are not respected if nextcloud mount is used. [\#133](https://github.com/PhilKes/NotallyX/issues/133)
|
||||
|
||||
## [v7.2.1](https://github.com/PhilKes/NotallyX/tree/v7.2.1) (2025-03-18)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.2.0...v7.2.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Note not automatically saved when App is killed by system [\#446](https://github.com/PhilKes/NotallyX/issues/446)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Auto Backup failed [\#456](https://github.com/PhilKes/NotallyX/issues/456)
|
||||
|
||||
## [v7.2.1](https://github.com/PhilKes/NotallyX/tree/v7.2.1) (2025-03-18)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.2.0...v7.2.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Note not automatically saved when App is killed by system [\#446](https://github.com/PhilKes/NotallyX/issues/446)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Auto Backup failed [\#456](https://github.com/PhilKes/NotallyX/issues/456)
|
||||
|
||||
## [v7.2.0](https://github.com/PhilKes/NotallyX/tree/v7.2.0) (2025-03-08)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.1.0...v7.2.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Sort notes by color [\#442](https://github.com/PhilKes/NotallyX/issues/442)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Unable to locate the 'Uncheck all' option [\#444](https://github.com/PhilKes/NotallyX/issues/444)
|
||||
- List crash when last unchecked item moved [\#436](https://github.com/PhilKes/NotallyX/issues/436)
|
||||
- Pasting multi line text in empty lists crash [\#434](https://github.com/PhilKes/NotallyX/issues/434)
|
||||
- Quickly tapping delete button crash [\#428](https://github.com/PhilKes/NotallyX/issues/428)
|
||||
- First list item can keep parent property [\#427](https://github.com/PhilKes/NotallyX/issues/427)
|
||||
- \(List\) Move last unchecked item above parent bug [\#425](https://github.com/PhilKes/NotallyX/issues/425)
|
||||
- Dragging child item to same position breaks parent association [\#422](https://github.com/PhilKes/NotallyX/issues/422)
|
||||
- Disabling list auto sorting crash [\#421](https://github.com/PhilKes/NotallyX/issues/421)
|
||||
- Crash while indenting a checklist item [\#419](https://github.com/PhilKes/NotallyX/issues/419)
|
||||
- List swipe as subtask crash [\#418](https://github.com/PhilKes/NotallyX/issues/418)
|
||||
- List crash [\#413](https://github.com/PhilKes/NotallyX/issues/413)
|
||||
- Checked parent + subtask places between item [\#410](https://github.com/PhilKes/NotallyX/issues/410)
|
||||
- App crashed while screen was off [\#408](https://github.com/PhilKes/NotallyX/issues/408)
|
||||
- Unchecked items can't be deleted [\#407](https://github.com/PhilKes/NotallyX/issues/407)
|
||||
- Some list items can't be set to subtask after unchecked [\#406](https://github.com/PhilKes/NotallyX/issues/406)
|
||||
- List items deleted [\#405](https://github.com/PhilKes/NotallyX/issues/405)
|
||||
- List item parent task becomes subtask [\#404](https://github.com/PhilKes/NotallyX/issues/404)
|
||||
- Check empty item crash [\#403](https://github.com/PhilKes/NotallyX/issues/403)
|
||||
- Drag problem in long checklist [\#396](https://github.com/PhilKes/NotallyX/issues/396)
|
||||
- List swap items bug [\#395](https://github.com/PhilKes/NotallyX/issues/395)
|
||||
- Background crashes [\#323](https://github.com/PhilKes/NotallyX/issues/323)
|
||||
|
||||
## [v7.1.0](https://github.com/PhilKes/NotallyX/tree/v7.1.0) (2025-02-20)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v7.0.0...v7.1.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Dark mode for note colors [\#352](https://github.com/PhilKes/NotallyX/issues/352)
|
||||
- Add "new color" option when deleting a color [\#347](https://github.com/PhilKes/NotallyX/issues/347)
|
||||
- Make "Start view" as default view [\#339](https://github.com/PhilKes/NotallyX/issues/339)
|
||||
- Display first item as title for lists without title [\#317](https://github.com/PhilKes/NotallyX/issues/317)
|
||||
- Remove delete option from top bar since it's already in the bottom more menu [\#316](https://github.com/PhilKes/NotallyX/issues/316)
|
||||
- Add "Export" to bottom menu [\#315](https://github.com/PhilKes/NotallyX/issues/315)
|
||||
- Move "Hide labels" switch up below the labels slider [\#311](https://github.com/PhilKes/NotallyX/issues/311)
|
||||
- Display widgets with the notes' colors [\#300](https://github.com/PhilKes/NotallyX/issues/300)
|
||||
- Allow Pinning a Specific Label as the Starting Page [\#269](https://github.com/PhilKes/NotallyX/issues/269)
|
||||
- Move checked / unchecked items in list [\#251](https://github.com/PhilKes/NotallyX/issues/251)
|
||||
- Moving all labels to the sidebar [\#240](https://github.com/PhilKes/NotallyX/issues/240)
|
||||
- Add "no label" category [\#219](https://github.com/PhilKes/NotallyX/issues/219)
|
||||
- Manual color selection [\#187](https://github.com/PhilKes/NotallyX/issues/187)
|
||||
- Pure Dark Mode [\#16](https://github.com/PhilKes/NotallyX/issues/16)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Moving group of task at first position wrong order [\#392](https://github.com/PhilKes/NotallyX/issues/392)
|
||||
- Parent task not checked in specific case [\#391](https://github.com/PhilKes/NotallyX/issues/391)
|
||||
- Add auto-backup no error notification [\#381](https://github.com/PhilKes/NotallyX/issues/381)
|
||||
- Backup on save setting not restored [\#373](https://github.com/PhilKes/NotallyX/issues/373)
|
||||
- Start View setting not restored [\#367](https://github.com/PhilKes/NotallyX/issues/367)
|
||||
- \(List\) Child item can't be place after the item below it [\#362](https://github.com/PhilKes/NotallyX/issues/362)
|
||||
- Android 9 Crash java.lang.NoSuchMethodError: getTextSelectHandleLeft\(\) [\#358](https://github.com/PhilKes/NotallyX/issues/358)
|
||||
- List items order bug [\#357](https://github.com/PhilKes/NotallyX/issues/357)
|
||||
- Lists parent-child items crash [\#356](https://github.com/PhilKes/NotallyX/issues/356)
|
||||
- List new items wrong position [\#354](https://github.com/PhilKes/NotallyX/issues/354)
|
||||
- Replacement color message title limited to 2 lines [\#348](https://github.com/PhilKes/NotallyX/issues/348)
|
||||
- \(Lists\) Delete checked items and undo crash [\#331](https://github.com/PhilKes/NotallyX/issues/331)
|
||||
- Sort List items strange bug [\#330](https://github.com/PhilKes/NotallyX/issues/330)
|
||||
- Backup folder re-select prompt closes automatically [\#324](https://github.com/PhilKes/NotallyX/issues/324)
|
||||
- Biometric lock can't be enabled [\#259](https://github.com/PhilKes/NotallyX/issues/259)
|
||||
|
||||
|
||||
## [v7.0.0](https://github.com/PhilKes/NotallyX/tree/v7.0.0) (2025-01-27)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.4.1...v7.0.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Add Reminders menu to navigation panel [\#294](https://github.com/PhilKes/NotallyX/issues/294)
|
||||
- Extend notes colors to full screen [\#264](https://github.com/PhilKes/NotallyX/issues/264)
|
||||
- Auto-backup on note modification [\#203](https://github.com/PhilKes/NotallyX/issues/203)
|
||||
- Option to show full date inside the note regardless of chosen date format [\#111](https://github.com/PhilKes/NotallyX/issues/111)
|
||||
- Reminder for Notes [\#85](https://github.com/PhilKes/NotallyX/issues/85)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Restoring settings and backup folder [\#310](https://github.com/PhilKes/NotallyX/issues/310)
|
||||
- Settings can't be imported [\#307](https://github.com/PhilKes/NotallyX/issues/307)
|
||||
- Label create/edit dialog broken [\#302](https://github.com/PhilKes/NotallyX/issues/302)
|
||||
- Rotating screen moves the cursor to the end of note [\#293](https://github.com/PhilKes/NotallyX/issues/293)
|
||||
- Creating an empty note corrupts auto backup archive [\#288](https://github.com/PhilKes/NotallyX/issues/288)
|
||||
- Changes to check lists \(checking items and removing items\) reverts when screen rotatates [\#287](https://github.com/PhilKes/NotallyX/issues/287)
|
||||
- Auto-backups stop exporting after set limit is reached [\#270](https://github.com/PhilKes/NotallyX/issues/270)
|
||||
- Link is not saved if it's the last edit [\#267](https://github.com/PhilKes/NotallyX/issues/267)
|
||||
- Labels hidden in overview not applied when importing JSON [\#266](https://github.com/PhilKes/NotallyX/issues/266)
|
||||
- An unexpected error occurred. Sorry for the inconvenience [\#262](https://github.com/PhilKes/NotallyX/issues/262)
|
||||
- List subtasks bugs [\#207](https://github.com/PhilKes/NotallyX/issues/207)
|
||||
- Remove link also remove text [\#201](https://github.com/PhilKes/NotallyX/issues/201)
|
||||
- Biometric Lock crash [\#177](https://github.com/PhilKes/NotallyX/issues/177)
|
||||
- Import from Evernote and Google Keep not working [\#134](https://github.com/PhilKes/NotallyX/issues/134)
|
||||
|
||||
## [v6.4.1](https://github.com/PhilKes/NotallyX/tree/v6.4.1) (2025-01-17)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.4.0...v6.4.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Bottom AppBar cutoff + chinese translations [\#176](https://github.com/PhilKes/NotallyX/issues/176)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Crash loop when enabling biometrics [\#256](https://github.com/PhilKes/NotallyX/issues/256)
|
||||
- Crash when creating notes in 6.4.0 [\#255](https://github.com/PhilKes/NotallyX/issues/255)
|
||||
|
||||
## [v6.4.0](https://github.com/PhilKes/NotallyX/tree/v6.4.0) (2025-01-17)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.3.0...v6.4.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Prevent word wrap in titles [\#220](https://github.com/PhilKes/NotallyX/issues/220)
|
||||
- Move more top menu to bottom appbar [\#206](https://github.com/PhilKes/NotallyX/issues/206)
|
||||
- App settings backup [\#204](https://github.com/PhilKes/NotallyX/issues/204)
|
||||
- Add a touch bar to scroll quickly [\#202](https://github.com/PhilKes/NotallyX/issues/202)
|
||||
- Select all notes menu button [\#186](https://github.com/PhilKes/NotallyX/issues/186)
|
||||
- One line entries in main menu [\#185](https://github.com/PhilKes/NotallyX/issues/185)
|
||||
- Some suggestions [\#183](https://github.com/PhilKes/NotallyX/issues/183)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Cutting text during search causes app crash [\#245](https://github.com/PhilKes/NotallyX/issues/245)
|
||||
- Long notes can't be shared [\#243](https://github.com/PhilKes/NotallyX/issues/243)
|
||||
- Trigger disable/enable dataOnExternalStorage on settings import [\#231](https://github.com/PhilKes/NotallyX/issues/231)
|
||||
- Fast scrollbar in notes overview lags [\#230](https://github.com/PhilKes/NotallyX/issues/230)
|
||||
- Handle user unenrolling device biometrics while biometric lock is enabled [\#229](https://github.com/PhilKes/NotallyX/issues/229)
|
||||
- Crash on import if no file explorer app installed [\#227](https://github.com/PhilKes/NotallyX/issues/227)
|
||||
- Assign label inactive checkboxes [\#221](https://github.com/PhilKes/NotallyX/issues/221)
|
||||
- List icon bug in main view [\#215](https://github.com/PhilKes/NotallyX/issues/215)
|
||||
- Import plain text issues [\#209](https://github.com/PhilKes/NotallyX/issues/209)
|
||||
- "Auto backup period in days" cursor buggy [\#192](https://github.com/PhilKes/NotallyX/issues/192)
|
||||
- Unable to view full link while editing [\#181](https://github.com/PhilKes/NotallyX/issues/181)
|
||||
- Scrollbar missing ... [\#178](https://github.com/PhilKes/NotallyX/issues/178)
|
||||
- Widget is invisible when placed [\#156](https://github.com/PhilKes/NotallyX/issues/156)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Items in Lists [\#184](https://github.com/PhilKes/NotallyX/issues/184)
|
||||
|
||||
## [v6.3.0](https://github.com/PhilKes/NotallyX/tree/v6.3.0) (2024-12-23)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.2.2...v6.3.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Possibility to directly report bug when crash occurs [\#170](https://github.com/PhilKes/NotallyX/issues/170)
|
||||
- "Link Note" text for notes without a title [\#166](https://github.com/PhilKes/NotallyX/issues/166)
|
||||
- Improve app theme's color contrasts [\#163](https://github.com/PhilKes/NotallyX/issues/163)
|
||||
- Paste text containing link does not convert to clickable link + polish translation [\#157](https://github.com/PhilKes/NotallyX/issues/157)
|
||||
- Bottom navigation to increase accessibility for one handed usage [\#129](https://github.com/PhilKes/NotallyX/issues/129)
|
||||
- Search in selected note [\#108](https://github.com/PhilKes/NotallyX/issues/108)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- App crashes when enabling biometric lock [\#168](https://github.com/PhilKes/NotallyX/issues/168)
|
||||
- Barely visible Navigation buttons on Motorola devices in Light Theme [\#161](https://github.com/PhilKes/NotallyX/issues/161)
|
||||
|
||||
## [v6.2.2](https://github.com/PhilKes/NotallyX/tree/v6.2.2) (2024-12-09)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.2.1...v6.2.2)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Encrypted backups [\#151](https://github.com/PhilKes/NotallyX/issues/151)
|
||||
- Parse list items when pasting a list formatted text into a list note [\#150](https://github.com/PhilKes/NotallyX/issues/150)
|
||||
- Chinese display of options interface [\#149](https://github.com/PhilKes/NotallyX/issues/149)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- List notes deleting item even though text is not empty on backspace pressed [\#142](https://github.com/PhilKes/NotallyX/issues/142)
|
||||
|
||||
## [v6.2.1](https://github.com/PhilKes/NotallyX/tree/v6.2.1) (2024-12-06)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.2.0...v6.2.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Add change color option inside of a Note [\#140](https://github.com/PhilKes/NotallyX/issues/140)
|
||||
- Let user choose whether add items in a todo-list at the bottom or top of already existing items [\#132](https://github.com/PhilKes/NotallyX/issues/132)
|
||||
- Migrate Theme to Material 3 [\#104](https://github.com/PhilKes/NotallyX/issues/104)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Exporting multiple notes with same title overwrites files [\#144](https://github.com/PhilKes/NotallyX/issues/144)
|
||||
- Single notes export as "Untitled.txt" [\#143](https://github.com/PhilKes/NotallyX/issues/143)
|
||||
|
||||
## [v6.2.0](https://github.com/PhilKes/NotallyX/tree/v6.2.0) (2024-12-03)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.1.2...v6.2.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Replace positions of "Link Note" and "Select all" [\#136](https://github.com/PhilKes/NotallyX/issues/136)
|
||||
- Allow to add Notes in Label View [\#128](https://github.com/PhilKes/NotallyX/issues/128)
|
||||
- Empty notes deleted forever [\#118](https://github.com/PhilKes/NotallyX/issues/118)
|
||||
- Sync devices using Syncthing [\#109](https://github.com/PhilKes/NotallyX/issues/109)
|
||||
- Import from txt files [\#103](https://github.com/PhilKes/NotallyX/issues/103)
|
||||
- Add more Export formats [\#62](https://github.com/PhilKes/NotallyX/issues/62)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Using delete option from inside a note won't delete the note [\#131](https://github.com/PhilKes/NotallyX/issues/131)
|
||||
- The app crashes when creating a link [\#112](https://github.com/PhilKes/NotallyX/issues/112)
|
||||
- Pinning does not work [\#110](https://github.com/PhilKes/NotallyX/issues/110)
|
||||
|
||||
## [v6.1.2](https://github.com/PhilKes/NotallyX/tree/v6.1.2) (2024-11-19)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.1.1...v6.1.2)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- F-Droid can't build [\#126](https://github.com/PhilKes/NotallyX/issues/126)
|
||||
- Undo in list can be quite destructive [\#125](https://github.com/PhilKes/NotallyX/issues/125)
|
||||
- Actions like pin/label/archive only work from overview, not individual note/list [\#124](https://github.com/PhilKes/NotallyX/issues/124)
|
||||
- Jumbled notes after opening settings [\#100](https://github.com/PhilKes/NotallyX/issues/100)
|
||||
|
||||
## [v6.1.1](https://github.com/PhilKes/NotallyX/tree/v6.1.1) (2024-11-14)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/v6.0...v6.1.1)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Show delete option in top bar after selecting a note [\#105](https://github.com/PhilKes/NotallyX/issues/105)
|
||||
- Clear previous search term by default [\#96](https://github.com/PhilKes/NotallyX/issues/96)
|
||||
- Color the navigation bar [\#95](https://github.com/PhilKes/NotallyX/issues/95)
|
||||
- Make multiselection easier [\#91](https://github.com/PhilKes/NotallyX/issues/91)
|
||||
- Auto Discard Empty Notes [\#86](https://github.com/PhilKes/NotallyX/issues/86)
|
||||
- Pin/label multiple Notes [\#78](https://github.com/PhilKes/NotallyX/issues/78)
|
||||
- Hide widget notes when app locked. [\#75](https://github.com/PhilKes/NotallyX/issues/75)
|
||||
- Creating categories [\#72](https://github.com/PhilKes/NotallyX/issues/72)
|
||||
- Add Search Bar to Archived and Deleted Notes Sections [\#68](https://github.com/PhilKes/NotallyX/issues/68)
|
||||
- Ability to swipe ListItem on DragHandle [\#67](https://github.com/PhilKes/NotallyX/issues/67)
|
||||
- Show Last Modified Dates in Notes [\#60](https://github.com/PhilKes/NotallyX/issues/60)
|
||||
- Import from major note apps [\#53](https://github.com/PhilKes/NotallyX/issues/53)
|
||||
- Display Labels in NavigationView [\#49](https://github.com/PhilKes/NotallyX/issues/49)
|
||||
- Unneeded Notification Permission on Image Upload [\#34](https://github.com/PhilKes/NotallyX/issues/34)
|
||||
- Linking Notes Within Notes [\#32](https://github.com/PhilKes/NotallyX/issues/32)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Deleted notes won't "delete forever" [\#117](https://github.com/PhilKes/NotallyX/issues/117)
|
||||
- Empty Auto Backup [\#101](https://github.com/PhilKes/NotallyX/issues/101)
|
||||
- Selector Moves Left in Widget When Selecting Untitled Notes [\#63](https://github.com/PhilKes/NotallyX/issues/63)
|
||||
|
||||
## [v6.0](https://github.com/PhilKes/NotallyX/tree/v6.0) (2024-10-28)
|
||||
|
||||
[Full Changelog](https://github.com/PhilKes/NotallyX/compare/a29bff9a2d1adcbea47cb024ab21426bd678c016...v6.0)
|
||||
|
||||
### Added Features
|
||||
|
||||
- Change icon and add monochrome icon [\#44](https://github.com/PhilKes/NotallyX/issues/44)
|
||||
- Improve copy&paste behaviour [\#40](https://github.com/PhilKes/NotallyX/issues/40)
|
||||
- Option to Change a Note in Widget [\#36](https://github.com/PhilKes/NotallyX/issues/36)
|
||||
- Improve auto-backups [\#31](https://github.com/PhilKes/NotallyX/issues/31)
|
||||
- Lock Notes via PIN/Fingerprint [\#30](https://github.com/PhilKes/NotallyX/issues/30)
|
||||
- More options for sorting Notes in Overview [\#29](https://github.com/PhilKes/NotallyX/issues/29)
|
||||
- Support subtasks in Widgets \(for list notes\) [\#6](https://github.com/PhilKes/NotallyX/issues/6)
|
||||
- Improving Image Display on Notes [\#15](https://github.com/PhilKes/NotallyX/issues/15)
|
||||
- File attachment [\#9](https://github.com/PhilKes/NotallyX/issues/9)
|
||||
- Highlighting Completed Tasks in widget [\#17](https://github.com/PhilKes/NotallyX/issues/17)
|
||||
- Encrypt backups [\#18](https://github.com/PhilKes/NotallyX/issues/18)
|
||||
- Undo deleting/archiving notes [\#19](https://github.com/PhilKes/NotallyX/issues/19)
|
||||
|
||||
### Fixed Bugs
|
||||
|
||||
- Title Change Doesn’t Update Last Modified Date [\#61](https://github.com/PhilKes/NotallyX/issues/61)
|
||||
- Biometric Lock Improvement [\#58](https://github.com/PhilKes/NotallyX/issues/58)
|
||||
- App crashes when pin lock is enabled [\#50](https://github.com/PhilKes/NotallyX/issues/50)
|
||||
- Undo action ignored Spans [\#47](https://github.com/PhilKes/NotallyX/issues/47)
|
||||
- Crash After Importing a Note [\#13](https://github.com/PhilKes/NotallyX/issues/13)
|
||||
- Tasks Disappear When Changing App Language [\#4](https://github.com/PhilKes/NotallyX/issues/4)
|
||||
- Unable to Swipe Back After Adding Tasks [\#5](https://github.com/PhilKes/NotallyX/issues/5)
|
||||
- App Crash When Importing Notes and Opening a Task Note [\#7](https://github.com/PhilKes/NotallyX/issues/7)
|
||||
- improving subtasks [\#8](https://github.com/PhilKes/NotallyX/issues/8)
|
||||
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
5
CONTRIBUTING.md
Normal file
5
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
### Contributing
|
||||
|
||||
Issues are currently disabled.
|
||||
|
||||
Please use the pull requests tab only for translations or bug fixes.
|
|
@ -1,25 +0,0 @@
|
|||
## Privacy Policy
|
||||
|
||||
This privacy policy applies to the NotallyX app (hereby referred to as "Application") for mobile devices that was created as an Open Source service. This service is intended for use "AS IS".
|
||||
|
||||
### What information does the Application obtain and how is it used?
|
||||
|
||||
The Application does not obtain any information when you download and use it. Registration is not required to use the Application.
|
||||
|
||||
### Does the Application collect precise real time location information of the device?
|
||||
|
||||
This Application does not collect precise information about the location of your mobile device.
|
||||
|
||||
### Do third parties see and/or have access to information obtained by the Application?
|
||||
|
||||
Since the Application does not collect any information, no data is shared with third parties.
|
||||
|
||||
### Your Consent
|
||||
|
||||
By using the Application, you are consenting to this Privacy Policy now and as amended by the developer.
|
||||
|
||||
### Contact Us
|
||||
|
||||
If you have any questions regarding privacy while using the Application, or have questions about the practices, please contact us via email at philkeyplaystore@gmail.com.
|
||||
|
||||
This privacy policy is effective as of 2024-10-30
|
1
Privacy-Policy.txt
Normal file
1
Privacy-Policy.txt
Normal file
|
@ -0,0 +1 @@
|
|||
No user data is collected
|
103
README.md
103
README.md
|
@ -1,88 +1,35 @@
|
|||
<h2 align="center">
|
||||
<img src="fastlane/metadata/android/en-US/images/icon.png" alt="icon" width="90"/>
|
||||
<br />
|
||||
<b>NotallyX | Minimalistic note taking app</b>
|
||||
<p>
|
||||
<center>
|
||||
<a href='https://play.google.com/store/apps/details?id=com.philkes.notallyx&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' height='80'/></a>
|
||||
<a href="https://f-droid.org/en/packages/com.philkes.notallyx"><img alt='IzzyOnDroid' height='80' src='https://fdroid.gitlab.io/artwork/badge/get-it-on.png' /></a>
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.philkes.notallyx"><img alt='F-Droid' height='80' src='https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png' /></a>
|
||||
</center>
|
||||
</p>
|
||||
</h2>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; width: 100%;">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" alt="Image 6" style="width: 32%;"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" alt="Image 2" style="width: 32%;"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" alt="Image 3" style="width: 32%;"/>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; width: 100%;">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" alt="Image 4" style="width: 32%;"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" alt="Image 5" style="width: 32%;"/>
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/7.png" alt="Image 7" style="width: 32%;"/>
|
||||
</div>
|
||||
### Background
|
||||
Notally was created because I wanted to make something that was beautiful and at the same time, useful. It's extremely light, there are minimal dependencies and lines of code.
|
||||
|
||||
### Features
|
||||
[Notally](https://github.com/OmGodse/Notally), but eXtended
|
||||
|
||||
* Create **rich text** notes with support for bold, italics, mono space and strike-through
|
||||
* Create **task lists** and order them with subtasks (+ auto-sort checked items to the end)
|
||||
* Set **reminders** with notifications for important notes
|
||||
* Complement your notes with any type of file such as **pictures**, PDFs, etc.
|
||||
* **Sort notes** by title, last modified date, creation date
|
||||
* **Color, pin and label** your notes for quick organisation
|
||||
* Add **clickable links** to notes with support for phone numbers, email addresses and web urls
|
||||
* **Undo/Redo actions**
|
||||
* Use **Home Screen Widget** to access important notes fast
|
||||
* **Lock your notes via Biometric/PIN**
|
||||
* Configurable **auto-backups**
|
||||
* Create quick audio notes
|
||||
* Display the notes either in a **List or Grid**
|
||||
* Quickly share notes by text
|
||||
* Extensive preferences to adjust views to your liking
|
||||
* Actions to quickly remove checked tasks
|
||||
* Adaptive android app icon
|
||||
* Widgets
|
||||
* Auto backup
|
||||
* Adjustable text size
|
||||
* Support for Lollipop devices and up
|
||||
* APK size of 1.4 MB (1.8 MB uncompressed)
|
||||
* Color, pin and label your notes for quick organisation
|
||||
* Complement your notes with pictures (JPG, PNG, WEBP)
|
||||
* Export notes as TXT, JSON, HTML or PDF files with formatting
|
||||
* Create rich text notes with support for bold, italics, mono space and strike-through
|
||||
* Add clickable links to notes with support for phone numbers, email addresses and web urls
|
||||
|
||||
---
|
||||
|
||||
### Bug Reports / Feature-Requests
|
||||
If you find any bugs or want to propose a new Feature/Enhancement, feel free to [create a new Issue](https://github.com/PhilKes/NotallyX/issues/new/choose)
|
||||
|
||||
When using the app and an unknown error occurs, causing the app to crash you will see a dialog (see showcase video in https://github.com/PhilKes/NotallyX/pull/171) from which you can immediately create a bug report on Github with the crash details pre-filled.
|
||||
|
||||
#### Beta Releases
|
||||
|
||||
I occasionally release BETA versions of the app during development, since its very valuable for me to get feedback before publicly releasing a new version.
|
||||
These BETA releases have another `applicationId` as the release versions, thats why when you install a BETA version it will show up on your device as a separate app called `NotallyX BETA`.
|
||||
BETA versions also have their own data, they do not use the data of your NotallyX app
|
||||
You can download the most recent BETA release [here on Github](https://github.com/PhilKes/NotallyX/releases/tag/beta)
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="70"/>](https://play.google.com/store/apps/details?id=com.omgodse.notally)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="70"/>](https://f-droid.org/packages/com.omgodse.notally/)
|
||||
|
||||
### Translations
|
||||
All translations are crowd sourced.
|
||||
To contribute:
|
||||
1. Download current [translations.xlsx](https://github.com/PhilKes/NotallyX/raw/refs/heads/main/app/translations.xlsx)
|
||||
2. Open in Excel/LibreOffice and add missing translations
|
||||
Notes:
|
||||
- Missing translations are marked in red
|
||||
- You can filter by key or any language column values
|
||||
- Non-Translatable strings are hidden and marked in gray, do not add translations for them
|
||||
- For plurals, some languages need/have more quantity strings than others, if a quantity string in the default language (english) is not needed the row is highlighted in yellow. If your language does not need that quantity string either, ignore them.
|
||||
3. Open a [Update Translations Issue](https://github.com/PhilKes/NotallyX/issues/new?assignees=&labels=translations&projects=&template=translation.md&title=%3CINSERT+LANGUAGE+HERE%3E+translations+update)
|
||||
4. I will create a Pull-Request to add your updated translations
|
||||
All translations are crowd sourced. To contribute, follow these [guidelines](https://m2.material.io/design/communication/writing.html) and email me or open a pull request.
|
||||
|
||||
See [Android Translations Converter](https://github.com/PhilKes/android-translations-converter-plugin) for more details
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="250"/><img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="250"/><img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="250"/>
|
||||
|
||||
### Contributing
|
||||
### Hall of fame
|
||||
* [Top 20 Android Apps 2021!](https://www.youtube.com/watch?v=bwz13aM0qJk)
|
||||
* [De-Googling Any Android Phone! (Google Apps Alternatives)](https://www.youtube.com/watch?v=RQUEgwgV99I)
|
||||
* [The BEST Private Notetaking Apps Explained](https://www.youtube.com/watch?v=BJw5tKPP1PY)
|
||||
* [Notally](https://www.noteapps.ca/notally/)
|
||||
* [The 9 Best Simple Note-Taking Apps for Android](https://www.makeuseof.com/simple-note-apps-android/)
|
||||
|
||||
If you would like to contribute code yourself, just grab any open issue (that has no other developer assigned yet), leave a comment that you want to work on it and start developing by forking this repo.
|
||||
### Copycats
|
||||
Clones of Notally keep popping up on the Play Store. They are not licensed under GPL3 and usually change a few colors, include ads, etc. Please [report them](https://support.google.com/googleplay/android-developer/contact/takedown) and [inform me](mailto:omgodseapps@gmail.com) if you find a new one.
|
||||
|
||||
The project is a default Android project written in Kotlin, I highly recommend using Android Studio for development. Also be sure to test your changes with an Android device/emulator that uses the same Android SDK Version as defined in the `build.gradle` `targetSdk`.
|
||||
|
||||
Before submitting your proposed changes as a Pull-Request, make sure all tests are still working (`./gradlew test`), and run `./gradlew ktfmtFormat` for common formatting (also executed automatically as pre-commit hook).
|
||||
|
||||
### Attribution
|
||||
The original Notally project was developed by [OmGodse](https://github.com/OmGodse) under the [GPL 3.0 License](https://github.com/OmGodse/Notally/blob/master/LICENSE.md).
|
||||
|
||||
In accordance to GPL 3.0, this project is licensed under the same [GPL 3.0 License](https://github.com/PhilKes/NotallyX/blob/master/LICENSE.md).
|
||||
* https://play.google.com/store/apps/details?id=com.sladjan.notes
|
||||
* https://play.google.com/store/apps/details?id=com.sladjan.notespro
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
|
@ -1,2 +1 @@
|
|||
/build
|
||||
/release
|
106
app/build.gradle
Normal file
106
app/build.gradle
Normal file
|
@ -0,0 +1,106 @@
|
|||
import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
|
||||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'com.ncorti.ktfmt.gradle' version '0.20.1'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
namespace 'com.omgodse.notally'
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.omgodse.notally'
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
versionCode 56
|
||||
versionName "6.0-RC1"
|
||||
resConfigs 'en', 'ca', 'cs', 'da', 'de', 'el', 'es', 'fr', 'hu', 'in', 'it', 'ja', 'my', 'nb', 'nl', 'nn', 'pl', 'pt-rBR', 'pt-rPT', 'ro', 'ru', 'sk', 'sv', 'tl', 'tr', 'uk', 'vi', 'zh-rCN'
|
||||
vectorDrawables.generatedDensities = []
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
release {
|
||||
crunchPngs false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions { jvmTarget = "1.8" }
|
||||
|
||||
buildFeatures { viewBinding true }
|
||||
|
||||
packagingOptions.resources {
|
||||
excludes += ["DebugProbesKt.bin", "META-INF/**.version", "kotlin/**.kotlin_builtins", "kotlin-tooling-metadata.json"]
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
|
||||
kotlinOptions.jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
ktfmt {
|
||||
kotlinLangStyle()
|
||||
}
|
||||
|
||||
tasks.register('ktfmtPrecommit', KtfmtFormatTask) {
|
||||
source = project.fileTree(rootDir)
|
||||
include("**/*.kt")
|
||||
}
|
||||
|
||||
tasks.register('installLocalGitHooks', Copy) {
|
||||
def scriptsDir = new File(rootProject.rootDir, '.scripts/')
|
||||
def hooksDir = new File(rootProject.rootDir, '.git/hooks')
|
||||
from(scriptsDir) {
|
||||
include 'pre-commit', 'pre-commit.bat'
|
||||
}
|
||||
into { hooksDir }
|
||||
inputs.files(file("${scriptsDir}/pre-commit"), file("${scriptsDir}/pre-commit.bat"))
|
||||
outputs.dir(hooksDir)
|
||||
fileMode 0775
|
||||
}
|
||||
//preBuild.dependsOn installLocalGitHooks // TODO: only format changed .kt files
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
final def navVersion = "2.3.5"
|
||||
final def roomVersion = "2.6.1"
|
||||
|
||||
ksp "androidx.room:room-compiler:$roomVersion"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
|
||||
implementation "androidx.work:work-runtime:2.9.0"
|
||||
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navVersion"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$navVersion"
|
||||
|
||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
|
||||
implementation 'com.github.zerobranch:SwipeLayout:1.3.1'
|
||||
|
||||
implementation "com.github.bumptech.glide:glide:4.15.1"
|
||||
implementation "com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0"
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "androidx.test:core:1.6.1"
|
||||
testImplementation "org.mockito:mockito-core:5.13.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:5.4.0"
|
||||
testImplementation "io.mockk:mockk:1.13.12"
|
||||
testImplementation "org.json:json:20180813"
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
|
||||
import org.apache.commons.configuration2.PropertiesConfiguration
|
||||
import org.apache.commons.configuration2.io.FileHandler
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.parcelize")
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.ncorti.ktfmt.gradle") version "0.20.1"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0"
|
||||
id("io.github.philkes.android-translations-converter") version "1.0.5"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.philkes.notallyx"
|
||||
compileSdk = 34
|
||||
ndkVersion = "29.0.13113456"
|
||||
defaultConfig {
|
||||
applicationId = "com.philkes.notallyx"
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = project.findProperty("app.versionCode").toString().toInt()
|
||||
versionName = project.findProperty("app.versionName").toString()
|
||||
resourceConfigurations += listOf(
|
||||
"en", "ca", "cs", "da", "de", "el", "es", "fr", "hu", "in", "it", "ja", "my", "nb", "nl", "nn", "pl", "pt-rBR", "pt-rPT", "ro", "ru", "sk", "sv", "tl", "tr", "uk", "vi", "zh-rCN", "zh-rTW"
|
||||
)
|
||||
vectorDrawables.generatedDensities?.clear()
|
||||
ndk {
|
||||
debugSymbolLevel= "FULL"
|
||||
}
|
||||
}
|
||||
ksp {
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(providers.gradleProperty("RELEASE_STORE_FILE").get())
|
||||
storePassword = providers.gradleProperty("RELEASE_STORE_PASSWORD").get()
|
||||
keyAlias = providers.gradleProperty("RELEASE_KEY_ALIAS").get()
|
||||
keyPassword = providers.gradleProperty("RELEASE_KEY_PASSWORD").get()
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-DEBUG"
|
||||
resValue("string", "app_name", "NotallyX DEBUG")
|
||||
}
|
||||
release {
|
||||
isCrunchPngs = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
create("beta"){
|
||||
initWith(getByName("release"))
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = "-BETA"
|
||||
resValue("string", "app_name", "NotallyX BETA")
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
this.outputs
|
||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
output.outputFileName = "NotallyX-$versionName.apk"
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes += listOf(
|
||||
"DebugProbesKt.bin",
|
||||
"META-INF/**.version",
|
||||
"kotlin/**.kotlin_builtins",
|
||||
"kotlin-tooling-metadata.json"
|
||||
)
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
ktfmt {
|
||||
kotlinLangStyle()
|
||||
}
|
||||
|
||||
tasks.register<KtfmtFormatTask>("ktfmtPrecommit") {
|
||||
source = project.fileTree(rootDir)
|
||||
include("**/*.kt")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("installLocalGitHooks") {
|
||||
val scriptsDir = File(rootProject.rootDir, ".scripts/")
|
||||
val hooksDir = File(rootProject.rootDir, ".git/hooks")
|
||||
from(scriptsDir) {
|
||||
include("pre-commit", "pre-commit.bat")
|
||||
}
|
||||
into(hooksDir)
|
||||
inputs.files(file("${scriptsDir}/pre-commit"), file("${scriptsDir}/pre-commit.bat"))
|
||||
outputs.dir(hooksDir)
|
||||
fileMode = 509 // 0775 octal in decimal
|
||||
// If this throws permission denied:
|
||||
// chmod +rwx ./.git/hooks/pre-commit*
|
||||
}
|
||||
|
||||
tasks.preBuild.dependsOn(tasks.named("installLocalGitHooks"), tasks.exportTranslationsToExcel)
|
||||
|
||||
tasks.register("generateChangelogs") {
|
||||
doLast {
|
||||
val githubToken = providers.gradleProperty("CHANGELOG_GITHUB_TOKEN").orNull
|
||||
|
||||
val command = mutableListOf(
|
||||
"bash",
|
||||
rootProject.file("generate-changelogs.sh").absolutePath,
|
||||
"v${project.findProperty("app.lastVersionName").toString()}",
|
||||
rootProject.file("CHANGELOG.md").absolutePath
|
||||
)
|
||||
if (!githubToken.isNullOrEmpty()) {
|
||||
command.add(githubToken)
|
||||
} else {
|
||||
println("CHANGELOG_GITHUB_TOKEN not found, which limits the allowed amount of Github API calls")
|
||||
}
|
||||
exec {
|
||||
commandLine(command)
|
||||
standardOutput = System.out
|
||||
errorOutput = System.err
|
||||
}
|
||||
|
||||
val config = PropertiesConfiguration()
|
||||
val fileHandler = FileHandler(config).apply {
|
||||
file = rootProject.file("gradle.properties")
|
||||
load()
|
||||
}
|
||||
val currentVersionName = config.getProperty("app.versionName")
|
||||
config.setProperty("app.lastVersionName", currentVersionName)
|
||||
fileHandler.save()
|
||||
println("Updated app.lastVersionName to $currentVersionName")
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("bundleRelease").configure {
|
||||
dependsOn(tasks.named("testReleaseUnitTest"))
|
||||
}
|
||||
tasks.named("assembleRelease").configure {
|
||||
dependsOn(tasks.named("testReleaseUnitTest"))
|
||||
finalizedBy(tasks.named("generateChangelogs"))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val navVersion = "2.3.5"
|
||||
val roomVersion = "2.6.1"
|
||||
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
ksp("androidx.room:room-compiler:$roomVersion")
|
||||
implementation("androidx.room:room-ktx:$roomVersion")
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.4.0")
|
||||
implementation("androidx.work:work-runtime:2.9.1")
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("cat.ereza:customactivityoncrash:2.4.0")
|
||||
implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0")
|
||||
implementation("com.github.bumptech.glide:glide:4.15.1")
|
||||
implementation("cn.Leaqi:SwipeDrawer:1.6")
|
||||
implementation("com.github.skydoves:colorpickerview:2.3.0")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||
implementation("me.zhanghai.android.fastscroll:library:1.3.0")
|
||||
implementation("net.lingala.zip4j:zip4j:2.11.5")
|
||||
implementation("net.zetetic:android-database-sqlcipher:4.5.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("org.jsoup:jsoup:1.18.1")
|
||||
implementation("org.ocpsoft.prettytime:prettytime:4.0.6.Final")
|
||||
implementation("org.simpleframework:simple-xml:2.7.1") {
|
||||
exclude(group = "xpp3", module = "xpp3")
|
||||
}
|
||||
|
||||
androidTestImplementation("androidx.room:room-testing:$roomVersion")
|
||||
androidTestImplementation("androidx.work:work-testing:2.9.1")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
testImplementation("androidx.test:core-ktx:1.6.1")
|
||||
testImplementation("androidx.test:core:1.6.1")
|
||||
testImplementation("io.mockk:mockk:1.13.12")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.24.2")
|
||||
testImplementation("org.json:json:20180813")
|
||||
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
|
||||
testImplementation("org.mockito:mockito-core:5.13.0")
|
||||
testImplementation("org.robolectric:robolectric:4.13")
|
||||
}
|
271494
app/obfuscation/mapping.txt
271494
app/obfuscation/mapping.txt
File diff suppressed because it is too large
Load diff
42
app/proguard-rules.pro
vendored
42
app/proguard-rules.pro
vendored
|
@ -1,6 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
@ -11,39 +11,13 @@
|
|||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
-keepattributes LineNumberTable,SourceFile
|
||||
-renamesourcefileattribute SourceFile
|
||||
-dontobfuscate
|
||||
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable
|
||||
-printmapping obfuscation/mapping.txt
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
-renamesourcefileattribute SourceFile
|
||||
-keep class ** extends androidx.navigation.Navigator
|
||||
-keep class ** implements org.ocpsoft.prettytime.TimeUnit
|
||||
|
||||
# SQLCipher
|
||||
-keep class net.sqlcipher.** { *; }
|
||||
-keep class net.sqlcipher.database.** { *; }
|
||||
|
||||
# SimpleXML
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation
|
||||
-keep interface org.simpleframework.xml.core.Label {
|
||||
public *;
|
||||
}
|
||||
-keep class * implements org.simpleframework.xml.core.Label {
|
||||
public *;
|
||||
}
|
||||
-keep interface org.simpleframework.xml.core.Parameter {
|
||||
public *;
|
||||
}
|
||||
-keep class * implements org.simpleframework.xml.core.Parameter {
|
||||
public *;
|
||||
}
|
||||
-keep interface org.simpleframework.xml.core.Extractor {
|
||||
public *;
|
||||
}
|
||||
-keep class * implements org.simpleframework.xml.core.Extractor {
|
||||
public *;
|
||||
}
|
||||
|
||||
-keep class * implements java.io.Serializable
|
|
@ -2,11 +2,11 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "a0ebadcc625f8b49bf549975d7288f10",
|
||||
"identityHash": "01fd1bdfcfa83b65d83ff1209b964329",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BaseNote",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL)",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -50,12 +50,6 @@
|
|||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "modifiedTimestamp",
|
||||
"columnName": "modifiedTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "labels",
|
||||
"columnName": "labels",
|
||||
|
@ -146,7 +140,7 @@
|
|||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a0ebadcc625f8b49bf549975d7288f10')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '01fd1bdfcfa83b65d83ff1209b964329')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 6,
|
||||
"identityHash": "3ac03ff6740f6a6bcb19de11c7b3d750",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BaseNote",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL, `reminders` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "folder",
|
||||
"columnName": "folder",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "modifiedTimestamp",
|
||||
"columnName": "modifiedTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "labels",
|
||||
"columnName": "labels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "body",
|
||||
"columnName": "body",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spans",
|
||||
"columnName": "spans",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "items",
|
||||
"columnName": "items",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "images",
|
||||
"columnName": "images",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "files",
|
||||
"columnName": "files",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "audios",
|
||||
"columnName": "audios",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reminders",
|
||||
"columnName": "reminders",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BaseNote_id_folder_pinned_timestamp_labels",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"folder",
|
||||
"pinned",
|
||||
"timestamp",
|
||||
"labels"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Label",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ac03ff6740f6a6bcb19de11c7b3d750')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "3ac03ff6740f6a6bcb19de11c7b3d750",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BaseNote",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL, `reminders` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "folder",
|
||||
"columnName": "folder",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "modifiedTimestamp",
|
||||
"columnName": "modifiedTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "labels",
|
||||
"columnName": "labels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "body",
|
||||
"columnName": "body",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spans",
|
||||
"columnName": "spans",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "items",
|
||||
"columnName": "items",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "images",
|
||||
"columnName": "images",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "files",
|
||||
"columnName": "files",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "audios",
|
||||
"columnName": "audios",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reminders",
|
||||
"columnName": "reminders",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BaseNote_id_folder_pinned_timestamp_labels",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"folder",
|
||||
"pinned",
|
||||
"timestamp",
|
||||
"labels"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Label",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ac03ff6740f6a6bcb19de11c7b3d750')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "3ac03ff6740f6a6bcb19de11c7b3d750",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BaseNote",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL, `reminders` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "folder",
|
||||
"columnName": "folder",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "modifiedTimestamp",
|
||||
"columnName": "modifiedTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "labels",
|
||||
"columnName": "labels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "body",
|
||||
"columnName": "body",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spans",
|
||||
"columnName": "spans",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "items",
|
||||
"columnName": "items",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "images",
|
||||
"columnName": "images",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "files",
|
||||
"columnName": "files",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "audios",
|
||||
"columnName": "audios",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reminders",
|
||||
"columnName": "reminders",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BaseNote_id_folder_pinned_timestamp_labels",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"folder",
|
||||
"pinned",
|
||||
"timestamp",
|
||||
"labels"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Label",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ac03ff6740f6a6bcb19de11c7b3d750')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "042b20b5b4cfc8415e6cf6348196e869",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BaseNote",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `folder` TEXT NOT NULL, `color` TEXT NOT NULL, `title` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `modifiedTimestamp` INTEGER NOT NULL, `labels` TEXT NOT NULL, `body` TEXT NOT NULL, `spans` TEXT NOT NULL, `items` TEXT NOT NULL, `images` TEXT NOT NULL, `files` TEXT NOT NULL, `audios` TEXT NOT NULL, `reminders` TEXT NOT NULL, `viewMode` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "folder",
|
||||
"columnName": "folder",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinned",
|
||||
"columnName": "pinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "modifiedTimestamp",
|
||||
"columnName": "modifiedTimestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "labels",
|
||||
"columnName": "labels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "body",
|
||||
"columnName": "body",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "spans",
|
||||
"columnName": "spans",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "items",
|
||||
"columnName": "items",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "images",
|
||||
"columnName": "images",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "files",
|
||||
"columnName": "files",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "audios",
|
||||
"columnName": "audios",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reminders",
|
||||
"columnName": "reminders",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewMode",
|
||||
"columnName": "viewMode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BaseNote_id_folder_pinned_timestamp_labels",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"folder",
|
||||
"pinned",
|
||||
"timestamp",
|
||||
"labels"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BaseNote_id_folder_pinned_timestamp_labels` ON `${TABLE_NAME}` (`id`, `folder`, `pinned`, `timestamp`, `labels`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "Label",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`value` TEXT NOT NULL, PRIMARY KEY(`value`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"value"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '042b20b5b4cfc8415e6cf6348196e869')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -6,27 +6,21 @@
|
|||
<uses-permission
|
||||
android:name="android.permission.ACCESS_NETWORK_STATE"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
android:name=".NotallyXApplication"
|
||||
android:name=".NotallyApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="productivity"
|
||||
android:dataExtractionRules="@xml/data_rules"
|
||||
android:fullBackupContent="@xml/backup_content"
|
||||
android:icon="@mipmap/notallyx"
|
||||
android:icon="@mipmap/notally"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales"
|
||||
android:roundIcon="@mipmap/notallyx_round"
|
||||
android:roundIcon="@mipmap/notally_round"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<provider
|
||||
|
@ -42,8 +36,9 @@
|
|||
</provider>
|
||||
|
||||
<activity
|
||||
android:name=".presentation.activity.main.MainActivity"
|
||||
android:exported="true">
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/MainActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -57,57 +52,34 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".presentation.activity.note.EditListActivity"
|
||||
android:name=".activities.MakeList"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".presentation.activity.note.EditNoteActivity"
|
||||
android:name=".activities.TakeNote"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="file" android:mimeType="text/*" />
|
||||
<data android:scheme="content" android:mimeType="text/*" />
|
||||
|
||||
<data android:scheme="file" android:mimeType="application/json" />
|
||||
<data android:scheme="content" android:mimeType="application/json" />
|
||||
|
||||
<data android:scheme="file" android:mimeType="application/xml" />
|
||||
<data android:scheme="content" android:mimeType="application/xml" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".presentation.activity.note.ViewImageActivity" />
|
||||
<activity android:name=".activities.ViewImage" />
|
||||
|
||||
<activity android:name=".presentation.activity.note.SelectLabelsActivity" />
|
||||
|
||||
<activity android:name=".presentation.activity.note.reminders.RemindersActivity" />
|
||||
<activity android:name=".activities.SelectLabels" />
|
||||
|
||||
<activity
|
||||
android:name=".presentation.activity.note.RecordAudioActivity"
|
||||
android:name=".activities.RecordAudio"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<activity android:name=".presentation.activity.note.PlayAudioActivity" />
|
||||
<activity android:name=".activities.PlayAudio" />
|
||||
|
||||
<activity
|
||||
android:name=".presentation.activity.ConfigureWidgetActivity"
|
||||
android:name=".activities.ConfigureWidget"
|
||||
android:exported="false">
|
||||
|
||||
<intent-filter>
|
||||
|
@ -116,23 +88,8 @@
|
|||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".presentation.activity.note.PickNoteActivity"
|
||||
android:exported="false">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".utils.ErrorActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/unknown_error"
|
||||
android:process=":error_activity">
|
||||
<intent-filter>
|
||||
<action android:name="cat.ereza.customactivityoncrash.ERROR" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".presentation.widget.WidgetProvider"
|
||||
android:name=".widget.WidgetProvider"
|
||||
android:exported="false"
|
||||
android:label="@string/single_note_or_list">
|
||||
|
||||
|
@ -146,27 +103,23 @@
|
|||
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".presentation.activity.note.reminders.ReminderReceiver" android:enabled="true" android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".presentation.widget.WidgetService"
|
||||
android:name=".widget.WidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
|
||||
<service
|
||||
android:name=".utils.audio.AudioRecordService"
|
||||
android:name=".AttachmentDeleteService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".audio.AudioRecordService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="microphone" />
|
||||
|
||||
<service
|
||||
android:name=".utils.audio.AudioPlayService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
android:name=".audio.AudioPlayService"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
package android.print
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.philkes.notallyx.utils.nameWithoutExtension
|
||||
|
||||
/**
|
||||
* Needs to be in android.print package to access the package private methods of
|
||||
* [PrintDocumentAdapter]
|
||||
*/
|
||||
fun Context.printPdf(file: DocumentFile, content: String, pdfPrintListener: PdfPrintListener) {
|
||||
val webView = WebView(this)
|
||||
webView.loadDataWithBaseURL(null, content, "text/html", "utf-8", null)
|
||||
webView.webViewClient =
|
||||
object : WebViewClient() {
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
val adapter = webView.createPrintDocumentAdapter(file.nameWithoutExtension!!)
|
||||
contentResolver.printPdf(file, adapter, pdfPrintListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ContentResolver.printPdf(
|
||||
file: DocumentFile,
|
||||
adapter: PrintDocumentAdapter,
|
||||
pdfPrintListener: PdfPrintListener,
|
||||
) {
|
||||
val onLayoutResult =
|
||||
object : PrintDocumentAdapter.LayoutResultCallback() {
|
||||
|
||||
override fun onLayoutFailed(error: CharSequence?) {
|
||||
pdfPrintListener.onFailure(error)
|
||||
}
|
||||
|
||||
override fun onLayoutFinished(info: PrintDocumentInfo?, changed: Boolean) {
|
||||
this@printPdf.writeToFile(file, adapter, pdfPrintListener)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.onLayout(null, createPrintAttributes(), null, onLayoutResult, null)
|
||||
}
|
||||
|
||||
private fun ContentResolver.writeToFile(
|
||||
file: DocumentFile,
|
||||
adapter: PrintDocumentAdapter,
|
||||
pdfPrintListener: PdfPrintListener,
|
||||
) {
|
||||
val onWriteResult =
|
||||
object : PrintDocumentAdapter.WriteResultCallback() {
|
||||
|
||||
override fun onWriteFailed(error: CharSequence?) {
|
||||
pdfPrintListener.onFailure(error)
|
||||
}
|
||||
|
||||
override fun onWriteFinished(pages: Array<out PageRange>?) {
|
||||
pdfPrintListener.onSuccess(file)
|
||||
}
|
||||
}
|
||||
|
||||
val pages = arrayOf(PageRange.ALL_PAGES)
|
||||
val fileDescriptor = openFileDescriptor(file.uri, "rw")
|
||||
adapter.onWrite(pages, fileDescriptor, null, onWriteResult)
|
||||
}
|
||||
|
||||
private fun createPrintAttributes(): PrintAttributes {
|
||||
return with(PrintAttributes.Builder()) {
|
||||
setMediaSize(PrintAttributes.MediaSize.ISO_A4)
|
||||
setMinMargins(PrintAttributes.Margins.NO_MARGINS)
|
||||
setResolution(PrintAttributes.Resolution("Standard", "Standard", 100, 100))
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
interface PdfPrintListener {
|
||||
|
||||
fun onSuccess(file: DocumentFile)
|
||||
|
||||
fun onFailure(message: CharSequence?)
|
||||
}
|
79
app/src/main/java/android/print/PostPDFGenerator.kt
Normal file
79
app/src/main/java/android/print/PostPDFGenerator.kt
Normal file
|
@ -0,0 +1,79 @@
|
|||
package android.print
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This class needs to be in android.print package to access the package private methods of
|
||||
* [PrintDocumentAdapter]
|
||||
*/
|
||||
object PostPDFGenerator {
|
||||
|
||||
fun create(file: File, content: String, context: Context, onResult: OnResult) {
|
||||
val webView = WebView(context)
|
||||
webView.loadDataWithBaseURL(null, content, "text/html", "utf-8", null)
|
||||
webView.webViewClient =
|
||||
object : WebViewClient() {
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
val adapter = webView.createPrintDocumentAdapter(file.nameWithoutExtension)
|
||||
print(file, adapter, onResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun print(file: File, adapter: PrintDocumentAdapter, onResult: OnResult) {
|
||||
val onLayoutResult =
|
||||
object : PrintDocumentAdapter.LayoutResultCallback() {
|
||||
|
||||
override fun onLayoutFailed(error: CharSequence?) {
|
||||
onResult.onFailure(error)
|
||||
}
|
||||
|
||||
override fun onLayoutFinished(info: PrintDocumentInfo?, changed: Boolean) {
|
||||
writeToFile(file, adapter, onResult)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.onLayout(null, getPrintAttributes(), null, onLayoutResult, null)
|
||||
}
|
||||
|
||||
private fun writeToFile(file: File, adapter: PrintDocumentAdapter, onResult: OnResult) {
|
||||
val onWriteResult =
|
||||
object : PrintDocumentAdapter.WriteResultCallback() {
|
||||
|
||||
override fun onWriteFailed(error: CharSequence?) {
|
||||
onResult.onFailure(error)
|
||||
}
|
||||
|
||||
override fun onWriteFinished(pages: Array<out PageRange>?) {
|
||||
onResult.onSuccess(file)
|
||||
}
|
||||
}
|
||||
|
||||
val pages = arrayOf(PageRange.ALL_PAGES)
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
|
||||
adapter.onWrite(pages, fileDescriptor, null, onWriteResult)
|
||||
}
|
||||
|
||||
private fun getPrintAttributes(): PrintAttributes {
|
||||
val builder = PrintAttributes.Builder()
|
||||
builder.setMediaSize(PrintAttributes.MediaSize.ISO_A4)
|
||||
builder.setMinMargins(PrintAttributes.Margins.NO_MARGINS)
|
||||
builder.setResolution(PrintAttributes.Resolution("Standard", "Standard", 100, 100))
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
interface OnResult {
|
||||
|
||||
fun onSuccess(file: File)
|
||||
|
||||
fun onFailure(message: CharSequence?)
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package androidx.recyclerview.widget
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import kotlin.math.abs
|
||||
|
||||
class NestedScrollViewItemTouchHelper(
|
||||
callback: Callback,
|
||||
private val scrollView: NestedScrollView,
|
||||
) : ItemTouchHelper(callback) {
|
||||
private var selectedStartY: Int = -1
|
||||
private var selectedStartScrollY: Float = -1f
|
||||
private var selectedView: View? = null
|
||||
private var dragScrollStartTimeInMs: Long = 0
|
||||
|
||||
private var lastmDy = 0f
|
||||
private var lastScrollY = 0
|
||||
private var tmpRect: Rect? = null
|
||||
|
||||
override fun select(selected: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.select(selected, actionState)
|
||||
if (selected != null) {
|
||||
selectedView = selected.itemView
|
||||
selectedStartY = selected.itemView.top
|
||||
selectedStartScrollY = scrollView!!.scrollY.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls [scrollView] when an item in [mRecyclerView] is dragged to the top or bottom of the
|
||||
* [scrollView].
|
||||
*
|
||||
* Inspired by
|
||||
* [https://stackoverflow.com/a/70699988/9748566](https://stackoverflow.com/a/70699988/9748566)
|
||||
*/
|
||||
override fun scrollIfNecessary(): Boolean {
|
||||
if (mSelected == null) {
|
||||
dragScrollStartTimeInMs = Long.MIN_VALUE
|
||||
return false
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
val scrollDuration =
|
||||
if (dragScrollStartTimeInMs == Long.MIN_VALUE) 0 else now - dragScrollStartTimeInMs
|
||||
val lm = mRecyclerView.layoutManager
|
||||
if (tmpRect == null) {
|
||||
tmpRect = Rect()
|
||||
}
|
||||
var scrollY = 0
|
||||
val currentScrollY = scrollView.scrollY
|
||||
|
||||
// We need to use the height of NestedScrollView, not RecyclerView's!
|
||||
val actualShowingHeight =
|
||||
scrollView.height - mRecyclerView.top - mRecyclerView.paddingBottom
|
||||
|
||||
lm!!.calculateItemDecorationsForChild(mSelected.itemView, tmpRect!!)
|
||||
if (lm.canScrollVertically()) {
|
||||
// Keep scrolling if the user didnt change the drag direction
|
||||
if (lastScrollY != 0 && abs(lastmDy) >= abs(mDy)) {
|
||||
scrollY = lastScrollY
|
||||
} else {
|
||||
// The true current Y of the item in NestedScrollView, not in RecyclerView!
|
||||
val curY = (selectedStartY + mDy - currentScrollY).toInt()
|
||||
// The true mDy should plus the initial scrollY and minus current scrollY of
|
||||
// NestedScrollView
|
||||
val checkDy = (mDy + selectedStartScrollY - currentScrollY).toInt()
|
||||
val topDiff = curY - tmpRect!!.top - mRecyclerView.paddingTop
|
||||
if (checkDy < 0 && topDiff < 0) { // User is draging the item out of the top edge.
|
||||
scrollY = topDiff
|
||||
} else if (checkDy > 0) { // User is draging the item out of the bottom edge.
|
||||
val bottomDiff = (curY + mSelected.itemView.height - actualShowingHeight) + 10
|
||||
if (bottomDiff >= 0) {
|
||||
scrollY = bottomDiff
|
||||
}
|
||||
} else {
|
||||
scrollY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
lastScrollY = scrollY
|
||||
lastmDy = mDy
|
||||
if (scrollY != 0) {
|
||||
scrollY =
|
||||
mCallback.interpolateOutOfBoundsScroll(
|
||||
mRecyclerView,
|
||||
mSelected.itemView.height,
|
||||
scrollY,
|
||||
actualShowingHeight,
|
||||
scrollDuration,
|
||||
)
|
||||
}
|
||||
if (scrollY != 0) {
|
||||
val maxScrollY = scrollView.childrenHeightsSum - scrollView.height
|
||||
// Check if we can scroll further before applying the scroll
|
||||
if (
|
||||
(scrollY < 0 && scrollView.scrollY > 0) ||
|
||||
(scrollY > 0 && scrollView.scrollY < maxScrollY)
|
||||
) {
|
||||
if (dragScrollStartTimeInMs == Long.MIN_VALUE) {
|
||||
dragScrollStartTimeInMs = now
|
||||
}
|
||||
scrollView.scrollBy(0, scrollY)
|
||||
// Update the dragged item position as well
|
||||
selectedView?.translationY = selectedView!!.translationY + scrollY
|
||||
return true
|
||||
}
|
||||
}
|
||||
dragScrollStartTimeInMs = Long.MIN_VALUE
|
||||
lastScrollY = 0
|
||||
lastmDy = 0f
|
||||
return false
|
||||
}
|
||||
|
||||
private val ViewGroup.childrenHeightsSum
|
||||
get() = children.map { it.measuredHeight }.sum()
|
||||
}
|
|
@ -1,18 +1,17 @@
|
|||
package com.philkes.notallyx.utils
|
||||
package com.omgodse.notally
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.philkes.notallyx.data.model.BaseNote
|
||||
import com.philkes.notallyx.presentation.view.misc.NotNullLiveData
|
||||
import com.omgodse.notally.image.Event
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
import com.omgodse.notally.preferences.BetterLiveData
|
||||
|
||||
class ActionMode {
|
||||
|
||||
val enabled = NotNullLiveData(false)
|
||||
val loading = NotNullLiveData(false)
|
||||
val count = NotNullLiveData(0)
|
||||
val enabled = BetterLiveData(false)
|
||||
val count = BetterLiveData(0)
|
||||
val selectedNotes = HashMap<Long, BaseNote>()
|
||||
val selectedIds = selectedNotes.keys
|
||||
val closeListener = MutableLiveData<Event<Set<Long>>>()
|
||||
var addListener: (() -> Unit)? = null
|
||||
|
||||
private fun refresh() {
|
||||
count.value = selectedNotes.size
|
||||
|
@ -24,12 +23,6 @@ class ActionMode {
|
|||
refresh()
|
||||
}
|
||||
|
||||
fun add(baseNotes: Collection<BaseNote>) {
|
||||
baseNotes.forEach { selectedNotes[it.id] = it }
|
||||
refresh()
|
||||
addListener?.invoke()
|
||||
}
|
||||
|
||||
fun remove(id: Long) {
|
||||
selectedNotes.remove(id)
|
||||
refresh()
|
||||
|
@ -44,17 +37,8 @@ class ActionMode {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSelected(availableItemIds: List<Long>?) {
|
||||
selectedNotes.keys
|
||||
.filter { availableItemIds?.contains(it) == false }
|
||||
.forEach { selectedNotes.remove(it) }
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun isEnabled() = enabled.value
|
||||
|
||||
// We assume selectedNotes.size is 1
|
||||
fun getFirstNote() = selectedNotes.values.first()
|
||||
|
||||
fun isEmpty() = selectedNotes.values.isEmpty()
|
||||
}
|
122
app/src/main/java/com/omgodse/notally/AttachmentDeleteService.kt
Normal file
122
app/src/main/java/com/omgodse/notally/AttachmentDeleteService.kt
Normal file
|
@ -0,0 +1,122 @@
|
|||
package com.omgodse.notally
|
||||
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
import com.omgodse.notally.miscellaneous.isImage
|
||||
import com.omgodse.notally.model.Attachment
|
||||
import com.omgodse.notally.model.Audio
|
||||
import com.omgodse.notally.model.FileAttachment
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AttachmentDeleteService : Service() {
|
||||
|
||||
private val scope = MainScope()
|
||||
private val channel = Channel<ArrayList<Attachment>>()
|
||||
|
||||
override fun onCreate() {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val builder = Notification.Builder(application)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = "com.omgodse.fileUpdates"
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
"Backups and Files",
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
manager.createNotificationChannel(channel)
|
||||
builder.setChannelId(channelId)
|
||||
}
|
||||
|
||||
builder.setContentTitle(getString(R.string.deleting_images))
|
||||
builder.setSmallIcon(R.drawable.notification_delete)
|
||||
builder.setProgress(0, 0, true)
|
||||
builder.setOnlyAlertOnce(true)
|
||||
|
||||
/*
|
||||
Prevent user from dismissing notification in Android 13 (33) and above
|
||||
https://developer.android.com/guide/components/foreground-services#user-dismiss-notification
|
||||
*/
|
||||
builder.setOngoing(true)
|
||||
|
||||
/*
|
||||
On Android 12 (31) and above, the system waits 10 seconds before showing the notification.
|
||||
https://developer.android.com/guide/components/foreground-services#notification-immediate
|
||||
*/
|
||||
startForeground(1, builder.build())
|
||||
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val imageRoot = IO.getExternalImagesDirectory(application)
|
||||
val audioRoot = IO.getExternalAudioDirectory(application)
|
||||
val fileRoot = IO.getExternalFilesDirectory(application)
|
||||
do {
|
||||
val attachments = channel.receive()
|
||||
attachments.forEachIndexed { index, attachment ->
|
||||
val file =
|
||||
when (attachment) {
|
||||
is Audio ->
|
||||
if (audioRoot != null) File(audioRoot, attachment.name)
|
||||
else null
|
||||
is FileAttachment -> {
|
||||
val root = if (attachment.isImage) imageRoot else fileRoot
|
||||
if (root != null) File(root, attachment.localName) else null
|
||||
}
|
||||
}
|
||||
if (file != null && file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
builder.setContentText(
|
||||
getString(R.string.count, index + 1, attachments.size)
|
||||
)
|
||||
builder.setProgress(attachments.size, index + 1, false)
|
||||
manager.notify(1, builder.build())
|
||||
}
|
||||
} while (!channel.isEmpty)
|
||||
channel.close()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
scope.launch {
|
||||
val list =
|
||||
requireNotNull(intent).getParcelableArrayListExtra<Attachment>(EXTRA_ATTACHMENTS)
|
||||
withContext(Dispatchers.IO) { channel.send(requireNotNull(list)) }
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_ATTACHMENTS = "com.omgodse.notally.EXTRA_ATTACHMENTS"
|
||||
|
||||
fun start(app: Application, list: ArrayList<out Attachment>) {
|
||||
val intent = Intent(app, AttachmentDeleteService::class.java)
|
||||
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, list)
|
||||
ContextCompat.startForegroundService(app, intent)
|
||||
}
|
||||
}
|
||||
}
|
8
app/src/main/java/com/omgodse/notally/BackupProgress.kt
Normal file
8
app/src/main/java/com/omgodse/notally/BackupProgress.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package com.omgodse.notally
|
||||
|
||||
class BackupProgress(
|
||||
val inProgress: Boolean,
|
||||
val current: Int,
|
||||
val total: Int,
|
||||
val indeterminate: Boolean,
|
||||
)
|
8
app/src/main/java/com/omgodse/notally/Cache.kt
Normal file
8
app/src/main/java/com/omgodse/notally/Cache.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package com.omgodse.notally
|
||||
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
|
||||
object Cache {
|
||||
|
||||
var list: List<BaseNote> = ArrayList()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.utils
|
||||
package com.omgodse.notally
|
||||
|
||||
import android.graphics.RectF
|
||||
import android.text.Selection
|
|
@ -1,11 +1,11 @@
|
|||
package com.philkes.notallyx.presentation.view.misc
|
||||
package com.omgodse.notally
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.philkes.notallyx.databinding.MenuItemBinding
|
||||
import com.omgodse.notally.databinding.MenuItemBinding
|
||||
|
||||
class MenuDialog(context: Context) : BottomSheetDialog(context) {
|
||||
|
37
app/src/main/java/com/omgodse/notally/NotallyApplication.kt
Normal file
37
app/src/main/java/com/omgodse/notally/NotallyApplication.kt
Normal file
|
@ -0,0 +1,37 @@
|
|||
package com.omgodse.notally
|
||||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.omgodse.notally.backup.AutoBackupWorker
|
||||
import com.omgodse.notally.preferences.Preferences
|
||||
import com.omgodse.notally.preferences.Theme
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NotallyApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val preferences = Preferences.getInstance(this)
|
||||
preferences.theme.observeForever { theme ->
|
||||
when (theme) {
|
||||
Theme.dark ->
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
Theme.light ->
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
Theme.followSystem ->
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val request =
|
||||
PeriodicWorkRequest.Builder(AutoBackupWorker::class.java, 12, TimeUnit.HOURS).build()
|
||||
WorkManager.getInstance(this)
|
||||
.enqueueUniquePeriodicWork("Auto Backup", ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
}
|
21
app/src/main/java/com/omgodse/notally/OverflowEditText.kt
Normal file
21
app/src/main/java/com/omgodse/notally/OverflowEditText.kt
Normal file
|
@ -0,0 +1,21 @@
|
|||
package com.omgodse.notally
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
|
||||
/**
|
||||
* Implementation that fixes a bug in Lollipop where clicking on the overflow icon in the custom
|
||||
* text selection mode causes it to end. For more information, see this ->
|
||||
* https://issuetracker.google.com/issues/36937508
|
||||
*/
|
||||
class OverflowEditText(context: Context, attrs: AttributeSet) : AppCompatEditText(context, attrs) {
|
||||
|
||||
var isActionModeOn = false
|
||||
|
||||
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
||||
if (!isActionModeOn) {
|
||||
super.onWindowFocusChanged(hasWindowFocus)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package com.omgodse.notally.activities
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.ActivityConfigureWidgetBinding
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
import com.omgodse.notally.model.Header
|
||||
import com.omgodse.notally.model.NotallyDatabase
|
||||
import com.omgodse.notally.preferences.Preferences
|
||||
import com.omgodse.notally.preferences.View
|
||||
import com.omgodse.notally.recyclerview.ItemListener
|
||||
import com.omgodse.notally.recyclerview.adapter.BaseNoteAdapter
|
||||
import com.omgodse.notally.viewmodels.BaseNoteModel
|
||||
import com.omgodse.notally.widget.WidgetProvider
|
||||
import java.util.Collections
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ConfigureWidget : AppCompatActivity(), ItemListener {
|
||||
|
||||
private lateinit var adapter: BaseNoteAdapter
|
||||
private val id by lazy {
|
||||
intent.getIntExtra(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityConfigureWidgetBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val result = Intent()
|
||||
result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
|
||||
setResult(RESULT_CANCELED, result)
|
||||
|
||||
val preferences = Preferences.getInstance(application)
|
||||
|
||||
val maxItems = preferences.maxItems
|
||||
val maxLines = preferences.maxLines
|
||||
val maxTitle = preferences.maxTitle
|
||||
val textSize = preferences.textSize.value
|
||||
val dateFormat = preferences.dateFormat.value
|
||||
val imagesRoot = IO.getExternalImagesDirectory(application)
|
||||
val filesRoot = IO.getExternalFilesDirectory(application)
|
||||
|
||||
adapter =
|
||||
BaseNoteAdapter(
|
||||
Collections.emptySet(),
|
||||
dateFormat,
|
||||
textSize,
|
||||
maxItems,
|
||||
maxLines,
|
||||
maxTitle,
|
||||
imagesRoot,
|
||||
filesRoot,
|
||||
this,
|
||||
)
|
||||
|
||||
binding.RecyclerView.adapter = adapter
|
||||
binding.RecyclerView.setHasFixedSize(true)
|
||||
|
||||
binding.RecyclerView.layoutManager =
|
||||
if (preferences.view.value == View.grid) {
|
||||
StaggeredGridLayoutManager(2, RecyclerView.VERTICAL)
|
||||
} else LinearLayoutManager(this)
|
||||
|
||||
val database = NotallyDatabase.getDatabase(application)
|
||||
|
||||
val pinned = Header(getString(R.string.pinned))
|
||||
val others = Header(getString(R.string.others))
|
||||
|
||||
lifecycleScope.launch {
|
||||
val notes =
|
||||
withContext(Dispatchers.IO) {
|
||||
val raw = database.getBaseNoteDao().getAllNotes()
|
||||
BaseNoteModel.transform(raw, pinned, others)
|
||||
}
|
||||
adapter.submitList(notes)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(position: Int) {
|
||||
if (position != -1) {
|
||||
val preferences = Preferences.getInstance(application)
|
||||
val noteId = (adapter.currentList[position] as BaseNote).id
|
||||
preferences.updateWidget(id, noteId)
|
||||
|
||||
val manager = AppWidgetManager.getInstance(this)
|
||||
WidgetProvider.updateWidget(this, manager, id, noteId)
|
||||
|
||||
val success = Intent()
|
||||
success.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
|
||||
setResult(RESULT_OK, success)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(position: Int) {}
|
||||
}
|
449
app/src/main/java/com/omgodse/notally/activities/MainActivity.kt
Normal file
449
app/src/main/java/com/omgodse/notally/activities/MainActivity.kt
Normal file
|
@ -0,0 +1,449 @@
|
|||
package com.omgodse.notally.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.print.PostPDFGenerator
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.navOptions
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.transition.platform.MaterialFade
|
||||
import com.omgodse.notally.MenuDialog
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.ActivityMainBinding
|
||||
import com.omgodse.notally.databinding.DialogColorBinding
|
||||
import com.omgodse.notally.miscellaneous.Operations
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.miscellaneous.applySpans
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
import com.omgodse.notally.model.Color
|
||||
import com.omgodse.notally.model.Folder
|
||||
import com.omgodse.notally.model.Type
|
||||
import com.omgodse.notally.recyclerview.ItemListener
|
||||
import com.omgodse.notally.recyclerview.adapter.ColorAdapter
|
||||
import com.omgodse.notally.viewmodels.BaseNoteModel
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var configuration: AppBarConfiguration
|
||||
|
||||
private val model: BaseNoteModel by viewModels()
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (model.actionMode.enabled.value) {
|
||||
model.actionMode.close(true)
|
||||
} else super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp(configuration)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.Toolbar)
|
||||
setupFAB()
|
||||
setupMenu()
|
||||
setupActionMode()
|
||||
setupNavigation()
|
||||
setupSearch()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_EXPORT_FILE && resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.let { uri -> model.writeCurrentFileToUri(uri) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFAB() {
|
||||
binding.TakeNote.setOnClickListener {
|
||||
val intent = Intent(this, TakeNote::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
binding.MakeList.setOnClickListener {
|
||||
val intent = Intent(this, MakeList::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMenu() {
|
||||
val menu = binding.NavigationView.menu
|
||||
menu.add(0, R.id.Notes, 0, R.string.notes).setCheckable(true).setIcon(R.drawable.home)
|
||||
menu.add(1, R.id.Labels, 0, R.string.labels).setCheckable(true).setIcon(R.drawable.label)
|
||||
menu.add(2, R.id.Deleted, 0, R.string.deleted).setCheckable(true).setIcon(R.drawable.delete)
|
||||
menu
|
||||
.add(2, R.id.Archived, 0, R.string.archived)
|
||||
.setCheckable(true)
|
||||
.setIcon(R.drawable.archive)
|
||||
menu
|
||||
.add(3, R.id.Settings, 0, R.string.settings)
|
||||
.setCheckable(true)
|
||||
.setIcon(R.drawable.settings)
|
||||
}
|
||||
|
||||
private fun setupActionMode() {
|
||||
binding.ActionMode.setNavigationOnClickListener { model.actionMode.close(true) }
|
||||
|
||||
val transition = MaterialFade()
|
||||
transition.secondaryAnimatorProvider = null
|
||||
|
||||
transition.excludeTarget(binding.NavHostFragment, true)
|
||||
transition.excludeChildren(binding.NavHostFragment, true)
|
||||
transition.excludeTarget(binding.TakeNote, true)
|
||||
transition.excludeTarget(binding.MakeList, true)
|
||||
transition.excludeTarget(binding.NavigationView, true)
|
||||
|
||||
model.actionMode.enabled.observe(this) { enabled ->
|
||||
TransitionManager.beginDelayedTransition(binding.RelativeLayout, transition)
|
||||
if (enabled) {
|
||||
binding.Toolbar.visibility = View.GONE
|
||||
binding.ActionMode.visibility = View.VISIBLE
|
||||
binding.DrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
} else {
|
||||
binding.Toolbar.visibility = View.VISIBLE
|
||||
binding.ActionMode.visibility = View.GONE
|
||||
binding.DrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNDEFINED)
|
||||
}
|
||||
}
|
||||
|
||||
val menu = binding.ActionMode.menu
|
||||
val pinned = menu.add(R.string.pin, R.drawable.pin) {}
|
||||
val share = menu.add(R.string.share, R.drawable.share) { share() }
|
||||
val labels = menu.add(R.string.labels, R.drawable.label) { label() }
|
||||
|
||||
val export = createExportMenu(menu)
|
||||
|
||||
val changeColor = menu.add(R.string.change_color, R.drawable.change_color) { changeColor() }
|
||||
val delete =
|
||||
menu.add(R.string.delete, R.drawable.delete) { model.moveBaseNotes(Folder.DELETED) }
|
||||
val archive =
|
||||
menu.add(R.string.archive, R.drawable.archive) { model.moveBaseNotes(Folder.ARCHIVED) }
|
||||
val restore =
|
||||
menu.add(R.string.restore, R.drawable.restore) { model.moveBaseNotes(Folder.NOTES) }
|
||||
val unarchive =
|
||||
menu.add(R.string.unarchive, R.drawable.unarchive) { model.moveBaseNotes(Folder.NOTES) }
|
||||
val deleteForever = menu.add(R.string.delete_forever, R.drawable.delete) { deleteForever() }
|
||||
|
||||
model.actionMode.count.observe(this) { count ->
|
||||
if (count == 0) {
|
||||
menu.forEach { item -> item.setVisible(false) }
|
||||
} else {
|
||||
binding.ActionMode.title = count.toString()
|
||||
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
if (count == 1) {
|
||||
if (baseNote.pinned) {
|
||||
pinned.setTitle(R.string.unpin)
|
||||
pinned.setIcon(R.drawable.unpin)
|
||||
} else {
|
||||
pinned.setTitle(R.string.pin)
|
||||
pinned.setIcon(R.drawable.pin)
|
||||
}
|
||||
pinned.onClick { model.pinBaseNote(!baseNote.pinned) }
|
||||
}
|
||||
|
||||
pinned.setVisible(count == 1)
|
||||
share.setVisible(count == 1)
|
||||
labels.setVisible(count == 1)
|
||||
export.setVisible(count == 1)
|
||||
changeColor.setVisible(true)
|
||||
|
||||
val folder = baseNote.folder
|
||||
delete.setVisible(folder == Folder.NOTES || folder == Folder.ARCHIVED)
|
||||
archive.setVisible(folder == Folder.NOTES)
|
||||
restore.setVisible(folder == Folder.DELETED)
|
||||
unarchive.setVisible(folder == Folder.ARCHIVED)
|
||||
deleteForever.setVisible(folder == Folder.DELETED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createExportMenu(menu: Menu): MenuItem {
|
||||
val export = menu.addSubMenu(R.string.export)
|
||||
export.setIcon(R.drawable.export)
|
||||
export.item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
||||
|
||||
export.add("PDF").onClick { exportToPDF() }
|
||||
export.add("TXT").onClick { exportToTXT() }
|
||||
export.add("JSON").onClick { exportToJSON() }
|
||||
export.add("HTML").onClick { exportToHTML() }
|
||||
|
||||
return export.item
|
||||
}
|
||||
|
||||
fun MenuItem.onClick(function: () -> Unit) {
|
||||
setOnMenuItemClickListener {
|
||||
function()
|
||||
return@setOnMenuItemClickListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun share() {
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
val body =
|
||||
when (baseNote.type) {
|
||||
Type.NOTE -> baseNote.body.applySpans(baseNote.spans)
|
||||
Type.LIST -> Operations.getBody(baseNote.items)
|
||||
}
|
||||
Operations.shareNote(this, baseNote.title, body)
|
||||
}
|
||||
|
||||
private fun changeColor() {
|
||||
val dialog = MaterialAlertDialogBuilder(this).setTitle(R.string.change_color).create()
|
||||
|
||||
val colorAdapter =
|
||||
ColorAdapter(
|
||||
object : ItemListener {
|
||||
override fun onClick(position: Int) {
|
||||
dialog.dismiss()
|
||||
val color = Color.entries[position]
|
||||
model.colorBaseNote(color)
|
||||
}
|
||||
|
||||
override fun onLongClick(position: Int) {}
|
||||
}
|
||||
)
|
||||
|
||||
val dialogBinding = DialogColorBinding.inflate(layoutInflater)
|
||||
dialogBinding.RecyclerView.adapter = colorAdapter
|
||||
|
||||
dialog.setView(dialogBinding.root)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun deleteForever() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.delete_selected_notes)
|
||||
.setPositiveButton(R.string.delete) { _, _ -> model.deleteBaseNotes() }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun label() {
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
lifecycleScope.launch {
|
||||
val labels = model.getAllLabels()
|
||||
if (labels.isNotEmpty()) {
|
||||
displaySelectLabelsDialog(labels, baseNote)
|
||||
} else {
|
||||
model.actionMode.close(true)
|
||||
navigateWithAnimation(R.id.Labels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displaySelectLabelsDialog(labels: Array<String>, baseNote: BaseNote) {
|
||||
val checkedPositions =
|
||||
BooleanArray(labels.size) { index -> baseNote.labels.contains(labels[index]) }
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.labels)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setMultiChoiceItems(labels, checkedPositions) { _, which, isChecked ->
|
||||
checkedPositions[which] = isChecked
|
||||
}
|
||||
.setPositiveButton(R.string.save) { _, _ ->
|
||||
val new = ArrayList<String>()
|
||||
checkedPositions.forEachIndexed { index, checked ->
|
||||
if (checked) {
|
||||
val label = labels[index]
|
||||
new.add(label)
|
||||
}
|
||||
}
|
||||
model.updateBaseNoteLabels(new, baseNote.id)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun exportToPDF() {
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
model.getPDFFile(
|
||||
baseNote,
|
||||
object : PostPDFGenerator.OnResult {
|
||||
|
||||
override fun onSuccess(file: File) {
|
||||
showFileOptionsDialog(file, "application/pdf")
|
||||
}
|
||||
|
||||
override fun onFailure(message: CharSequence?) {
|
||||
Toast.makeText(
|
||||
this@MainActivity,
|
||||
R.string.something_went_wrong,
|
||||
Toast.LENGTH_SHORT,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun exportToTXT() {
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
lifecycleScope.launch {
|
||||
val file = model.getTXTFile(baseNote)
|
||||
showFileOptionsDialog(file, "text/plain")
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportToJSON() {
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
lifecycleScope.launch {
|
||||
val file = model.getJSONFile(baseNote)
|
||||
showFileOptionsDialog(file, "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportToHTML() {
|
||||
val baseNote = model.actionMode.getFirstNote()
|
||||
lifecycleScope.launch {
|
||||
val file = model.getHTMLFile(baseNote)
|
||||
showFileOptionsDialog(file, "text/html")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFileOptionsDialog(file: File, mimeType: String) {
|
||||
val uri = FileProvider.getUriForFile(this, "${packageName}.provider", file)
|
||||
|
||||
MenuDialog(this)
|
||||
.add(R.string.share) { shareFile(uri, mimeType) }
|
||||
.add(R.string.view_file) { viewFile(uri, mimeType) }
|
||||
.add(R.string.save_to_device) { saveFileToDevice(file, mimeType) }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun viewFile(uri: Uri, mimeType: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(uri, mimeType)
|
||||
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val chooser = Intent.createChooser(intent, getString(R.string.view_note))
|
||||
startActivity(chooser)
|
||||
}
|
||||
|
||||
private fun shareFile(uri: Uri, mimeType: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = mimeType
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
|
||||
val chooser = Intent.createChooser(intent, null)
|
||||
startActivity(chooser)
|
||||
}
|
||||
|
||||
private fun saveFileToDevice(file: File, mimeType: String) {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.type = mimeType
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.putExtra(Intent.EXTRA_TITLE, file.nameWithoutExtension)
|
||||
|
||||
model.currentFile = file
|
||||
startActivityForResult(intent, REQUEST_EXPORT_FILE)
|
||||
}
|
||||
|
||||
private fun setupNavigation() {
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.NavHostFragment) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
configuration = AppBarConfiguration(binding.NavigationView.menu, binding.DrawerLayout)
|
||||
setupActionBarWithNavController(navController, configuration)
|
||||
|
||||
var fragmentIdToLoad: Int? = null
|
||||
binding.NavigationView.setNavigationItemSelectedListener { item ->
|
||||
fragmentIdToLoad = item.itemId
|
||||
binding.DrawerLayout.closeDrawer(GravityCompat.START)
|
||||
return@setNavigationItemSelectedListener true
|
||||
}
|
||||
|
||||
binding.DrawerLayout.addDrawerListener(
|
||||
object : DrawerLayout.SimpleDrawerListener() {
|
||||
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
if (
|
||||
fragmentIdToLoad != null &&
|
||||
navController.currentDestination?.id != fragmentIdToLoad
|
||||
) {
|
||||
navigateWithAnimation(requireNotNull(fragmentIdToLoad))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
fragmentIdToLoad = destination.id
|
||||
binding.NavigationView.setCheckedItem(destination.id)
|
||||
handleDestinationChange(destination)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDestinationChange(destination: NavDestination) {
|
||||
if (destination.id == R.id.Notes) {
|
||||
binding.TakeNote.show()
|
||||
binding.MakeList.show()
|
||||
} else {
|
||||
binding.TakeNote.hide()
|
||||
binding.MakeList.hide()
|
||||
}
|
||||
|
||||
val inputManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
if (destination.id == R.id.Search) {
|
||||
binding.EnterSearchKeyword.visibility = View.VISIBLE
|
||||
binding.EnterSearchKeyword.requestFocus()
|
||||
inputManager.showSoftInput(binding.EnterSearchKeyword, InputMethodManager.SHOW_IMPLICIT)
|
||||
} else {
|
||||
binding.EnterSearchKeyword.visibility = View.GONE
|
||||
inputManager.hideSoftInputFromWindow(binding.EnterSearchKeyword.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateWithAnimation(id: Int) {
|
||||
val options = navOptions {
|
||||
launchSingleTop = true
|
||||
anim {
|
||||
exit = androidx.navigation.ui.R.anim.nav_default_exit_anim
|
||||
enter = androidx.navigation.ui.R.anim.nav_default_enter_anim
|
||||
popExit = androidx.navigation.ui.R.anim.nav_default_pop_exit_anim
|
||||
popEnter = androidx.navigation.ui.R.anim.nav_default_pop_enter_anim
|
||||
}
|
||||
popUpTo(navController.graph.startDestination) { inclusive = false }
|
||||
}
|
||||
navController.navigate(id, null, options)
|
||||
}
|
||||
|
||||
private fun setupSearch() {
|
||||
binding.EnterSearchKeyword.setText(model.keyword)
|
||||
binding.EnterSearchKeyword.doAfterTextChanged { text ->
|
||||
model.keyword = requireNotNull(text).trim().toString()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_EXPORT_FILE = 10
|
||||
}
|
||||
}
|
118
app/src/main/java/com/omgodse/notally/activities/MakeList.kt
Normal file
118
app/src/main/java/com/omgodse/notally/activities/MakeList.kt
Normal file
|
@ -0,0 +1,118 @@
|
|||
package com.omgodse.notally.activities
|
||||
|
||||
import android.os.Build
|
||||
import android.view.MenuItem
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.changehistory.ChangeHistory
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.miscellaneous.setOnNextAction
|
||||
import com.omgodse.notally.model.Type
|
||||
import com.omgodse.notally.preferences.ListItemSorting
|
||||
import com.omgodse.notally.preferences.Preferences
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
import com.omgodse.notally.recyclerview.adapter.MakeListAdapter
|
||||
import com.omgodse.notally.sorting.ListItemNoSortCallback
|
||||
import com.omgodse.notally.sorting.ListItemSortedByCheckedCallback
|
||||
import com.omgodse.notally.sorting.ListItemSortedList
|
||||
import com.omgodse.notally.sorting.toMutableList
|
||||
import com.omgodse.notally.widget.WidgetProvider
|
||||
|
||||
class MakeList : NotallyActivity(Type.LIST) {
|
||||
|
||||
private lateinit var adapter: MakeListAdapter
|
||||
private lateinit var items: ListItemSortedList
|
||||
|
||||
private lateinit var listManager: ListManager
|
||||
|
||||
override suspend fun saveNote() {
|
||||
model.saveNote(items.toMutableList())
|
||||
WidgetProvider.sendBroadcast(application, longArrayOf(model.id))
|
||||
}
|
||||
|
||||
override fun setupToolbar() {
|
||||
super.setupToolbar()
|
||||
binding.Toolbar.menu.add(
|
||||
1,
|
||||
R.string.delete_checked_items,
|
||||
R.drawable.delete_all,
|
||||
MenuItem.SHOW_AS_ACTION_IF_ROOM,
|
||||
) {
|
||||
listManager.deleteCheckedItems()
|
||||
}
|
||||
binding.Toolbar.menu.add(
|
||||
1,
|
||||
R.string.check_all_items,
|
||||
R.drawable.checkbox_fill,
|
||||
MenuItem.SHOW_AS_ACTION_IF_ROOM,
|
||||
) {
|
||||
listManager.changeCheckedForAll(true)
|
||||
}
|
||||
binding.Toolbar.menu.add(
|
||||
1,
|
||||
R.string.uncheck_all_items,
|
||||
R.drawable.checkbox,
|
||||
MenuItem.SHOW_AS_ACTION_IF_ROOM,
|
||||
) {
|
||||
listManager.changeCheckedForAll(false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
binding.Toolbar.menu.setGroupDividerEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initActionManager(undo: MenuItem, redo: MenuItem) {
|
||||
changeHistory = ChangeHistory {
|
||||
undo.isEnabled = changeHistory.canUndo()
|
||||
redo.isEnabled = changeHistory.canRedo()
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureUI() {
|
||||
binding.EnterTitle.setOnNextAction { listManager.moveFocusToNext(-1) }
|
||||
|
||||
if (model.isNewNote) {
|
||||
if (model.items.isEmpty()) {
|
||||
listManager.add(pushChange = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupListeners() {
|
||||
super.setupListeners()
|
||||
binding.AddItem.setOnClickListener { listManager.add() }
|
||||
}
|
||||
|
||||
override fun setStateFromModel() {
|
||||
super.setStateFromModel()
|
||||
val elevation = resources.displayMetrics.density * 2
|
||||
listManager =
|
||||
ListManager(
|
||||
binding.RecyclerView,
|
||||
changeHistory,
|
||||
preferences,
|
||||
getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager,
|
||||
)
|
||||
adapter =
|
||||
MakeListAdapter(
|
||||
model.textSize,
|
||||
elevation,
|
||||
Preferences.getInstance(application),
|
||||
listManager,
|
||||
)
|
||||
val sortCallback =
|
||||
when (preferences.listItemSorting.value) {
|
||||
ListItemSorting.autoSortByChecked -> ListItemSortedByCheckedCallback(adapter)
|
||||
else -> ListItemNoSortCallback(adapter)
|
||||
}
|
||||
items = ListItemSortedList(sortCallback)
|
||||
if (sortCallback is ListItemSortedByCheckedCallback) {
|
||||
sortCallback.setList(items)
|
||||
}
|
||||
items.init(model.items)
|
||||
adapter.setList(items)
|
||||
binding.RecyclerView.adapter = adapter
|
||||
listManager.adapter = adapter
|
||||
listManager.initList(items)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,607 @@
|
|||
package com.omgodse.notally.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.Editable
|
||||
import android.util.TypedValue
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.changehistory.ChangeHistory
|
||||
import com.omgodse.notally.databinding.ActivityNotallyBinding
|
||||
import com.omgodse.notally.databinding.DialogProgressBinding
|
||||
import com.omgodse.notally.image.FileError
|
||||
import com.omgodse.notally.miscellaneous.Constants
|
||||
import com.omgodse.notally.miscellaneous.Operations
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.miscellaneous.displayFormattedTimestamp
|
||||
import com.omgodse.notally.model.Audio
|
||||
import com.omgodse.notally.model.FileAttachment
|
||||
import com.omgodse.notally.model.Folder
|
||||
import com.omgodse.notally.model.Type
|
||||
import com.omgodse.notally.preferences.Preferences
|
||||
import com.omgodse.notally.preferences.TextSize
|
||||
import com.omgodse.notally.recyclerview.adapter.AudioAdapter
|
||||
import com.omgodse.notally.recyclerview.adapter.ErrorAdapter
|
||||
import com.omgodse.notally.recyclerview.adapter.PreviewFileAdapter
|
||||
import com.omgodse.notally.recyclerview.adapter.PreviewImageAdapter
|
||||
import com.omgodse.notally.viewmodels.NotallyModel
|
||||
import com.omgodse.notally.widget.WidgetProvider
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class NotallyActivity(private val type: Type) : AppCompatActivity() {
|
||||
|
||||
internal lateinit var binding: ActivityNotallyBinding
|
||||
|
||||
internal val model: NotallyModel by viewModels()
|
||||
internal lateinit var preferences: Preferences
|
||||
internal lateinit var changeHistory: ChangeHistory
|
||||
|
||||
override fun finish() {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
saveNote()
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putLong("id", model.id)
|
||||
lifecycleScope.launch { saveNote() }
|
||||
}
|
||||
|
||||
open suspend fun saveNote() {
|
||||
model.saveNote()
|
||||
WidgetProvider.sendBroadcast(application, longArrayOf(model.id))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
preferences = Preferences.getInstance(application)
|
||||
model.type = type
|
||||
initialiseBinding()
|
||||
setContentView(binding.root)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val persistedId = savedInstanceState?.getLong("id")
|
||||
val selectedId = intent.getLongExtra(Constants.SelectedBaseNote, 0L)
|
||||
val id = persistedId ?: selectedId
|
||||
model.setState(id)
|
||||
|
||||
if (model.isNewNote && intent.action == Intent.ACTION_SEND) {
|
||||
handleSharedNote()
|
||||
}
|
||||
|
||||
setupToolbar()
|
||||
setupListeners()
|
||||
setStateFromModel()
|
||||
|
||||
configureUI()
|
||||
binding.ScrollView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
when (requestCode) {
|
||||
REQUEST_ADD_IMAGES -> {
|
||||
val uri = data?.data
|
||||
val clipData = data?.clipData
|
||||
if (uri != null) {
|
||||
val uris = arrayOf(uri)
|
||||
model.addImages(uris)
|
||||
} else if (clipData != null) {
|
||||
val uris =
|
||||
Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
|
||||
model.addImages(uris)
|
||||
}
|
||||
}
|
||||
REQUEST_VIEW_IMAGES -> {
|
||||
val list =
|
||||
data?.getParcelableArrayListExtra<FileAttachment>(ViewImage.DELETED_IMAGES)
|
||||
if (!list.isNullOrEmpty()) {
|
||||
model.deleteImages(list)
|
||||
}
|
||||
}
|
||||
REQUEST_SELECT_LABELS -> {
|
||||
val list = data?.getStringArrayListExtra(SelectLabels.SELECTED_LABELS)
|
||||
if (list != null && list != model.labels) {
|
||||
model.setLabels(list)
|
||||
Operations.bindLabels(binding.LabelGroup, model.labels, model.textSize)
|
||||
}
|
||||
}
|
||||
REQUEST_RECORD_AUDIO -> model.addAudio()
|
||||
REQUEST_PLAY_AUDIO -> {
|
||||
val audio = data?.getParcelableExtra<Audio>(PlayAudio.AUDIO)
|
||||
if (audio != null) {
|
||||
model.deleteAudio(audio)
|
||||
}
|
||||
}
|
||||
REQUEST_ATTACH_FILES -> {
|
||||
val uri = data?.data
|
||||
val clipData = data?.clipData
|
||||
if (uri != null) {
|
||||
val uris = arrayOf(uri)
|
||||
model.addFiles(uris)
|
||||
} else if (clipData != null) {
|
||||
val uris =
|
||||
Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
|
||||
model.addFiles(uris)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
when (requestCode) {
|
||||
REQUEST_NOTIFICATION_PERMISSION -> selectImages()
|
||||
REQUEST_AUDIO_PERMISSION -> {
|
||||
if (
|
||||
grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
recordAudio()
|
||||
} else handleRejection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun initActionManager(undo: MenuItem, redo: MenuItem) {
|
||||
changeHistory = ChangeHistory {
|
||||
undo.isEnabled = changeHistory.canUndo()
|
||||
redo.isEnabled = changeHistory.canRedo()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun setupToolbar() {
|
||||
binding.Toolbar.setNavigationOnClickListener { finish() }
|
||||
|
||||
val menu = binding.Toolbar.menu
|
||||
val pin =
|
||||
menu.add(R.string.pin, R.drawable.pin, MenuItem.SHOW_AS_ACTION_ALWAYS) { item ->
|
||||
pin(item)
|
||||
}
|
||||
bindPinned(pin)
|
||||
|
||||
val undo =
|
||||
menu.add(R.string.undo, R.drawable.undo, MenuItem.SHOW_AS_ACTION_ALWAYS) {
|
||||
changeHistory.undo()
|
||||
}
|
||||
val redo =
|
||||
menu.add(R.string.redo, R.drawable.redo, MenuItem.SHOW_AS_ACTION_ALWAYS) {
|
||||
changeHistory.redo()
|
||||
}
|
||||
|
||||
initActionManager(undo, redo)
|
||||
|
||||
undo.isEnabled = changeHistory.canUndo()
|
||||
redo.isEnabled = changeHistory.canRedo()
|
||||
|
||||
menu.add(R.string.share, R.drawable.share) { share() }
|
||||
menu.add(R.string.labels, R.drawable.label) { label() }
|
||||
menu.add(R.string.add_images, R.drawable.add_images) {
|
||||
checkNotificationPermission { selectImages() }
|
||||
}
|
||||
menu.add(R.string.attach_file, R.drawable.text_file) {
|
||||
checkNotificationPermission { selectFiles() }
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
menu.add(R.string.record_audio, R.drawable.record_audio) { checkAudioPermission() }
|
||||
}
|
||||
|
||||
when (model.folder) {
|
||||
Folder.NOTES -> {
|
||||
menu.add(R.string.delete, R.drawable.delete) { delete() }
|
||||
menu.add(R.string.archive, R.drawable.archive) { archive() }
|
||||
}
|
||||
Folder.DELETED -> {
|
||||
menu.add(R.string.restore, R.drawable.restore) { restore() }
|
||||
menu.add(R.string.delete_forever, R.drawable.delete) { deleteForever() }
|
||||
}
|
||||
Folder.ARCHIVED -> {
|
||||
menu.add(R.string.delete, R.drawable.delete) { delete() }
|
||||
menu.add(R.string.unarchive, R.drawable.unarchive) { restore() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun configureUI()
|
||||
|
||||
open fun setupListeners() {
|
||||
binding.EnterTitle.doAfterTextChanged { text ->
|
||||
model.title = requireNotNull(text).trim().toString()
|
||||
}
|
||||
}
|
||||
|
||||
open fun setStateFromModel() {
|
||||
binding.DateCreated.displayFormattedTimestamp(model.timestamp, preferences.dateFormat.value)
|
||||
|
||||
binding.EnterTitle.setText(model.title)
|
||||
Operations.bindLabels(binding.LabelGroup, model.labels, model.textSize)
|
||||
|
||||
setColor()
|
||||
}
|
||||
|
||||
private fun handleSharedNote() {
|
||||
val title = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
|
||||
val string = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
val charSequence = intent.getCharSequenceExtra(Operations.extraCharSequence)
|
||||
val body = charSequence ?: string
|
||||
|
||||
if (body != null) {
|
||||
model.body = Editable.Factory.getInstance().newEditable(body)
|
||||
}
|
||||
if (title != null) {
|
||||
model.title = title
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(24)
|
||||
private fun checkAudioPermission() {
|
||||
val permission = Manifest.permission.RECORD_AUDIO
|
||||
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (shouldShowRequestPermissionRationale(permission)) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.please_grant_notally_audio)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.continue_) { _, _ ->
|
||||
requestPermissions(arrayOf(permission), REQUEST_AUDIO_PERMISSION)
|
||||
}
|
||||
.show()
|
||||
} else requestPermissions(arrayOf(permission), REQUEST_AUDIO_PERMISSION)
|
||||
} else recordAudio()
|
||||
}
|
||||
|
||||
private fun checkNotificationPermission(onSuccess: () -> Unit) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (shouldShowRequestPermissionRationale(permission)) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.please_grant_notally_notification)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> onSuccess() }
|
||||
.setPositiveButton(R.string.continue_) { _, _ ->
|
||||
requestPermissions(arrayOf(permission), REQUEST_NOTIFICATION_PERMISSION)
|
||||
}
|
||||
.setOnDismissListener { onSuccess() }
|
||||
.show()
|
||||
} else requestPermissions(arrayOf(permission), REQUEST_NOTIFICATION_PERMISSION)
|
||||
} else onSuccess()
|
||||
} else onSuccess()
|
||||
}
|
||||
|
||||
private fun recordAudio() {
|
||||
if (model.audioRoot != null) {
|
||||
val intent = Intent(this, RecordAudio::class.java)
|
||||
startActivityForResult(intent, REQUEST_RECORD_AUDIO)
|
||||
} else Toast.makeText(this, R.string.insert_an_sd_card_audio, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun handleRejection() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.to_record_audio)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.settings) { _, _ ->
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.parse("package:${packageName}")
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun selectImages() {
|
||||
if (model.imageRoot != null) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "image/*"
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
startActivityForResult(intent, REQUEST_ADD_IMAGES)
|
||||
} else Toast.makeText(this, R.string.insert_an_sd_card_images, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun selectFiles() {
|
||||
if (model.filesRoot != null) {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "*/*"
|
||||
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
startActivityForResult(intent, REQUEST_ATTACH_FILES)
|
||||
} else Toast.makeText(this, R.string.insert_an_sd_card_files, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun share() {
|
||||
val body =
|
||||
when (type) {
|
||||
Type.NOTE -> model.body
|
||||
Type.LIST -> Operations.getBody(model.items.toMutableList())
|
||||
}
|
||||
Operations.shareNote(this, model.title, body)
|
||||
}
|
||||
|
||||
private fun label() {
|
||||
val intent = Intent(this, SelectLabels::class.java)
|
||||
intent.putStringArrayListExtra(SelectLabels.SELECTED_LABELS, model.labels)
|
||||
startActivityForResult(intent, REQUEST_SELECT_LABELS)
|
||||
}
|
||||
|
||||
private fun delete() {
|
||||
model.folder = Folder.DELETED
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun restore() {
|
||||
model.folder = Folder.NOTES
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun archive() {
|
||||
model.folder = Folder.ARCHIVED
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun deleteForever() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.delete_note_forever)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
model.deleteBaseNote()
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun pin(item: MenuItem) {
|
||||
model.pinned = !model.pinned
|
||||
bindPinned(item)
|
||||
}
|
||||
|
||||
private fun setupImages() {
|
||||
val adapter =
|
||||
PreviewImageAdapter(model.imageRoot) { position ->
|
||||
val intent = Intent(this, ViewImage::class.java)
|
||||
intent.putExtra(ViewImage.POSITION, position)
|
||||
intent.putExtra(Constants.SelectedBaseNote, model.id)
|
||||
startActivityForResult(intent, REQUEST_VIEW_IMAGES)
|
||||
}
|
||||
|
||||
adapter.registerAdapterDataObserver(
|
||||
object : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
binding.ImagePreview.scrollToPosition(positionStart)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.ImagePreview.setHasFixedSize(true)
|
||||
binding.ImagePreview.adapter = adapter
|
||||
binding.ImagePreview.layoutManager =
|
||||
LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
|
||||
PagerSnapHelper().attachToRecyclerView(binding.ImagePreview)
|
||||
|
||||
model.images.observe(this) { list ->
|
||||
adapter.submitList(list)
|
||||
binding.ImagePreview.isVisible = list.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFiles() {
|
||||
val adapter =
|
||||
PreviewFileAdapter({ fileAttachment ->
|
||||
if (model.filesRoot == null) {
|
||||
return@PreviewFileAdapter
|
||||
}
|
||||
val intent =
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
val file = File(model.filesRoot, fileAttachment.localName)
|
||||
val uri =
|
||||
FileProvider.getUriForFile(
|
||||
this@NotallyActivity,
|
||||
"${packageName}.provider",
|
||||
file,
|
||||
)
|
||||
setDataAndType(uri, fileAttachment.mimeType)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
startActivity(intent)
|
||||
}) { fileAttachment ->
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.delete_file, fileAttachment.originalName))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
model.deleteFiles(arrayListOf(fileAttachment))
|
||||
}
|
||||
.show()
|
||||
return@PreviewFileAdapter true
|
||||
}
|
||||
|
||||
binding.FilesPreview.setHasFixedSize(true)
|
||||
binding.FilesPreview.adapter = adapter
|
||||
|
||||
binding.FilesPreview.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
model.files.observe(this) { list ->
|
||||
adapter.submitList(list)
|
||||
val visible = list.isNotEmpty()
|
||||
binding.FilesPreview.isVisible = visible
|
||||
if (visible) {
|
||||
binding.FilesPreview.post {
|
||||
binding.FilesPreview.scrollToPosition(adapter.itemCount)
|
||||
binding.FilesPreview.requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayFileErrors(errors: List<FileError>) {
|
||||
val recyclerView = RecyclerView(this)
|
||||
recyclerView.layoutParams =
|
||||
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
recyclerView.adapter = ErrorAdapter(errors)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
recyclerView.scrollIndicators =
|
||||
View.SCROLL_INDICATOR_TOP or View.SCROLL_INDICATOR_BOTTOM
|
||||
}
|
||||
val message =
|
||||
if (errors.isNotEmpty() && errors[0].fileType == NotallyModel.FileType.IMAGE) {
|
||||
R.plurals.cant_add_images
|
||||
} else {
|
||||
R.plurals.cant_add_files
|
||||
}
|
||||
val title = resources.getQuantityString(message, errors.size, errors.size)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setView(recyclerView)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setupAudios() {
|
||||
val adapter = AudioAdapter { position: Int ->
|
||||
if (position != -1) {
|
||||
val audio = model.audios.value[position]
|
||||
val intent = Intent(this, PlayAudio::class.java)
|
||||
intent.putExtra(PlayAudio.AUDIO, audio)
|
||||
startActivityForResult(intent, REQUEST_PLAY_AUDIO)
|
||||
}
|
||||
}
|
||||
binding.AudioRecyclerView.adapter = adapter
|
||||
|
||||
model.audios.observe(this) { list ->
|
||||
adapter.submitList(list)
|
||||
binding.AudioHeader.isVisible = list.isNotEmpty()
|
||||
binding.AudioRecyclerView.isVisible = list.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setColor() {
|
||||
val color = Operations.extractColor(model.color, this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
window.statusBarColor = color
|
||||
}
|
||||
binding.root.setBackgroundColor(color)
|
||||
binding.RecyclerView.setBackgroundColor(color)
|
||||
binding.Toolbar.backgroundTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
private fun initialiseBinding() {
|
||||
binding = ActivityNotallyBinding.inflate(layoutInflater)
|
||||
when (type) {
|
||||
Type.NOTE -> {
|
||||
binding.AddItem.visibility = View.GONE
|
||||
binding.RecyclerView.visibility = View.GONE
|
||||
}
|
||||
Type.LIST -> {
|
||||
binding.EnterBody.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val title = TextSize.getEditTitleSize(model.textSize)
|
||||
val date = TextSize.getDisplayBodySize(model.textSize)
|
||||
val body = TextSize.getEditBodySize(model.textSize)
|
||||
|
||||
binding.EnterTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, title)
|
||||
binding.DateCreated.setTextSize(TypedValue.COMPLEX_UNIT_SP, date)
|
||||
binding.EnterBody.setTextSize(TypedValue.COMPLEX_UNIT_SP, body)
|
||||
|
||||
setupImages()
|
||||
setupFiles()
|
||||
setupAudios()
|
||||
setupProgressDialog()
|
||||
|
||||
binding.root.isSaveFromParentEnabled = false
|
||||
}
|
||||
|
||||
private fun setupProgressDialog() {
|
||||
val dialogBinding = DialogProgressBinding.inflate(layoutInflater)
|
||||
val dialog =
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(dialogBinding.root)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
model.addingFiles.observe(this) { progress ->
|
||||
if (progress.inProgress) {
|
||||
dialog.setTitle(
|
||||
if (progress.fileType == NotallyModel.FileType.IMAGE) {
|
||||
R.string.adding_images
|
||||
} else {
|
||||
R.string.adding_files
|
||||
}
|
||||
)
|
||||
dialog.show()
|
||||
dialogBinding.ProgressBar.max = progress.total
|
||||
dialogBinding.ProgressBar.setProgressCompat(progress.current, true)
|
||||
dialogBinding.Count.text =
|
||||
getString(R.string.count, progress.current, progress.total)
|
||||
} else dialog.dismiss()
|
||||
}
|
||||
|
||||
model.eventBus.observe(this) { event ->
|
||||
event.handle { errors -> displayFileErrors(errors) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPinned(item: MenuItem) {
|
||||
val icon: Int
|
||||
val title: Int
|
||||
if (model.pinned) {
|
||||
icon = R.drawable.unpin
|
||||
title = R.string.unpin
|
||||
} else {
|
||||
icon = R.drawable.pin
|
||||
title = R.string.pin
|
||||
}
|
||||
item.setTitle(title)
|
||||
item.setIcon(icon)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_ADD_IMAGES = 30
|
||||
private const val REQUEST_VIEW_IMAGES = 31
|
||||
private const val REQUEST_NOTIFICATION_PERMISSION = 32
|
||||
private const val REQUEST_SELECT_LABELS = 33
|
||||
private const val REQUEST_RECORD_AUDIO = 34
|
||||
private const val REQUEST_PLAY_AUDIO = 35
|
||||
private const val REQUEST_AUDIO_PERMISSION = 36
|
||||
private const val REQUEST_ATTACH_FILES = 37
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.presentation.activity.note
|
||||
package com.omgodse.notally.activities
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
|
@ -7,22 +7,17 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.databinding.ActivityPlayAudioBinding
|
||||
import com.philkes.notallyx.presentation.activity.LockedActivity
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
import com.philkes.notallyx.utils.audio.AudioPlayService
|
||||
import com.philkes.notallyx.utils.audio.LocalBinder
|
||||
import com.philkes.notallyx.utils.getExternalAudioDirectory
|
||||
import com.philkes.notallyx.utils.getUriForFile
|
||||
import com.philkes.notallyx.utils.wrapWithChooser
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.audio.AudioPlayService
|
||||
import com.omgodse.notally.audio.LocalBinder
|
||||
import com.omgodse.notally.databinding.ActivityPlayAudioBinding
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.model.Audio
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
@ -31,23 +26,20 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
||||
class PlayAudio : AppCompatActivity() {
|
||||
|
||||
private var service: AudioPlayService? = null
|
||||
private lateinit var connection: ServiceConnection
|
||||
private lateinit var exportFileActivityResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private lateinit var audio: Audio
|
||||
private lateinit var binding: ActivityPlayAudioBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPlayAudioBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
audio =
|
||||
requireNotNull(
|
||||
intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_AUDIO, Audio::class.java) }
|
||||
)
|
||||
audio = requireNotNull(intent.getParcelableExtra(AUDIO))
|
||||
binding.AudioControlView.setDuration(audio.duration)
|
||||
|
||||
val intent = Intent(this, AudioPlayService::class.java)
|
||||
|
@ -60,7 +52,7 @@ class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
|||
val service = (binder as LocalBinder<AudioPlayService>).getService()
|
||||
service.initialise(audio)
|
||||
service.onStateChange = { updateUI(service) }
|
||||
this@PlayAudioActivity.service = service
|
||||
this@PlayAudio.service = service
|
||||
updateUI(service)
|
||||
}
|
||||
|
||||
|
@ -71,20 +63,9 @@ class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
|||
|
||||
binding.Play.setOnClickListener { service?.play() }
|
||||
|
||||
audio.duration?.let {
|
||||
binding.AudioControlView.onSeekComplete = { milliseconds ->
|
||||
service?.seek(milliseconds)
|
||||
}
|
||||
}
|
||||
binding.AudioControlView.onSeekComplete = { milliseconds -> service?.seek(milliseconds) }
|
||||
|
||||
setupToolbar(binding)
|
||||
|
||||
exportFileActivityResultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri -> writeAudioToUri(uri) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -100,39 +81,43 @@ class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_EXPORT_FILE && resultCode == RESULT_OK) {
|
||||
data?.data?.let { uri -> writeAudioToUri(uri) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar(binding: ActivityPlayAudioBinding) {
|
||||
binding.Toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||
|
||||
binding.Toolbar.menu.apply {
|
||||
add(R.string.share, R.drawable.share) { share() }
|
||||
add(R.string.save_to_device, R.drawable.save) { saveToDevice() }
|
||||
add(R.string.delete, R.drawable.delete) { delete() }
|
||||
}
|
||||
binding.Toolbar.menu.add(R.string.share, R.drawable.share) { share() }
|
||||
binding.Toolbar.menu.add(R.string.save_to_device, R.drawable.save) { saveToDevice() }
|
||||
binding.Toolbar.menu.add(R.string.delete, R.drawable.delete) { delete() }
|
||||
}
|
||||
|
||||
private fun share() {
|
||||
val audioRoot = application.getExternalAudioDirectory()
|
||||
val audioRoot = IO.getExternalAudioDirectory(application)
|
||||
val file = if (audioRoot != null) File(audioRoot, audio.name) else null
|
||||
if (file != null && file.exists()) {
|
||||
val uri = getUriForFile(file)
|
||||
val intent =
|
||||
Intent(Intent.ACTION_SEND)
|
||||
.apply {
|
||||
type = "audio/mp4"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
}
|
||||
.wrapWithChooser(this@PlayAudioActivity)
|
||||
startActivity(intent)
|
||||
val uri = FileProvider.getUriForFile(this, "$packageName.provider", file)
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "audio/mp4"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
|
||||
val chooser = Intent.createChooser(intent, null)
|
||||
startActivity(chooser)
|
||||
}
|
||||
}
|
||||
|
||||
private fun delete() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.delete_audio_recording_forever)
|
||||
.setCancelButton()
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
val intent = Intent()
|
||||
intent.putExtra(EXTRA_AUDIO, audio)
|
||||
intent.putExtra(AUDIO, audio)
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
|
@ -140,29 +125,25 @@ class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
|||
}
|
||||
|
||||
private fun saveToDevice() {
|
||||
val audioRoot = application.getExternalAudioDirectory()
|
||||
val audioRoot = IO.getExternalAudioDirectory(application)
|
||||
val file = if (audioRoot != null) File(audioRoot, audio.name) else null
|
||||
if (file != null && file.exists()) {
|
||||
val intent =
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.apply {
|
||||
type = "audio/mp4"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
.wrapWithChooser(this)
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.type = "audio/mp4"
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
val formatter = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.SHORT)
|
||||
val title = formatter.format(audio.timestamp)
|
||||
|
||||
intent.putExtra(Intent.EXTRA_TITLE, title)
|
||||
exportFileActivityResultLauncher.launch(intent)
|
||||
startActivityForResult(intent, REQUEST_EXPORT_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeAudioToUri(uri: Uri) {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val audioRoot = application.getExternalAudioDirectory()
|
||||
val audioRoot = IO.getExternalAudioDirectory(application)
|
||||
val file = if (audioRoot != null) File(audioRoot, audio.name) else null
|
||||
if (file != null && file.exists()) {
|
||||
val output = contentResolver.openOutputStream(uri) as FileOutputStream
|
||||
|
@ -173,8 +154,7 @@ class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
|||
output.close()
|
||||
}
|
||||
}
|
||||
Toast.makeText(this@PlayAudioActivity, R.string.saved_to_device, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
Toast.makeText(this@PlayAudio, R.string.saved_to_device, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,6 +183,7 @@ class PlayAudioActivity : LockedActivity<ActivityPlayAudioBinding>() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_AUDIO = "notallyx.intent.extra.AUDIO"
|
||||
const val AUDIO = "AUDIO"
|
||||
private const val REQUEST_EXPORT_FILE = 50
|
||||
}
|
||||
}
|
|
@ -1,33 +1,29 @@
|
|||
package com.philkes.notallyx.presentation.activity.note
|
||||
package com.omgodse.notally.activities
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.databinding.ActivityRecordAudioBinding
|
||||
import com.philkes.notallyx.presentation.activity.LockedActivity
|
||||
import com.philkes.notallyx.utils.audio.AudioRecordService
|
||||
import com.philkes.notallyx.utils.audio.LocalBinder
|
||||
import com.philkes.notallyx.utils.audio.Status
|
||||
import com.philkes.notallyx.utils.getTempAudioFile
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.audio.AudioRecordService
|
||||
import com.omgodse.notally.audio.LocalBinder
|
||||
import com.omgodse.notally.audio.Status
|
||||
import com.omgodse.notally.databinding.ActivityRecordAudioBinding
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
|
||||
@RequiresApi(24)
|
||||
class RecordAudioActivity : LockedActivity<ActivityRecordAudioBinding>() {
|
||||
class RecordAudio : AppCompatActivity() {
|
||||
|
||||
private var service: AudioRecordService? = null
|
||||
private lateinit var connection: ServiceConnection
|
||||
private lateinit var serviceStatusObserver: Observer<Status>
|
||||
private lateinit var cancelRecordCallback: OnBackPressedCallback
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityRecordAudioBinding.inflate(layoutInflater)
|
||||
val binding = ActivityRecordAudioBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val intent = Intent(this, AudioRecordService::class.java)
|
||||
|
@ -35,9 +31,10 @@ class RecordAudioActivity : LockedActivity<ActivityRecordAudioBinding>() {
|
|||
|
||||
connection =
|
||||
object : ServiceConnection {
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
service = (binder as LocalBinder<AudioRecordService>).getService()
|
||||
service?.status?.observe(this@RecordAudioActivity, serviceStatusObserver)
|
||||
updateUI(binding, requireNotNull(service))
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||
|
@ -48,11 +45,12 @@ class RecordAudioActivity : LockedActivity<ActivityRecordAudioBinding>() {
|
|||
binding.Main.setOnClickListener {
|
||||
val service = this.service
|
||||
if (service != null) {
|
||||
when (service.status.value) {
|
||||
when (service.status) {
|
||||
Status.PAUSED -> service.resume()
|
||||
Status.READY -> service.start()
|
||||
Status.RECORDING -> service.pause()
|
||||
}
|
||||
updateUI(binding, service)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,30 +61,13 @@ class RecordAudioActivity : LockedActivity<ActivityRecordAudioBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
binding.Toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
|
||||
|
||||
cancelRecordCallback =
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
MaterialAlertDialogBuilder(this@RecordAudioActivity)
|
||||
.setMessage(R.string.save_recording)
|
||||
.setPositiveButton(R.string.save) { _, _ -> stopRecording(service!!) }
|
||||
.setNegativeButton(R.string.discard) { _, _ -> discard(service!!) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(cancelRecordCallback)
|
||||
serviceStatusObserver = Observer { status ->
|
||||
updateUI(binding, service!!)
|
||||
cancelRecordCallback.isEnabled = status != Status.READY
|
||||
}
|
||||
binding.Toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
service?.let {
|
||||
if (service != null) {
|
||||
unbindService(connection)
|
||||
it.status.removeObserver(serviceStatusObserver)
|
||||
service = null
|
||||
}
|
||||
if (isFinishing) {
|
||||
|
@ -95,9 +76,22 @@ class RecordAudioActivity : LockedActivity<ActivityRecordAudioBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
val service = this.service
|
||||
if (service != null) {
|
||||
if (service.status != Status.READY) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.save_recording)
|
||||
.setPositiveButton(R.string.save) { _, _ -> stopRecording(service) }
|
||||
.setNegativeButton(R.string.discard) { _, _ -> discard(service) }
|
||||
.show()
|
||||
} else super.onBackPressed()
|
||||
} else super.onBackPressed()
|
||||
}
|
||||
|
||||
private fun discard(service: AudioRecordService) {
|
||||
service.stop()
|
||||
getTempAudioFile().delete()
|
||||
IO.getTempAudioFile(this).delete()
|
||||
finish()
|
||||
}
|
||||
|
||||
|
@ -109,7 +103,7 @@ class RecordAudioActivity : LockedActivity<ActivityRecordAudioBinding>() {
|
|||
|
||||
private fun updateUI(binding: ActivityRecordAudioBinding, service: AudioRecordService) {
|
||||
binding.Timer.base = service.getBase()
|
||||
when (service.status.value) {
|
||||
when (service.status) {
|
||||
Status.READY -> {
|
||||
binding.Stop.isEnabled = false
|
||||
binding.Main.setText(R.string.start)
|
|
@ -1,23 +1,26 @@
|
|||
package com.philkes.notallyx.presentation.activity.note
|
||||
package com.omgodse.notally.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.philkes.notallyx.R
|
||||
import com.philkes.notallyx.data.model.Label
|
||||
import com.philkes.notallyx.databinding.ActivityLabelBinding
|
||||
import com.philkes.notallyx.databinding.DialogInputBinding
|
||||
import com.philkes.notallyx.presentation.activity.LockedActivity
|
||||
import com.philkes.notallyx.presentation.add
|
||||
import com.philkes.notallyx.presentation.setCancelButton
|
||||
import com.philkes.notallyx.presentation.showAndFocus
|
||||
import com.philkes.notallyx.presentation.showToast
|
||||
import com.philkes.notallyx.presentation.view.main.label.SelectableLabelAdapter
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.ActivityLabelBinding
|
||||
import com.omgodse.notally.databinding.DialogInputBinding
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.model.Label
|
||||
import com.omgodse.notally.recyclerview.adapter.SelectableLabelAdapter
|
||||
import com.omgodse.notally.viewmodels.LabelModel
|
||||
|
||||
class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
|
||||
class SelectLabels : AppCompatActivity() {
|
||||
|
||||
private val model: LabelModel by viewModels()
|
||||
private lateinit var binding: ActivityLabelBinding
|
||||
|
||||
private lateinit var selectedLabels: ArrayList<String>
|
||||
|
||||
|
@ -26,12 +29,12 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
|
|||
binding = ActivityLabelBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val savedList = savedInstanceState?.getStringArrayList(EXTRA_SELECTED_LABELS)
|
||||
val passedList = requireNotNull(intent.getStringArrayListExtra(EXTRA_SELECTED_LABELS))
|
||||
val savedList = savedInstanceState?.getStringArrayList(SELECTED_LABELS)
|
||||
val passedList = requireNotNull(intent.getStringArrayListExtra(SELECTED_LABELS))
|
||||
selectedLabels = savedList ?: passedList
|
||||
|
||||
val result = Intent()
|
||||
result.putExtra(EXTRA_SELECTED_LABELS, selectedLabels)
|
||||
result.putExtra(SELECTED_LABELS, selectedLabels)
|
||||
setResult(RESULT_OK, result)
|
||||
|
||||
setupToolbar()
|
||||
|
@ -40,14 +43,12 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
|
|||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putStringArrayList(EXTRA_SELECTED_LABELS, selectedLabels)
|
||||
outState.putStringArrayList(SELECTED_LABELS, selectedLabels)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
binding.Toolbar.apply {
|
||||
setNavigationOnClickListener { finish() }
|
||||
menu.add(R.string.add_label, R.drawable.add) { addLabel() }
|
||||
}
|
||||
binding.Toolbar.setNavigationOnClickListener { finish() }
|
||||
binding.Toolbar.menu.add(R.string.add_label, R.drawable.add) { addLabel() }
|
||||
}
|
||||
|
||||
private fun addLabel() {
|
||||
|
@ -56,26 +57,28 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
|
|||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.add_label)
|
||||
.setView(binding.root)
|
||||
.setCancelButton()
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
val value = binding.EditText.text.toString().trim()
|
||||
if (value.isNotEmpty()) {
|
||||
val label = Label(value)
|
||||
baseModel.insertLabel(label) { success ->
|
||||
model.insertLabel(label) { success ->
|
||||
if (success) {
|
||||
dialog.dismiss()
|
||||
} else showToast(R.string.label_exists)
|
||||
} else Toast.makeText(this, R.string.label_exists, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.showAndFocus(binding.EditText, allowFullSize = true)
|
||||
.show()
|
||||
|
||||
binding.EditText.requestFocus()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val labelAdapter = SelectableLabelAdapter(selectedLabels)
|
||||
labelAdapter.onChecked = { position, checked ->
|
||||
val adapter = SelectableLabelAdapter(selectedLabels)
|
||||
adapter.onChecked = { position, checked ->
|
||||
if (position != -1) {
|
||||
val label = labelAdapter.currentList[position]
|
||||
val label = adapter.currentList[position]
|
||||
if (checked) {
|
||||
if (!selectedLabels.contains(label)) {
|
||||
selectedLabels.add(label)
|
||||
|
@ -84,16 +87,12 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
binding.MainListView.apply {
|
||||
setHasFixedSize(true)
|
||||
adapter = labelAdapter
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(this@SelectLabelsActivity, RecyclerView.VERTICAL)
|
||||
)
|
||||
}
|
||||
binding.RecyclerView.setHasFixedSize(true)
|
||||
binding.RecyclerView.adapter = adapter
|
||||
binding.RecyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
|
||||
baseModel.labels.observe(this) { labels ->
|
||||
labelAdapter.submitList(labels)
|
||||
model.labels.observe(this) { labels ->
|
||||
adapter.submitList(labels)
|
||||
if (labels.isEmpty()) {
|
||||
binding.EmptyState.visibility = View.VISIBLE
|
||||
} else binding.EmptyState.visibility = View.INVISIBLE
|
||||
|
@ -101,6 +100,6 @@ class SelectLabelsActivity : LockedActivity<ActivityLabelBinding>() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SELECTED_LABELS = "notallyx.intent.extra.SELECTED_LABELS"
|
||||
const val SELECTED_LABELS = "SELECTED_LABELS"
|
||||
}
|
||||
}
|
187
app/src/main/java/com/omgodse/notally/activities/TakeNote.kt
Normal file
187
app/src/main/java/com/omgodse/notally/activities/TakeNote.kt
Normal file
|
@ -0,0 +1,187 @@
|
|||
package com.omgodse.notally.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.Editable
|
||||
import android.text.Spanned
|
||||
import android.text.TextWatcher
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.util.Patterns
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.core.text.getSpans
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.omgodse.notally.LinkMovementMethod
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.miscellaneous.createTextWatcherWithHistory
|
||||
import com.omgodse.notally.miscellaneous.setOnNextAction
|
||||
import com.omgodse.notally.model.Type
|
||||
|
||||
class TakeNote : NotallyActivity(Type.NOTE) {
|
||||
|
||||
private lateinit var enterBodyTextWatcher: TextWatcher
|
||||
|
||||
override fun configureUI() {
|
||||
binding.EnterTitle.setOnNextAction { binding.EnterBody.requestFocus() }
|
||||
|
||||
setupEditor()
|
||||
|
||||
if (model.isNewNote) {
|
||||
binding.EnterBody.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupListeners() {
|
||||
super.setupListeners()
|
||||
enterBodyTextWatcher = run {
|
||||
binding.EnterBody.createTextWatcherWithHistory(changeHistory) { text: String ->
|
||||
model.body = Editable.Factory.getInstance().newEditable(text)
|
||||
}
|
||||
}
|
||||
binding.EnterBody.addTextChangedListener(enterBodyTextWatcher)
|
||||
}
|
||||
|
||||
override fun setStateFromModel() {
|
||||
super.setStateFromModel()
|
||||
updateEditText()
|
||||
}
|
||||
|
||||
private fun updateEditText() {
|
||||
binding.EnterBody.removeTextChangedListener(enterBodyTextWatcher)
|
||||
binding.EnterBody.text = model.body
|
||||
binding.EnterBody.addTextChangedListener(enterBodyTextWatcher)
|
||||
}
|
||||
|
||||
private fun setupEditor() {
|
||||
setupMovementMethod()
|
||||
|
||||
binding.EnterBody.customSelectionActionModeCallback =
|
||||
object : ActionMode.Callback {
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?) = false
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?) = false
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
binding.EnterBody.isActionModeOn = true
|
||||
// Try block is there because this will crash on MiUI as Xiaomi has a broken
|
||||
// ActionMode implementation
|
||||
try {
|
||||
if (menu != null) {
|
||||
menu.add(R.string.bold, 0) {
|
||||
applySpan(StyleSpan(Typeface.BOLD))
|
||||
mode?.finish()
|
||||
}
|
||||
menu.add(R.string.link, 0) {
|
||||
applySpan(URLSpan(null))
|
||||
mode?.finish()
|
||||
}
|
||||
menu.add(R.string.italic, 0) {
|
||||
applySpan(StyleSpan(Typeface.ITALIC))
|
||||
mode?.finish()
|
||||
}
|
||||
menu.add(R.string.monospace, 0) {
|
||||
applySpan(TypefaceSpan("monospace"))
|
||||
mode?.finish()
|
||||
}
|
||||
menu.add(R.string.strikethrough, 0) {
|
||||
applySpan(StrikethroughSpan())
|
||||
mode?.finish()
|
||||
}
|
||||
menu.add(R.string.clear_formatting, 0) {
|
||||
removeSpans()
|
||||
mode?.finish()
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
binding.EnterBody.isActionModeOn = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMovementMethod() {
|
||||
val items = arrayOf(getString(R.string.edit), getString(R.string.open_link))
|
||||
val movementMethod = LinkMovementMethod { span ->
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setItems(items) { dialog, which ->
|
||||
if (which == 1) {
|
||||
val spanStart = binding.EnterBody.text?.getSpanStart(span)
|
||||
val spanEnd = binding.EnterBody.text?.getSpanEnd(span)
|
||||
|
||||
ifBothNotNullAndInvalid(spanStart, spanEnd) { start, end ->
|
||||
val text = binding.EnterBody.text?.substring(start, end)
|
||||
if (text != null) {
|
||||
val link = getURLFrom(text)
|
||||
val uri = Uri.parse(link)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (exception: Exception) {
|
||||
Toast.makeText(this, R.string.cant_open_link, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
binding.EnterBody.movementMethod = movementMethod
|
||||
}
|
||||
|
||||
private fun removeSpans() {
|
||||
val selectionEnd = binding.EnterBody.selectionEnd
|
||||
val selectionStart = binding.EnterBody.selectionStart
|
||||
|
||||
ifBothNotNullAndInvalid(selectionStart, selectionEnd) { start, end ->
|
||||
binding.EnterBody.text?.getSpans<CharacterStyle>(start, end)?.forEach { span ->
|
||||
binding.EnterBody.text?.removeSpan(span)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySpan(span: Any) {
|
||||
val selectionEnd = binding.EnterBody.selectionEnd
|
||||
val selectionStart = binding.EnterBody.selectionStart
|
||||
|
||||
ifBothNotNullAndInvalid(selectionStart, selectionEnd) { start, end ->
|
||||
binding.EnterBody.text?.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ifBothNotNullAndInvalid(
|
||||
start: Int?,
|
||||
end: Int?,
|
||||
function: (start: Int, end: Int) -> Unit,
|
||||
) {
|
||||
if (start != null && start != -1 && end != null && end != -1) {
|
||||
function.invoke(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getURLFrom(text: String): String {
|
||||
return when {
|
||||
text.matches(Patterns.PHONE.toRegex()) -> "tel:$text"
|
||||
text.matches(Patterns.EMAIL_ADDRESS.toRegex()) -> "mailto:$text"
|
||||
text.matches(Patterns.DOMAIN_NAME.toRegex()) -> "http://$text"
|
||||
else -> text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
212
app/src/main/java/com/omgodse/notally/activities/ViewImage.kt
Normal file
212
app/src/main/java/com/omgodse/notally/activities/ViewImage.kt
Normal file
|
@ -0,0 +1,212 @@
|
|||
package com.omgodse.notally.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.ActivityViewImageBinding
|
||||
import com.omgodse.notally.miscellaneous.Constants
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.model.Converters
|
||||
import com.omgodse.notally.model.FileAttachment
|
||||
import com.omgodse.notally.model.NotallyDatabase
|
||||
import com.omgodse.notally.recyclerview.adapter.ImageAdapter
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ViewImage : AppCompatActivity() {
|
||||
|
||||
private var currentImage: FileAttachment? = null
|
||||
private lateinit var deletedImages: ArrayList<FileAttachment>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityViewImageBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val savedList = savedInstanceState?.getParcelableArrayList<FileAttachment>(DELETED_IMAGES)
|
||||
deletedImages = savedList ?: ArrayList()
|
||||
|
||||
val result = Intent()
|
||||
result.putExtra(DELETED_IMAGES, deletedImages)
|
||||
setResult(RESULT_OK, result)
|
||||
|
||||
val savedImage = savedInstanceState?.getParcelable<FileAttachment>(CURRENT_IMAGE)
|
||||
if (savedImage != null) {
|
||||
currentImage = savedImage
|
||||
}
|
||||
|
||||
binding.RecyclerView.setHasFixedSize(true)
|
||||
binding.RecyclerView.layoutManager =
|
||||
LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
|
||||
PagerSnapHelper().attachToRecyclerView(binding.RecyclerView)
|
||||
|
||||
val initial = intent.getIntExtra(POSITION, 0)
|
||||
binding.RecyclerView.scrollToPosition(initial)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val database = NotallyDatabase.getDatabase(application)
|
||||
val id = intent.getLongExtra(Constants.SelectedBaseNote, 0)
|
||||
|
||||
val json = withContext(Dispatchers.IO) { database.getBaseNoteDao().getImages(id) }
|
||||
val original = Converters.jsonToFiles(json)
|
||||
val images = ArrayList<FileAttachment>(original.size)
|
||||
original.filterNotTo(images) { image -> deletedImages.contains(image) }
|
||||
|
||||
val mediaRoot = IO.getExternalImagesDirectory(application)
|
||||
val adapter = ImageAdapter(mediaRoot, images)
|
||||
binding.RecyclerView.adapter = adapter
|
||||
setupToolbar(binding, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(CURRENT_IMAGE, currentImage)
|
||||
outState.putParcelableArrayList(DELETED_IMAGES, deletedImages)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_EXPORT_FILE && resultCode == RESULT_OK) {
|
||||
data?.data?.let { uri -> writeImageToUri(uri) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar(binding: ActivityViewImageBinding, adapter: ImageAdapter) {
|
||||
binding.Toolbar.setNavigationOnClickListener { finish() }
|
||||
|
||||
val layoutManager = binding.RecyclerView.layoutManager as LinearLayoutManager
|
||||
adapter.registerAdapterDataObserver(
|
||||
object : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
val position = layoutManager.findFirstVisibleItemPosition()
|
||||
binding.Toolbar.title = "${position + 1} / ${adapter.itemCount}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.RecyclerView.addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val position = layoutManager.findFirstVisibleItemPosition()
|
||||
binding.Toolbar.title = "${position + 1} / ${adapter.itemCount}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.Toolbar.menu.add(R.string.share, R.drawable.share) {
|
||||
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
if (position != -1) {
|
||||
val image = adapter.items[position]
|
||||
share(image)
|
||||
}
|
||||
}
|
||||
binding.Toolbar.menu.add(R.string.save_to_device, R.drawable.save) {
|
||||
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
if (position != -1) {
|
||||
val image = adapter.items[position]
|
||||
saveToDevice(image)
|
||||
}
|
||||
}
|
||||
binding.Toolbar.menu.add(R.string.delete, R.drawable.delete) {
|
||||
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
if (position != -1) {
|
||||
delete(position, adapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(image: FileAttachment) {
|
||||
val mediaRoot = IO.getExternalImagesDirectory(application)
|
||||
val file = if (mediaRoot != null) File(mediaRoot, image.localName) else null
|
||||
if (file != null && file.exists()) {
|
||||
val uri = FileProvider.getUriForFile(this, "$packageName.provider", file)
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = image.mimeType
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
|
||||
// Necessary for sharesheet to show a preview of the image
|
||||
// Check ->
|
||||
// https://commonsware.com/blog/2021/01/07/action_send-share-sheet-clipdata.html
|
||||
intent.clipData = ClipData.newRawUri(null, uri)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val chooser = Intent.createChooser(intent, null)
|
||||
startActivity(chooser)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveToDevice(image: FileAttachment) {
|
||||
val mediaRoot = IO.getExternalImagesDirectory(application)
|
||||
val file = if (mediaRoot != null) File(mediaRoot, image.localName) else null
|
||||
if (file != null && file.exists()) {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.type = image.mimeType
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.putExtra(Intent.EXTRA_TITLE, "Notally Image")
|
||||
|
||||
currentImage = image
|
||||
startActivityForResult(intent, REQUEST_EXPORT_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeImageToUri(uri: Uri) {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mediaRoot = IO.getExternalImagesDirectory(application)
|
||||
val file =
|
||||
if (mediaRoot != null) File(mediaRoot, requireNotNull(currentImage).localName)
|
||||
else null
|
||||
if (file != null && file.exists()) {
|
||||
val output = contentResolver.openOutputStream(uri) as FileOutputStream
|
||||
output.channel.truncate(0)
|
||||
val input = FileInputStream(file)
|
||||
input.copyTo(output)
|
||||
input.close()
|
||||
output.close()
|
||||
}
|
||||
}
|
||||
Toast.makeText(this@ViewImage, R.string.saved_to_device, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun delete(position: Int, adapter: ImageAdapter) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.delete_image_forever)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
val image = adapter.items.removeAt(position)
|
||||
deletedImages.add(image)
|
||||
adapter.notifyItemRemoved(position)
|
||||
if (adapter.items.isEmpty()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val POSITION = "POSITION"
|
||||
const val CURRENT_IMAGE = "CURRENT_IMAGE"
|
||||
const val DELETED_IMAGES = "DELETED_IMAGES"
|
||||
private const val REQUEST_EXPORT_FILE = 40
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.presentation.view.note.audio
|
||||
package com.omgodse.notally.audio
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
|
@ -9,7 +9,7 @@ import android.view.View
|
|||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.philkes.notallyx.R
|
||||
import com.omgodse.notally.R
|
||||
|
||||
class AudioControlView(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs) {
|
||||
|
||||
|
@ -22,7 +22,7 @@ class AudioControlView(context: Context, attrs: AttributeSet) : RelativeLayout(c
|
|||
private var running = false
|
||||
|
||||
private var base = 0L
|
||||
private var duration: Long? = null
|
||||
private var duration = 0L
|
||||
private val recycle = StringBuilder(8)
|
||||
|
||||
private var seeking = false
|
||||
|
@ -58,10 +58,10 @@ class AudioControlView(context: Context, attrs: AttributeSet) : RelativeLayout(c
|
|||
setCurrentPosition(0)
|
||||
}
|
||||
|
||||
fun setDuration(milliseconds: Long?) {
|
||||
fun setDuration(milliseconds: Long) {
|
||||
duration = milliseconds
|
||||
progress.valueTo = milliseconds?.toFloat() ?: Int.MAX_VALUE.toFloat()
|
||||
length.text = milliseconds?.let { DateUtils.formatElapsedTime(recycle, it / 1000) } ?: "-"
|
||||
progress.valueTo = milliseconds.toFloat()
|
||||
length.text = DateUtils.formatElapsedTime(recycle, milliseconds / 1000)
|
||||
}
|
||||
|
||||
fun setCurrentPosition(position: Int) {
|
||||
|
@ -78,10 +78,8 @@ class AudioControlView(context: Context, attrs: AttributeSet) : RelativeLayout(c
|
|||
@Synchronized
|
||||
private fun updateComponents(now: Long) {
|
||||
var milliseconds = now - base
|
||||
duration?.let {
|
||||
if (milliseconds > it) {
|
||||
milliseconds = it
|
||||
}
|
||||
if (milliseconds > duration) {
|
||||
milliseconds = duration
|
||||
}
|
||||
chronometer.text = DateUtils.formatElapsedTime(recycle, milliseconds / 1000)
|
||||
if (!seeking) {
|
|
@ -1,12 +1,12 @@
|
|||
package com.philkes.notallyx.utils.audio
|
||||
package com.omgodse.notally.audio
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.media.MediaPlayer
|
||||
import android.os.IBinder
|
||||
import com.philkes.notallyx.data.model.Audio
|
||||
import com.philkes.notallyx.utils.getExternalAudioDirectory
|
||||
import com.philkes.notallyx.utils.log
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
import com.omgodse.notally.miscellaneous.Operations
|
||||
import com.omgodse.notally.model.Audio
|
||||
import java.io.File
|
||||
|
||||
class AudioPlayService : Service() {
|
||||
|
@ -21,19 +21,17 @@ class AudioPlayService : Service() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
player =
|
||||
MediaPlayer().apply {
|
||||
setOnPreparedListener { setState(PREPARED) }
|
||||
setOnCompletionListener { setState(COMPLETED) }
|
||||
setOnSeekCompleteListener { setState(stateBeforeSeeking) }
|
||||
setOnErrorListener { _, what, extra ->
|
||||
player = MediaPlayer()
|
||||
player.setOnPreparedListener { setState(PREPARED) }
|
||||
player.setOnCompletionListener { setState(COMPLETED) }
|
||||
player.setOnSeekCompleteListener { setState(stateBeforeSeeking) }
|
||||
player.setOnErrorListener { _, what, extra ->
|
||||
errorType = what
|
||||
errorCode = extra
|
||||
setState(ERROR)
|
||||
return@setOnErrorListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
@ -46,7 +44,7 @@ class AudioPlayService : Service() {
|
|||
|
||||
fun initialise(audio: Audio) {
|
||||
if (state == IDLE) {
|
||||
val audioRoot = application.getExternalAudioDirectory()
|
||||
val audioRoot = IO.getExternalAudioDirectory(application)
|
||||
if (audioRoot != null) {
|
||||
try {
|
||||
val file = File(audioRoot, audio.name)
|
||||
|
@ -55,7 +53,7 @@ class AudioPlayService : Service() {
|
|||
player.prepareAsync()
|
||||
} catch (exception: Exception) {
|
||||
setIOError()
|
||||
application.log(TAG, throwable = exception)
|
||||
Operations.log(application, exception)
|
||||
}
|
||||
} else setIOError()
|
||||
}
|
||||
|
@ -112,7 +110,6 @@ class AudioPlayService : Service() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AudioPlayService"
|
||||
const val IDLE = 0
|
||||
const val INITIALISED = 1
|
||||
const val PREPARED = 2
|
|
@ -0,0 +1,135 @@
|
|||
package com.omgodse.notally.audio
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaRecorder
|
||||
import android.os.Build
|
||||
import android.os.SystemClock
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.activities.RecordAudio
|
||||
import com.omgodse.notally.audio.Status.PAUSED
|
||||
import com.omgodse.notally.audio.Status.READY
|
||||
import com.omgodse.notally.audio.Status.RECORDING
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
|
||||
@RequiresApi(24)
|
||||
class AudioRecordService : Service() {
|
||||
|
||||
var status = READY
|
||||
private var lastStart = 0L
|
||||
private var audioDuration = 0L
|
||||
|
||||
private lateinit var recorder: MediaRecorder
|
||||
private lateinit var manager: NotificationManager
|
||||
private lateinit var builder: Notification.Builder
|
||||
|
||||
override fun onCreate() {
|
||||
manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
builder = Notification.Builder(this)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = "com.omgodse.audio"
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
"Audio Recordings",
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
)
|
||||
manager.createNotificationChannel(channel)
|
||||
builder.setChannelId(channelId)
|
||||
}
|
||||
|
||||
builder.setSmallIcon(R.drawable.record_audio)
|
||||
builder.setOnlyAlertOnce(true)
|
||||
|
||||
/*
|
||||
Prevent user from dismissing notification in Android 13 (33) and above
|
||||
https://developer.android.com/guide/components/foreground-services#user-dismiss-notification
|
||||
*/
|
||||
builder.setOngoing(true)
|
||||
|
||||
/*
|
||||
On Android 12 (31) and above, the system waits 10 seconds before showing the notification.
|
||||
https://developer.android.com/guide/components/foreground-services#notification-immediate
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
}
|
||||
|
||||
val intent = Intent(this, RecordAudio::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
builder.setContentIntent(pendingIntent)
|
||||
|
||||
startForeground(2, buildNotification())
|
||||
|
||||
recorder =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MediaRecorder(this)
|
||||
} else MediaRecorder()
|
||||
|
||||
recorder.setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||
|
||||
val output = IO.getTempAudioFile(this)
|
||||
recorder.setOutputFile(output.path)
|
||||
recorder.prepare()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
recorder.release()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?) = LocalBinder(this)
|
||||
|
||||
fun start() {
|
||||
recorder.start()
|
||||
status = RECORDING
|
||||
lastStart = SystemClock.elapsedRealtime()
|
||||
manager.notify(2, buildNotification())
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
recorder.resume()
|
||||
status = RECORDING
|
||||
lastStart = SystemClock.elapsedRealtime()
|
||||
manager.notify(2, buildNotification())
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
recorder.pause()
|
||||
status = PAUSED
|
||||
audioDuration += SystemClock.elapsedRealtime() - lastStart
|
||||
lastStart = 0L
|
||||
manager.notify(2, buildNotification())
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
recorder.stop()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
fun getBase(): Long {
|
||||
return if (lastStart != 0L) {
|
||||
lastStart - audioDuration
|
||||
} else SystemClock.elapsedRealtime() - audioDuration
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
val title =
|
||||
when (status) {
|
||||
READY -> getString(R.string.ready_to_record)
|
||||
PAUSED -> getString(R.string.paused)
|
||||
RECORDING -> getString(R.string.recording)
|
||||
}
|
||||
builder.setContentTitle(title)
|
||||
builder.setContentText(getString(R.string.tap_for_more_options))
|
||||
return builder.build()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.utils.audio
|
||||
package com.omgodse.notally.audio
|
||||
|
||||
import android.app.Service
|
||||
import android.os.Binder
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.utils.audio
|
||||
package com.omgodse.notally.audio
|
||||
|
||||
enum class Status {
|
||||
READY,
|
|
@ -0,0 +1,38 @@
|
|||
package com.omgodse.notally.backup
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.omgodse.notally.preferences.AutoBackup
|
||||
import com.omgodse.notally.preferences.Preferences
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class AutoBackupWorker(private val context: Context, params: WorkerParameters) :
|
||||
Worker(context, params) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
val app = context.applicationContext as Application
|
||||
val preferences = Preferences.getInstance(app)
|
||||
val backupPath = preferences.autoBackup.value
|
||||
|
||||
if (backupPath != AutoBackup.emptyPath) {
|
||||
val uri = Uri.parse(backupPath)
|
||||
val folder = requireNotNull(DocumentFile.fromTreeUri(app, uri))
|
||||
|
||||
if (folder.exists()) {
|
||||
val formatter =
|
||||
SimpleDateFormat("yyyyMMdd HHmmss '(Notally Backup)'", Locale.ENGLISH)
|
||||
val name = formatter.format(System.currentTimeMillis())
|
||||
val zipFile = requireNotNull(folder.createFile("application/zip", name))
|
||||
val outputStream = requireNotNull(app.contentResolver.openOutputStream(zipFile.uri))
|
||||
doBackup(outputStream, app)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
}
|
76
app/src/main/java/com/omgodse/notally/backup/BackUpUtils.kt
Normal file
76
app/src/main/java/com/omgodse/notally/backup/BackUpUtils.kt
Normal file
|
@ -0,0 +1,76 @@
|
|||
package com.omgodse.notally.backup
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.omgodse.notally.BackupProgress
|
||||
import com.omgodse.notally.miscellaneous.Export
|
||||
import com.omgodse.notally.miscellaneous.IO
|
||||
import com.omgodse.notally.miscellaneous.Operations
|
||||
import com.omgodse.notally.model.Converters
|
||||
import com.omgodse.notally.model.NotallyDatabase
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun doBackup(
|
||||
outputStream: OutputStream,
|
||||
app: Application,
|
||||
backupProgress: MutableLiveData<BackupProgress>? = null,
|
||||
) {
|
||||
val zipStream = ZipOutputStream(outputStream)
|
||||
|
||||
val database = NotallyDatabase.getDatabase(app)
|
||||
|
||||
database.checkpoint()
|
||||
Export.backupDatabase(app, zipStream)
|
||||
|
||||
val imageRoot = IO.getExternalImagesDirectory(app)
|
||||
val fileRoot = IO.getExternalFilesDirectory(app)
|
||||
val audioRoot = IO.getExternalAudioDirectory(app)
|
||||
|
||||
val images = database.getBaseNoteDao().getAllImages()
|
||||
val files = database.getBaseNoteDao().getAllFiles()
|
||||
val audios = database.getBaseNoteDao().getAllAudios()
|
||||
val total = images.size + files.size + audios.size
|
||||
images
|
||||
.asSequence()
|
||||
.flatMap { string -> Converters.jsonToFiles(string) }
|
||||
.forEachIndexed { index, image ->
|
||||
try {
|
||||
Export.backupFile(zipStream, imageRoot, "Images", image.localName)
|
||||
} catch (exception: Exception) {
|
||||
Operations.log(app, exception)
|
||||
} finally {
|
||||
backupProgress?.postValue(BackupProgress(true, index + 1, total, false))
|
||||
}
|
||||
}
|
||||
files
|
||||
.asSequence()
|
||||
.flatMap { string -> Converters.jsonToFiles(string) }
|
||||
.forEachIndexed { index, file ->
|
||||
try {
|
||||
Export.backupFile(zipStream, fileRoot, "Files", file.localName)
|
||||
} catch (exception: Exception) {
|
||||
Operations.log(app, exception)
|
||||
} finally {
|
||||
backupProgress?.postValue(
|
||||
BackupProgress(true, images.size + index + 1, total, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
audios
|
||||
.asSequence()
|
||||
.flatMap { string -> Converters.jsonToAudios(string) }
|
||||
.forEachIndexed { index, audio ->
|
||||
try {
|
||||
Export.backupFile(zipStream, audioRoot, "Audios", audio.name)
|
||||
} catch (exception: Exception) {
|
||||
Operations.log(app, exception)
|
||||
} finally {
|
||||
backupProgress?.postValue(
|
||||
BackupProgress(true, images.size + files.size + index + 1, total, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
zipStream.close()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.utils.changehistory
|
||||
package com.omgodse.notally.changehistory
|
||||
|
||||
interface Change {
|
||||
fun redo()
|
|
@ -0,0 +1,21 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class ChangeCheckedForAllChange(
|
||||
internal val checked: Boolean,
|
||||
internal val changedIds: Collection<Int>,
|
||||
private val listManager: ListManager,
|
||||
) : Change {
|
||||
override fun redo() {
|
||||
listManager.checkByIds(checked, changedIds, true)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
listManager.checkByIds(!checked, changedIds, true)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ChangeCheckedForAllChange checked: $checked changedIds: ${changedIds.joinToString(",")}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import android.util.Log
|
||||
|
||||
class ChangeHistory(private val onStackChanged: (stackPointer: Int) -> Unit) {
|
||||
private val TAG = "ChangeHistory"
|
||||
private val changeStack = ArrayList<Change>()
|
||||
private var stackPointer = -1
|
||||
|
||||
fun push(change: Change) {
|
||||
popRedos()
|
||||
changeStack.add(change)
|
||||
stackPointer++
|
||||
Log.d(TAG, "addChange: $change")
|
||||
onStackChanged.invoke(stackPointer)
|
||||
}
|
||||
|
||||
fun redo() {
|
||||
stackPointer++
|
||||
if (stackPointer >= changeStack.size) {
|
||||
throw RuntimeException("There is no Change to redo!")
|
||||
}
|
||||
val makeListAction = changeStack[stackPointer]
|
||||
Log.d(TAG, "redo: $makeListAction")
|
||||
makeListAction.redo()
|
||||
onStackChanged.invoke(stackPointer)
|
||||
}
|
||||
|
||||
fun undo() {
|
||||
if (stackPointer < 0) {
|
||||
throw RuntimeException("There is no Change to undo!")
|
||||
}
|
||||
val makeListAction = changeStack[stackPointer]
|
||||
Log.d(TAG, "undo: $makeListAction")
|
||||
makeListAction.undo()
|
||||
stackPointer--
|
||||
onStackChanged.invoke(stackPointer)
|
||||
}
|
||||
|
||||
fun canRedo(): Boolean {
|
||||
return stackPointer >= -1 && stackPointer < changeStack.size - 1
|
||||
}
|
||||
|
||||
fun canUndo(): Boolean {
|
||||
return stackPointer > -1
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
stackPointer = -1
|
||||
changeStack.clear()
|
||||
}
|
||||
|
||||
internal fun lookUp(position: Int = 0): Change {
|
||||
if (stackPointer - position < 0) {
|
||||
throw IllegalArgumentException("ChangeHistory only has $stackPointer changes!")
|
||||
}
|
||||
return changeStack[stackPointer - position]
|
||||
}
|
||||
|
||||
private fun popRedos() {
|
||||
while (changeStack.size > stackPointer + 1) {
|
||||
changeStack.removeAt(stackPointer + 1)
|
||||
}
|
||||
// changeStack.subList(stackPointer, changeStack.size).clear()
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.model.ListItem
|
||||
import com.omgodse.notally.model.toReadableString
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class DeleteCheckedChange(
|
||||
internal val deletedItems: List<ListItem>,
|
||||
private val listManager: ListManager,
|
||||
) : Change {
|
||||
override fun redo() {
|
||||
listManager.deleteCheckedItems(pushChange = false)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
deletedItems.forEach { listManager.add(it.order!!, it) }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "DeleteCheckedChange deletedItems:\n${deletedItems.toReadableString()}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import android.text.TextWatcher
|
||||
import android.widget.EditText
|
||||
|
||||
class EditTextChange(
|
||||
private val editText: EditText,
|
||||
textBefore: String,
|
||||
textAfter: String,
|
||||
private val listener: TextWatcher,
|
||||
private val updateModel: (newValue: String) -> Unit,
|
||||
) : ValueChange<String>(textAfter, textBefore) {
|
||||
|
||||
private val cursorPosition = editText.selectionStart
|
||||
|
||||
override fun update(value: String, isUndo: Boolean) {
|
||||
updateModel.invoke(value)
|
||||
editText.removeTextChangedListener(listener)
|
||||
editText.setText(value)
|
||||
editText.requestFocus()
|
||||
editText.setSelection(Math.max(0, cursorPosition - (if (isUndo) 1 else 0)))
|
||||
editText.addTextChangedListener(listener)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.model.ListItem
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class ListAddChange(
|
||||
position: Int,
|
||||
internal val deletedItemId: Int,
|
||||
internal val itemBeforeInsert: ListItem,
|
||||
private val listManager: ListManager,
|
||||
) : ListChange(position) {
|
||||
override fun redo() {
|
||||
listManager.add(position, item = itemBeforeInsert, pushChange = false)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
listManager.deleteById(
|
||||
deletedItemId,
|
||||
childrenToDelete = itemBeforeInsert.children,
|
||||
pushChange = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Add at position: $position"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
package com.philkes.notallyx.utils.changehistory
|
||||
package com.omgodse.notally.changehistory
|
||||
|
||||
abstract class ListChange(internal val position: Int) : Change
|
|
@ -0,0 +1,15 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class ListCheckedChange(checked: Boolean, itemId: Int, private val listManager: ListManager) :
|
||||
ListIdValueChange<Boolean>(checked, !checked, itemId) {
|
||||
|
||||
override fun update(itemId: Int, value: Boolean, isUndo: Boolean) {
|
||||
listManager.changeCheckedById(itemId, value, pushChange = false)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CheckedChange id: $itemId isChecked: $newValue"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.model.ListItem
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class ListDeleteChange(
|
||||
internal val itemOrder: Int,
|
||||
internal val deletedItem: ListItem,
|
||||
private val listManager: ListManager,
|
||||
) : Change {
|
||||
override fun redo() {
|
||||
listManager.deleteById(deletedItem.id, pushChange = false)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
listManager.add(itemOrder, deletedItem, pushChange = false)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "DeleteChange id: ${deletedItem.id} itemOrder: $itemOrder deletedItem: $deletedItem"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import android.text.TextWatcher
|
||||
import android.widget.EditText
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
open class ListEditTextChange(
|
||||
private val editText: EditText,
|
||||
position: Int,
|
||||
private val textBefore: String,
|
||||
private val textAfter: String,
|
||||
private val listener: TextWatcher,
|
||||
private val listManager: ListManager,
|
||||
) : ListPositionValueChange<String>(textAfter, textBefore, position) {
|
||||
private val cursorPosition = editText.selectionStart
|
||||
|
||||
override fun update(position: Int, value: String, isUndo: Boolean) {
|
||||
listManager.changeText(editText, listener, position, textBefore, value, pushChange = false)
|
||||
editText.removeTextChangedListener(listener)
|
||||
editText.setText(value)
|
||||
editText.requestFocus()
|
||||
editText.setSelection(Math.max(0, cursorPosition - (if (isUndo) 1 else 0)))
|
||||
editText.addTextChangedListener(listener)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CheckedText at $position from: $textBefore to: $textAfter"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
abstract class ListIdValueChange<T>(
|
||||
internal val newValue: T,
|
||||
internal val oldValue: T,
|
||||
internal val itemId: Int,
|
||||
) : Change {
|
||||
|
||||
override fun redo() {
|
||||
update(itemId, newValue, false)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
update(itemId, oldValue, true)
|
||||
}
|
||||
|
||||
abstract fun update(itemId: Int, value: T, isUndo: Boolean)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class ListIsChildChange(isChild: Boolean, position: Int, private val listManager: ListManager) :
|
||||
ListPositionValueChange<Boolean>(isChild, !isChild, position) {
|
||||
|
||||
override fun update(position: Int, value: Boolean, isUndo: Boolean) {
|
||||
listManager.changeIsChild(position, value, pushChange = false)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "IsChildChange position: $position isChild: $newValue"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
import com.omgodse.notally.model.ListItem
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
|
||||
class ListMoveChange(
|
||||
positionFrom: Int,
|
||||
internal val positionTo: Int,
|
||||
internal var positionAfter: Int,
|
||||
internal val itemBeforeMove: ListItem,
|
||||
internal val listManager: ListManager,
|
||||
) : ListChange(positionFrom) {
|
||||
override fun redo() {
|
||||
positionAfter = listManager.move(position, positionTo, pushChange = false)!!
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
listManager.undoMove(positionAfter, position, itemBeforeMove)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "MoveChange from: $position to: $positionTo after: $positionAfter itemBeforeMove: $itemBeforeMove"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
package com.philkes.notallyx.utils.changehistory
|
||||
|
||||
import com.philkes.notallyx.utils.truncate
|
||||
package com.omgodse.notally.changehistory
|
||||
|
||||
abstract class ListPositionValueChange<T>(
|
||||
internal val newValue: T,
|
||||
|
@ -17,8 +15,4 @@ abstract class ListPositionValueChange<T>(
|
|||
}
|
||||
|
||||
abstract fun update(position: Int, value: T, isUndo: Boolean)
|
||||
|
||||
override fun toString(): String {
|
||||
return "${javaClass.simpleName} at $position from: ${oldValue.toString().truncate(100)} to: ${newValue.toString().truncate(100)}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.omgodse.notally.changehistory
|
||||
|
||||
abstract class ValueChange<T>(protected val newValue: T, protected val oldValue: T) : Change {
|
||||
|
||||
override fun redo() {
|
||||
update(newValue, false)
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
update(newValue, true)
|
||||
}
|
||||
|
||||
abstract fun update(value: T, isUndo: Boolean)
|
||||
}
|
10
app/src/main/java/com/omgodse/notally/fragments/Archived.kt
Normal file
10
app/src/main/java/com/omgodse/notally/fragments/Archived.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import com.omgodse.notally.R
|
||||
|
||||
class Archived : NotallyFragment() {
|
||||
|
||||
override fun getBackground() = R.drawable.archive
|
||||
|
||||
override fun getObservable() = model.archivedNotes
|
||||
}
|
26
app/src/main/java/com/omgodse/notally/fragments/Deleted.kt
Normal file
26
app/src/main/java/com/omgodse/notally/fragments/Deleted.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
|
||||
class Deleted : NotallyFragment() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
menu.add(R.string.delete_all, R.drawable.delete_all) { deleteAllNotes() }
|
||||
}
|
||||
|
||||
private fun deleteAllNotes() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.delete_all_notes)
|
||||
.setPositiveButton(R.string.delete) { _, _ -> model.deleteAllBaseNotes() }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun getBackground() = R.drawable.delete
|
||||
|
||||
override fun getObservable() = model.deletedNotes
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.miscellaneous.Constants
|
||||
import com.omgodse.notally.model.Item
|
||||
|
||||
class DisplayLabel : NotallyFragment() {
|
||||
|
||||
override fun getBackground() = R.drawable.label
|
||||
|
||||
override fun getObservable(): LiveData<List<Item>> {
|
||||
val label = requireNotNull(requireArguments().getString(Constants.SelectedLabel))
|
||||
return model.getNotesByLabel(label)
|
||||
}
|
||||
}
|
158
app/src/main/java/com/omgodse/notally/fragments/Labels.kt
Normal file
158
app/src/main/java/com/omgodse/notally/fragments/Labels.kt
Normal file
|
@ -0,0 +1,158 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.omgodse.notally.MenuDialog
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.DialogInputBinding
|
||||
import com.omgodse.notally.databinding.FragmentNotesBinding
|
||||
import com.omgodse.notally.miscellaneous.Constants
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
import com.omgodse.notally.model.Label
|
||||
import com.omgodse.notally.recyclerview.ItemListener
|
||||
import com.omgodse.notally.recyclerview.adapter.LabelAdapter
|
||||
import com.omgodse.notally.viewmodels.BaseNoteModel
|
||||
|
||||
class Labels : Fragment(), ItemListener {
|
||||
|
||||
private var adapter: LabelAdapter? = null
|
||||
private var binding: FragmentNotesBinding? = null
|
||||
|
||||
private val model: BaseNoteModel by activityViewModels()
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
binding = null
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
adapter = LabelAdapter(this)
|
||||
|
||||
binding?.RecyclerView?.setHasFixedSize(true)
|
||||
binding?.RecyclerView?.adapter = adapter
|
||||
binding?.RecyclerView?.layoutManager = LinearLayoutManager(requireContext())
|
||||
val itemDecoration = DividerItemDecoration(requireContext(), RecyclerView.VERTICAL)
|
||||
binding?.RecyclerView?.addItemDecoration(itemDecoration)
|
||||
|
||||
binding?.RecyclerView?.setPadding(0, 0, 0, 0)
|
||||
binding?.ImageView?.setImageResource(R.drawable.label)
|
||||
|
||||
setupObserver()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
setHasOptionsMenu(true)
|
||||
binding = FragmentNotesBinding.inflate(inflater)
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
menu.add(R.string.add_label, R.drawable.add) { displayAddLabelDialog() }
|
||||
}
|
||||
|
||||
override fun onClick(position: Int) {
|
||||
adapter?.currentList?.get(position)?.let { value ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Constants.SelectedLabel, value)
|
||||
findNavController().navigate(R.id.LabelsToDisplayLabel, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(position: Int) {
|
||||
adapter?.currentList?.get(position)?.let { value ->
|
||||
MenuDialog(requireContext())
|
||||
.add(R.string.edit) { displayEditLabelDialog(value) }
|
||||
.add(R.string.delete) { confirmDeletion(value) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObserver() {
|
||||
model.labels.observe(viewLifecycleOwner) { labels ->
|
||||
adapter?.submitList(labels)
|
||||
binding?.ImageView?.isVisible = labels.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayAddLabelDialog() {
|
||||
val inflater = LayoutInflater.from(requireContext())
|
||||
val dialogBinding = DialogInputBinding.inflate(inflater)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.add_label)
|
||||
.setView(dialogBinding.root)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
val value = dialogBinding.EditText.text.toString().trim()
|
||||
if (value.isNotEmpty()) {
|
||||
val label = Label(value)
|
||||
model.insertLabel(label) { success: Boolean ->
|
||||
if (success) {
|
||||
dialog.dismiss()
|
||||
} else
|
||||
Toast.makeText(context, R.string.label_exists, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
dialogBinding.EditText.requestFocus()
|
||||
}
|
||||
|
||||
private fun confirmDeletion(value: String) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.delete_label)
|
||||
.setMessage(R.string.your_notes_associated)
|
||||
.setPositiveButton(R.string.delete) { _, _ -> model.deleteLabel(value) }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displayEditLabelDialog(oldValue: String) {
|
||||
val dialogBinding = DialogInputBinding.inflate(layoutInflater)
|
||||
|
||||
dialogBinding.EditText.setText(oldValue)
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(dialogBinding.root)
|
||||
.setTitle(R.string.edit_label)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
val value = dialogBinding.EditText.text.toString().trim()
|
||||
if (value.isNotEmpty()) {
|
||||
model.updateLabel(oldValue, value) { success ->
|
||||
if (success) {
|
||||
dialog.dismiss()
|
||||
} else
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.label_exists,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
dialogBinding.EditText.requestFocus()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.omgodse.notally.activities.MakeList
|
||||
import com.omgodse.notally.activities.TakeNote
|
||||
import com.omgodse.notally.databinding.FragmentNotesBinding
|
||||
import com.omgodse.notally.miscellaneous.Constants
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
import com.omgodse.notally.model.Item
|
||||
import com.omgodse.notally.model.Type
|
||||
import com.omgodse.notally.preferences.View as ViewPref
|
||||
import com.omgodse.notally.recyclerview.ItemListener
|
||||
import com.omgodse.notally.recyclerview.adapter.BaseNoteAdapter
|
||||
import com.omgodse.notally.viewmodels.BaseNoteModel
|
||||
|
||||
abstract class NotallyFragment : Fragment(), ItemListener {
|
||||
|
||||
private var adapter: BaseNoteAdapter? = null
|
||||
internal var binding: FragmentNotesBinding? = null
|
||||
|
||||
internal val model: BaseNoteModel by activityViewModels()
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
binding = null
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding?.ImageView?.setImageResource(getBackground())
|
||||
|
||||
setupAdapter()
|
||||
setupRecyclerView()
|
||||
setupObserver()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
setHasOptionsMenu(true)
|
||||
binding = FragmentNotesBinding.inflate(inflater)
|
||||
return binding?.root
|
||||
}
|
||||
|
||||
// See [RecyclerView.ViewHolder.getAdapterPosition]
|
||||
override fun onClick(position: Int) {
|
||||
if (position != -1) {
|
||||
adapter?.currentList?.get(position)?.let { item ->
|
||||
if (item is BaseNote) {
|
||||
if (model.actionMode.isEnabled()) {
|
||||
handleNoteSelection(item.id, position, item)
|
||||
} else {
|
||||
when (item.type) {
|
||||
Type.NOTE -> goToActivity(TakeNote::class.java, item)
|
||||
Type.LIST -> goToActivity(MakeList::class.java, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(position: Int) {
|
||||
if (position != -1) {
|
||||
adapter?.currentList?.get(position)?.let { item ->
|
||||
if (item is BaseNote) {
|
||||
handleNoteSelection(item.id, position, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNoteSelection(id: Long, position: Int, baseNote: BaseNote) {
|
||||
if (model.actionMode.selectedNotes.contains(id)) {
|
||||
model.actionMode.remove(id)
|
||||
} else model.actionMode.add(id, baseNote)
|
||||
adapter?.notifyItemChanged(position, 0)
|
||||
}
|
||||
|
||||
private fun setupAdapter() {
|
||||
val textSize = model.preferences.textSize.value
|
||||
val maxItems = model.preferences.maxItems
|
||||
val maxLines = model.preferences.maxLines
|
||||
val maxTitle = model.preferences.maxTitle
|
||||
val dateFormat = model.preferences.dateFormat.value
|
||||
|
||||
adapter =
|
||||
BaseNoteAdapter(
|
||||
model.actionMode.selectedIds,
|
||||
dateFormat,
|
||||
textSize,
|
||||
maxItems,
|
||||
maxLines,
|
||||
maxTitle,
|
||||
model.imageRoot,
|
||||
model.fileRoot,
|
||||
this,
|
||||
)
|
||||
adapter?.registerAdapterDataObserver(
|
||||
object : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (itemCount > 0) {
|
||||
binding?.RecyclerView?.scrollToPosition(positionStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
binding?.RecyclerView?.adapter = adapter
|
||||
binding?.RecyclerView?.setHasFixedSize(true)
|
||||
}
|
||||
|
||||
private fun setupObserver() {
|
||||
getObservable().observe(viewLifecycleOwner) { list ->
|
||||
adapter?.submitList(list)
|
||||
binding?.ImageView?.isVisible = list.isEmpty()
|
||||
}
|
||||
|
||||
model.actionMode.closeListener.observe(viewLifecycleOwner) { event ->
|
||||
event.handle { ids ->
|
||||
adapter?.currentList?.forEachIndexed { index, item ->
|
||||
if (item is BaseNote && ids.contains(item.id)) {
|
||||
adapter?.notifyItemChanged(index, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
binding?.RecyclerView?.layoutManager =
|
||||
if (model.preferences.view.value == ViewPref.grid) {
|
||||
StaggeredGridLayoutManager(2, RecyclerView.VERTICAL)
|
||||
} else LinearLayoutManager(requireContext())
|
||||
}
|
||||
|
||||
private fun goToActivity(activity: Class<*>, baseNote: BaseNote) {
|
||||
val intent = Intent(requireContext(), activity)
|
||||
intent.putExtra(Constants.SelectedBaseNote, baseNote.id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
abstract fun getBackground(): Int
|
||||
|
||||
abstract fun getObservable(): LiveData<List<Item>>
|
||||
}
|
20
app/src/main/java/com/omgodse/notally/fragments/Notes.kt
Normal file
20
app/src/main/java/com/omgodse/notally/fragments/Notes.kt
Normal file
|
@ -0,0 +1,20 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.miscellaneous.add
|
||||
|
||||
class Notes : NotallyFragment() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
menu.add(R.string.search, R.drawable.search) {
|
||||
findNavController().navigate(R.id.NotesToSearch)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getObservable() = model.baseNotes
|
||||
|
||||
override fun getBackground() = R.drawable.notebook
|
||||
}
|
39
app/src/main/java/com/omgodse/notally/fragments/Search.kt
Normal file
39
app/src/main/java/com/omgodse/notally/fragments/Search.kt
Normal file
|
@ -0,0 +1,39 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.model.Folder
|
||||
|
||||
class Search : NotallyFragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding?.ChipGroup?.visibility = View.VISIBLE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding?.RecyclerView?.scrollIndicators = View.SCROLL_INDICATOR_TOP
|
||||
}
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val checked =
|
||||
when (model.folder) {
|
||||
Folder.NOTES -> R.id.Notes
|
||||
Folder.DELETED -> R.id.Deleted
|
||||
Folder.ARCHIVED -> R.id.Archived
|
||||
}
|
||||
|
||||
binding?.ChipGroup?.check(checked)
|
||||
|
||||
binding?.ChipGroup?.setOnCheckedChangeListener { _, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.Notes -> model.folder = Folder.NOTES
|
||||
R.id.Deleted -> model.folder = Folder.DELETED
|
||||
R.id.Archived -> model.folder = Folder.ARCHIVED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBackground() = R.drawable.search
|
||||
|
||||
override fun getObservable() = model.searchResults
|
||||
}
|
282
app/src/main/java/com/omgodse/notally/fragments/Settings.kt
Normal file
282
app/src/main/java/com/omgodse/notally/fragments/Settings.kt
Normal file
|
@ -0,0 +1,282 @@
|
|||
package com.omgodse.notally.fragments
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.omgodse.notally.BackupProgress
|
||||
import com.omgodse.notally.MenuDialog
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.DialogProgressBinding
|
||||
import com.omgodse.notally.databinding.FragmentSettingsBinding
|
||||
import com.omgodse.notally.databinding.PreferenceBinding
|
||||
import com.omgodse.notally.databinding.PreferenceSeekbarBinding
|
||||
import com.omgodse.notally.miscellaneous.Operations
|
||||
import com.omgodse.notally.preferences.*
|
||||
import com.omgodse.notally.viewmodels.BaseNoteModel
|
||||
|
||||
class Settings : Fragment() {
|
||||
|
||||
private val model: BaseNoteModel by activityViewModels()
|
||||
|
||||
private fun setupBinding(binding: FragmentSettingsBinding) {
|
||||
model.preferences.view.observe(viewLifecycleOwner) { value ->
|
||||
binding.View.setup(View, value)
|
||||
}
|
||||
|
||||
model.preferences.theme.observe(viewLifecycleOwner) { value ->
|
||||
binding.Theme.setup(Theme, value)
|
||||
}
|
||||
|
||||
model.preferences.dateFormat.observe(viewLifecycleOwner) { value ->
|
||||
binding.DateFormat.setup(DateFormat, value)
|
||||
}
|
||||
|
||||
model.preferences.textSize.observe(viewLifecycleOwner) { value ->
|
||||
binding.TextSize.setup(TextSize, value)
|
||||
}
|
||||
|
||||
model.preferences.listItemSorting.observe(viewLifecycleOwner) { value ->
|
||||
binding.CheckedListItemSorting.setup(ListItemSorting, value)
|
||||
}
|
||||
|
||||
binding.MaxItems.setup(MaxItems, model.preferences.maxItems)
|
||||
|
||||
binding.MaxLines.setup(MaxLines, model.preferences.maxLines)
|
||||
|
||||
binding.MaxTitle.setup(MaxTitle, model.preferences.maxTitle)
|
||||
|
||||
model.preferences.autoBackup.observe(viewLifecycleOwner) { value ->
|
||||
binding.AutoBackup.setup(AutoBackup, value)
|
||||
}
|
||||
|
||||
binding.ImportBackup.setOnClickListener { importBackup() }
|
||||
|
||||
binding.ExportBackup.setOnClickListener { exportBackup() }
|
||||
|
||||
setupProgressDialog(R.string.exporting_backup, model.exportingBackup)
|
||||
setupProgressDialog(R.string.importing_backup, model.importingBackup)
|
||||
|
||||
binding.GitHub.setOnClickListener { openLink("https://github.com/OmGodse/Notally") }
|
||||
|
||||
binding.Libraries.setOnClickListener { displayLibraries() }
|
||||
|
||||
binding.Rate.setOnClickListener {
|
||||
openLink("https://play.google.com/store/apps/details?id=com.omgodse.notally")
|
||||
}
|
||||
|
||||
binding.SendFeedback.setOnClickListener { sendEmailWithLog() }
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
val binding = FragmentSettingsBinding.inflate(inflater)
|
||||
setupBinding(binding)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
intent?.data?.let { uri ->
|
||||
when (requestCode) {
|
||||
REQUEST_IMPORT_BACKUP -> model.importBackup(uri)
|
||||
REQUEST_EXPORT_BACKUP -> model.exportBackup(uri)
|
||||
REQUEST_CHOOSE_FOLDER -> model.setAutoBackupPath(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportBackup() {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.type = "application/zip"
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.putExtra(Intent.EXTRA_TITLE, "Notally Backup")
|
||||
startActivityForResult(intent, REQUEST_EXPORT_BACKUP)
|
||||
}
|
||||
|
||||
private fun importBackup() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.type = "*/*"
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/zip", "text/xml"))
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
startActivityForResult(intent, REQUEST_IMPORT_BACKUP)
|
||||
}
|
||||
|
||||
private fun setupProgressDialog(titleId: Int, liveData: MutableLiveData<BackupProgress>) {
|
||||
val dialogBinding = DialogProgressBinding.inflate(layoutInflater)
|
||||
val dialog =
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(titleId)
|
||||
.setView(dialogBinding.root)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
liveData.observe(viewLifecycleOwner) { progress ->
|
||||
if (progress.inProgress) {
|
||||
if (progress.indeterminate) {
|
||||
dialogBinding.ProgressBar.isIndeterminate = true
|
||||
dialogBinding.Count.setText(R.string.calculating)
|
||||
} else {
|
||||
dialogBinding.ProgressBar.max = progress.total
|
||||
dialogBinding.ProgressBar.setProgressCompat(progress.current, true)
|
||||
dialogBinding.Count.text =
|
||||
getString(R.string.count, progress.current, progress.total)
|
||||
}
|
||||
dialog.show()
|
||||
} else dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEmailWithLog() {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.selector = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
|
||||
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("omgodseapps@gmail.com"))
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "Notally [Feedback]")
|
||||
|
||||
val app = requireContext().applicationContext as Application
|
||||
val log = Operations.getLog(app)
|
||||
if (log.exists()) {
|
||||
val uri = FileProvider.getUriForFile(app, "${app.packageName}.provider", log)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (exception: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.install_an_email, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayLibraries() {
|
||||
val libraries =
|
||||
arrayOf(
|
||||
"Glide",
|
||||
"Pretty Time",
|
||||
"Swipe Layout",
|
||||
"Work Manager",
|
||||
"Subsampling Scale ImageView",
|
||||
"Material Components for Android",
|
||||
)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.libraries)
|
||||
.setItems(libraries) { _, which ->
|
||||
when (which) {
|
||||
0 -> openLink("https://github.com/bumptech/glide")
|
||||
1 -> openLink("https://github.com/ocpsoft/prettytime")
|
||||
2 ->
|
||||
openLink(
|
||||
"https://github.com/rambler-digital-solutions/swipe-layout-android"
|
||||
)
|
||||
3 -> openLink("https://developer.android.com/jetpack/androidx/releases/work")
|
||||
4 -> openLink("https://github.com/davemorrissey/subsampling-scale-image-view")
|
||||
5 ->
|
||||
openLink(
|
||||
"https://github.com/material-components/material-components-android"
|
||||
)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displayChooseFolderDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.notes_will_be)
|
||||
.setPositiveButton(R.string.choose_folder) { _, _ ->
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
startActivityForResult(intent, REQUEST_CHOOSE_FOLDER)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun PreferenceBinding.setup(info: ListInfo, value: String) {
|
||||
Title.setText(info.title)
|
||||
|
||||
val entries = info.getEntries(requireContext())
|
||||
val entryValues = info.getEntryValues()
|
||||
|
||||
val checked = entryValues.indexOf(value)
|
||||
val displayValue = entries[checked]
|
||||
|
||||
Value.text = displayValue
|
||||
|
||||
root.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(info.title)
|
||||
.setSingleChoiceItems(entries, checked) { dialog, which ->
|
||||
dialog.cancel()
|
||||
val newValue = entryValues[which]
|
||||
model.savePreference(info, newValue)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun PreferenceBinding.setup(info: AutoBackup, value: String) {
|
||||
Title.setText(info.title)
|
||||
|
||||
if (value == info.emptyPath) {
|
||||
Value.setText(R.string.tap_to_set_up)
|
||||
|
||||
root.setOnClickListener { displayChooseFolderDialog() }
|
||||
} else {
|
||||
val uri = Uri.parse(value)
|
||||
val folder = requireNotNull(DocumentFile.fromTreeUri(requireContext(), uri))
|
||||
if (folder.exists()) {
|
||||
Value.text = folder.name
|
||||
} else Value.setText(R.string.cant_find_folder)
|
||||
|
||||
root.setOnClickListener {
|
||||
MenuDialog(requireContext())
|
||||
.add(R.string.disable_auto_backup) { model.disableAutoBackup() }
|
||||
.add(R.string.choose_another_folder) { displayChooseFolderDialog() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PreferenceSeekbarBinding.setup(info: SeekbarInfo, initialValue: Int) {
|
||||
Title.setText(info.title)
|
||||
|
||||
Slider.valueTo = info.max.toFloat()
|
||||
Slider.valueFrom = info.min.toFloat()
|
||||
|
||||
Slider.value = initialValue.toFloat()
|
||||
|
||||
Slider.addOnChangeListener { _, value, _ -> model.savePreference(info, value.toInt()) }
|
||||
}
|
||||
|
||||
private fun openLink(link: String) {
|
||||
val uri = Uri.parse(link)
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (exception: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.install_a_browser, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_IMPORT_BACKUP = 20
|
||||
private const val REQUEST_EXPORT_BACKUP = 21
|
||||
private const val REQUEST_CHOOSE_FOLDER = 22
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.presentation.view.misc
|
||||
package com.omgodse.notally.image
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.utils
|
||||
package com.omgodse.notally.image
|
||||
|
||||
class Event<T>(val data: T) {
|
||||
|
5
app/src/main/java/com/omgodse/notally/image/FileError.kt
Normal file
5
app/src/main/java/com/omgodse/notally/image/FileError.kt
Normal file
|
@ -0,0 +1,5 @@
|
|||
package com.omgodse.notally.image
|
||||
|
||||
import com.omgodse.notally.viewmodels.NotallyModel
|
||||
|
||||
class FileError(val name: String, val description: String, val fileType: NotallyModel.FileType)
|
10
app/src/main/java/com/omgodse/notally/image/FileProgress.kt
Normal file
10
app/src/main/java/com/omgodse/notally/image/FileProgress.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package com.omgodse.notally.image
|
||||
|
||||
import com.omgodse.notally.viewmodels.NotallyModel
|
||||
|
||||
class FileProgress(
|
||||
val inProgress: Boolean,
|
||||
val current: Int,
|
||||
val total: Int,
|
||||
val fileType: NotallyModel.FileType,
|
||||
)
|
62
app/src/main/java/com/omgodse/notally/legacy/Migrations.kt
Normal file
62
app/src/main/java/com/omgodse/notally/legacy/Migrations.kt
Normal file
|
@ -0,0 +1,62 @@
|
|||
package com.omgodse.notally.legacy
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
import com.omgodse.notally.model.Folder
|
||||
import com.omgodse.notally.model.Label
|
||||
import java.io.File
|
||||
|
||||
// Backwards compatibility from v3.2 to v3.3
|
||||
object Migrations {
|
||||
|
||||
fun clearAllLabels(app: Application) {
|
||||
val preferences = getLabelsPreferences(app)
|
||||
preferences.edit().clear().commit()
|
||||
}
|
||||
|
||||
fun clearAllFolders(app: Application) {
|
||||
getNotePath(app).listFiles()?.forEach { file -> file.delete() }
|
||||
getDeletedPath(app).listFiles()?.forEach { file -> file.delete() }
|
||||
getArchivedPath(app).listFiles()?.forEach { file -> file.delete() }
|
||||
}
|
||||
|
||||
fun getPreviousLabels(app: Application): List<Label> {
|
||||
val preferences = getLabelsPreferences(app)
|
||||
val labels = requireNotNull(preferences.getStringSet("labelItems", emptySet()))
|
||||
return labels.map { value -> Label(value) }
|
||||
}
|
||||
|
||||
fun getPreviousNotes(app: Application): List<BaseNote> {
|
||||
val list = ArrayList<BaseNote>()
|
||||
getNotePath(app).listFiles()?.mapTo(list) { file ->
|
||||
XMLUtils.readBaseNoteFromFile(file, Folder.NOTES)
|
||||
}
|
||||
getDeletedPath(app).listFiles()?.mapTo(list) { file ->
|
||||
XMLUtils.readBaseNoteFromFile(file, Folder.DELETED)
|
||||
}
|
||||
getArchivedPath(app).listFiles()?.mapTo(list) { file ->
|
||||
XMLUtils.readBaseNoteFromFile(file, Folder.ARCHIVED)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private fun getNotePath(app: Application) = getFolder(app, "notes")
|
||||
|
||||
private fun getDeletedPath(app: Application) = getFolder(app, "deleted")
|
||||
|
||||
private fun getArchivedPath(app: Application) = getFolder(app, "archived")
|
||||
|
||||
private fun getLabelsPreferences(app: Application): SharedPreferences {
|
||||
return app.getSharedPreferences("labelsPreferences", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private fun getFolder(app: Application, name: String): File {
|
||||
val folder = File(app.filesDir, name)
|
||||
if (!folder.exists()) {
|
||||
folder.mkdir()
|
||||
}
|
||||
return folder
|
||||
}
|
||||
}
|
154
app/src/main/java/com/omgodse/notally/legacy/XMLUtils.kt
Normal file
154
app/src/main/java/com/omgodse/notally/legacy/XMLUtils.kt
Normal file
|
@ -0,0 +1,154 @@
|
|||
package com.omgodse.notally.legacy
|
||||
|
||||
import com.omgodse.notally.model.BaseNote
|
||||
import com.omgodse.notally.model.Color
|
||||
import com.omgodse.notally.model.Folder
|
||||
import com.omgodse.notally.model.Label
|
||||
import com.omgodse.notally.model.ListItem
|
||||
import com.omgodse.notally.model.SpanRepresentation
|
||||
import com.omgodse.notally.model.Type
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
|
||||
object XMLUtils {
|
||||
|
||||
fun readBaseNoteFromFile(file: File, folder: Folder): BaseNote {
|
||||
val inputStream = FileInputStream(file)
|
||||
val parser = XmlPullParserFactory.newInstance().newPullParser()
|
||||
parser.setInput(inputStream, null)
|
||||
parser.next()
|
||||
return parseBaseNote(parser, parser.name, folder)
|
||||
}
|
||||
|
||||
fun readBackupFromStream(inputStream: InputStream): Pair<List<BaseNote>, List<Label>> {
|
||||
val parser = XmlPullParserFactory.newInstance().newPullParser()
|
||||
parser.setInput(inputStream, null)
|
||||
|
||||
val baseNotes = ArrayList<BaseNote>()
|
||||
val labels = ArrayList<Label>()
|
||||
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG) {
|
||||
when (parser.name) {
|
||||
"notes" -> parseList(parser, parser.name, baseNotes, Folder.NOTES)
|
||||
"deleted-notes" -> parseList(parser, parser.name, baseNotes, Folder.DELETED)
|
||||
"archived-notes" -> parseList(parser, parser.name, baseNotes, Folder.ARCHIVED)
|
||||
"label" -> labels.add(Label(parser.nextText()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(baseNotes, labels)
|
||||
}
|
||||
|
||||
private fun parseList(
|
||||
parser: XmlPullParser,
|
||||
rootTag: String,
|
||||
list: ArrayList<BaseNote>,
|
||||
folder: Folder,
|
||||
) {
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG) {
|
||||
val note = parseBaseNote(parser, parser.name, folder)
|
||||
list.add(note)
|
||||
} else if (parser.eventType == XmlPullParser.END_TAG) {
|
||||
if (parser.name == rootTag) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBaseNote(parser: XmlPullParser, rootTag: String, folder: Folder): BaseNote {
|
||||
var color = Color.DEFAULT
|
||||
|
||||
var body = String()
|
||||
var title = String()
|
||||
var timestamp = 0L
|
||||
var pinned = false
|
||||
val items = ArrayList<ListItem>()
|
||||
|
||||
val labels = ArrayList<String>()
|
||||
val spans = ArrayList<SpanRepresentation>()
|
||||
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG) {
|
||||
when (parser.name) {
|
||||
"color" -> color = Color.valueOf(parser.nextText())
|
||||
"title" -> title = parser.nextText()
|
||||
"body" -> body = parser.nextText()
|
||||
"date-created" -> timestamp = parser.nextText().toLong()
|
||||
"pinned" -> pinned = parser.nextText().toBoolean()
|
||||
"label" -> labels.add(parser.nextText())
|
||||
"item" -> items.add(parseItem(parser, parser.name))
|
||||
"span" -> spans.add(parseSpan(parser))
|
||||
}
|
||||
} else if (parser.eventType == XmlPullParser.END_TAG) {
|
||||
if (parser.name == rootTag) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can be either `note` or `list`
|
||||
val type =
|
||||
if (rootTag == "note") {
|
||||
Type.NOTE
|
||||
} else Type.LIST
|
||||
return BaseNote(
|
||||
0,
|
||||
type,
|
||||
folder,
|
||||
color,
|
||||
title,
|
||||
pinned,
|
||||
timestamp,
|
||||
labels,
|
||||
body,
|
||||
spans,
|
||||
items,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseItem(parser: XmlPullParser, rootTag: String): ListItem {
|
||||
var body = String()
|
||||
var checked = false
|
||||
var isChild = false
|
||||
var order: Int? = null
|
||||
|
||||
// TODO: migration required?
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG) {
|
||||
when (parser.name) {
|
||||
"text" -> body = parser.nextText()
|
||||
"checked" -> checked = parser.nextText()?.toBoolean() ?: false
|
||||
"isChild" -> isChild = parser.nextText()?.toBoolean() ?: false
|
||||
"order" -> order = parser.nextText()?.toInt()
|
||||
}
|
||||
} else if (parser.eventType == XmlPullParser.END_TAG) {
|
||||
if (parser.name == rootTag) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ListItem(body, checked, isChild, order, mutableListOf())
|
||||
}
|
||||
|
||||
private fun parseSpan(parser: XmlPullParser): SpanRepresentation {
|
||||
val start = parser.getAttributeValue(null, "start").toInt()
|
||||
val end = parser.getAttributeValue(null, "end").toInt()
|
||||
val bold = parser.getAttributeValue(null, "bold")?.toBoolean() ?: false
|
||||
val link = parser.getAttributeValue(null, "link")?.toBoolean() ?: false
|
||||
val italic = parser.getAttributeValue(null, "italic")?.toBoolean() ?: false
|
||||
val monospace = parser.getAttributeValue(null, "monospace")?.toBoolean() ?: false
|
||||
val strikethrough = parser.getAttributeValue(null, "strike")?.toBoolean() ?: false
|
||||
return SpanRepresentation(bold, link, italic, monospace, strikethrough, start, end)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.omgodse.notally.miscellaneous
|
||||
|
||||
object Constants {
|
||||
const val SelectedLabel = "SelectedLabel"
|
||||
const val SelectedBaseNote = "SelectedBaseNote"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.omgodse.notally.miscellaneous
|
||||
|
||||
import android.app.Application
|
||||
import com.omgodse.notally.model.NotallyDatabase
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
object Export {
|
||||
|
||||
fun backupDatabase(app: Application, zipStream: ZipOutputStream) {
|
||||
val entry = ZipEntry(NotallyDatabase.DatabaseName)
|
||||
zipStream.putNextEntry(entry)
|
||||
|
||||
val file = app.getDatabasePath(NotallyDatabase.DatabaseName)
|
||||
val inputStream = FileInputStream(file)
|
||||
inputStream.copyTo(zipStream)
|
||||
inputStream.close()
|
||||
|
||||
zipStream.closeEntry()
|
||||
}
|
||||
|
||||
fun backupFile(zipStream: ZipOutputStream, root: File?, folder: String, name: String) {
|
||||
val file = if (root != null) File(root, name) else null
|
||||
if (file != null && file.exists()) {
|
||||
val entry = ZipEntry("$folder/$name")
|
||||
zipStream.putNextEntry(entry)
|
||||
|
||||
val inputStream = FileInputStream(file)
|
||||
inputStream.copyTo(zipStream)
|
||||
inputStream.close()
|
||||
|
||||
zipStream.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
package com.omgodse.notally.miscellaneous
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Typeface
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import android.text.TextWatcher
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.text.style.URLSpan
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.TextView
|
||||
import com.omgodse.notally.activities.TakeNote
|
||||
import com.omgodse.notally.changehistory.ChangeHistory
|
||||
import com.omgodse.notally.changehistory.EditTextChange
|
||||
import com.omgodse.notally.model.FileAttachment
|
||||
import com.omgodse.notally.model.SpanRepresentation
|
||||
import com.omgodse.notally.recyclerview.ListManager
|
||||
import java.util.Date
|
||||
import kotlin.math.roundToInt
|
||||
import org.ocpsoft.prettytime.PrettyTime
|
||||
|
||||
/**
|
||||
* For some reason, this method crashes sometimes with an IndexOutOfBoundsException that I've not
|
||||
* been able to replicate. When this happens, to prevent the entire app from crashing and becoming
|
||||
* unusable, the exception is suppressed.
|
||||
*/
|
||||
fun String.applySpans(representations: List<SpanRepresentation>): Editable {
|
||||
val editable = Editable.Factory.getInstance().newEditable(this)
|
||||
representations.forEach { (bold, link, italic, monospace, strikethrough, start, end) ->
|
||||
try {
|
||||
if (bold) {
|
||||
editable.setSpan(StyleSpan(Typeface.BOLD), start, end)
|
||||
}
|
||||
if (italic) {
|
||||
editable.setSpan(StyleSpan(Typeface.ITALIC), start, end)
|
||||
}
|
||||
if (link) {
|
||||
val url = getURL(start, end)
|
||||
editable.setSpan(URLSpan(url), start, end)
|
||||
}
|
||||
if (monospace) {
|
||||
editable.setSpan(TypefaceSpan("monospace"), start, end)
|
||||
}
|
||||
if (strikethrough) {
|
||||
editable.setSpan(StrikethroughSpan(), start, end)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
return editable
|
||||
}
|
||||
|
||||
private fun String.getURL(start: Int, end: Int): String {
|
||||
return if (end <= length) {
|
||||
TakeNote.getURLFrom(substring(start, end))
|
||||
} else TakeNote.getURLFrom(substring(start, length))
|
||||
}
|
||||
|
||||
private fun Spannable.setSpan(span: Any, start: Int, end: Int) {
|
||||
if (end <= length) {
|
||||
setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
} else setSpan(span, start, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
|
||||
fun EditText.setOnNextAction(onNext: () -> Unit) {
|
||||
setRawInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
|
||||
|
||||
setOnKeyListener { v, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
onNext()
|
||||
return@setOnKeyListener true
|
||||
} else return@setOnKeyListener false
|
||||
}
|
||||
|
||||
setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
onNext()
|
||||
return@setOnEditorActionListener true
|
||||
} else return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
fun Menu.add(title: Int, drawable: Int, onClick: (item: MenuItem) -> Unit): MenuItem {
|
||||
return add(Menu.NONE, title, drawable, MenuItem.SHOW_AS_ACTION_IF_ROOM, onClick)
|
||||
}
|
||||
|
||||
fun Menu.add(
|
||||
title: Int,
|
||||
drawable: Int,
|
||||
showAsAction: Int,
|
||||
onClick: (item: MenuItem) -> Unit,
|
||||
): MenuItem {
|
||||
return add(Menu.NONE, title, drawable, showAsAction, onClick)
|
||||
}
|
||||
|
||||
fun Menu.add(
|
||||
groupId: Int,
|
||||
title: Int,
|
||||
drawable: Int,
|
||||
showAsAction: Int,
|
||||
onClick: (item: MenuItem) -> Unit,
|
||||
): MenuItem {
|
||||
val menuItem = add(groupId, Menu.NONE, Menu.NONE, title)
|
||||
menuItem.setIcon(drawable)
|
||||
menuItem.setOnMenuItemClickListener { item ->
|
||||
onClick(item)
|
||||
return@setOnMenuItemClickListener false
|
||||
}
|
||||
menuItem.setShowAsAction(showAsAction)
|
||||
return menuItem
|
||||
}
|
||||
|
||||
fun TextView.displayFormattedTimestamp(timestamp: Long, dateFormat: String) {
|
||||
if (dateFormat != com.omgodse.notally.preferences.DateFormat.none) {
|
||||
visibility = View.VISIBLE
|
||||
text = formatTimestamp(timestamp, dateFormat)
|
||||
} else visibility = View.GONE
|
||||
}
|
||||
|
||||
fun RemoteViews.displayFormattedTimestamp(id: Int, timestamp: Long, dateFormat: String) {
|
||||
if (dateFormat != com.omgodse.notally.preferences.DateFormat.none) {
|
||||
setViewVisibility(id, View.VISIBLE)
|
||||
setTextViewText(id, formatTimestamp(timestamp, dateFormat))
|
||||
} else setViewVisibility(id, View.GONE)
|
||||
}
|
||||
|
||||
val Int.dp: Int
|
||||
get() = (this / Resources.getSystem().displayMetrics.density).roundToInt()
|
||||
|
||||
/**
|
||||
* Creates a TextWatcher for an EditText that is part of a list. Everytime the text is changed, a
|
||||
* Change is added to the ChangeHistory.
|
||||
*
|
||||
* @param positionGetter Function to determine the current position of the EditText in the list
|
||||
* (e.g. the current adapterPosition when using RecyclerViewer.Adapter)
|
||||
* @param updateModel Function to update the model. Is called on any text changes and on undo/redo.
|
||||
*/
|
||||
fun EditText.createListTextWatcherWithHistory(listManager: ListManager, positionGetter: () -> Int) =
|
||||
object : TextWatcher {
|
||||
private lateinit var currentTextBefore: String
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
currentTextBefore = s.toString()
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
listManager.changeText(
|
||||
this@createListTextWatcherWithHistory,
|
||||
this,
|
||||
positionGetter.invoke(),
|
||||
currentTextBefore,
|
||||
requireNotNull(s).toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun EditText.createTextWatcherWithHistory(
|
||||
changeHistory: ChangeHistory,
|
||||
updateModel: (text: String) -> Unit,
|
||||
) =
|
||||
object : TextWatcher {
|
||||
private lateinit var currentTextBefore: String
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
currentTextBefore = s.toString()
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val textBefore = currentTextBefore
|
||||
val textAfter = requireNotNull(s).toString()
|
||||
updateModel.invoke(textAfter)
|
||||
|
||||
changeHistory.push(
|
||||
EditTextChange(
|
||||
this@createTextWatcherWithHistory,
|
||||
textAfter,
|
||||
textBefore,
|
||||
this,
|
||||
updateModel,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val FileAttachment.isImage: Boolean
|
||||
get() {
|
||||
return mimeType.startsWith("image/")
|
||||
}
|
||||
|
||||
private fun formatTimestamp(timestamp: Long, dateFormat: String): String {
|
||||
val date = Date(timestamp)
|
||||
return when (dateFormat) {
|
||||
com.omgodse.notally.preferences.DateFormat.relative -> PrettyTime().format(date)
|
||||
else -> java.text.DateFormat.getDateInstance(java.text.DateFormat.FULL).format(date)
|
||||
}
|
||||
}
|
68
app/src/main/java/com/omgodse/notally/miscellaneous/IO.kt
Normal file
68
app/src/main/java/com/omgodse/notally/miscellaneous/IO.kt
Normal file
|
@ -0,0 +1,68 @@
|
|||
package com.omgodse.notally.miscellaneous
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
|
||||
object IO {
|
||||
|
||||
private fun createDirectory(file: File) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.createDirectory(file.toPath())
|
||||
} else file.mkdir()
|
||||
}
|
||||
|
||||
fun getExternalImagesDirectory(app: Application) = getExternalDirectory(app, "Images")
|
||||
|
||||
fun getExternalAudioDirectory(app: Application) = getExternalDirectory(app, "Audios")
|
||||
|
||||
fun getExternalFilesDirectory(app: Application) = getExternalDirectory(app, "Files")
|
||||
|
||||
private fun getExternalDirectory(app: Application, name: String): File? {
|
||||
var file: File? = null
|
||||
|
||||
try {
|
||||
val mediaDir = app.externalMediaDirs.firstOrNull()
|
||||
if (mediaDir != null) {
|
||||
file = File(mediaDir, name)
|
||||
if (file.exists()) {
|
||||
if (!file.isDirectory) {
|
||||
file.delete()
|
||||
createDirectory(file)
|
||||
}
|
||||
} else createDirectory(file)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
exception.printStackTrace()
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fun getTempAudioFile(context: Context): File {
|
||||
return File(context.externalCacheDir, "Temp.m4a")
|
||||
}
|
||||
|
||||
fun copyStreamToFile(input: InputStream, destination: File) {
|
||||
val output = FileOutputStream(destination)
|
||||
input.copyTo(output)
|
||||
input.close()
|
||||
output.close()
|
||||
}
|
||||
|
||||
fun renameFile(file: File, name: String): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val source = file.toPath()
|
||||
val destination = source.resolveSibling(name)
|
||||
Files.move(source, destination)
|
||||
true // If move failed, an exception would have been thrown
|
||||
} else {
|
||||
val destination = file.resolveSibling(name)
|
||||
file.renameTo(destination)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package com.omgodse.notally.miscellaneous
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.RelativeCornerSize
|
||||
import com.google.android.material.shape.RoundedCornerTreatment
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.omgodse.notally.BuildConfig
|
||||
import com.omgodse.notally.R
|
||||
import com.omgodse.notally.databinding.LabelBinding
|
||||
import com.omgodse.notally.model.Color
|
||||
import com.omgodse.notally.model.ListItem
|
||||
import com.omgodse.notally.preferences.TextSize
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.PrintWriter
|
||||
import java.text.DateFormat
|
||||
|
||||
object Operations {
|
||||
|
||||
const val extraCharSequence = "com.omgodse.notally.extra.charSequence"
|
||||
|
||||
fun getLog(app: Application): File {
|
||||
val folder = File(app.filesDir, "logs")
|
||||
folder.mkdir()
|
||||
return File(folder, "Log.v1.txt")
|
||||
}
|
||||
|
||||
fun log(app: Application, throwable: Throwable) {
|
||||
val file = getLog(app)
|
||||
val output = FileOutputStream(file, true)
|
||||
val writer = PrintWriter(OutputStreamWriter(output, Charsets.UTF_8))
|
||||
|
||||
val formatter = DateFormat.getDateTimeInstance()
|
||||
val time = formatter.format(System.currentTimeMillis())
|
||||
|
||||
writer.println("[Start]")
|
||||
throwable.printStackTrace(writer)
|
||||
writer.println("Version code : " + BuildConfig.VERSION_CODE)
|
||||
writer.println("Version name : " + BuildConfig.VERSION_NAME)
|
||||
writer.println("Model : " + Build.MODEL)
|
||||
writer.println("Device : " + Build.DEVICE)
|
||||
writer.println("Brand : " + Build.BRAND)
|
||||
writer.println("Manufacturer : " + Build.MANUFACTURER)
|
||||
writer.println("Android : " + Build.VERSION.SDK_INT)
|
||||
writer.println("Time : $time")
|
||||
writer.println("[End]")
|
||||
|
||||
writer.close()
|
||||
}
|
||||
|
||||
fun extractColor(color: Color, context: Context): Int {
|
||||
val id =
|
||||
when (color) {
|
||||
Color.DEFAULT -> R.color.Default
|
||||
Color.CORAL -> R.color.Coral
|
||||
Color.ORANGE -> R.color.Orange
|
||||
Color.SAND -> R.color.Sand
|
||||
Color.STORM -> R.color.Storm
|
||||
Color.FOG -> R.color.Fog
|
||||
Color.SAGE -> R.color.Sage
|
||||
Color.MINT -> R.color.Mint
|
||||
Color.DUSK -> R.color.Dusk
|
||||
Color.FLOWER -> R.color.Flower
|
||||
Color.BLOSSOM -> R.color.Blossom
|
||||
Color.CLAY -> R.color.Clay
|
||||
}
|
||||
return ContextCompat.getColor(context, id)
|
||||
}
|
||||
|
||||
fun shareNote(context: Context, title: String, body: CharSequence) {
|
||||
val text = body.toString()
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(extraCharSequence, body)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
intent.putExtra(Intent.EXTRA_TITLE, title)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, title)
|
||||
val chooser = Intent.createChooser(intent, null)
|
||||
context.startActivity(chooser)
|
||||
}
|
||||
|
||||
fun getBody(list: List<ListItem>) = buildString {
|
||||
for (item in list) {
|
||||
val check = if (item.checked) "[✓]" else "[ ]"
|
||||
val childIndentation = if (item.isChild) " " else ""
|
||||
appendLine("$childIndentation$check ${item.body}")
|
||||
}
|
||||
}
|
||||
|
||||
fun bindLabels(group: ChipGroup, labels: List<String>, textSize: String) {
|
||||
if (labels.isEmpty()) {
|
||||
group.visibility = View.GONE
|
||||
} else {
|
||||
group.visibility = View.VISIBLE
|
||||
group.removeAllViews()
|
||||
|
||||
val inflater = LayoutInflater.from(group.context)
|
||||
val labelSize = TextSize.getDisplayBodySize(textSize)
|
||||
|
||||
for (label in labels) {
|
||||
val view = LabelBinding.inflate(inflater, group, true).root
|
||||
view.background = getOutlinedDrawable(group.context)
|
||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, labelSize)
|
||||
view.text = label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOutlinedDrawable(context: Context): MaterialShapeDrawable {
|
||||
val model =
|
||||
ShapeAppearanceModel.builder()
|
||||
.setAllCorners(RoundedCornerTreatment())
|
||||
.setAllCornerSizes(RelativeCornerSize(0.5f))
|
||||
.build()
|
||||
|
||||
val drawable = MaterialShapeDrawable(model)
|
||||
drawable.fillColor = ColorStateList.valueOf(0)
|
||||
drawable.strokeWidth = context.resources.displayMetrics.density
|
||||
drawable.strokeColor = ContextCompat.getColorStateList(context, R.color.chip_stroke)
|
||||
|
||||
return drawable
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.data.model
|
||||
package com.omgodse.notally.model
|
||||
|
||||
import android.os.Parcelable
|
||||
|
5
app/src/main/java/com/omgodse/notally/model/Audio.kt
Normal file
5
app/src/main/java/com/omgodse/notally/model/Audio.kt
Normal file
|
@ -0,0 +1,5 @@
|
|||
package com.omgodse.notally.model
|
||||
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize data class Audio(var name: String, val duration: Long, val timestamp: Long) : Attachment
|
23
app/src/main/java/com/omgodse/notally/model/BaseNote.kt
Normal file
23
app/src/main/java/com/omgodse/notally/model/BaseNote.kt
Normal file
|
@ -0,0 +1,23 @@
|
|||
package com.omgodse.notally.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(indices = [Index(value = ["id", "folder", "pinned", "timestamp", "labels"])])
|
||||
data class BaseNote(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||
val type: Type,
|
||||
val folder: Folder,
|
||||
val color: Color,
|
||||
val title: String,
|
||||
val pinned: Boolean,
|
||||
val timestamp: Long,
|
||||
val labels: List<String>,
|
||||
val body: String,
|
||||
val spans: List<SpanRepresentation>,
|
||||
val items: List<ListItem>,
|
||||
val images: List<FileAttachment>,
|
||||
val files: List<FileAttachment>,
|
||||
val audios: List<Audio>,
|
||||
) : Item
|
16
app/src/main/java/com/omgodse/notally/model/Color.kt
Normal file
16
app/src/main/java/com/omgodse/notally/model/Color.kt
Normal file
|
@ -0,0 +1,16 @@
|
|||
package com.omgodse.notally.model
|
||||
|
||||
enum class Color {
|
||||
DEFAULT,
|
||||
CORAL,
|
||||
ORANGE,
|
||||
SAND,
|
||||
STORM,
|
||||
FOG,
|
||||
SAGE,
|
||||
MINT,
|
||||
DUSK,
|
||||
FLOWER,
|
||||
BLOSSOM,
|
||||
CLAY,
|
||||
}
|
166
app/src/main/java/com/omgodse/notally/model/Converters.kt
Normal file
166
app/src/main/java/com/omgodse/notally/model/Converters.kt
Normal file
|
@ -0,0 +1,166 @@
|
|||
package com.omgodse.notally.model
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
object Converters {
|
||||
|
||||
@TypeConverter fun labelsToJson(labels: List<String>) = JSONArray(labels).toString()
|
||||
|
||||
@TypeConverter fun jsonToLabels(json: String) = JSONArray(json).iterable<String>().toList()
|
||||
|
||||
@TypeConverter
|
||||
fun filesToJson(files: List<FileAttachment>): String {
|
||||
val objects =
|
||||
files.map { file ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("localName", file.localName)
|
||||
jsonObject.put("originalName", file.originalName)
|
||||
jsonObject.put("mimeType", file.mimeType)
|
||||
}
|
||||
return JSONArray(objects).toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToFiles(json: String): List<FileAttachment> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
val localName = getSafeLocalName(jsonObject)
|
||||
val originalName = getSafeOriginalName(jsonObject)
|
||||
val mimeType = jsonObject.getString("mimeType")
|
||||
FileAttachment(localName, originalName, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun audiosToJson(audios: List<Audio>): String {
|
||||
val objects =
|
||||
audios.map { audio ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("name", audio.name)
|
||||
jsonObject.put("duration", audio.duration)
|
||||
jsonObject.put("timestamp", audio.timestamp)
|
||||
}
|
||||
return JSONArray(objects).toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAudios(json: String): List<Audio> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
val name = jsonObject.getString("name")
|
||||
val duration = jsonObject.getLong("duration")
|
||||
val timestamp = jsonObject.getLong("timestamp")
|
||||
Audio(name, duration, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToSpans(json: String): List<SpanRepresentation> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
val bold = jsonObject.getSafeBoolean("bold")
|
||||
val link = jsonObject.getSafeBoolean("link")
|
||||
val italic = jsonObject.getSafeBoolean("italic")
|
||||
val monospace = jsonObject.getSafeBoolean("monospace")
|
||||
val strikethrough = jsonObject.getSafeBoolean("strikethrough")
|
||||
val start = jsonObject.getInt("start")
|
||||
val end = jsonObject.getInt("end")
|
||||
SpanRepresentation(bold, link, italic, monospace, strikethrough, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun spansToJson(list: List<SpanRepresentation>) = spansToJSONArray(list).toString()
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToItems(json: String): List<ListItem> {
|
||||
val iterable = JSONArray(json).iterable<JSONObject>()
|
||||
return iterable.map { jsonObject ->
|
||||
val body = jsonObject.getString("body")
|
||||
val checked = jsonObject.getBoolean("checked")
|
||||
val isChild = jsonObject.getSafeBoolean("isChild")
|
||||
val order = jsonObject.getSafeInt("order")
|
||||
ListItem(body, checked, isChild, order, mutableListOf())
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter fun itemsToJson(list: List<ListItem>) = itemsToJSONArray(list).toString()
|
||||
|
||||
fun itemsToJSONArray(list: List<ListItem>): JSONArray {
|
||||
val objects =
|
||||
list.map { item ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("body", item.body)
|
||||
jsonObject.put("checked", item.checked)
|
||||
jsonObject.put("isChild", item.isChild)
|
||||
jsonObject.put("order", item.order)
|
||||
}
|
||||
return JSONArray(objects)
|
||||
}
|
||||
|
||||
fun spansToJSONArray(list: List<SpanRepresentation>): JSONArray {
|
||||
val objects =
|
||||
list.map { representation ->
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("bold", representation.bold)
|
||||
jsonObject.put("link", representation.link)
|
||||
jsonObject.put("italic", representation.italic)
|
||||
jsonObject.put("monospace", representation.monospace)
|
||||
jsonObject.put("strikethrough", representation.strikethrough)
|
||||
jsonObject.put("start", representation.start)
|
||||
jsonObject.put("end", representation.end)
|
||||
}
|
||||
return JSONArray(objects)
|
||||
}
|
||||
|
||||
private fun getSafeLocalName(jsonObject: JSONObject): String {
|
||||
return try {
|
||||
jsonObject.getString("localName")
|
||||
} catch (e: JSONException) {
|
||||
jsonObject.getString("name")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSafeOriginalName(jsonObject: JSONObject): String {
|
||||
return try {
|
||||
jsonObject.getString("originalName")
|
||||
} catch (e: JSONException) {
|
||||
getSafeLocalName(jsonObject).substringAfterLast("/")
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.getSafeBoolean(name: String): Boolean {
|
||||
return try {
|
||||
getBoolean(name)
|
||||
} catch (exception: JSONException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.getSafeInt(name: String): Int? {
|
||||
return try {
|
||||
getInt(name)
|
||||
} catch (exception: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> JSONArray.iterable() = Iterable {
|
||||
object : Iterator<T> {
|
||||
var index = 0
|
||||
|
||||
override fun next(): T {
|
||||
val element = get(index)
|
||||
index++
|
||||
return element as T
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return index < length()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package com.philkes.notallyx.data.model
|
||||
package com.omgodse.notally.model
|
||||
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class FileAttachment(var localName: String, var originalName: String, var mimeType: String) :
|
||||
data class FileAttachment(var localName: String, var originalName: String, val mimeType: String) :
|
||||
Attachment
|
7
app/src/main/java/com/omgodse/notally/model/Folder.kt
Normal file
7
app/src/main/java/com/omgodse/notally/model/Folder.kt
Normal file
|
@ -0,0 +1,7 @@
|
|||
package com.omgodse.notally.model
|
||||
|
||||
enum class Folder {
|
||||
NOTES,
|
||||
DELETED,
|
||||
ARCHIVED,
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
package com.philkes.notallyx.data.model
|
||||
package com.omgodse.notally.model
|
||||
|
||||
class Header(val label: String) : Item
|
3
app/src/main/java/com/omgodse/notally/model/Item.kt
Normal file
3
app/src/main/java/com/omgodse/notally/model/Item.kt
Normal file
|
@ -0,0 +1,3 @@
|
|||
package com.omgodse.notally.model
|
||||
|
||||
sealed interface Item
|
|
@ -1,4 +1,4 @@
|
|||
package com.philkes.notallyx.data.model
|
||||
package com.omgodse.notally.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue